Merge branch 'main' into feature/caldav-subtasks
# Conflicts: # go.mod # go.sum
This commit is contained in:
commit
2ae966c0ad
22
.drone.yml
22
.drone.yml
@ -39,7 +39,7 @@ volumes:
|
||||
|
||||
services:
|
||||
- name: test-mysql-unit
|
||||
image: mariadb:10
|
||||
image: mariadb:11
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: vikunjatest
|
||||
MYSQL_DATABASE: vikunjatest
|
||||
@ -47,7 +47,7 @@ services:
|
||||
- name: tmp-mysql-unit
|
||||
path: /var/lib/mysql
|
||||
- name: test-mysql-integration
|
||||
image: mariadb:10
|
||||
image: mariadb:11
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: vikunjatest
|
||||
MYSQL_DATABASE: vikunjatest
|
||||
@ -55,7 +55,7 @@ services:
|
||||
- name: tmp-mysql-integration
|
||||
path: /var/lib/mysql
|
||||
- name: test-mysql-migration
|
||||
image: mariadb:10
|
||||
image: mariadb:11
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: vikunjatest
|
||||
MYSQL_DATABASE: vikunjatest
|
||||
@ -63,7 +63,7 @@ services:
|
||||
- name: tmp-mysql-migration
|
||||
path: /var/lib/mysql
|
||||
- name: test-postgres-unit
|
||||
image: postgres:14
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_PASSWORD: vikunjatest
|
||||
POSTGRES_DB: vikunjatest
|
||||
@ -73,7 +73,7 @@ services:
|
||||
commands:
|
||||
- docker-entrypoint.sh -c fsync=off -c full_page_writes=off # turns of wal
|
||||
- name: test-postgres-integration
|
||||
image: postgres:14
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_PASSWORD: vikunjatest
|
||||
POSTGRES_DB: vikunjatest
|
||||
@ -83,7 +83,7 @@ services:
|
||||
commands:
|
||||
- docker-entrypoint.sh -c fsync=off -c full_page_writes=off # turns of wal
|
||||
- name: test-postgres-migration
|
||||
image: postgres:14
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_PASSWORD: vikunjatest
|
||||
POSTGRES_DB: vikunjatest
|
||||
@ -133,15 +133,13 @@ steps:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: lint
|
||||
image: golang:1.19-alpine
|
||||
image: golangci/golangci-lint:v1.54.2
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
depends_on: [ build ]
|
||||
commands:
|
||||
- export "GOROOT=$(go env GOROOT)"
|
||||
- apk --no-cache add build-base git
|
||||
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.2
|
||||
- ./mage-static check:golangci
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
@ -553,7 +551,7 @@ steps:
|
||||
|
||||
# Build os packages and push it to our bucket
|
||||
- name: build-os-packages-unstable
|
||||
image: goreleaser/nfpm:v2.30.1
|
||||
image: goreleaser/nfpm:v2.33.1
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
@ -569,7 +567,7 @@ steps:
|
||||
depends_on: [ after-build-compress ]
|
||||
|
||||
- name: build-os-packages-version
|
||||
image: goreleaser/nfpm:v2.30.1
|
||||
image: goreleaser/nfpm:v2.33.1
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
@ -778,6 +776,6 @@ steps:
|
||||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: 6bc74f5b7e9c51e725100e05f07cdac656d6c3d49d19c2b112aed812c86e7a9a
|
||||
hmac: 3ad78b828f36d4473527b8c6ee0985a5bf6f290fe73b3f7381a41d8c4937ffaa
|
||||
|
||||
...
|
||||
|
23
.github/workflows/lockdown.yml
vendored
Normal file
23
.github/workflows/lockdown.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
name: 'Repo Lockdown'
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: opened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/repo-lockdown@v3
|
||||
with:
|
||||
pr-comment: 'Hi! Thank you for your contribution.
|
||||
|
||||
This repo is only a mirror and unfortunately we can''t accept PRs made here. Please re-submit your changes to [our Gitea instance](https://kolaente.dev/vikunja/api/pulls).
|
||||
|
||||
Also check out the [contribution guidelines](https://vikunja.io/docs/development/#pull-requests).
|
||||
|
||||
Thank you for your understanding.'
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -27,3 +27,4 @@ vikunja-dump*
|
||||
vendor/
|
||||
os-packages/
|
||||
mage_output_file.go
|
||||
mage-static
|
||||
|
@ -103,3 +103,6 @@ issues:
|
||||
- text: 'string `labels` has 3 occurrences, make it a constant'
|
||||
linters:
|
||||
- goconst
|
||||
- text: 'string `off` has 6 occurrences, make it a constant'
|
||||
linters:
|
||||
- goconst
|
||||
|
3
CONTRIBUTING.md
Normal file
3
CONTRIBUTING.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Contribution Guidelines
|
||||
|
||||
Please check out the guidelines on https://vikunja.io/docs/development/
|
@ -3,7 +3,7 @@
|
||||
# │─││ │││ │ │
|
||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||
|
||||
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.20.x AS builder
|
||||
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.21.x AS builder
|
||||
|
||||
RUN go install github.com/magefile/mage@latest && \
|
||||
mv /go/bin/mage /usr/local/go/bin
|
||||
|
@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
curl -X POST http://localhost:3456/api/v1/register -H 'Content-Type: application/json' -d '{"username":"demo","password":"demo","email":"demo@vikunja.io"}'
|
||||
BEARER=`curl -X POST -H 'Content-Type: application/json' -d '{"username": "demo", "password":"demo"}' localhost:3456/api/v1/login | jq -r '.token'`
|
||||
|
||||
echo "Bearer: $BEARER"
|
||||
|
||||
curl -X POST localhost:3456/api/v1/tokenTest -H "Authorization: Bearer $BEARER"
|
||||
|
||||
curl -X PUT localhost:3456/api/v1/namespaces/1/lists -H 'Content-Type: application/json' -H "Authorization: Bearer $BEARER" -d '{"title":"lorem"}'
|
||||
curl -X PUT localhost:3456/api/v1/lists/1 -H 'Content-Type: application/json' -H "Authorization: Bearer $BEARER" -d '{"text":"lorem"}'
|
||||
curl -X PUT -H "Authorization: Bearer $BEARER" localhost:3456/api/v1/tasks/1/attachments -F 'files=@/home/konrad/Pictures/Wallpaper/greg-rakozy-_Q4mepyyjMw-unsplash.jpg'
|
@ -1,29 +0,0 @@
|
||||
### Authorization by token, part 1. Retrieve and save token.
|
||||
POST http://localhost:8080/api/v1/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "user3",
|
||||
"password": "1234"
|
||||
}
|
||||
|
||||
> {% client.global.set("auth_token", response.body.token); %}
|
||||
|
||||
### Register
|
||||
|
||||
POST http://localhost:8080/api/v1/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "user",
|
||||
"password": "1234",
|
||||
"email": "5@knt.li"
|
||||
}
|
||||
|
||||
###
|
||||
# Token test
|
||||
POST http://localhost:8080/api/v1/tokenTest
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
###
|
@ -1,70 +0,0 @@
|
||||
# Get all labels
|
||||
GET http://localhost:8080/api/v1/labels
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
# Add a new label
|
||||
PUT http://localhost:8080/api/v1/labels
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "test5"
|
||||
}
|
||||
|
||||
###
|
||||
# Delete a label
|
||||
DELETE http://localhost:8080/api/v1/labels/6
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
# Update a label
|
||||
POST http://localhost:8080/api/v1/labels/1
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "testschinkenbrot",
|
||||
"description": "käsebrot"
|
||||
}
|
||||
|
||||
###
|
||||
# Get one label
|
||||
GET http://localhost:8080/api/v1/labels/1
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
# Get all labels on a task
|
||||
GET http://localhost:8080/api/v1/tasks/3565/labels
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
# Add a new label to a task
|
||||
PUT http://localhost:8080/api/v1/tasks/35236365/labels
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"label_id": 1
|
||||
}
|
||||
|
||||
###
|
||||
# Delete a label from a task
|
||||
DELETE http://localhost:8080/api/v1/tasks/3565/labels/1
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
# Add a new label to a task
|
||||
POST http://localhost:8080/api/v1/tasks/3565/labels/bulk
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"labels": [
|
||||
{"id": 1},
|
||||
{"id": 2},
|
||||
{"id": 3}
|
||||
]
|
||||
}
|
||||
|
||||
###
|
@ -1,177 +0,0 @@
|
||||
# Get all lists
|
||||
GET http://localhost:8080/api/v1/namespaces/35/lists
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get one list
|
||||
GET http://localhost:8080/api/v1/lists/3
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Add a new list
|
||||
PUT http://localhost:8080/api/v1/namespaces/35/lists
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "test"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# Add a new item
|
||||
PUT http://localhost:8080/api/v1/lists/1
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"text": "Task",
|
||||
"description": "Schinken"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# Delete a task from a list
|
||||
DELETE http://localhost:8080/api/v1/lists/14
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get all teams who have access to that list
|
||||
GET http://localhost:8080/api/v1/lists/28/teams
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Give a team access to that list
|
||||
PUT http://localhost:8080/api/v1/lists/1/teams
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"team_id":2, "right": 1}
|
||||
|
||||
###
|
||||
|
||||
# Update a teams access to that list
|
||||
POST http://localhost:8080/api/v1/lists/1/teams/2
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"right": 0}
|
||||
|
||||
###
|
||||
|
||||
# Delete a team from a list
|
||||
DELETE http://localhost:8080/api/v1/lists/10235/teams/1
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Delete a team from a list
|
||||
DELETE http://localhost:8080/api/v1/lists/10235/teams/1
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get all users who have access to that list
|
||||
GET http://localhost:8080/api/v1/lists/28/users
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Give a user access to that list
|
||||
PUT http://localhost:8080/api/v1/lists/3/users
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"userID":"user4", "right":1}
|
||||
|
||||
###
|
||||
|
||||
# Update a users access to that list
|
||||
POST http://localhost:8080/api/v1/lists/30/users/3
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"right":2}
|
||||
|
||||
###
|
||||
|
||||
# Delete a user from a list
|
||||
DELETE http://localhost:8080/api/v1/lists/28/users/3
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get all pending tasks
|
||||
GET http://localhost:8080/api/v1/tasks/all
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get all pending tasks with priorities
|
||||
GET http://localhost:8080/api/v1/tasks/all?sort=priorityasc
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get all pending tasks in a range
|
||||
GET http://localhost:8080/api/v1/tasks/all/dueadateasc/1546784000/1548784000
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get all pending tasks in caldav
|
||||
GET http://localhost:8080/api/v1/tasks/caldav
|
||||
#Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Update a task
|
||||
POST http://localhost:8080/api/v1/tasks/3565
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"priority": 0
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# Bulk update multiple tasks at once
|
||||
POST http://localhost:8080/api/v1/tasks/bulk
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"task_ids": [3518,3519,3521],
|
||||
"text":"bulkupdated"
|
||||
}
|
||||
|
||||
###
|
||||
# Get all assignees
|
||||
GET http://localhost:8080/api/v1/tasks/3565/assignees
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Add a bunch of assignees
|
||||
PUT http://localhost:8080/api/v1/tasks/3565/assignees/bulk
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"assignees": [
|
||||
{"id": 17}
|
||||
]
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# Get all users who have access to a list
|
||||
GET http://localhost:8080/api/v1/lists/3/users
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
@ -1,71 +0,0 @@
|
||||
# Get all namespaces
|
||||
GET http://localhost:8080/api/v1/namespaces
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get one namespaces
|
||||
GET http://localhost:8080/api/v1/namespaces/-1
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get all users who have access to that namespace
|
||||
GET http://localhost:8080/api/v1/namespaces/12/users
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Give a user access to that namespace
|
||||
PUT http://localhost:8080/api/v1/namespaces/1/users
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"user_id":3, "right": 0}
|
||||
|
||||
###
|
||||
|
||||
# Update a users access to that namespace
|
||||
POST http://localhost:8080/api/v1/namespaces/1/users/3
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"right": 2}
|
||||
|
||||
###
|
||||
|
||||
# Delete a user from a namespace
|
||||
DELETE http://localhost:8080/api/v1/namespaces/1/users/2
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get all teams who have access to that namespace
|
||||
GET http://localhost:8080/api/v1/namespaces/1/teams
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Give a team access to that namespace
|
||||
PUT http://localhost:8080/api/v1/namespaces/1/teams
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"team_id":3, "right": 0}
|
||||
|
||||
###
|
||||
|
||||
# Update a teams access to that namespace
|
||||
POST http://localhost:8080/api/v1/namespaces/1/teams/1
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{"right": 0}
|
||||
|
||||
###
|
||||
|
||||
# Delete a team from a namespace
|
||||
DELETE http://localhost:8080/api/v1/namespaces/1/teams/2
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
@ -1,29 +0,0 @@
|
||||
# Get all teams
|
||||
GET http://localhost:8080/api/v1/teams
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Get one team
|
||||
GET http://localhost:8080/api/v1/teams/28
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
# Add a new member to that team
|
||||
PUT http://localhost:8080/api/v1/teams/28/members
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"user_id": 2
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# Delete a member from a team
|
||||
DELETE http://localhost:8080/api/v1/teams/28/members/2
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
@ -1,53 +0,0 @@
|
||||
|
||||
# Get all users
|
||||
GET http://localhost:8080/api/v1/user
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
######
|
||||
# Search for a user
|
||||
GET http://localhost:8080/api/v1/users?s=3
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
||||
## Update password
|
||||
|
||||
POST http://localhost:8080/api/v1/user/password
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"old_password": "1234",
|
||||
"new_password": "1234"
|
||||
}
|
||||
|
||||
### Request a password to reset a password
|
||||
POST http://localhost:8080/api/v1/user/password/token
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
|
||||
{
|
||||
"email": "k@knt.li"
|
||||
}
|
||||
|
||||
### Request a token to reset a password
|
||||
POST http://localhost:8080/api/v1/user/password/reset
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
|
||||
{
|
||||
"token": "eAsZzakgqARnjzXHqsHqZtSUKuiOhoJjHANhgTxUIDBSalhbtdpAdLeywGXzVDBuRQGNpHdMxoHXhLVSlzpJsFvuoJgMdkhRhkNhaQXfufuZCdtUlerZHSJQLgYMUryHIxIREcmZLtWoZVrYyARkCvkyFhcGtoCwQOEjAOEZMQQuxTVoGYfAqcfNggQnerUcXCiRIgRtkusXSnltomhaeyRwAbrckXFeXxUjslgplSGqSTOqJTYuhrSzAVTwNvuYyvuXLaZoNnJEyeVDWlRydnxfgUQjQZOKwCBRWVQPKpZhlslLUyUAMsRQkHITkruQCjDnOGCCRsSNplbNCEuDmMfpWYHSQAcQIDZtbQWkxzpfmHDMQvvKPPrxEnrTErlvTfKDKICFYPQxXNpNE",
|
||||
"new_password": "1234"
|
||||
}
|
||||
|
||||
### Confirm a users email address
|
||||
|
||||
POST http://localhost:8080/api/v1/user/confirm
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
|
||||
{
|
||||
"token": ""
|
||||
}
|
||||
|
||||
###
|
@ -342,7 +342,17 @@ defaultsettings:
|
||||
default_project_id: 0
|
||||
# Start of the week for the user. `0` is sunday, `1` is monday and so on.
|
||||
week_start: 0
|
||||
# The language of the user interface. Must be an ISO 639-1 language code. Will default to the browser language the user uses when signing up.
|
||||
# The language of the user interface. Must be an ISO 639-1 language code followed by an ISO 3166-1 alpha-2 country code. Check https://kolaente.dev/vikunja/frontend/src/branch/main/src/i18n/lang for a list of possible languages. Will default to the browser language the user uses when signing up.
|
||||
language: <unset>
|
||||
# The time zone of each individual user. This will affect when users get reminders and overdue task emails.
|
||||
timezone: <time zone set at service.timezone>
|
||||
|
||||
webhooks:
|
||||
# Whether to enable support for webhooks
|
||||
enabled: true
|
||||
# The timout in seconds until a webhook request fails when no response has been received.
|
||||
timoutseconds: 30
|
||||
# The URL of [a mole instance](https://github.com/frain-dev/mole) to use to proxy outgoing webhook requests. You should use this and configure appropriately if you're not the only one using your Vikunja instance. More info about why: https://webhooks.fyi/best-practices/webhook-providers#implement-security-on-egress-communication. Must be used in combination with `webhooks.password` (see below).
|
||||
proxyurl:
|
||||
# The proxy password to use when authenticating against the proxy.
|
||||
proxypassword:
|
||||
|
@ -17,7 +17,9 @@ menu:
|
||||
## General
|
||||
|
||||
To contribute to Vikunja, fork the project and work on the main branch.
|
||||
Once you feel like your changes are ready, open a PR in the respective repo.
|
||||
Once you feel like your changes are ready, open a PR in the respective repo [on our Gitea instance](https://kolaente.dev/vikunja).
|
||||
We cannot accept PRs on mirror sites.
|
||||
|
||||
A maintainer will take a look and give you feedback. Once everyone is happy, the PR gets merged and released.
|
||||
|
||||
If you plan to do a bigger change, it is better to open an issue for discussion first.
|
||||
@ -26,7 +28,7 @@ If you plan to do a bigger change, it is better to open an issue for discussion
|
||||
|
||||
The code for the api is located at [code.vikunja.io/api](https://code.vikunja.io/api).
|
||||
|
||||
We use go modules to manage third-party libraries for Vikunja, so you'll need at least go `1.17` to use these.
|
||||
You'll need at least Go 1.21 to build Vikunja's api.
|
||||
|
||||
A lot of developing tasks are automated using a Magefile, so make sure to [take a look at it]({{< ref "mage.md">}}).
|
||||
|
||||
@ -38,11 +40,51 @@ Make sure to check the other doc articles for specific development tasks like [t
|
||||
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
|
||||
More instructions can be found in the repo's README.
|
||||
|
||||
You need to have [pnpm](https://pnpm.io/) and nodejs in version 16 or 18 installed.
|
||||
You need to have [pnpm](https://pnpm.io/) and Node.JS in version 18 or higher installed.
|
||||
|
||||
## Git flow
|
||||
## Pull Requests
|
||||
|
||||
All Pull Requests must be made [on our Gitea instance](https://kolaente.dev/vikunja).
|
||||
We cannot accept PRs on mirror sites.
|
||||
|
||||
Please try to make your pull request easy to review.
|
||||
For that, please read the [*Best Practices for Faster Reviews*](https://github.com/kubernetes/community/blob/261cb0fd089b64002c91e8eddceebf032462ccd6/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) guide.
|
||||
It has lots of useful tips for any project you may want to contribute to.
|
||||
Some of the key points:
|
||||
|
||||
- Make small pull requests.
|
||||
The smaller, the faster to review and the more likely it will be merged soon.
|
||||
- Don't make changes unrelated to your PR.
|
||||
Maybe there are typos on some comments, maybe refactoring would be welcome on a function…
|
||||
but if that is not related to your PR, please make *another* PR for that.
|
||||
- Split big pull requests into multiple small ones.
|
||||
An incremental change will be faster to review than a huge PR.
|
||||
- Allow edits by maintainers. This way, the maintainers will take care of merging the PR later on instead of you.
|
||||
|
||||
### PR title and summary
|
||||
|
||||
In the PR title, describe the problem you are fixing, not how you are fixing it.
|
||||
Use the first comment as a summary of your PR.
|
||||
In the PR summary, you can describe exactly how you are fixing this problem.
|
||||
Keep this summary up-to-date as the PR evolves.
|
||||
|
||||
If your PR changes the UI, you must add **after** screenshots in the PR summary.
|
||||
If your PR closes an issue, you must note that in a way that both GitHub and Gitea understand, i.e. by appending a paragraph like
|
||||
|
||||
```text
|
||||
Fixes/Closes/Resolves #<ISSUE_NR_X>.
|
||||
Fixes/Closes/Resolves #<ISSUE_NR_Y>.
|
||||
```
|
||||
|
||||
to your summary.
|
||||
Each issue that will be closed must stand on a separate line.
|
||||
|
||||
If your PR is related to a discussion in the forum, you must add a link to the forum discussion.
|
||||
|
||||
### Git flow
|
||||
|
||||
The `main` branch is the latest and bleeding edge branch with all changes. Unstable releases are automatically created from this branch.
|
||||
New Pull-Requests should be made against the `main` branch.
|
||||
|
||||
A release gets tagged from the main branch with the version name as tag name.
|
||||
|
||||
@ -52,4 +94,4 @@ Backports and point-releases should go to a `release/version` branch, based on t
|
||||
|
||||
We're using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) because they greatly simplify generating release notes.
|
||||
|
||||
It is not required to use them when creating a PR, but appreciated.
|
||||
It is not required to use them when creating a PR, but appreciated.
|
@ -1307,7 +1307,7 @@ Environment path: `VIKUNJA_DEFAULTSETTINGS_WEEK_START`
|
||||
|
||||
### language
|
||||
|
||||
The language of the user interface. Must be an ISO 639-1 language code. Will default to the browser language the user uses when signing up.
|
||||
The language of the user interface. Must be an ISO 639-1 language code followed by an ISO 3166-1 alpha-2 country code. Check https://kolaente.dev/vikunja/frontend/src/branch/main/src/i18n/lang for a list of possible languages. Will default to the browser language the user uses when signing up.
|
||||
|
||||
Default: `<unset>`
|
||||
|
||||
@ -1327,3 +1327,53 @@ Full path: `defaultsettings.timezone`
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_TIMEZONE`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## webhooks
|
||||
|
||||
|
||||
|
||||
### enabled
|
||||
|
||||
Whether to enable support for webhooks
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `webhooks.enabled`
|
||||
|
||||
Environment path: `VIKUNJA_WEBHOOKS_ENABLED`
|
||||
|
||||
|
||||
### timoutseconds
|
||||
|
||||
The timout in seconds until a webhook request fails when no response has been received.
|
||||
|
||||
Default: `30`
|
||||
|
||||
Full path: `webhooks.timoutseconds`
|
||||
|
||||
Environment path: `VIKUNJA_WEBHOOKS_TIMOUTSECONDS`
|
||||
|
||||
|
||||
### proxyurl
|
||||
|
||||
The URL of [a mole instance](https://github.com/frain-dev/mole) to use to proxy outgoing webhook requests. You should use this and configure appropriately if you're not the only one using your Vikunja instance. More info about why: https://webhooks.fyi/best-practices/webhook-providers#implement-security-on-egress-communication. Must be used in combination with `webhooks.password` (see below).
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `webhooks.proxyurl`
|
||||
|
||||
Environment path: `VIKUNJA_WEBHOOKS_PROXYURL`
|
||||
|
||||
|
||||
### proxypassword
|
||||
|
||||
The proxy password to use when authenticating against the proxy.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `webhooks.proxypassword`
|
||||
|
||||
Environment path: `VIKUNJA_WEBHOOKS_PROXYPASSWORD`
|
||||
|
||||
|
||||
|
@ -1,147 +1,316 @@
|
||||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Reverse Proxy"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# Setup behind a reverse proxy which also serves the frontend
|
||||
|
||||
These examples assume you have an instance of the backend running on your server listening on port `3456`.
|
||||
If you've changed this setting, you need to update the server configurations accordingly.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## NGINX
|
||||
|
||||
Below are two example configurations which you can put in your `nginx.conf`:
|
||||
|
||||
You may need to adjust `server_name` and `root` accordingly.
|
||||
|
||||
### with gzip enabled (recommended)
|
||||
|
||||
{{< highlight conf >}}
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /path/to/vikunja/static/frontend/files;
|
||||
try_files $uri $uri/ /;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location ~* ^/(api|dav|\.well-known)/ {
|
||||
proxy_pass http://localhost:3456;
|
||||
client_max_body_size 20M;
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> If you change the max upload size in Vikunja's settings, you'll need to also change the <code>client_max_body_size</code> in the nginx proxy config.
|
||||
</div>
|
||||
|
||||
### without gzip
|
||||
|
||||
{{< highlight conf >}}
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /path/to/vikunja/static/frontend/files;
|
||||
try_files $uri $uri/ /;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location ~* ^/(api|dav|\.well-known)/ {
|
||||
proxy_pass http://localhost:3456;
|
||||
client_max_body_size 20M;
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> If you change the max upload size in Vikunja's settings, you'll need to also change the <code>client_max_body_size</code> in the nginx proxy config.
|
||||
</div>
|
||||
|
||||
## NGINX Proxy Manager (NPM)
|
||||
|
||||
1. Create a standard Proxy Host for the Vikunja Frontend within NPM and point it to the URL you plan to use. The next several steps will enable the Proxy Host to successfully navigate to the API (on port 3456).
|
||||
2. Verify that the page will pull up in your browser. (Do not bother trying to log in. It won't work. Trust me.)
|
||||
3. Now, we'll work with the NPM container, so you need to identify the container name for your NPM installation. e.g. NGINX-PM
|
||||
4. From the command line, enter `sudo docker exec -it [NGINX-PM container name] /bin/bash` and navigate to the proxy hosts folder where the `.conf` files are stashed. Probably `/data/nginx/proxy_host`. (This folder is a persistent folder created in the NPM container and mounted by NPM.)
|
||||
5. Locate the `.conf` file where the server_name inside the file matches your Vikunja Proxy Host. Once found, add the following code, unchanged, just above the existing location block in that file. (They are listed by number, not name.)
|
||||
```nginx
|
||||
location ~* ^/(api|dav|\.well-known)/ {
|
||||
proxy_pass http://api:3456;
|
||||
client_max_body_size 20M;
|
||||
}
|
||||
```
|
||||
6. After saving the edited file, return to NPM's UI browser window and refresh the page to verify your Proxy Host for Vikunja is still online.
|
||||
7. Now, switch over to your Vikunja browser window and hit refresh. If you configured your URL correctly in original Vikunja container, you should be all set and the browser will correctly show Vikunja. If not, you'll need to adjust the address in the top of the login subscreen to match your proxy address.
|
||||
|
||||
## Apache
|
||||
|
||||
Put the following config in `cat /etc/apache2/sites-available/vikunja.conf`:
|
||||
|
||||
{{< highlight aconf >}}
|
||||
<VirtualHost *:80>
|
||||
ServerName localhost
|
||||
|
||||
<Proxy *>
|
||||
Order Deny,Allow
|
||||
Allow from all
|
||||
</Proxy>
|
||||
ProxyPass /api http://localhost:3456/api
|
||||
ProxyPassReverse /api http://localhost:3456/api
|
||||
ProxyPass /dav http://localhost:3456/dav
|
||||
ProxyPassReverse /dav http://localhost:3456/dav
|
||||
ProxyPass /.well-known http://localhost:3456/.well-known
|
||||
ProxyPassReverse /.well-known http://localhost:3456/.well-known
|
||||
|
||||
DocumentRoot /var/www/html
|
||||
RewriteEngine On
|
||||
RewriteRule ^\/?(favicon\.ico|assets|audio|fonts|images|manifest\.webmanifest|robots\.txt|sw\.js|workbox-.*|api|dav|\.well-known) - [L]
|
||||
RewriteRule ^(.*)$ /index.html [QSA,L]
|
||||
</VirtualHost>
|
||||
{{< /highlight >}}
|
||||
|
||||
**Note:** The apache modules `proxy`, `proxy_http` and `rewrite` must be enabled for this.
|
||||
|
||||
For more details see the [frontend apache configuration]({{< ref "install-frontend.md#apache">}}).
|
||||
|
||||
## Caddy
|
||||
|
||||
{{< highlight conf >}}
|
||||
vikunja.domainname.tld {
|
||||
@paths {
|
||||
path /api/* /.well-known/* /dav/*
|
||||
}
|
||||
handle @paths {
|
||||
reverse_proxy 127.0.0.1:3456
|
||||
}
|
||||
|
||||
handle {
|
||||
encode zstd gzip
|
||||
root * /var/www/html/vikunja
|
||||
try_files {path} index.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Reverse Proxy"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# Setup behind a reverse proxy which also serves the frontend
|
||||
|
||||
These examples assume you have an instance of the backend running on your server listening on port `3456`.
|
||||
If you've changed this setting, you need to update the server configurations accordingly.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## NGINX
|
||||
|
||||
Below are two example configurations which you can put in your `nginx.conf`:
|
||||
|
||||
You may need to adjust `server_name` and `root` accordingly.
|
||||
|
||||
### with gzip enabled (recommended)
|
||||
|
||||
{{< highlight conf >}}
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /path/to/vikunja/static/frontend/files;
|
||||
try_files $uri $uri/ /;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location ~* ^/(api|dav|\.well-known)/ {
|
||||
proxy_pass http://localhost:3456;
|
||||
client_max_body_size 20M;
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> If you change the max upload size in Vikunja's settings, you'll need to also change the <code>client_max_body_size</code> in the nginx proxy config.
|
||||
</div>
|
||||
|
||||
### without gzip
|
||||
|
||||
{{< highlight conf >}}
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /path/to/vikunja/static/frontend/files;
|
||||
try_files $uri $uri/ /;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location ~* ^/(api|dav|\.well-known)/ {
|
||||
proxy_pass http://localhost:3456;
|
||||
client_max_body_size 20M;
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> If you change the max upload size in Vikunja's settings, you'll need to also change the <code>client_max_body_size</code> in the nginx proxy config.
|
||||
</div>
|
||||
|
||||
## NGINX Proxy Manager (NPM)
|
||||
|
||||
### Method 1
|
||||
|
||||
Following the [Docker Walkthrough]({{< ref "docker-start-to-finish.md" >}}) guide, you should be able to get Vikunja to work via HTTP connection to your server IP.
|
||||
|
||||
From there, all you have to do is adjust the following things:
|
||||
|
||||
#### In `docker-compose.yml`
|
||||
|
||||
Under `api:`,
|
||||
|
||||
1. Change `VIKUNJA_SERVICE_FRONTENDURL:` to your desired domain with `https://` and `/`.
|
||||
|
||||
2. Expose your desired port on host under `ports:`.
|
||||
|
||||
example:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
image: vikunja/api
|
||||
environment:
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_SERVICE_JWTSECRET: <your-random-secret>
|
||||
VIKUNJA_SERVICE_FRONTENDURL: https://vikunja.your-domain.com/ # change vikunja.your-domain.com to your desired domain/subdomain.
|
||||
ports:
|
||||
- 3456:3456 # Change 3456 on the left to the port of your choice.
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Under `frontend:`,
|
||||
|
||||
1. Add `VIKUNJA_API_URL:` under `environment:` and input your desired `API` domain with `https://` and `/api/v1/`. The `API` domain should be different from the one in `VIKUNJA_SERVICE_FRONTENDURL:`.
|
||||
|
||||
example:
|
||||
|
||||
```yaml
|
||||
frontend:
|
||||
image: vikunja/frontend
|
||||
environment:
|
||||
VIKUNJA_API_URL: https://api.your-domain.com/api/v1/ # change api.your-domain.com to your desired domain/subdomain, it should be different from your frontend domain
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Under `proxy:`,
|
||||
|
||||
1. Since we'll be using Nginx Proxy Manager, it should by default uses the port `80` and thus you should change `ports:` to expose another port not occupied by any service.
|
||||
|
||||
example:
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
image: nginx
|
||||
ports:
|
||||
- 1078:80 # change the number infront (host port) to whatever you desire, but make sure it's not 80 which will be used by Nginx Proxy Manager
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- api
|
||||
- frontend
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
#### In your DNS provider
|
||||
|
||||
Add two `A` records that points to your server IP.
|
||||
|
||||
1. `vikunja` for accessing the frontend
|
||||
|
||||
2. `api` for accessing the api
|
||||
|
||||
You are of course free to change them to whatever domain/subdomain you desire and modify the `docker-compose.yml` accordingly but the two should be different.
|
||||
|
||||
(Tested on Cloudflare DNS. Settings are different for different DNS provider, in this case the end result should bei `vikunja.your-domain.com` and `api.your-domain.com` respectively.)
|
||||
|
||||
#### In Nginx Proxy Manager
|
||||
|
||||
Add two Proxy Host as you normally would, and you don't have to add anything extra in Advanced.
|
||||
|
||||
##### Frontend
|
||||
|
||||
Under `Details`:
|
||||
|
||||
```
|
||||
Domain Names:
|
||||
vikunja.your-domain.com
|
||||
Scheme:
|
||||
http
|
||||
Forward Hostname/IP:
|
||||
your-server-ip
|
||||
Forward Port:
|
||||
1078
|
||||
Cached Assets:
|
||||
Optional.
|
||||
Block Common Exploits:
|
||||
Toggled.
|
||||
Websockets Support:
|
||||
Toggled.
|
||||
```
|
||||
|
||||
Under `SSL`:
|
||||
|
||||
```
|
||||
SSL Certificate:
|
||||
However you prefer.
|
||||
Force SSL:
|
||||
Toggled.
|
||||
HTTP/2 Support:
|
||||
Toggled.
|
||||
HSTS Enabled:
|
||||
Toggled.
|
||||
HSTS Subdomains:
|
||||
Toggled.
|
||||
Use a DNS Challenge:
|
||||
Not toggled.
|
||||
Email Address for Let's Encrypt:
|
||||
your-email@email.com
|
||||
```
|
||||
|
||||
##### API
|
||||
|
||||
Under `Details`:
|
||||
|
||||
```
|
||||
Domain Names:
|
||||
api.your-domain.com
|
||||
Scheme:
|
||||
http
|
||||
Forward Hostname/IP:
|
||||
your-server-ip
|
||||
Forward Port:
|
||||
3456
|
||||
Cached Assets:
|
||||
Optional.
|
||||
Block Common Exploits:
|
||||
Toggled.
|
||||
Websockets Support:
|
||||
Toggled.
|
||||
```
|
||||
|
||||
Under `SSL`:
|
||||
|
||||
```
|
||||
SSL Certificate:
|
||||
However you prefer.
|
||||
Force SSL:
|
||||
Toggled.
|
||||
HTTP/2 Support:
|
||||
Toggled.
|
||||
HSTS Enabled:
|
||||
Toggled.
|
||||
HSTS Subdomains:
|
||||
Toggled.
|
||||
Use a DNS Challenge:
|
||||
Not toggled.
|
||||
Email Address for Let's Encrypt:
|
||||
your-email@email.com
|
||||
```
|
||||
|
||||
Your Vikunja service should now work and your HTTPS frontend should be able to reach the API after `docker-compose`.
|
||||
|
||||
### Method 2
|
||||
|
||||
1. Create a standard Proxy Host for the Vikunja Frontend within NPM and point it to the URL you plan to use. The next several steps will enable the Proxy Host to successfully navigate to the API (on port 3456).
|
||||
2. Verify that the page will pull up in your browser. (Do not bother trying to log in. It won't work. Trust me.)
|
||||
3. Now, we'll work with the NPM container, so you need to identify the container name for your NPM installation. e.g. NGINX-PM
|
||||
4. From the command line, enter `sudo docker exec -it [NGINX-PM container name] /bin/bash` and navigate to the proxy hosts folder where the `.conf` files are stashed. Probably `/data/nginx/proxy_host`. (This folder is a persistent folder created in the NPM container and mounted by NPM.)
|
||||
5. Locate the `.conf` file where the server_name inside the file matches your Vikunja Proxy Host. Once found, add the following code, unchanged, just above the existing location block in that file. (They are listed by number, not name.)
|
||||
```nginx
|
||||
location ~* ^/(api|dav|\.well-known)/ {
|
||||
proxy_pass http://api:3456;
|
||||
client_max_body_size 20M;
|
||||
}
|
||||
```
|
||||
6. After saving the edited file, return to NPM's UI browser window and refresh the page to verify your Proxy Host for Vikunja is still online.
|
||||
7. Now, switch over to your Vikunja browser window and hit refresh. If you configured your URL correctly in original Vikunja container, you should be all set and the browser will correctly show Vikunja. If not, you'll need to adjust the address in the top of the login subscreen to match your proxy address.
|
||||
|
||||
## Apache
|
||||
|
||||
Put the following config in `cat /etc/apache2/sites-available/vikunja.conf`:
|
||||
|
||||
{{< highlight aconf >}}
|
||||
<VirtualHost *:80>
|
||||
ServerName localhost
|
||||
|
||||
<Proxy *>
|
||||
Order Deny,Allow
|
||||
Allow from all
|
||||
</Proxy>
|
||||
ProxyPass /api http://localhost:3456/api
|
||||
ProxyPassReverse /api http://localhost:3456/api
|
||||
ProxyPass /dav http://localhost:3456/dav
|
||||
ProxyPassReverse /dav http://localhost:3456/dav
|
||||
ProxyPass /.well-known http://localhost:3456/.well-known
|
||||
ProxyPassReverse /.well-known http://localhost:3456/.well-known
|
||||
|
||||
DocumentRoot /var/www/html
|
||||
RewriteEngine On
|
||||
RewriteRule ^\/?(favicon\.ico|assets|audio|fonts|images|manifest\.webmanifest|robots\.txt|sw\.js|workbox-.*|api|dav|\.well-known) - [L]
|
||||
RewriteRule ^(.*)$ /index.html [QSA,L]
|
||||
</VirtualHost>
|
||||
{{< /highlight >}}
|
||||
|
||||
**Note:** The apache modules `proxy`, `proxy_http` and `rewrite` must be enabled for this.
|
||||
|
||||
For more details see the [frontend apache configuration]({{< ref "install-frontend.md#apache">}}).
|
||||
|
||||
## Caddy
|
||||
|
||||
{{< highlight conf >}}
|
||||
vikunja.domainname.tld {
|
||||
@paths {
|
||||
path /api/* /.well-known/* /dav/*
|
||||
}
|
||||
handle @paths {
|
||||
reverse_proxy 127.0.0.1:3456
|
||||
}
|
||||
|
||||
handle {
|
||||
encode zstd gzip
|
||||
root * /var/www/html/vikunja
|
||||
try_files {path} index.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
23
docs/content/doc/setup/typesense.md
Normal file
23
docs/content/doc/setup/typesense.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: "Typesense"
|
||||
date: 2023-09-29T12:23:55+02:00
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# Use Typesense for enhanced search capabilities
|
||||
|
||||
Vikunja supports using [Typesense](https://typesense.org/) for a better search experience.
|
||||
Typesense allows fast fulltext search including fuzzy matching support.
|
||||
It may return different results than what you'd get with a database-only search, but generally, the results are more relevant to what you're looking for.
|
||||
|
||||
This document explains how to set up and use Typesense with Vikunja.
|
||||
|
||||
## Setup
|
||||
|
||||
1. First, install Typesense on your system. Refer to [their documentation](https://typesense.org/docs/guide/install-typesense.html) for specific instructions.
|
||||
2. Once Typesense is available on your system and reachable by Vikunja, add the relevant configuration keys to your Vikunja config. [Check out the docs article about this]({{< ref "config.md#typesense">}}).
|
||||
3. Index all tasks currently in Vikunja. To do that, run the `vikunja index` command with the api binary. This may take a while, depending on the size of your instance.
|
||||
4. Restart the api. From now on, all task changes will be automatically indexed in Typesense.
|
43
docs/content/doc/usage/n8n.md
Normal file
43
docs/content/doc/usage/n8n.md
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
title: "n8n"
|
||||
date: 2023-10-24T19:31:35+02:00
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
---
|
||||
|
||||
# Using Vikunja with n8n
|
||||
|
||||
Vikunja maintains a [community node](https://github.com/go-vikunja/n8n-vikunja-nodes) for [n8n](https://n8n.io),
|
||||
allowing you to easily integrate Vikunja with all kinds of other tools and services.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Installation
|
||||
|
||||
To install the node in your n8n installation:
|
||||
|
||||
1. In your n8n instance, go to **Settings > Community Nodes**.
|
||||
2. Select Install.
|
||||
3. Enter `n8n-nodes-vikunja` as the npm Package Name
|
||||
4. Agree to the risks of using community nodes: select I understand the risks of installing unverified code from a
|
||||
public source.
|
||||
5. Select Install. n8n installs the node, and returns to the Community Nodes list in Settings.
|
||||
6. Vikunja actions and triggers are now available in n8n.
|
||||
|
||||
[Official n8n docs about the installation](https://docs.n8n.io/integrations/community-nodes/installation/)
|
||||
|
||||
## Authentication
|
||||
|
||||
To authenticate your automation against Vikunja:
|
||||
|
||||
1. In Vikunja, go to **Settings > API Tokens** and create a new token. Use all scopes for the kind of task you want to
|
||||
do. \
|
||||
*Note:* If you want to use the webhook trigger node, the api token should have permissions to create, read and delete
|
||||
webhooks.
|
||||
2. Now in n8n, go to **Credentials** and then click on **Add Credential**.
|
||||
3. Search for `Vikunja API` and click *Continue*
|
||||
4. Enter the API key you created in step 1.
|
||||
5. Enter the API URL of your Vikunja instance, with `/api/v1` suffix.
|
||||
6. When you now create a Vikunja node, select the created credentials.
|
58
docs/content/doc/usage/webhooks.md
Normal file
58
docs/content/doc/usage/webhooks.md
Normal file
@ -0,0 +1,58 @@
|
||||
---
|
||||
title: "Webhooks"
|
||||
date: 2023-10-17T19:51:32+02:00
|
||||
draft: false
|
||||
type: doc
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
---
|
||||
|
||||
# Webhooks
|
||||
|
||||
Starting with version 0.22.0, Vikunja allows you to define webhooks to notify other services of events happening within Vikunja.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## How to create webhooks
|
||||
|
||||
To create a webhook, in the project options select "Webhooks". The form will allow you to create and modify webhooks.
|
||||
|
||||
Check out [the api docs](https://try.vikunja.io/api/v1/docs#tag/webhooks) for information about how to create webhooks programatically.
|
||||
|
||||
## Available events and their payload
|
||||
|
||||
All events registered as webhook events in [the event listeners definition](https://kolaente.dev/vikunja/api/src/branch/main/pkg/models/listeners.go#L69) can be used as webhook target.
|
||||
|
||||
A webhook payload will look similar to this:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_name": "task.created",
|
||||
"time": "2023-10-17T19:39:32.924194436+02:00",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
The `data` property will contain the raw event data as it was registered in the `listeners.go` file.
|
||||
|
||||
The `time` property holds the time when the webhook payload data was sent.
|
||||
It always uses the ISO 8601 format with date, time and time zone offset.
|
||||
|
||||
## Security considerations
|
||||
|
||||
### Signing
|
||||
|
||||
Vikunja allows you to provide a secret when creating the webhook.
|
||||
If you set a secret, all outgoing webhook requests will contain an `X-Vikunja-Signature` header with an HMAC signature over the webhook json payload.
|
||||
|
||||
Check out [webhooks.fyi](https://webhooks.fyi/security/hmac) for more information about how to validate the HMAC signature.
|
||||
|
||||
### Hosting webhook infrastructure
|
||||
|
||||
Vikunja has support to use [mole](https://github.com/frain-dev/mole) as a proxy for outgoing webhook requests.
|
||||
This allows you to prevent SSRF attacts on your own infrastructure.
|
||||
|
||||
You should use this and [configure it appropriately]({{< ref "../setup/config.md">}}#webhooks) if you're not the only one using your Vikunja instance.
|
||||
|
||||
Check out [webhooks.fyi](https://webhooks.fyi/best-practices/webhook-providers#implement-security-on-egress-communication) for more information about the attack vector and reasoning to prevent this.
|
83
go.mod
83
go.mod
@ -19,31 +19,31 @@ module code.vikunja.io/api
|
||||
require (
|
||||
code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3
|
||||
dario.cat/mergo v1.0.0
|
||||
github.com/ThreeDotsLabs/watermill v1.2.0
|
||||
github.com/ThreeDotsLabs/watermill v1.3.5
|
||||
github.com/adlio/trello v1.10.0
|
||||
github.com/arran4/golang-ical v0.1.0
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
|
||||
github.com/bbrks/go-blurhash v1.1.1
|
||||
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
|
||||
github.com/coreos/go-oidc/v3 v3.6.0
|
||||
github.com/coreos/go-oidc/v3 v3.7.0
|
||||
github.com/cweill/gotests v1.6.0
|
||||
github.com/d4l3k/messagediff v1.2.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49
|
||||
github.com/gabriel-vasile/mimetype v1.4.2
|
||||
github.com/getsentry/sentry-go v0.23.0
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/getsentry/sentry-go v0.25.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.9.0
|
||||
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6
|
||||
github.com/labstack/echo-jwt/v4 v4.2.0
|
||||
github.com/labstack/echo/v4 v4.11.1
|
||||
github.com/labstack/echo/v4 v4.11.2
|
||||
github.com/labstack/gommon v0.4.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/magefile/mage v1.15.0
|
||||
@ -51,33 +51,32 @@ require (
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/redis/go-redis/v9 v9.0.5
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/redis/go-redis/v9 v9.2.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/spf13/afero v1.9.5
|
||||
github.com/spf13/afero v1.10.0
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/spf13/viper v1.17.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/swaggo/swag v1.8.12
|
||||
github.com/swaggo/swag v1.16.2
|
||||
github.com/tkuchiki/go-timezone v0.2.2
|
||||
github.com/typesense/typesense-go v0.8.0
|
||||
github.com/ulule/limiter/v3 v3.11.2
|
||||
github.com/wneessen/go-mail v0.4.0
|
||||
github.com/yuin/goldmark v1.5.4
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
|
||||
golang.org/x/image v0.11.0
|
||||
golang.org/x/oauth2 v0.10.0
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/sys v0.11.0
|
||||
golang.org/x/term v0.11.0
|
||||
github.com/yuin/goldmark v1.5.6
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/image v0.13.0
|
||||
golang.org/x/oauth2 v0.13.0
|
||||
golang.org/x/sync v0.4.0
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/term v0.13.0
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20230711181658-617d3b65dd40
|
||||
src.techknowlogick.com/xormigrate v1.5.0
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20231019133136-ecfba3dfed5d
|
||||
src.techknowlogick.com/xormigrate v1.7.0
|
||||
xorm.io/builder v0.3.13
|
||||
xorm.io/xorm v1.3.2
|
||||
xorm.io/xorm v1.3.4
|
||||
)
|
||||
|
||||
require (
|
||||
@ -97,11 +96,10 @@ require (
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deepmap/oapi-codegen v1.13.4 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||
@ -125,7 +123,7 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
@ -143,22 +141,24 @@ require (
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/onsi/gomega v1.16.0 // indirect
|
||||
github.com/paulmach/orb v0.9.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/sony/gobreaker v0.5.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
@ -168,18 +168,23 @@ require (
|
||||
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect
|
||||
go.opentelemetry.io/otel v1.15.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.15.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.2
|
||||
|
@ -412,7 +412,7 @@ func checkGolangCiLintInstalled() {
|
||||
mg.Deps(initVars)
|
||||
if err := exec.Command("golangci-lint").Run(); err != nil && strings.Contains(err.Error(), "executable file not found") {
|
||||
fmt.Println("Please manually install golangci-lint by running")
|
||||
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.2")
|
||||
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ var (
|
||||
userFlagEnableUser bool
|
||||
userFlagDisableUser bool
|
||||
userFlagDeleteNow bool
|
||||
userFlagDeleteConfirm bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -73,6 +74,9 @@ func init() {
|
||||
// User deletion flags
|
||||
userDeleteCmd.Flags().BoolVarP(&userFlagDeleteNow, "now", "n", false, "If provided, deletes the user immediately instead of sending them an email first.")
|
||||
|
||||
// Bypass confirm prompt
|
||||
userDeleteCmd.Flags().BoolVarP(&userFlagDeleteConfirm, "confirm", "c", false, "Bypasses any prompts confirming the deletion request, use with caution!")
|
||||
|
||||
userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd, userDeleteCmd)
|
||||
rootCmd.AddCommand(userCmd)
|
||||
}
|
||||
@ -100,12 +104,16 @@ func getPasswordFromFlagOrInput() (pw string) {
|
||||
}
|
||||
|
||||
func getUserFromArg(s *xorm.Session, arg string) *user.User {
|
||||
filter := user.User{}
|
||||
id, err := strconv.ParseInt(arg, 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid user id: %s", err)
|
||||
log.Infof("Invalid user ID [%s], assuming username instead", arg)
|
||||
filter.Username = arg
|
||||
} else {
|
||||
filter.ID = id
|
||||
}
|
||||
|
||||
u, err := user.GetUserWithEmail(s, &user.User{ID: id})
|
||||
u, err := user.GetUserWithEmail(s, &filter)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not get user: %s", err)
|
||||
}
|
||||
@ -322,7 +330,7 @@ var userDeleteCmd = &cobra.Command{
|
||||
initialize.FullInit()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if userFlagDeleteNow {
|
||||
if userFlagDeleteNow && !userFlagDeleteConfirm {
|
||||
fmt.Println("You requested to delete the user immediately. Are you sure?")
|
||||
fmt.Println(`To confirm, please type "yes, I confirm" in all uppercase:`)
|
||||
|
||||
|
@ -19,7 +19,6 @@ package config
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@ -29,6 +28,7 @@ import (
|
||||
"time"
|
||||
_ "time/tzdata" // Imports time zone data instead of relying on the os
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@ -172,6 +172,11 @@ const (
|
||||
DefaultSettingsLanguage Key = `defaultsettings.language`
|
||||
DefaultSettingsTimezone Key = `defaultsettings.timezone`
|
||||
DefaultSettingsOverdueTaskRemindersTime Key = `defaultsettings.overdue_tasks_reminders_time`
|
||||
|
||||
WebhooksEnabled Key = `webhooks.enabled`
|
||||
WebhooksTimeoutSeconds Key = `webhooks.timeoutseconds`
|
||||
WebhooksProxyURL Key = `webhooks.proxyurl`
|
||||
WebhooksProxyPassword Key = `webhooks.proxypassword`
|
||||
)
|
||||
|
||||
// GetString returns a string config value
|
||||
@ -387,6 +392,9 @@ func InitDefaultConfig() {
|
||||
DefaultSettingsAvatarProvider.setDefault("initials")
|
||||
DefaultSettingsOverdueTaskRemindersEnabled.setDefault(true)
|
||||
DefaultSettingsOverdueTaskRemindersTime.setDefault("9:00")
|
||||
// Webhook
|
||||
WebhooksEnabled.setDefault(true)
|
||||
WebhooksTimeoutSeconds.setDefault(30)
|
||||
}
|
||||
|
||||
// InitConfig initializes the config, sets defaults etc.
|
||||
@ -400,13 +408,17 @@ func InitConfig() {
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// Just load environment variables
|
||||
_ = viper.ReadInConfig()
|
||||
log.ConfigLogger(LogEnabled.GetBool(), LogStandard.GetString(), LogPath.GetString(), LogLevel.GetString())
|
||||
|
||||
// Load the config file
|
||||
viper.AddConfigPath(ServiceRootpath.GetString())
|
||||
viper.AddConfigPath("/etc/vikunja/")
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Printf("No home directory found, not using config from ~/.config/vikunja/. Error was: %s\n", err.Error())
|
||||
log.Debugf("No home directory found, not using config from ~/.config/vikunja/. Error was: %s\n", err.Error())
|
||||
} else {
|
||||
viper.AddConfigPath(path.Join(homeDir, ".config", "vikunja"))
|
||||
}
|
||||
@ -415,15 +427,18 @@ func InitConfig() {
|
||||
viper.SetConfigName("config")
|
||||
|
||||
err = viper.ReadInConfig()
|
||||
|
||||
if viper.ConfigFileUsed() != "" {
|
||||
log.Printf("Using config file: %s", viper.ConfigFileUsed())
|
||||
log.Infof("Using config file: %s", viper.ConfigFileUsed())
|
||||
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
log.Println("Using default config.")
|
||||
log.Warning(err.Error())
|
||||
log.Warning("Using default config.")
|
||||
} else {
|
||||
log.ConfigLogger(LogEnabled.GetBool(), LogStandard.GetString(), LogPath.GetString(), LogLevel.GetString())
|
||||
}
|
||||
} else {
|
||||
log.Println("No config file found, using default or config from environment variables.")
|
||||
log.Info("No config file found, using default or config from environment variables.")
|
||||
}
|
||||
|
||||
if RateLimitStore.GetString() == "keyvalue" {
|
||||
@ -455,7 +470,7 @@ func InitConfig() {
|
||||
}
|
||||
|
||||
if ServiceEnableMetrics.GetBool() {
|
||||
log.Println("WARNING: service.enablemetrics is deprecated and will be removed in a future release. Please use metrics.enable.")
|
||||
log.Warning("service.enablemetrics is deprecated and will be removed in a future release. Please use metrics.enable.")
|
||||
MetricsEnabled.Set(true)
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ func CreateDBEngine() (engine *xorm.Engine, err error) {
|
||||
}
|
||||
engine.SetTZDatabase(loc)
|
||||
engine.SetMapper(names.GonicMapper{})
|
||||
logger := log.NewXormLogger("")
|
||||
logger := log.NewXormLogger(config.LogEnabled.GetBool(), config.LogDatabase.GetString(), config.LogDatabaseLevel.GetString())
|
||||
engine.SetLogger(logger)
|
||||
|
||||
x = engine
|
||||
|
@ -52,7 +52,7 @@ func CreateTestEngine() (engine *xorm.Engine, err error) {
|
||||
}
|
||||
|
||||
engine.SetMapper(names.GonicMapper{})
|
||||
logger := log.NewXormLogger("DEBUG")
|
||||
logger := log.NewXormLogger(config.LogEnabled.GetBool(), config.LogDatabase.GetString(), "DEBUG")
|
||||
logger.ShowSQL(os.Getenv("UNIT_TESTS_VERBOSE") == "1")
|
||||
engine.SetLogger(logger)
|
||||
engine.SetTZLocation(config.GetTimeZone())
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
vmetrics "code.vikunja.io/api/pkg/metrics"
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
@ -39,7 +40,7 @@ type Event interface {
|
||||
|
||||
// InitEvents sets up everything needed to work with events
|
||||
func InitEvents() (err error) {
|
||||
logger := log.NewWatermillLogger()
|
||||
logger := log.NewWatermillLogger(config.LogEnabled.GetBool(), config.LogEvents.GetString(), config.LogEventsLevel.GetString())
|
||||
|
||||
router, err := message.NewRouter(
|
||||
message.RouterConfig{},
|
||||
|
@ -35,6 +35,9 @@ import (
|
||||
|
||||
// LightInit will only fullInit config, redis, logger but no db connection.
|
||||
func LightInit() {
|
||||
// Set logger
|
||||
log.InitLogger()
|
||||
|
||||
// Init the config
|
||||
config.InitConfig()
|
||||
|
||||
@ -43,9 +46,6 @@ func LightInit() {
|
||||
|
||||
// Init keyvalue store
|
||||
keyvalue.InitStorage()
|
||||
|
||||
// Set logger
|
||||
log.InitLogger()
|
||||
}
|
||||
|
||||
// InitEngines intializes all db connections
|
||||
|
@ -22,9 +22,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"github.com/op/go-logging"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// ErrFmt holds the format for all the console logging
|
||||
@ -41,42 +39,45 @@ const logModule = `vikunja`
|
||||
// loginstance is the instance of the logger which is used under the hood to log
|
||||
var logInstance = logging.MustGetLogger(logModule)
|
||||
|
||||
// logpath is the path in which log files will be written.
|
||||
// This value is a mere fallback for other modules that could but shouldn't be used before calling ConfigLogger
|
||||
var logPath = "."
|
||||
|
||||
// InitLogger initializes the global log handler
|
||||
func InitLogger() {
|
||||
if !config.LogEnabled.GetBool() {
|
||||
// Disable all logging when loggin in general is disabled, overwriting everything a user might have set.
|
||||
config.LogStandard.Set("off")
|
||||
config.LogDatabase.Set("off")
|
||||
config.LogHTTP.Set("off")
|
||||
config.LogEcho.Set("off")
|
||||
config.LogEvents.Set("off")
|
||||
return
|
||||
}
|
||||
|
||||
// This show correct caller functions
|
||||
logInstance.ExtraCalldepth = 1
|
||||
|
||||
if config.LogStandard.GetString() == "file" {
|
||||
err := os.Mkdir(config.LogPath.GetString(), 0744)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
Fatalf("Could not create log folder: %s", err.Error())
|
||||
}
|
||||
// Init with stdout and INFO as default format and level
|
||||
logBackend := logging.NewLogBackend(os.Stdout, "", 0)
|
||||
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(Fmt+"\n"))
|
||||
|
||||
backendLeveled := logging.AddModuleLevel(backend)
|
||||
backendLeveled.SetLevel(logging.INFO, logModule)
|
||||
|
||||
logInstance.SetBackend(backendLeveled)
|
||||
}
|
||||
|
||||
// ConfigLogger configures the global log handler
|
||||
func ConfigLogger(configLogEnabled bool, configLogStandard string, configLogPath string, configLogLevel string) {
|
||||
lvl := strings.ToUpper(configLogLevel)
|
||||
level, err := logging.LogLevel(lvl)
|
||||
if err != nil {
|
||||
Fatalf("Error setting standard log level %s: %s", lvl, err.Error())
|
||||
}
|
||||
|
||||
logPath = configLogPath
|
||||
|
||||
// The backend is the part which actually handles logging the log entries somewhere.
|
||||
cf := config.LogStandard.GetString()
|
||||
var backend logging.Backend
|
||||
backend = &NoopBackend{}
|
||||
if cf != "off" && cf != "false" {
|
||||
stdWriter := GetLogWriter("standard")
|
||||
|
||||
logBackend := logging.NewLogBackend(stdWriter, "", 0)
|
||||
backend = logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(Fmt+"\n"))
|
||||
if configLogStandard == "false" {
|
||||
configLogStandard = "off"
|
||||
Warning("log.standard value 'false' is deprecated and will be removed in a future release. Please use the value 'off'.")
|
||||
}
|
||||
|
||||
level, err := logging.LogLevel(strings.ToUpper(config.LogLevel.GetString()))
|
||||
if err != nil {
|
||||
Fatalf("Error setting database log level: %s", err.Error())
|
||||
if configLogEnabled && configLogStandard != "off" {
|
||||
logBackend := logging.NewLogBackend(GetLogWriter(configLogStandard, "standard"), "", 0)
|
||||
backend = logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(Fmt+"\n"))
|
||||
}
|
||||
|
||||
backendLeveled := logging.AddModuleLevel(backend)
|
||||
@ -86,11 +87,14 @@ func InitLogger() {
|
||||
}
|
||||
|
||||
// GetLogWriter returns the writer to where the normal log goes, depending on the config
|
||||
func GetLogWriter(logfile string) (writer io.Writer) {
|
||||
func GetLogWriter(logfmt string, logfile string) (writer io.Writer) {
|
||||
writer = os.Stdout // Set the default case to prevent nil pointer panics
|
||||
switch viper.GetString("log." + logfile) {
|
||||
switch logfmt {
|
||||
case "file":
|
||||
fullLogFilePath := config.LogPath.GetString() + "/" + logfile + ".log"
|
||||
if err := os.MkdirAll(logPath, 0744); err != nil {
|
||||
Fatalf("Could not create log path: %s", err.Error())
|
||||
}
|
||||
fullLogFilePath := logPath + "/" + logfile + ".log"
|
||||
f, err := os.OpenFile(fullLogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
Fatalf("Could not create logfile %s: %s", fullLogFilePath, err.Error())
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"github.com/op/go-logging"
|
||||
"xorm.io/xorm/log"
|
||||
)
|
||||
@ -33,19 +32,24 @@ type MailLogger struct {
|
||||
const mailFormat = `%{color}%{time:` + time.RFC3339Nano + `}: %{level}` + "\t" + `▶ [MAIL] %{id:03x}%{color:reset} %{message}`
|
||||
const mailLogModule = `vikunja_mail`
|
||||
|
||||
func NewMailLogger() *MailLogger {
|
||||
lvl := strings.ToUpper(config.LogMailLevel.GetString())
|
||||
// NewMailLogger creates and initializes a new mail logger
|
||||
func NewMailLogger(configLogEnabled bool, configLogMail string, configLogMailLevel string) *MailLogger {
|
||||
lvl := strings.ToUpper(configLogMailLevel)
|
||||
level, err := logging.LogLevel(lvl)
|
||||
if err != nil {
|
||||
Criticalf("Error setting database log level: %s", err.Error())
|
||||
Criticalf("Error setting mail log level %s: %s", lvl, err.Error())
|
||||
}
|
||||
|
||||
mailLogger := &MailLogger{
|
||||
logger: logging.MustGetLogger(mailLogModule),
|
||||
}
|
||||
|
||||
logBackend := logging.NewLogBackend(GetLogWriter("mail"), "", 0)
|
||||
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(mailFormat+"\n"))
|
||||
var backend logging.Backend
|
||||
backend = &NoopBackend{}
|
||||
if configLogEnabled && configLogMail != "off" {
|
||||
logBackend := logging.NewLogBackend(GetLogWriter(configLogMail, "mail"), "", 0)
|
||||
backend = logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(mailFormat+"\n"))
|
||||
}
|
||||
|
||||
backendLeveled := logging.AddModuleLevel(backend)
|
||||
backendLeveled.SetLevel(level, mailLogModule)
|
||||
|
@ -21,7 +21,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
@ -34,8 +33,9 @@ type WatermillLogger struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
func NewWatermillLogger() *WatermillLogger {
|
||||
lvl := strings.ToUpper(config.LogEventsLevel.GetString())
|
||||
// NewXormLogger creates and initializes a new watermill logger
|
||||
func NewWatermillLogger(configLogEnabled bool, configLogEvents string, configLogEventsLevel string) *WatermillLogger {
|
||||
lvl := strings.ToUpper(configLogEventsLevel)
|
||||
level, err := logging.LogLevel(lvl)
|
||||
if err != nil {
|
||||
Criticalf("Error setting events log level %s: %s", lvl, err.Error())
|
||||
@ -45,11 +45,14 @@ func NewWatermillLogger() *WatermillLogger {
|
||||
logger: logging.MustGetLogger(watermillLogModule),
|
||||
}
|
||||
|
||||
cf := config.LogEvents.GetString()
|
||||
var backend logging.Backend
|
||||
backend = &NoopBackend{}
|
||||
if cf != "off" && cf != "false" {
|
||||
logBackend := logging.NewLogBackend(GetLogWriter("events"), "", 0)
|
||||
if configLogEvents == "false" {
|
||||
configLogEvents = "off"
|
||||
Warning("log.events value 'false' is deprecated and will be removed in a future release. Please use the value 'off'.")
|
||||
}
|
||||
if configLogEnabled && configLogEvents != "off" {
|
||||
logBackend := logging.NewLogBackend(GetLogWriter(configLogEvents, "events"), "", 0)
|
||||
backend = logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(watermillFmt+"\n"))
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"github.com/op/go-logging"
|
||||
"xorm.io/xorm/log"
|
||||
)
|
||||
@ -38,21 +37,23 @@ type XormLogger struct {
|
||||
}
|
||||
|
||||
// NewXormLogger creates and initializes a new xorm logger
|
||||
func NewXormLogger(lvl string) *XormLogger {
|
||||
if lvl == "" {
|
||||
lvl = strings.ToUpper(config.LogDatabaseLevel.GetString())
|
||||
}
|
||||
func NewXormLogger(configLogEnabled bool, configLogDatabase string, configLogDatabaseLevel string) *XormLogger {
|
||||
lvl := strings.ToUpper(configLogDatabaseLevel)
|
||||
level, err := logging.LogLevel(lvl)
|
||||
if err != nil {
|
||||
Criticalf("Error setting database log level: %s", err.Error())
|
||||
Criticalf("Error setting database log level %s: %s", lvl, err.Error())
|
||||
}
|
||||
|
||||
xormLogger := &XormLogger{
|
||||
logger: logging.MustGetLogger(xormLogModule),
|
||||
}
|
||||
|
||||
logBackend := logging.NewLogBackend(GetLogWriter("database"), "", 0)
|
||||
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(XormFmt+"\n"))
|
||||
var backend logging.Backend
|
||||
backend = &NoopBackend{}
|
||||
if configLogEnabled && configLogDatabase != "off" {
|
||||
logBackend := logging.NewLogBackend(GetLogWriter(configLogDatabase, "database"), "", 0)
|
||||
backend = logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(XormFmt+"\n"))
|
||||
}
|
||||
|
||||
backendLeveled := logging.AddModuleLevel(backend)
|
||||
backendLeveled.SetLevel(level, xormLogModule)
|
||||
@ -64,8 +65,8 @@ func NewXormLogger(lvl string) *XormLogger {
|
||||
case logging.ERROR:
|
||||
xormLogger.level = log.LOG_ERR
|
||||
case logging.WARNING:
|
||||
case logging.NOTICE:
|
||||
xormLogger.level = log.LOG_WARNING
|
||||
case logging.NOTICE:
|
||||
case logging.INFO:
|
||||
xormLogger.level = log.LOG_INFO
|
||||
case logging.DEBUG:
|
||||
|
@ -56,7 +56,7 @@ func getClient() (*mail.Client, error) {
|
||||
ServerName: config.MailerHost.GetString(),
|
||||
}),
|
||||
mail.WithTimeout((config.MailerQueueTimeout.GetDuration() + 3) * time.Second), // 3s more for us to close before mail server timeout
|
||||
mail.WithLogger(log.NewMailLogger()),
|
||||
mail.WithLogger(log.NewMailLogger(config.LogEnabled.GetBool(), config.LogMail.GetString(), config.LogMailLevel.GetString())),
|
||||
mail.WithDebugLog(),
|
||||
}
|
||||
|
||||
|
52
pkg/migration/20230913202615.go
Normal file
52
pkg/migration/20230913202615.go
Normal file
@ -0,0 +1,52 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type webhooks20230913202615 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"webhook"`
|
||||
TargetURL string `xorm:"not null" valid:"minstringlength(1)" minLength:"1" json:"target_url"`
|
||||
Events []string `xorm:"JSON not null" valid:"minstringlength(1)" minLength:"1" json:"event"`
|
||||
ProjectID int64 `xorm:"bigint not null index" json:"project_id" param:"project"`
|
||||
Secret string `xorm:"null" json:"secret"`
|
||||
CreatedByID int64 `xorm:"bigint not null" json:"-"`
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
}
|
||||
|
||||
func (webhooks20230913202615) TableName() string {
|
||||
return "webhooks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20230913202615",
|
||||
Description: "",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(webhooks20230913202615{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
96
pkg/migration/20231022144641.go
Normal file
96
pkg/migration/20231022144641.go
Normal file
@ -0,0 +1,96 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
templatehtml "html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func convertMarkdownToHTML(input string) (output string, err error) {
|
||||
md := []byte(templatehtml.HTMLEscapeString(input))
|
||||
var buf bytes.Buffer
|
||||
err = goldmark.Convert(md, &buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//#nosec - the html is escaped few lines before
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func convertDescription(tx *xorm.Engine, table string, column string) (err error) {
|
||||
items := []map[string]interface{}{}
|
||||
err = tx.Table(table).
|
||||
Select("id, " + column).
|
||||
Find(&items)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, task := range items {
|
||||
if task[column] == "" || strings.HasPrefix(task[column].(string), "<") {
|
||||
continue
|
||||
}
|
||||
|
||||
task[column], err = convertMarkdownToHTML(task[column].(string))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = tx.Where("id = ?", task["id"]).
|
||||
Table(table).
|
||||
Cols(column).
|
||||
Update(task)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20231022144641",
|
||||
Description: "Convert all descriptions to HTML",
|
||||
Migrate: func(tx *xorm.Engine) (err error) {
|
||||
|
||||
for _, table := range []string{
|
||||
"tasks",
|
||||
"labels",
|
||||
"projects",
|
||||
"saved_filters",
|
||||
"teams",
|
||||
} {
|
||||
err = convertDescription(tx, table, "description")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = convertDescription(tx, "task_comments", "comment")
|
||||
return
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
@ -56,7 +56,7 @@ func initMigration(x *xorm.Engine) *xormigrate.Xormigrate {
|
||||
})
|
||||
|
||||
m := xormigrate.New(x, migrations)
|
||||
logger := log.NewXormLogger("")
|
||||
logger := log.NewXormLogger(config.LogEnabled.GetBool(), config.LogEvents.GetString(), config.LogEventsLevel.GetString())
|
||||
m.SetLogger(logger)
|
||||
m.InitSchema(initSchema)
|
||||
return m
|
||||
|
@ -122,9 +122,9 @@ func HashToken(token, salt string) string {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param page query int false "The page number for tasks. Used for pagination. If not provided, the first page of results is returned."
|
||||
// @Param per_page query int false "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page."
|
||||
// @Param s query string false "Search tasks by task text."
|
||||
// @Param page query int false "The page number, used for pagination. If not provided, the first page of results is returned."
|
||||
// @Param per_page query int false "The maximum number of tokens per page. This parameter is limited by the configured maximum of items per page."
|
||||
// @Param s query string false "Search tokens by their title."
|
||||
// @Success 200 {array} models.APIToken "The list of all tokens"
|
||||
// @Failure 500 {object} models.Message "Internal server error"
|
||||
// @Router /tokens [get]
|
||||
|
@ -110,6 +110,17 @@ func (err ValidationHTTPError) Error() string {
|
||||
return theErr.Error()
|
||||
}
|
||||
|
||||
func InvalidFieldError(fields []string) error {
|
||||
return ValidationHTTPError{
|
||||
HTTPError: web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeInvalidData,
|
||||
Message: "Invalid Data",
|
||||
},
|
||||
InvalidFields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
// ===========
|
||||
// Project errors
|
||||
// ===========
|
||||
@ -405,7 +416,7 @@ func (err *ErrCannotArchiveDefaultProject) HTTPError() web.HTTPError {
|
||||
// Task errors
|
||||
// ==============
|
||||
|
||||
// ErrTaskCannotBeEmpty represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
|
||||
// ErrTaskCannotBeEmpty represents a "ErrTaskCannotBeEmpty" kind of error.
|
||||
type ErrTaskCannotBeEmpty struct{}
|
||||
|
||||
// IsErrTaskCannotBeEmpty checks if an error is a ErrProjectDoesNotExist.
|
||||
@ -415,7 +426,7 @@ func IsErrTaskCannotBeEmpty(err error) bool {
|
||||
}
|
||||
|
||||
func (err ErrTaskCannotBeEmpty) Error() string {
|
||||
return "Project task title cannot be empty."
|
||||
return "Task title cannot be empty."
|
||||
}
|
||||
|
||||
// ErrCodeTaskCannotBeEmpty holds the unique world-error code of this error
|
||||
@ -423,7 +434,7 @@ const ErrCodeTaskCannotBeEmpty = 4001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTaskCannotBeEmpty) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTaskCannotBeEmpty, Message: "You must provide at least a project task title."}
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTaskCannotBeEmpty, Message: "You must provide at least a task title."}
|
||||
}
|
||||
|
||||
// ErrTaskDoesNotExist represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
|
||||
|
@ -27,8 +27,8 @@ import (
|
||||
|
||||
// TaskCreatedEvent represents an event where a task has been created
|
||||
type TaskCreatedEvent struct {
|
||||
Task *Task
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskCreatedEvent
|
||||
@ -38,8 +38,8 @@ func (t *TaskCreatedEvent) Name() string {
|
||||
|
||||
// TaskUpdatedEvent represents an event where a task has been updated
|
||||
type TaskUpdatedEvent struct {
|
||||
Task *Task
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskUpdatedEvent
|
||||
@ -49,8 +49,8 @@ func (t *TaskUpdatedEvent) Name() string {
|
||||
|
||||
// TaskDeletedEvent represents a TaskDeletedEvent event
|
||||
type TaskDeletedEvent struct {
|
||||
Task *Task
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskDeletedEvent
|
||||
@ -60,9 +60,9 @@ func (t *TaskDeletedEvent) Name() string {
|
||||
|
||||
// TaskAssigneeCreatedEvent represents an event where a task has been assigned to a user
|
||||
type TaskAssigneeCreatedEvent struct {
|
||||
Task *Task
|
||||
Assignee *user.User
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Assignee *user.User `json:"assignee"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskAssigneeCreatedEvent
|
||||
@ -72,9 +72,9 @@ func (t *TaskAssigneeCreatedEvent) Name() string {
|
||||
|
||||
// TaskAssigneeDeletedEvent represents a TaskAssigneeDeletedEvent event
|
||||
type TaskAssigneeDeletedEvent struct {
|
||||
Task *Task
|
||||
Assignee *user.User
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Assignee *user.User `json:"assignee"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskAssigneeDeletedEvent
|
||||
@ -84,9 +84,9 @@ func (t *TaskAssigneeDeletedEvent) Name() string {
|
||||
|
||||
// TaskCommentCreatedEvent represents an event where a task comment has been created
|
||||
type TaskCommentCreatedEvent struct {
|
||||
Task *Task
|
||||
Comment *TaskComment
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Comment *TaskComment `json:"comment"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskCommentCreatedEvent
|
||||
@ -96,9 +96,9 @@ func (t *TaskCommentCreatedEvent) Name() string {
|
||||
|
||||
// TaskCommentUpdatedEvent represents a TaskCommentUpdatedEvent event
|
||||
type TaskCommentUpdatedEvent struct {
|
||||
Task *Task
|
||||
Comment *TaskComment
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Comment *TaskComment `json:"comment"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskCommentUpdatedEvent
|
||||
@ -108,9 +108,9 @@ func (t *TaskCommentUpdatedEvent) Name() string {
|
||||
|
||||
// TaskCommentDeletedEvent represents a TaskCommentDeletedEvent event
|
||||
type TaskCommentDeletedEvent struct {
|
||||
Task *Task
|
||||
Comment *TaskComment
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Comment *TaskComment `json:"comment"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskCommentDeletedEvent
|
||||
@ -120,9 +120,9 @@ func (t *TaskCommentDeletedEvent) Name() string {
|
||||
|
||||
// TaskAttachmentCreatedEvent represents a TaskAttachmentCreatedEvent event
|
||||
type TaskAttachmentCreatedEvent struct {
|
||||
Task *Task
|
||||
Attachment *TaskAttachment
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Attachment *TaskAttachment `json:"attachment"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskAttachmentCreatedEvent
|
||||
@ -132,9 +132,9 @@ func (t *TaskAttachmentCreatedEvent) Name() string {
|
||||
|
||||
// TaskAttachmentDeletedEvent represents a TaskAttachmentDeletedEvent event
|
||||
type TaskAttachmentDeletedEvent struct {
|
||||
Task *Task
|
||||
Attachment *TaskAttachment
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Attachment *TaskAttachment `json:"attachment"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskAttachmentDeletedEvent
|
||||
@ -144,9 +144,9 @@ func (t *TaskAttachmentDeletedEvent) Name() string {
|
||||
|
||||
// TaskRelationCreatedEvent represents a TaskRelationCreatedEvent event
|
||||
type TaskRelationCreatedEvent struct {
|
||||
Task *Task
|
||||
Relation *TaskRelation
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Relation *TaskRelation `json:"relation"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskRelationCreatedEvent
|
||||
@ -156,9 +156,9 @@ func (t *TaskRelationCreatedEvent) Name() string {
|
||||
|
||||
// TaskRelationDeletedEvent represents a TaskRelationDeletedEvent event
|
||||
type TaskRelationDeletedEvent struct {
|
||||
Task *Task
|
||||
Relation *TaskRelation
|
||||
Doer *user.User
|
||||
Task *Task `json:"task"`
|
||||
Relation *TaskRelation `json:"relation"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TaskRelationDeletedEvent
|
||||
@ -172,8 +172,8 @@ func (t *TaskRelationDeletedEvent) Name() string {
|
||||
|
||||
// ProjectCreatedEvent represents an event where a project has been created
|
||||
type ProjectCreatedEvent struct {
|
||||
Project *Project
|
||||
Doer *user.User
|
||||
Project *Project `json:"project"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for ProjectCreatedEvent
|
||||
@ -183,23 +183,23 @@ func (l *ProjectCreatedEvent) Name() string {
|
||||
|
||||
// ProjectUpdatedEvent represents an event where a project has been updated
|
||||
type ProjectUpdatedEvent struct {
|
||||
Project *Project
|
||||
Doer web.Auth
|
||||
Project *Project `json:"project"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for ProjectUpdatedEvent
|
||||
func (l *ProjectUpdatedEvent) Name() string {
|
||||
func (p *ProjectUpdatedEvent) Name() string {
|
||||
return "project.updated"
|
||||
}
|
||||
|
||||
// ProjectDeletedEvent represents an event where a project has been deleted
|
||||
type ProjectDeletedEvent struct {
|
||||
Project *Project
|
||||
Doer web.Auth
|
||||
Project *Project `json:"project"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for ProjectDeletedEvent
|
||||
func (t *ProjectDeletedEvent) Name() string {
|
||||
func (p *ProjectDeletedEvent) Name() string {
|
||||
return "project.deleted"
|
||||
}
|
||||
|
||||
@ -209,25 +209,25 @@ func (t *ProjectDeletedEvent) Name() string {
|
||||
|
||||
// ProjectSharedWithUserEvent represents an event where a project has been shared with a user
|
||||
type ProjectSharedWithUserEvent struct {
|
||||
Project *Project
|
||||
User *user.User
|
||||
Doer web.Auth
|
||||
Project *Project `json:"project"`
|
||||
User *user.User `json:"user"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for ProjectSharedWithUserEvent
|
||||
func (l *ProjectSharedWithUserEvent) Name() string {
|
||||
func (p *ProjectSharedWithUserEvent) Name() string {
|
||||
return "project.shared.user"
|
||||
}
|
||||
|
||||
// ProjectSharedWithTeamEvent represents an event where a project has been shared with a team
|
||||
type ProjectSharedWithTeamEvent struct {
|
||||
Project *Project
|
||||
Team *Team
|
||||
Doer web.Auth
|
||||
Project *Project `json:"project"`
|
||||
Team *Team `json:"team"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for ProjectSharedWithTeamEvent
|
||||
func (l *ProjectSharedWithTeamEvent) Name() string {
|
||||
func (p *ProjectSharedWithTeamEvent) Name() string {
|
||||
return "project.shared.team"
|
||||
}
|
||||
|
||||
@ -237,9 +237,9 @@ func (l *ProjectSharedWithTeamEvent) Name() string {
|
||||
|
||||
// TeamMemberAddedEvent defines an event where a user is added to a team
|
||||
type TeamMemberAddedEvent struct {
|
||||
Team *Team
|
||||
Member *user.User
|
||||
Doer *user.User
|
||||
Team *Team `json:"team"`
|
||||
Member *user.User `json:"member"`
|
||||
Doer *user.User `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TeamMemberAddedEvent
|
||||
@ -249,8 +249,8 @@ func (t *TeamMemberAddedEvent) Name() string {
|
||||
|
||||
// TeamCreatedEvent represents a TeamCreatedEvent event
|
||||
type TeamCreatedEvent struct {
|
||||
Team *Team
|
||||
Doer web.Auth
|
||||
Team *Team `json:"team"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TeamCreatedEvent
|
||||
@ -260,8 +260,8 @@ func (t *TeamCreatedEvent) Name() string {
|
||||
|
||||
// TeamDeletedEvent represents a TeamDeletedEvent event
|
||||
type TeamDeletedEvent struct {
|
||||
Team *Team
|
||||
Doer web.Auth
|
||||
Team *Team `json:"team"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TeamDeletedEvent
|
||||
@ -271,7 +271,7 @@ func (t *TeamDeletedEvent) Name() string {
|
||||
|
||||
// UserDataExportRequestedEvent represents a UserDataExportRequestedEvent event
|
||||
type UserDataExportRequestedEvent struct {
|
||||
User *user.User
|
||||
User *user.User `json:"user"`
|
||||
}
|
||||
|
||||
// Name defines the name for UserDataExportRequestedEvent
|
||||
|
@ -98,7 +98,7 @@ func getDefaultBucketID(s *xorm.Session, project *Project) (bucketID int64, err
|
||||
// ReadAll returns all buckets with their tasks for a certain project
|
||||
// @Summary Get all kanban buckets of a project
|
||||
// @Description Returns all kanban buckets with belong to a project including their tasks. Buckets are always sorted by their `position` in ascending order. Tasks are sorted by their `kanban_position` in ascending order.
|
||||
// @tags task
|
||||
// @tags project
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
@ -234,7 +234,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
||||
// Create creates a new bucket
|
||||
// @Summary Create a new bucket
|
||||
// @Description Creates a new kanban bucket on a project.
|
||||
// @tags task
|
||||
// @tags project
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
@ -265,7 +265,7 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Update Updates an existing bucket
|
||||
// @Summary Update an existing bucket
|
||||
// @Description Updates an existing kanban bucket.
|
||||
// @tags task
|
||||
// @tags project
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
@ -292,7 +292,7 @@ func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
// Delete removes a bucket, but no tasks
|
||||
// @Summary Deletes an existing bucket
|
||||
// @Description Deletes an existing kanban bucket and dissociates all of its task. It does not delete any tasks. You cannot delete the last bucket on a project.
|
||||
// @tags task
|
||||
// @tags project
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
|
@ -20,6 +20,8 @@ import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
@ -32,8 +34,8 @@ type Label struct {
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The label description.
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
// The color this label has
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||
// The color this label has in hex format.
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|7)" maxLength:"7"`
|
||||
|
||||
CreatedByID int64 `xorm:"bigint not null" json:"-"`
|
||||
// The user who created this label
|
||||
@ -71,6 +73,7 @@ func (l *Label) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
l.HexColor = utils.NormalizeHex(l.HexColor)
|
||||
l.CreatedBy = u
|
||||
l.CreatedByID = u.ID
|
||||
|
||||
@ -94,6 +97,9 @@ func (l *Label) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /labels/{id} [put]
|
||||
func (l *Label) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
l.HexColor = utils.NormalizeHex(l.HexColor)
|
||||
|
||||
_, err = s.
|
||||
ID(l.ID).
|
||||
Cols(
|
||||
|
@ -403,8 +403,8 @@ func (ltb *LabelTaskBulk) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, l := range labels {
|
||||
task.Labels = append(task.Labels, &l.Label)
|
||||
for i := range labels {
|
||||
task.Labels = append(task.Labels, &labels[i].Label)
|
||||
}
|
||||
return task.UpdateTaskLabels(s, a, ltb.Labels)
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package models
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
|
||||
@ -63,6 +64,25 @@ func RegisterListeners() {
|
||||
events.RegisterListener((&TaskRelationDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
|
||||
if config.TypesenseEnabled.GetBool() {
|
||||
events.RegisterListener((&TaskDeletedEvent{}).Name(), &RemoveTaskFromTypesense{})
|
||||
events.RegisterListener((&TaskCreatedEvent{}).Name(), &AddTaskToTypesense{})
|
||||
}
|
||||
if config.WebhooksEnabled.GetBool() {
|
||||
RegisterEventForWebhook(&TaskCreatedEvent{})
|
||||
RegisterEventForWebhook(&TaskUpdatedEvent{})
|
||||
RegisterEventForWebhook(&TaskDeletedEvent{})
|
||||
RegisterEventForWebhook(&TaskAssigneeCreatedEvent{})
|
||||
RegisterEventForWebhook(&TaskAssigneeDeletedEvent{})
|
||||
RegisterEventForWebhook(&TaskCommentCreatedEvent{})
|
||||
RegisterEventForWebhook(&TaskCommentUpdatedEvent{})
|
||||
RegisterEventForWebhook(&TaskCommentDeletedEvent{})
|
||||
RegisterEventForWebhook(&TaskAttachmentCreatedEvent{})
|
||||
RegisterEventForWebhook(&TaskAttachmentDeletedEvent{})
|
||||
RegisterEventForWebhook(&TaskRelationCreatedEvent{})
|
||||
RegisterEventForWebhook(&TaskRelationDeletedEvent{})
|
||||
RegisterEventForWebhook(&ProjectUpdatedEvent{})
|
||||
RegisterEventForWebhook(&ProjectDeletedEvent{})
|
||||
RegisterEventForWebhook(&ProjectSharedWithUserEvent{})
|
||||
RegisterEventForWebhook(&ProjectSharedWithTeamEvent{})
|
||||
}
|
||||
}
|
||||
|
||||
@ -506,6 +526,38 @@ func (s *RemoveTaskFromTypesense) Handle(msg *message.Message) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// AddTaskToTypesense represents a listener
|
||||
type AddTaskToTypesense struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the AddTaskToTypesense listener
|
||||
func (l *AddTaskToTypesense) Name() string {
|
||||
return "add.task.to.typesense"
|
||||
}
|
||||
|
||||
// Handle is executed when the event AddTaskToTypesense listens on is fired
|
||||
func (l *AddTaskToTypesense) Handle(msg *message.Message) (err error) {
|
||||
event := &TaskCreatedEvent{}
|
||||
err = json.Unmarshal(msg.Payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("New task %d created, adding to typesense…", event.Task.ID)
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
ttask, err := getTypesenseTaskForTask(s, event.Task, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = typesenseClient.Collection("tasks").
|
||||
Documents().
|
||||
Create(ttask)
|
||||
return
|
||||
}
|
||||
|
||||
///////
|
||||
// Project Event Listeners
|
||||
|
||||
@ -576,6 +628,100 @@ func (s *SendProjectCreatedNotification) Handle(msg *message.Message) (err error
|
||||
return nil
|
||||
}
|
||||
|
||||
// WebhookListener represents a listener
|
||||
type WebhookListener struct {
|
||||
EventName string
|
||||
}
|
||||
|
||||
// Name defines the name for the WebhookListener listener
|
||||
func (wl *WebhookListener) Name() string {
|
||||
return "webhook.listener"
|
||||
}
|
||||
|
||||
type WebhookPayload struct {
|
||||
EventName string `json:"event_name"`
|
||||
Time time.Time `json:"time"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func getProjectIDFromAnyEvent(eventPayload map[string]interface{}) int64 {
|
||||
if task, has := eventPayload["task"]; has {
|
||||
t := task.(map[string]interface{})
|
||||
if projectID, has := t["project_id"]; has {
|
||||
switch v := projectID.(type) {
|
||||
case int64:
|
||||
return v
|
||||
case float64:
|
||||
return int64(v)
|
||||
}
|
||||
return projectID.(int64)
|
||||
}
|
||||
}
|
||||
|
||||
if project, has := eventPayload["project"]; has {
|
||||
t := project.(map[string]interface{})
|
||||
if projectID, has := t["id"]; has {
|
||||
switch v := projectID.(type) {
|
||||
case int64:
|
||||
return v
|
||||
case float64:
|
||||
return int64(v)
|
||||
}
|
||||
return projectID.(int64)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Handle is executed when the event WebhookListener listens on is fired
|
||||
func (wl *WebhookListener) Handle(msg *message.Message) (err error) {
|
||||
var event map[string]interface{}
|
||||
err = json.Unmarshal(msg.Payload, &event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectID := getProjectIDFromAnyEvent(event)
|
||||
if projectID == 0 {
|
||||
log.Debugf("event %s does not contain a project id, not handling webhook", wl.EventName)
|
||||
return nil
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
ws := []*Webhook{}
|
||||
err = s.Where("project_id = ?", projectID).
|
||||
Find(&ws)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var webhook *Webhook
|
||||
for _, w := range ws {
|
||||
for _, e := range w.Events {
|
||||
if e == wl.EventName {
|
||||
webhook = w
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if webhook == nil {
|
||||
log.Debugf("Did not find any webhook for the %s event for project %d, not sending", wl.EventName, projectID)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = webhook.sendWebhookPayload(&WebhookPayload{
|
||||
EventName: wl.EventName,
|
||||
Time: time.Now(),
|
||||
Data: event,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
///////
|
||||
// Team Events
|
||||
|
||||
|
@ -32,17 +32,18 @@ import (
|
||||
|
||||
// ReminderDueNotification represents a ReminderDueNotification notification
|
||||
type ReminderDueNotification struct {
|
||||
User *user.User `json:"user"`
|
||||
Task *Task `json:"task"`
|
||||
User *user.User `json:"user"`
|
||||
Task *Task `json:"task"`
|
||||
Project *Project `json:"project"`
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for ReminderDueNotification
|
||||
func (n *ReminderDueNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
To(n.User.Email).
|
||||
Subject(`Reminder for "`+n.Task.Title+`"`).
|
||||
Subject(`Reminder for "`+n.Task.Title+`" (`+n.Project.Title+`)`).
|
||||
Greeting("Hi "+n.User.GetName()+",").
|
||||
Line(`This is a friendly reminder of the task "`+n.Task.Title+`".`).
|
||||
Line(`This is a friendly reminder of the task "`+n.Task.Title+`" (`+n.Project.Title+`).`).
|
||||
Action("Open Task", config.ServiceFrontendurl.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)).
|
||||
Line("Have a nice day!")
|
||||
}
|
||||
@ -203,17 +204,18 @@ func (n *TeamMemberAddedNotification) Name() string {
|
||||
|
||||
// UndoneTaskOverdueNotification represents a UndoneTaskOverdueNotification notification
|
||||
type UndoneTaskOverdueNotification struct {
|
||||
User *user.User
|
||||
Task *Task
|
||||
User *user.User
|
||||
Task *Task
|
||||
Project *Project
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for UndoneTaskOverdueNotification
|
||||
func (n *UndoneTaskOverdueNotification) ToMail() *notifications.Mail {
|
||||
until := time.Until(n.Task.DueDate).Round(1*time.Hour) * -1
|
||||
return notifications.NewMail().
|
||||
Subject(`Task "`+n.Task.Title+`" is overdue`).
|
||||
Subject(`Task "`+n.Task.Title+`" (`+n.Project.Title+`) is overdue`).
|
||||
Greeting("Hi "+n.User.GetName()+",").
|
||||
Line(`This is a friendly reminder of the task "`+n.Task.Title+`" which is overdue since `+utils.HumanizeDuration(until)+` and not yet done.`).
|
||||
Line(`This is a friendly reminder of the task "`+n.Task.Title+`" (`+n.Project.Title+`) which is overdue since `+utils.HumanizeDuration(until)+` and not yet done.`).
|
||||
Action("Open Task", config.ServiceFrontendurl.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)).
|
||||
Line("Have a nice day!")
|
||||
}
|
||||
@ -230,8 +232,9 @@ func (n *UndoneTaskOverdueNotification) Name() string {
|
||||
|
||||
// UndoneTasksOverdueNotification represents a UndoneTasksOverdueNotification notification
|
||||
type UndoneTasksOverdueNotification struct {
|
||||
User *user.User
|
||||
Tasks map[int64]*Task
|
||||
User *user.User
|
||||
Tasks map[int64]*Task
|
||||
Projects map[int64]*Project
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for UndoneTasksOverdueNotification
|
||||
@ -249,7 +252,7 @@ func (n *UndoneTasksOverdueNotification) ToMail() *notifications.Mail {
|
||||
overdueLine := ""
|
||||
for _, task := range sortedTasks {
|
||||
until := time.Until(task.DueDate).Round(1*time.Hour) * -1
|
||||
overdueLine += `* [` + task.Title + `](` + config.ServiceFrontendurl.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `), overdue since ` + utils.HumanizeDuration(until) + "\n"
|
||||
overdueLine += `* [` + task.Title + `](` + config.ServiceFrontendurl.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `) (` + n.Projects[task.ProjectID].Title + `), overdue since ` + utils.HumanizeDuration(until) + "\n"
|
||||
}
|
||||
|
||||
return notifications.NewMail().
|
||||
|
@ -22,13 +22,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
@ -45,7 +46,7 @@ type Project struct {
|
||||
// The unique project short identifier. Used to build task identifiers.
|
||||
Identifier string `xorm:"varchar(10) null" json:"identifier" valid:"runelength(0|10)" minLength:"0" maxLength:"10"`
|
||||
// The hex color of this project
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|7)" maxLength:"7"`
|
||||
|
||||
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
|
||||
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
|
||||
@ -314,6 +315,18 @@ func GetProjectSimplByTaskID(s *xorm.Session, taskID int64) (l *Project, err err
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
// GetProjectsSimplByTaskIDs gets a list of projects by a task ids
|
||||
func GetProjectsSimplByTaskIDs(s *xorm.Session, taskIDs []int64) (ps map[int64]*Project, err error) {
|
||||
ps = make(map[int64]*Project)
|
||||
err = s.
|
||||
Select("projects.*").
|
||||
Table(Project{}).
|
||||
Join("INNER", "tasks", "projects.id = tasks.project_id").
|
||||
In("tasks.id", taskIDs).
|
||||
Find(&ps)
|
||||
return
|
||||
}
|
||||
|
||||
// GetProjectsByIDs returns a map of projects from a slice with project ids
|
||||
func GetProjectsByIDs(s *xorm.Session, projectIDs []int64) (projects map[int64]*Project, err error) {
|
||||
projects = make(map[int64]*Project, len(projectIDs))
|
||||
@ -706,6 +719,8 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth, createBackl
|
||||
return
|
||||
}
|
||||
|
||||
project.HexColor = utils.NormalizeHex(project.HexColor)
|
||||
|
||||
_, err = s.Insert(project)
|
||||
if err != nil {
|
||||
return
|
||||
@ -819,6 +834,8 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
|
||||
}
|
||||
}
|
||||
|
||||
project.HexColor = utils.NormalizeHex(project.HexColor)
|
||||
|
||||
_, err = s.
|
||||
ID(project.ID).
|
||||
Cols(colsToUpdate...).
|
||||
|
@ -210,8 +210,8 @@ func (tl *TeamProject) ReadAll(s *xorm.Session, a web.Auth, search string, page
|
||||
}
|
||||
|
||||
teams := []*Team{}
|
||||
for _, t := range all {
|
||||
teams = append(teams, &t.Team)
|
||||
for i := range all {
|
||||
teams = append(teams, &all[i].Team)
|
||||
}
|
||||
|
||||
err = addMoreInfoToTeams(s, teams)
|
||||
|
@ -261,6 +261,9 @@ func TestProject_Delete(t *testing.T) {
|
||||
db.AssertMissing(t, "projects", map[string]interface{}{
|
||||
"id": 1,
|
||||
})
|
||||
db.AssertMissing(t, "tasks", map[string]interface{}{
|
||||
"id": 1,
|
||||
})
|
||||
})
|
||||
t.Run("with background", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
|
@ -217,7 +217,7 @@ func (lu *ProjectUser) ReadAll(s *xorm.Session, a web.Auth, search string, page
|
||||
|
||||
// Obfuscate all user emails
|
||||
for _, u := range all {
|
||||
u.Email = ""
|
||||
u.User.Email = ""
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
|
@ -72,8 +72,8 @@ func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User, doer
|
||||
}
|
||||
|
||||
t.Assignees = make([]*user.User, 0, len(currentAssignees))
|
||||
for _, assignee := range currentAssignees {
|
||||
t.Assignees = append(t.Assignees, &assignee.User)
|
||||
for i := range currentAssignees {
|
||||
t.Assignees = append(t.Assignees, ¤tAssignees[i].User)
|
||||
}
|
||||
|
||||
// If we don't have any new assignees, delete everything right away. Saves us some hassle.
|
||||
@ -349,8 +349,8 @@ func (ba *BulkAssignees) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range assignees {
|
||||
task.Assignees = append(task.Assignees, &a.User)
|
||||
for i := range assignees {
|
||||
task.Assignees = append(task.Assignees, &assignees[i].User)
|
||||
}
|
||||
|
||||
err = task.updateTaskAssignees(s, ba.Assignees, a)
|
||||
|
@ -132,10 +132,24 @@ func RegisterOverdueReminderCron() {
|
||||
|
||||
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(uts))
|
||||
|
||||
taskIDs := []int64{}
|
||||
for _, ut := range uts {
|
||||
for _, t := range ut.tasks {
|
||||
taskIDs = append(taskIDs, t.ID)
|
||||
}
|
||||
}
|
||||
|
||||
projects, err := GetProjectsSimplByTaskIDs(s, taskIDs)
|
||||
if err != nil {
|
||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not get projects for tasks: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, ut := range uts {
|
||||
var n notifications.Notification = &UndoneTasksOverdueNotification{
|
||||
User: ut.user,
|
||||
Tasks: ut.tasks,
|
||||
User: ut.user,
|
||||
Tasks: ut.tasks,
|
||||
Projects: projects,
|
||||
}
|
||||
|
||||
if len(ut.tasks) == 1 {
|
||||
@ -143,8 +157,9 @@ func RegisterOverdueReminderCron() {
|
||||
// first entry without knowing the key of it.
|
||||
for _, t := range ut.tasks {
|
||||
n = &UndoneTaskOverdueNotification{
|
||||
User: ut.user,
|
||||
Task: t,
|
||||
User: ut.user,
|
||||
Task: t,
|
||||
Project: projects[t.ProjectID],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,10 +117,10 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
|
||||
return
|
||||
}
|
||||
|
||||
for _, assignee := range assignees {
|
||||
for i := range assignees {
|
||||
taskUsers = append(taskUsers, &taskUser{
|
||||
Task: taskMap[assignee.TaskID],
|
||||
User: &assignee.User,
|
||||
Task: taskMap[assignees[i].TaskID],
|
||||
User: &assignees[i].User,
|
||||
})
|
||||
}
|
||||
|
||||
@ -173,6 +173,11 @@ func getTasksWithRemindersDueAndTheirUsers(s *xorm.Session, now time.Time) (remi
|
||||
|
||||
seen := make(map[int64]map[int64]bool)
|
||||
|
||||
projects, err := GetProjectsSimplByTaskIDs(s, taskIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Time zone cache per time zone string to avoid parsing the same time zone over and over again
|
||||
tzs := make(map[string]*time.Location)
|
||||
// Figure out which reminders are actually due in the time zone of the users
|
||||
@ -208,8 +213,9 @@ func getTasksWithRemindersDueAndTheirUsers(s *xorm.Session, now time.Time) (remi
|
||||
actualReminder := r.Reminder.In(tz)
|
||||
if (actualReminder.After(now) && actualReminder.Before(now.Add(time.Minute))) || actualReminder.Equal(now) {
|
||||
reminderNotifications = append(reminderNotifications, &ReminderDueNotification{
|
||||
User: u.User,
|
||||
Task: u.Task,
|
||||
User: u.User,
|
||||
Task: u.Task,
|
||||
Project: projects[u.Task.ProjectID],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -92,8 +92,13 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
||||
// To still find tasks with nil values, we exclude 0s when comparing with >/< values.
|
||||
for _, f := range opts.filters {
|
||||
if f.field == "reminders" {
|
||||
f.field = "reminder" // This is the name in the db
|
||||
filter, err := getFilterCond(f, opts.filterIncludeNulls)
|
||||
filter, err := getFilterCond(&taskFilter{
|
||||
// recreating the struct here to avoid modifying it when reusing the opts struct
|
||||
field: "reminder",
|
||||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
}
|
||||
@ -105,8 +110,13 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
||||
if f.comparator == taskFilterComparatorLike {
|
||||
return nil, totalCount, err
|
||||
}
|
||||
f.field = "username"
|
||||
filter, err := getFilterCond(f, opts.filterIncludeNulls)
|
||||
filter, err := getFilterCond(&taskFilter{
|
||||
// recreating the struct here to avoid modifying it when reusing the opts struct
|
||||
field: "username",
|
||||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
}
|
||||
@ -115,8 +125,13 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
||||
}
|
||||
|
||||
if f.field == "labels" || f.field == "label_id" {
|
||||
f.field = "label_id"
|
||||
filter, err := getFilterCond(f, opts.filterIncludeNulls)
|
||||
filter, err := getFilterCond(&taskFilter{
|
||||
// recreating the struct here to avoid modifying it when reusing the opts struct
|
||||
field: "label_id",
|
||||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
}
|
||||
@ -125,8 +140,13 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
||||
}
|
||||
|
||||
if f.field == "parent_project" || f.field == "parent_project_id" {
|
||||
f.field = "parent_project_id"
|
||||
filter, err := getFilterCond(f, opts.filterIncludeNulls)
|
||||
filter, err := getFilterCond(&taskFilter{
|
||||
// recreating the struct here to avoid modifying it when reusing the opts struct
|
||||
field: "parent_project_id",
|
||||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
}
|
||||
@ -374,11 +394,14 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
||||
Q: opts.search,
|
||||
QueryBy: "title, identifier, description, comments.comment",
|
||||
Page: pointer.Int(opts.page),
|
||||
PerPage: pointer.Int(opts.perPage),
|
||||
ExhaustiveSearch: pointer.True(),
|
||||
FilterBy: pointer.String(strings.Join(filterBy, " && ")),
|
||||
}
|
||||
|
||||
if opts.perPage > 0 {
|
||||
params.PerPage = pointer.Int(opts.perPage)
|
||||
}
|
||||
|
||||
if sortby != "" {
|
||||
params.SortBy = pointer.String(sortby)
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/web"
|
||||
|
||||
"dario.cat/mergo"
|
||||
@ -78,7 +79,7 @@ type Task struct {
|
||||
// An array of labels which are associated with this task.
|
||||
Labels []*Label `xorm:"-" json:"labels"`
|
||||
// The task color in hex
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|7)" maxLength:"7"`
|
||||
// Determines how far a task is left from being done
|
||||
PercentDone float64 `xorm:"DOUBLE null" json:"percent_done"`
|
||||
|
||||
@ -417,10 +418,10 @@ func addAssigneesToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Ta
|
||||
return
|
||||
}
|
||||
// Put the assignees in the task map
|
||||
for _, a := range taskAssignees {
|
||||
for i, a := range taskAssignees {
|
||||
if a != nil {
|
||||
a.Email = "" // Obfuscate the email
|
||||
taskMap[a.TaskID].Assignees = append(taskMap[a.TaskID].Assignees, &a.User)
|
||||
a.User.Email = "" // Obfuscate the email
|
||||
taskMap[a.TaskID].Assignees = append(taskMap[a.TaskID].Assignees, &taskAssignees[i].User)
|
||||
}
|
||||
}
|
||||
|
||||
@ -436,9 +437,9 @@ func addLabelsToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Task)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, l := range labels {
|
||||
for i, l := range labels {
|
||||
if l != nil {
|
||||
taskMap[l.TaskID].Labels = append(taskMap[l.TaskID].Labels, &l.Label)
|
||||
taskMap[l.TaskID].Labels = append(taskMap[l.TaskID].Labels, &labels[i].Label)
|
||||
}
|
||||
}
|
||||
|
||||
@ -731,7 +732,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
||||
|
||||
t.ID = 0
|
||||
|
||||
// Check if we have at least a text
|
||||
// Check if we have at least a title
|
||||
if t.Title == "" {
|
||||
return ErrTaskCannotBeEmpty{}
|
||||
}
|
||||
@ -768,7 +769,11 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
||||
// If no position was supplied, set a default one
|
||||
t.Position = calculateDefaultPosition(t.Index, t.Position)
|
||||
t.KanbanPosition = calculateDefaultPosition(t.Index, t.KanbanPosition)
|
||||
if _, err = s.Insert(t); err != nil {
|
||||
|
||||
t.HexColor = utils.NormalizeHex(t.HexColor)
|
||||
|
||||
_, err = s.Insert(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -959,6 +964,8 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
t.HexColor = utils.NormalizeHex(t.HexColor)
|
||||
|
||||
//////
|
||||
// Mergo does ignore nil values. Because of that, we need to check all parameters and set the updated to
|
||||
// nil/their nil value in the struct which is inserted.
|
||||
@ -1081,8 +1088,13 @@ func recalculateTaskKanbanPositions(s *xorm.Session, bucketID int64) (err error)
|
||||
|
||||
currentPosition := maxPosition / float64(len(allTasks)) * (float64(i + 1))
|
||||
|
||||
// Here we use "NoAutoTime() to prevent the ORM from updating column "updated" automatically.
|
||||
// Otherwise, this signals to CalDAV clients that the task has changed, which is not the case.
|
||||
// Consequence: when synchronizing a list of tasks, the first one immediately changes the date of all the
|
||||
// following ones from the same batch, which are then unable to be updated.
|
||||
_, err = s.Cols("kanban_position").
|
||||
Where("id = ?", task.ID).
|
||||
NoAutoTime().
|
||||
Update(&Task{KanbanPosition: currentPosition})
|
||||
if err != nil {
|
||||
return
|
||||
@ -1109,8 +1121,13 @@ func recalculateTaskPositions(s *xorm.Session, projectID int64) (err error) {
|
||||
|
||||
currentPosition := maxPosition / float64(len(allTasks)) * (float64(i + 1))
|
||||
|
||||
// Here we use "NoAutoTime() to prevent the ORM from updating column "updated" automatically.
|
||||
// Otherwise, this signals to CalDAV clients that the task has changed, which is not the case.
|
||||
// Consequence: when synchronizing a list of tasks, the first one immediately changes the date of all the
|
||||
// following ones from the same batch, which are then unable to be updated.
|
||||
_, err = s.Cols("position").
|
||||
Where("id = ?", task.ID).
|
||||
NoAutoTime().
|
||||
Update(&Task{Position: currentPosition})
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -159,7 +159,7 @@ func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) {
|
||||
if _, exists := teamMap[u.TeamID]; !exists {
|
||||
continue
|
||||
}
|
||||
u.Email = ""
|
||||
u.User.Email = ""
|
||||
teamMap[u.TeamID].Members = append(teamMap[u.TeamID].Members, u)
|
||||
}
|
||||
|
||||
|
@ -226,6 +226,11 @@ func ReindexAllTasks() (err error) {
|
||||
return fmt.Errorf("could not get all tasks: %s", err.Error())
|
||||
}
|
||||
|
||||
err = indexDummyTask()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not index dummy task: %w", err)
|
||||
}
|
||||
|
||||
err = reindexTasks(s, tasks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not reindex all tasks: %s", err.Error())
|
||||
@ -242,6 +247,36 @@ func ReindexAllTasks() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func getTypesenseTaskForTask(s *xorm.Session, task *Task, projectsCache map[int64]*Project) (ttask *typesenseTask, err error) {
|
||||
ttask = convertTaskToTypesenseTask(task)
|
||||
|
||||
var p *Project
|
||||
if projectsCache == nil {
|
||||
p, err = GetProjectSimpleByID(s, task.ProjectID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not fetch project %d: %s", task.ProjectID, err.Error())
|
||||
}
|
||||
} else {
|
||||
var has bool
|
||||
p, has = projectsCache[task.ProjectID]
|
||||
if !has {
|
||||
p, err = GetProjectSimpleByID(s, task.ProjectID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not fetch project %d: %s", task.ProjectID, err.Error())
|
||||
}
|
||||
projectsCache[task.ProjectID] = p
|
||||
}
|
||||
}
|
||||
|
||||
comment := &TaskComment{TaskID: task.ID}
|
||||
ttask.Comments, _, _, err = comment.ReadAll(s, &user.User{ID: p.OwnerID}, "", -1, -1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not fetch comments for task %d: %s", task.ID, err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func reindexTasks(s *xorm.Session, tasks map[int64]*Task) (err error) {
|
||||
|
||||
if len(tasks) == 0 {
|
||||
@ -258,24 +293,13 @@ func reindexTasks(s *xorm.Session, tasks map[int64]*Task) (err error) {
|
||||
|
||||
typesenseTasks := []interface{}{}
|
||||
for _, task := range tasks {
|
||||
searchTask := convertTaskToTypesenseTask(task)
|
||||
|
||||
p, has := projects[task.ProjectID]
|
||||
if !has {
|
||||
p, err = GetProjectSimpleByID(s, task.ProjectID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not fetch project %d: %s", task.ProjectID, err.Error())
|
||||
}
|
||||
projects[task.ProjectID] = p
|
||||
}
|
||||
|
||||
comment := &TaskComment{TaskID: task.ID}
|
||||
searchTask.Comments, _, _, err = comment.ReadAll(s, &user.User{ID: p.OwnerID}, "", -1, -1)
|
||||
ttask, err := getTypesenseTaskForTask(s, task, projects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not fetch comments for task %d: %s", task.ID, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
typesenseTasks = append(typesenseTasks, searchTask)
|
||||
typesenseTasks = append(typesenseTasks, ttask)
|
||||
}
|
||||
|
||||
_, err = typesenseClient.Collection("tasks").
|
||||
@ -292,6 +316,82 @@ func reindexTasks(s *xorm.Session, tasks map[int64]*Task) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func indexDummyTask() (err error) {
|
||||
// The initial sync should contain one dummy task with all related fields populated so that typesense
|
||||
// creates the indexes properly. A little hacky, but gets the job done.
|
||||
dummyTask := &typesenseTask{
|
||||
ID: "-100",
|
||||
Title: "Dummytask",
|
||||
Created: time.Now().Unix(),
|
||||
Updated: time.Now().Unix(),
|
||||
Reminders: []*TaskReminder{
|
||||
{
|
||||
ID: -10,
|
||||
TaskID: -100,
|
||||
Reminder: time.Now(),
|
||||
RelativePeriod: 10,
|
||||
RelativeTo: ReminderRelationDueDate,
|
||||
Created: time.Now(),
|
||||
},
|
||||
},
|
||||
Assignees: []*user.User{
|
||||
{
|
||||
ID: -100,
|
||||
Username: "dummy",
|
||||
Name: "dummy",
|
||||
Email: "dummy@vikunja",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
},
|
||||
},
|
||||
Labels: []*Label{
|
||||
{
|
||||
ID: -110,
|
||||
Title: "dummylabel",
|
||||
Description: "Lorem Ipsum Dummy",
|
||||
HexColor: "000000",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
},
|
||||
},
|
||||
Attachments: []*TaskAttachment{
|
||||
{
|
||||
ID: -120,
|
||||
TaskID: -100,
|
||||
Created: time.Now(),
|
||||
},
|
||||
},
|
||||
Comments: []*TaskComment{
|
||||
{
|
||||
ID: -220,
|
||||
Comment: "Lorem Ipsum Dummy",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
Author: &user.User{
|
||||
ID: -100,
|
||||
Username: "dummy",
|
||||
Name: "dummy",
|
||||
Email: "dummy@vikunja",
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = typesenseClient.Collection("tasks").
|
||||
Documents().
|
||||
Create(dummyTask)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = typesenseClient.Collection("tasks").
|
||||
Document(dummyTask.ID).
|
||||
Delete()
|
||||
return
|
||||
}
|
||||
|
||||
type typesenseTask struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
@ -406,6 +506,7 @@ func SyncUpdatedTasksIntoTypesense() (err error) {
|
||||
|
||||
err = s.
|
||||
Where("updated >= ?", lastSync.SyncStartedAt).
|
||||
And("updated != created"). // new tasks are already indexed via the event handler
|
||||
Find(tasks)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
|
298
pkg/models/webhooks.go
Normal file
298
pkg/models/webhooks.go
Normal file
@ -0,0 +1,298 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/version"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
var webhookClient *http.Client
|
||||
|
||||
type Webhook struct {
|
||||
// The generated ID of this webhook target
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"webhook"`
|
||||
// The target URL where the POST request with the webhook payload will be made
|
||||
TargetURL string `xorm:"not null" valid:"required,url" json:"target_url"`
|
||||
// The webhook events which should fire this webhook target
|
||||
Events []string `xorm:"JSON not null" valid:"required" json:"events"`
|
||||
// The project ID of the project this webhook target belongs to
|
||||
ProjectID int64 `xorm:"bigint not null index" json:"project_id" param:"project"`
|
||||
// If provided, webhook requests will be signed using HMAC. Check out the docs about how to use this: https://vikunja.io/docs/webhooks/#signing
|
||||
Secret string `xorm:"null" json:"secret"`
|
||||
|
||||
// The user who initially created the webhook target.
|
||||
CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"`
|
||||
CreatedByID int64 `xorm:"bigint not null" json:"-"`
|
||||
|
||||
// A timestamp when this webhook target was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this webhook target was last updated. You cannot change this value.
|
||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (w *Webhook) TableName() string {
|
||||
return "webhooks"
|
||||
}
|
||||
|
||||
var availableWebhookEvents map[string]bool
|
||||
var availableWebhookEventsLock *sync.Mutex
|
||||
|
||||
func init() {
|
||||
availableWebhookEvents = make(map[string]bool)
|
||||
availableWebhookEventsLock = &sync.Mutex{}
|
||||
}
|
||||
|
||||
func RegisterEventForWebhook(event events.Event) {
|
||||
availableWebhookEventsLock.Lock()
|
||||
defer availableWebhookEventsLock.Unlock()
|
||||
|
||||
availableWebhookEvents[event.Name()] = true
|
||||
events.RegisterListener(event.Name(), &WebhookListener{
|
||||
EventName: event.Name(),
|
||||
})
|
||||
}
|
||||
|
||||
func GetAvailableWebhookEvents() []string {
|
||||
evts := []string{}
|
||||
for e := range availableWebhookEvents {
|
||||
evts = append(evts, e)
|
||||
}
|
||||
|
||||
sort.Strings(evts)
|
||||
|
||||
return evts
|
||||
}
|
||||
|
||||
// Create creates a webhook target
|
||||
// @Summary Create a webhook target
|
||||
// @Description Create a webhook target which receives POST requests about specified events from a project.
|
||||
// @tags webhooks
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Project ID"
|
||||
// @Param webhook body models.Webhook true "The webhook target object with required fields"
|
||||
// @Success 200 {object} models.Webhook "The created webhook target."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid webhook object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id}/webhooks [put]
|
||||
func (w *Webhook) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
if !strings.HasPrefix(w.TargetURL, "http") {
|
||||
return InvalidFieldError([]string{"target_url"})
|
||||
}
|
||||
|
||||
for _, event := range w.Events {
|
||||
if _, has := availableWebhookEvents[event]; !has {
|
||||
return InvalidFieldError([]string{"events"})
|
||||
}
|
||||
}
|
||||
|
||||
w.CreatedByID = a.GetID()
|
||||
_, err = s.Insert(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.CreatedBy, err = user.GetUserByID(s, a.GetID())
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll returns all webhook targets for a project
|
||||
// @Summary Get all api webhook targets for the specified project
|
||||
// @Description Get all api webhook targets for the specified project.
|
||||
// @tags webhooks
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||
// @Param per_page query int false "The maximum number of items per bucket per page. This parameter is limited by the configured maximum of items per page."
|
||||
// @Param id path int true "Project ID"
|
||||
// @Success 200 {array} models.Webhook "The list of all webhook targets"
|
||||
// @Failure 500 {object} models.Message "Internal server error"
|
||||
// @Router /projects/{id}/webhooks [get]
|
||||
func (w *Webhook) ReadAll(s *xorm.Session, a web.Auth, _ string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
p := &Project{ID: w.ProjectID}
|
||||
can, _, err := p.CanRead(s, a)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
if !can {
|
||||
return nil, 0, 0, ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
ws := []*Webhook{}
|
||||
err = s.Where("project_id = ?", w.ProjectID).
|
||||
Limit(getLimitFromPageIndex(page, perPage)).
|
||||
Find(&ws)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
total, err := s.Where("project_id = ?", w.ProjectID).
|
||||
Count(&Webhook{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userIDs := []int64{}
|
||||
for _, webhook := range ws {
|
||||
userIDs = append(userIDs, webhook.CreatedByID)
|
||||
}
|
||||
|
||||
users, err := user.GetUsersByIDs(s, userIDs)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
for _, webhook := range ws {
|
||||
webhook.Secret = ""
|
||||
webhook.CreatedBy = users[webhook.CreatedByID]
|
||||
}
|
||||
|
||||
return ws, len(ws), total, err
|
||||
}
|
||||
|
||||
// Update updates a webhook target
|
||||
// @Summary Change a webhook target's events.
|
||||
// @Description Change a webhook target's events. You cannot change other values of a webhook.
|
||||
// @tags webhooks
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Project ID"
|
||||
// @Param webhookID path int true "Webhook ID"
|
||||
// @Success 200 {object} models.Webhook "Updated webhook target"
|
||||
// @Failure 404 {object} web.HTTPError "The webhok target does not exist"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id}/webhooks/{webhookID} [post]
|
||||
func (w *Webhook) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
for _, event := range w.Events {
|
||||
if _, has := availableWebhookEvents[event]; !has {
|
||||
return InvalidFieldError([]string{"events"})
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.Where("id = ?", w.ID).
|
||||
Cols("events").
|
||||
Update(w)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete deletes a webhook target
|
||||
// @Summary Deletes an existing webhook target
|
||||
// @Description Delete any of the project's webhook targets.
|
||||
// @tags webhooks
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Project ID"
|
||||
// @Param webhookID path int true "Webhook ID"
|
||||
// @Success 200 {object} models.Message "Successfully deleted."
|
||||
// @Failure 404 {object} web.HTTPError "The webhok target does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id}/webhooks/{webhookID} [delete]
|
||||
func (w *Webhook) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
_, err = s.Where("id = ?", w.ID).Delete(&Webhook{})
|
||||
return
|
||||
}
|
||||
|
||||
func getWebhookHTTPClient() (client *http.Client) {
|
||||
|
||||
if webhookClient != nil {
|
||||
return webhookClient
|
||||
}
|
||||
|
||||
client = http.DefaultClient
|
||||
client.Timeout = time.Duration(config.WebhooksTimeoutSeconds.GetInt()) * time.Second
|
||||
|
||||
if config.WebhooksProxyURL.GetString() == "" || config.WebhooksProxyPassword.GetString() == "" {
|
||||
webhookClient = client
|
||||
return
|
||||
}
|
||||
|
||||
proxyURL, _ := url.Parse(config.WebhooksProxyURL.GetString())
|
||||
|
||||
client.Transport = &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
ProxyConnectHeader: http.Header{
|
||||
"Proxy-Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("vikunja:"+config.WebhooksProxyPassword.GetString()))},
|
||||
"User-Agent": []string{"Vikunja/" + version.Version},
|
||||
},
|
||||
}
|
||||
|
||||
webhookClient = client
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (w *Webhook) sendWebhookPayload(p *WebhookPayload) (err error) {
|
||||
payload, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, w.TargetURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(w.Secret) > 0 {
|
||||
sig256 := hmac.New(sha256.New, []byte(w.Secret))
|
||||
_, err = sig256.Write(payload)
|
||||
if err != nil {
|
||||
log.Errorf("Could not generate webhook signature for Webhook %d: %s", w.ID, err)
|
||||
}
|
||||
signature := hex.EncodeToString(sig256.Sum(nil))
|
||||
req.Header.Add("X-Vikunja-Signature", signature)
|
||||
}
|
||||
|
||||
req.Header.Add("User-Agent", "Vikunja/"+version.Version)
|
||||
|
||||
client := getWebhookHTTPClient()
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
log.Debugf("Sent webhook payload for webhook %d for event %s", w.ID, p.EventName)
|
||||
return
|
||||
}
|
49
pkg/models/webhooks_rights.go
Normal file
49
pkg/models/webhooks_rights.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func (w *Webhook) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
p := &Project{ID: w.ProjectID}
|
||||
return p.CanRead(s, a)
|
||||
}
|
||||
|
||||
func (w *Webhook) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return w.canDoWebhook(s, a)
|
||||
}
|
||||
|
||||
func (w *Webhook) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return w.canDoWebhook(s, a)
|
||||
}
|
||||
|
||||
func (w *Webhook) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
return w.canDoWebhook(s, a)
|
||||
}
|
||||
|
||||
func (w *Webhook) canDoWebhook(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
_, isShareAuth := a.(*LinkSharing)
|
||||
if isShareAuth {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
p := &Project{ID: w.ProjectID}
|
||||
return p.CanUpdate(s, a)
|
||||
}
|
@ -55,7 +55,7 @@ func insertFromStructure(s *xorm.Session, str []*models.ProjectWithTasksAndBucke
|
||||
childRelations := make(map[int64][]int64) // old id is the key, slice of old children ids
|
||||
projectsByOldID := make(map[int64]*models.Project) // old id is the key
|
||||
// Create all projects
|
||||
for _, p := range str {
|
||||
for i, p := range str {
|
||||
oldID := p.ID
|
||||
|
||||
if p.ParentProjectID != 0 {
|
||||
@ -67,7 +67,7 @@ func insertFromStructure(s *xorm.Session, str []*models.ProjectWithTasksAndBucke
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectsByOldID[oldID] = &p.Project
|
||||
projectsByOldID[oldID] = &str[i].Project
|
||||
}
|
||||
|
||||
// parent / child relations
|
||||
@ -198,8 +198,8 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
||||
|
||||
tasksByOldID := make(map[int64]*models.TaskWithComments, len(tasks))
|
||||
// Create all tasks
|
||||
for _, t := range tasks {
|
||||
setBucketOrDefault(&t.Task)
|
||||
for i, t := range tasks {
|
||||
setBucketOrDefault(&tasks[i].Task)
|
||||
|
||||
oldid := t.ID
|
||||
t.ProjectID = project.ID
|
||||
|
@ -97,3 +97,11 @@ func MarkNotificationAsRead(s *xorm.Session, notification *DatabaseNotification,
|
||||
Update(notification)
|
||||
return
|
||||
}
|
||||
|
||||
func MarkAllNotificationsAsRead(s *xorm.Session, userID int64) (err error) {
|
||||
_, err = s.
|
||||
Where("notifiable_id = ?", userID).
|
||||
Cols("read_at").
|
||||
Update(&DatabaseNotification{ReadAt: time.Now()})
|
||||
return
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ type vikunjaInfos struct {
|
||||
UserDeletionEnabled bool `json:"user_deletion_enabled"`
|
||||
TaskCommentsEnabled bool `json:"task_comments_enabled"`
|
||||
DemoModeEnabled bool `json:"demo_mode_enabled"`
|
||||
WebhooksEnabled bool `json:"webhooks_enabled"`
|
||||
}
|
||||
|
||||
type authInfo struct {
|
||||
@ -94,6 +95,7 @@ func Info(c echo.Context) error {
|
||||
UserDeletionEnabled: config.ServiceEnableUserDeletion.GetBool(),
|
||||
TaskCommentsEnabled: config.ServiceEnableTaskComments.GetBool(),
|
||||
DemoModeEnabled: config.ServiceDemoMode.GetBool(),
|
||||
WebhooksEnabled: config.WebhooksEnabled.GetBool(),
|
||||
AvailableMigrators: []string{
|
||||
(&vikunja_file.FileMigrator{}).Name(),
|
||||
(&ticktick.Migrator{}).Name(),
|
||||
|
56
pkg/routes/api/v1/notifications.go
Normal file
56
pkg/routes/api/v1/notifications.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// MarkAllNotificationsAsRead marks all notifications of a user as read
|
||||
// @Summary Mark all notifications of a user as read
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Message "All notifications marked as read."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /notifications [post]
|
||||
func MarkAllNotificationsAsRead(c echo.Context) error {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
a, err := auth.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, is := a.(*models.LinkSharing); is {
|
||||
return echo.ErrForbidden
|
||||
}
|
||||
|
||||
err = notifications.MarkAllNotificationsAsRead(s, a.GetID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{Message: "success"})
|
||||
}
|
@ -47,20 +47,11 @@ type UserDeletionRequestConfirm struct {
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /user/deletion/request [post]
|
||||
func UserRequestDeletion(c echo.Context) error {
|
||||
var deletionRequest UserPasswordConfirmation
|
||||
if err := c.Bind(&deletionRequest); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
|
||||
}
|
||||
|
||||
err := c.Validate(deletionRequest)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err = s.Begin()
|
||||
err := s.Begin()
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
@ -71,10 +62,22 @@ func UserRequestDeletion(c echo.Context) error {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = user.CheckUserPassword(u, deletionRequest.Password)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
if u.IsLocalUser() {
|
||||
var deletionRequest UserPasswordConfirmation
|
||||
if err := c.Bind(&deletionRequest); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
|
||||
}
|
||||
|
||||
err = c.Validate(deletionRequest)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
err = user.CheckUserPassword(u, deletionRequest.Password)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
}
|
||||
|
||||
err = user.RequestDeletion(s, u)
|
||||
@ -155,20 +158,11 @@ func UserConfirmDeletion(c echo.Context) error {
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /user/deletion/cancel [post]
|
||||
func UserCancelDeletion(c echo.Context) error {
|
||||
var deletionRequest UserPasswordConfirmation
|
||||
if err := c.Bind(&deletionRequest); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
|
||||
}
|
||||
|
||||
err := c.Validate(deletionRequest)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err = s.Begin()
|
||||
err := s.Begin()
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
@ -179,10 +173,22 @@ func UserCancelDeletion(c echo.Context) error {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = user.CheckUserPassword(u, deletionRequest.Password)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
if u.IsLocalUser() {
|
||||
var deletionRequest UserPasswordConfirmation
|
||||
if err := c.Bind(&deletionRequest); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
|
||||
}
|
||||
|
||||
err = c.Validate(deletionRequest)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
err = user.CheckUserPassword(u, deletionRequest.Password)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
}
|
||||
|
||||
err = user.CancelDeletion(s, u)
|
||||
|
38
pkg/routes/api/v1/webhooks.go
Normal file
38
pkg/routes/api/v1/webhooks.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// GetAvailableWebhookEvents returns a list of all possible webhook target events
|
||||
// @Summary Get all possible webhook events
|
||||
// @Description Get all possible webhook events to use when creating or updating a webhook target.
|
||||
// @tags webhooks
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} string "The list of all possible webhook events"
|
||||
// @Failure 500 {object} models.Message "Internal server error"
|
||||
// @Router /webhooks/events [get]
|
||||
func GetAvailableWebhookEvents(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, models.GetAvailableWebhookEvents())
|
||||
}
|
@ -172,14 +172,14 @@ func (vcls *VikunjaCaldavProjectStorage) GetResourcesByFilters(rpath string, _ *
|
||||
// That project is coming from a previous "getProjectRessource" in L177
|
||||
if vcls.project.Tasks != nil {
|
||||
var resources []data.Resource
|
||||
for _, t := range vcls.project.Tasks {
|
||||
for i := range vcls.project.Tasks {
|
||||
rr := VikunjaProjectResourceAdapter{
|
||||
project: vcls.project,
|
||||
task: &t.Task,
|
||||
task: &vcls.project.Tasks[i].Task,
|
||||
isCollection: false,
|
||||
}
|
||||
r := data.NewResource(getTaskURL(&t.Task), &rr)
|
||||
r.Name = t.Title
|
||||
r := data.NewResource(getTaskURL(&vcls.project.Tasks[i].Task), &rr)
|
||||
r.Name = vcls.project.Tasks[i].Title
|
||||
resources = append(resources, r)
|
||||
}
|
||||
return resources, nil
|
||||
@ -428,8 +428,8 @@ func persistLabels(s *xorm.Session, a web.Auth, task *models.Task, labels []*mod
|
||||
}
|
||||
|
||||
labelMap := make(map[string]*models.Label)
|
||||
for _, l := range existingLabels {
|
||||
labelMap[l.Title] = &l.Label
|
||||
for i := range existingLabels {
|
||||
labelMap[existingLabels[i].Title] = &existingLabels[i].Label
|
||||
}
|
||||
|
||||
for _, label := range labels {
|
||||
|
@ -95,19 +95,19 @@ func NewEcho() *echo.Echo {
|
||||
e.HideBanner = true
|
||||
|
||||
if l, ok := e.Logger.(*elog.Logger); ok {
|
||||
if config.LogEcho.GetString() == "off" {
|
||||
if !config.LogEnabled.GetBool() || config.LogEcho.GetString() == "off" {
|
||||
l.SetLevel(elog.OFF)
|
||||
}
|
||||
l.EnableColor()
|
||||
l.SetHeader(log.ErrFmt)
|
||||
l.SetOutput(log.GetLogWriter("echo"))
|
||||
l.SetOutput(log.GetLogWriter(config.LogEcho.GetString(), "echo"))
|
||||
}
|
||||
|
||||
// Logger
|
||||
if config.LogHTTP.GetString() != "off" {
|
||||
if !config.LogEnabled.GetBool() || config.LogHTTP.GetString() != "off" {
|
||||
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Format: log.WebFmt + "\n",
|
||||
Output: log.GetLogWriter("http"),
|
||||
Output: log.GetLogWriter(config.LogHTTP.GetString(), "http"),
|
||||
}))
|
||||
}
|
||||
|
||||
@ -535,6 +535,7 @@ func registerAPIRoutes(a *echo.Group) {
|
||||
}
|
||||
a.GET("/notifications", notificationHandler.ReadAllWeb)
|
||||
a.POST("/notifications/:notificationid", notificationHandler.UpdateWeb)
|
||||
a.POST("/notifications", apiv1.MarkAllNotificationsAsRead)
|
||||
|
||||
// Migrations
|
||||
m := a.Group("/migration")
|
||||
@ -574,6 +575,20 @@ func registerAPIRoutes(a *echo.Group) {
|
||||
a.GET("/tokens", apiTokenProvider.ReadAllWeb)
|
||||
a.PUT("/tokens", apiTokenProvider.CreateWeb)
|
||||
a.DELETE("/tokens/:token", apiTokenProvider.DeleteWeb)
|
||||
|
||||
// Webhooks
|
||||
if config.WebhooksEnabled.GetBool() {
|
||||
webhookProvider := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.Webhook{}
|
||||
},
|
||||
}
|
||||
a.GET("/projects/:project/webhooks", webhookProvider.ReadAllWeb)
|
||||
a.PUT("/projects/:project/webhooks", webhookProvider.CreateWeb)
|
||||
a.DELETE("/projects/:project/webhooks/:webhook", webhookProvider.DeleteWeb)
|
||||
a.POST("/projects/:project/webhooks/:webhook", webhookProvider.UpdateWeb)
|
||||
a.GET("/webhooks/events", apiv1.GetAvailableWebhookEvents)
|
||||
}
|
||||
}
|
||||
|
||||
func registerMigrations(m *echo.Group) {
|
||||
|
@ -17,11 +17,8 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
@ -43,14 +40,7 @@ func (cv *CustomValidator) Validate(i interface{}) error {
|
||||
errs = append(errs, field+": "+e)
|
||||
}
|
||||
|
||||
return models.ValidationHTTPError{
|
||||
HTTPError: web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: models.ErrCodeInvalidData,
|
||||
Message: "Invalid Data",
|
||||
},
|
||||
InvalidFields: errs,
|
||||
}
|
||||
return models.InvalidFieldError(errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
// Code generated by swaggo/swag. DO NOT EDIT.
|
||||
|
||||
// Package swagger Code generated by swaggo/swag. DO NOT EDIT
|
||||
package swagger
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
@ -1290,6 +1289,32 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"sharing"
|
||||
],
|
||||
"summary": "Mark all notifications of a user as read",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "All notifications marked as read.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/notifications/{id}": {
|
||||
@ -1881,7 +1906,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
"project"
|
||||
],
|
||||
"summary": "Get all kanban buckets of a project",
|
||||
"parameters": [
|
||||
@ -1973,7 +1998,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
"project"
|
||||
],
|
||||
"summary": "Create a new bucket",
|
||||
"parameters": [
|
||||
@ -2426,6 +2451,230 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/projects/{id}/webhooks": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get all api webhook targets for the specified project.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"summary": "Get all api webhook targets for the specified project",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "The maximum number of items per bucket per page. This parameter is limited by the configured maximum of items per page.",
|
||||
"name": "per_page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of all webhook targets",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.Webhook"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Create a webhook target which receives POST requests about specified events from a project.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"summary": "Create a webhook target",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "The webhook target object with required fields",
|
||||
"name": "webhook",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Webhook"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The created webhook target.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Webhook"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid webhook object provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/projects/{id}/webhooks/{webhookID}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Change a webhook target's events. You cannot change other values of a webhook.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"summary": "Change a webhook target's events.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Webhook ID",
|
||||
"name": "webhookID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Updated webhook target",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Webhook"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The webhok target does not exist",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Delete any of the project's webhook targets.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"summary": "Deletes an existing webhook target",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Webhook ID",
|
||||
"name": "webhookID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successfully deleted.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The webhok target does not exist.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/projects/{projectID}/buckets/{bucketID}": {
|
||||
"post": {
|
||||
"security": [
|
||||
@ -2441,7 +2690,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
"project"
|
||||
],
|
||||
"summary": "Update an existing bucket",
|
||||
"parameters": [
|
||||
@ -2510,7 +2759,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
"project"
|
||||
],
|
||||
"summary": "Deletes an existing bucket",
|
||||
"parameters": [
|
||||
@ -5428,19 +5677,19 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "The page number for tasks. Used for pagination. If not provided, the first page of results is returned.",
|
||||
"description": "The page number, used for pagination. If not provided, the first page of results is returned.",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page.",
|
||||
"description": "The maximum number of tokens per page. This parameter is limited by the configured maximum of items per page.",
|
||||
"name": "per_page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search tasks by task text.",
|
||||
"description": "Search tokens by their title.",
|
||||
"name": "s",
|
||||
"in": "query"
|
||||
}
|
||||
@ -6784,6 +7033,43 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/webhooks/events": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get all possible webhook events to use when creating or updating a webhook target.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"summary": "Get all possible webhook events",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of all possible webhook events",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/{username}/avatar": {
|
||||
"get": {
|
||||
"description": "Returns the user avatar as image.",
|
||||
@ -7100,7 +7386,7 @@ const docTemplate = `{
|
||||
"hex_color": {
|
||||
"description": "The task color in hex",
|
||||
"type": "string",
|
||||
"maxLength": 6
|
||||
"maxLength": 7
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique, numeric id of this task.",
|
||||
@ -7250,9 +7536,9 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"hex_color": {
|
||||
"description": "The color this label has",
|
||||
"description": "The color this label has in hex format.",
|
||||
"type": "string",
|
||||
"maxLength": 6
|
||||
"maxLength": 7
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique, numeric id of this label.",
|
||||
@ -7390,7 +7676,7 @@ const docTemplate = `{
|
||||
"hex_color": {
|
||||
"description": "The hex color of this project",
|
||||
"type": "string",
|
||||
"maxLength": 6
|
||||
"maxLength": 7
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique, numeric id of this project.",
|
||||
@ -7718,7 +8004,7 @@ const docTemplate = `{
|
||||
"hex_color": {
|
||||
"description": "The task color in hex",
|
||||
"type": "string",
|
||||
"maxLength": 6
|
||||
"maxLength": 7
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique, numeric id of this task.",
|
||||
@ -8189,6 +8475,50 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Webhook": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created": {
|
||||
"description": "A timestamp when this webhook target was created. You cannot change this value.",
|
||||
"type": "string"
|
||||
},
|
||||
"created_by": {
|
||||
"description": "The user who initially created the webhook target.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/user.User"
|
||||
}
|
||||
]
|
||||
},
|
||||
"events": {
|
||||
"description": "The webhook events which should fire this webhook target",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"description": "The generated ID of this webhook target",
|
||||
"type": "integer"
|
||||
},
|
||||
"project_id": {
|
||||
"description": "The project ID of the project this webhook target belongs to",
|
||||
"type": "integer"
|
||||
},
|
||||
"secret": {
|
||||
"description": "If provided, webhook requests will be signed using HMAC. Check out the docs about how to use this: https://vikunja.io/docs/webhooks/#signing",
|
||||
"type": "string"
|
||||
},
|
||||
"target_url": {
|
||||
"description": "The target URL where the POST request with the webhook payload will be made",
|
||||
"type": "string"
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this webhook target was last updated. You cannot change this value.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications.DatabaseNotification": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -8619,6 +8949,9 @@ const docTemplate = `{
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"webhooks_enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -8656,6 +8989,8 @@ var SwaggerInfo = &swag.Spec{
|
||||
Description: "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Rights\nAll endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.\nThis can be used to show or hide ui elements based on the rights the user has.\n# Errors\nAll errors have an error code and a human-readable error message in addition to the http status code. You should always check for the status code in the response, not only the http status code.\nDue to limitations in the swagger library we're using for this document, only one error per http status code is documented here. Make sure to check the [error docs](https://vikunja.io/docs/errors/) in Vikunja's documentation for a full list of available error codes.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer <jwt-token>`-header to authenticate successfully.\n\n**API Token:** You can create scoped API tokens for your user and use the token to make authenticated requests in the context of that user. The token must be provided via an `Authorization: Bearer <token>` header, similar to jwt auth. See the documentation for the `api` group to manage token creation and revocation.\n\n**BasicAuth:** Only used when requesting tasks via CalDAV.\n<!-- ReDoc-Inject: <security-definitions> -->",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -1281,6 +1281,32 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"sharing"
|
||||
],
|
||||
"summary": "Mark all notifications of a user as read",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "All notifications marked as read.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/notifications/{id}": {
|
||||
@ -1872,7 +1898,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
"project"
|
||||
],
|
||||
"summary": "Get all kanban buckets of a project",
|
||||
"parameters": [
|
||||
@ -1964,7 +1990,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
"project"
|
||||
],
|
||||
"summary": "Create a new bucket",
|
||||
"parameters": [
|
||||
@ -2417,6 +2443,230 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/projects/{id}/webhooks": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get all api webhook targets for the specified project.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"summary": "Get all api webhook targets for the specified project",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "The maximum number of items per bucket per page. This parameter is limited by the configured maximum of items per page.",
|
||||
"name": "per_page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of all webhook targets",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.Webhook"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Create a webhook target which receives POST requests about specified events from a project.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"summary": "Create a webhook target",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "The webhook target object with required fields",
|
||||
"name": "webhook",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Webhook"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The created webhook target.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Webhook"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid webhook object provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/projects/{id}/webhooks/{webhookID}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Change a webhook target's events. You cannot change other values of a webhook.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"summary": "Change a webhook target's events.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Webhook ID",
|
||||
"name": "webhookID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Updated webhook target",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Webhook"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The webhok target does not exist",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Delete any of the project's webhook targets.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"summary": "Deletes an existing webhook target",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Project ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Webhook ID",
|
||||
"name": "webhookID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successfully deleted.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The webhok target does not exist.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/projects/{projectID}/buckets/{bucketID}": {
|
||||
"post": {
|
||||
"security": [
|
||||
@ -2432,7 +2682,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
"project"
|
||||
],
|
||||
"summary": "Update an existing bucket",
|
||||
"parameters": [
|
||||
@ -2501,7 +2751,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
"project"
|
||||
],
|
||||
"summary": "Deletes an existing bucket",
|
||||
"parameters": [
|
||||
@ -5419,19 +5669,19 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "The page number for tasks. Used for pagination. If not provided, the first page of results is returned.",
|
||||
"description": "The page number, used for pagination. If not provided, the first page of results is returned.",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page.",
|
||||
"description": "The maximum number of tokens per page. This parameter is limited by the configured maximum of items per page.",
|
||||
"name": "per_page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search tasks by task text.",
|
||||
"description": "Search tokens by their title.",
|
||||
"name": "s",
|
||||
"in": "query"
|
||||
}
|
||||
@ -6775,6 +7025,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/webhooks/events": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get all possible webhook events to use when creating or updating a webhook target.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"webhooks"
|
||||
],
|
||||
"summary": "Get all possible webhook events",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of all possible webhook events",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/{username}/avatar": {
|
||||
"get": {
|
||||
"description": "Returns the user avatar as image.",
|
||||
@ -7091,7 +7378,7 @@
|
||||
"hex_color": {
|
||||
"description": "The task color in hex",
|
||||
"type": "string",
|
||||
"maxLength": 6
|
||||
"maxLength": 7
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique, numeric id of this task.",
|
||||
@ -7241,9 +7528,9 @@
|
||||
"type": "string"
|
||||
},
|
||||
"hex_color": {
|
||||
"description": "The color this label has",
|
||||
"description": "The color this label has in hex format.",
|
||||
"type": "string",
|
||||
"maxLength": 6
|
||||
"maxLength": 7
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique, numeric id of this label.",
|
||||
@ -7381,7 +7668,7 @@
|
||||
"hex_color": {
|
||||
"description": "The hex color of this project",
|
||||
"type": "string",
|
||||
"maxLength": 6
|
||||
"maxLength": 7
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique, numeric id of this project.",
|
||||
@ -7709,7 +7996,7 @@
|
||||
"hex_color": {
|
||||
"description": "The task color in hex",
|
||||
"type": "string",
|
||||
"maxLength": 6
|
||||
"maxLength": 7
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique, numeric id of this task.",
|
||||
@ -8180,6 +8467,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Webhook": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created": {
|
||||
"description": "A timestamp when this webhook target was created. You cannot change this value.",
|
||||
"type": "string"
|
||||
},
|
||||
"created_by": {
|
||||
"description": "The user who initially created the webhook target.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/user.User"
|
||||
}
|
||||
]
|
||||
},
|
||||
"events": {
|
||||
"description": "The webhook events which should fire this webhook target",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"description": "The generated ID of this webhook target",
|
||||
"type": "integer"
|
||||
},
|
||||
"project_id": {
|
||||
"description": "The project ID of the project this webhook target belongs to",
|
||||
"type": "integer"
|
||||
},
|
||||
"secret": {
|
||||
"description": "If provided, webhook requests will be signed using HMAC. Check out the docs about how to use this: https://vikunja.io/docs/webhooks/#signing",
|
||||
"type": "string"
|
||||
},
|
||||
"target_url": {
|
||||
"description": "The target URL where the POST request with the webhook payload will be made",
|
||||
"type": "string"
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this webhook target was last updated. You cannot change this value.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications.DatabaseNotification": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -8610,6 +8941,9 @@
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"webhooks_enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -190,7 +190,7 @@ definitions:
|
||||
type: string
|
||||
hex_color:
|
||||
description: The task color in hex
|
||||
maxLength: 6
|
||||
maxLength: 7
|
||||
type: string
|
||||
id:
|
||||
description: The unique, numeric id of this task.
|
||||
@ -317,8 +317,8 @@ definitions:
|
||||
description: The label description.
|
||||
type: string
|
||||
hex_color:
|
||||
description: The color this label has
|
||||
maxLength: 6
|
||||
description: The color this label has in hex format.
|
||||
maxLength: 7
|
||||
type: string
|
||||
id:
|
||||
description: The unique, numeric id of this label.
|
||||
@ -429,7 +429,7 @@ definitions:
|
||||
type: integer
|
||||
hex_color:
|
||||
description: The hex color of this project
|
||||
maxLength: 6
|
||||
maxLength: 7
|
||||
type: string
|
||||
id:
|
||||
description: The unique, numeric id of this project.
|
||||
@ -675,7 +675,7 @@ definitions:
|
||||
type: string
|
||||
hex_color:
|
||||
description: The task color in hex
|
||||
maxLength: 6
|
||||
maxLength: 7
|
||||
type: string
|
||||
id:
|
||||
description: The unique, numeric id of this task.
|
||||
@ -1040,6 +1040,40 @@ definitions:
|
||||
minLength: 1
|
||||
type: string
|
||||
type: object
|
||||
models.Webhook:
|
||||
properties:
|
||||
created:
|
||||
description: A timestamp when this webhook target was created. You cannot
|
||||
change this value.
|
||||
type: string
|
||||
created_by:
|
||||
allOf:
|
||||
- $ref: '#/definitions/user.User'
|
||||
description: The user who initially created the webhook target.
|
||||
events:
|
||||
description: The webhook events which should fire this webhook target
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
id:
|
||||
description: The generated ID of this webhook target
|
||||
type: integer
|
||||
project_id:
|
||||
description: The project ID of the project this webhook target belongs to
|
||||
type: integer
|
||||
secret:
|
||||
description: 'If provided, webhook requests will be signed using HMAC. Check
|
||||
out the docs about how to use this: https://vikunja.io/docs/webhooks/#signing'
|
||||
type: string
|
||||
target_url:
|
||||
description: The target URL where the POST request with the webhook payload
|
||||
will be made
|
||||
type: string
|
||||
updated:
|
||||
description: A timestamp when this webhook target was last updated. You cannot
|
||||
change this value.
|
||||
type: string
|
||||
type: object
|
||||
notifications.DatabaseNotification:
|
||||
properties:
|
||||
created:
|
||||
@ -1352,6 +1386,8 @@ definitions:
|
||||
type: boolean
|
||||
version:
|
||||
type: string
|
||||
webhooks_enabled:
|
||||
type: boolean
|
||||
type: object
|
||||
web.HTTPError:
|
||||
properties:
|
||||
@ -2252,6 +2288,23 @@ paths:
|
||||
summary: Get all notifications for the current user
|
||||
tags:
|
||||
- subscriptions
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: All notifications marked as read.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
summary: Mark all notifications of a user as read
|
||||
tags:
|
||||
- sharing
|
||||
/notifications/{id}:
|
||||
post:
|
||||
consumes:
|
||||
@ -2698,7 +2751,7 @@ paths:
|
||||
- JWTKeyAuth: []
|
||||
summary: Get all kanban buckets of a project
|
||||
tags:
|
||||
- task
|
||||
- project
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
@ -2738,7 +2791,7 @@ paths:
|
||||
- JWTKeyAuth: []
|
||||
summary: Create a new bucket
|
||||
tags:
|
||||
- task
|
||||
- project
|
||||
/projects/{id}/projectusers:
|
||||
get:
|
||||
consumes:
|
||||
@ -3004,6 +3057,154 @@ paths:
|
||||
summary: Add a user to a project
|
||||
tags:
|
||||
- sharing
|
||||
/projects/{id}/webhooks:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get all api webhook targets for the specified project.
|
||||
parameters:
|
||||
- description: The page number. Used for pagination. If not provided, the first
|
||||
page of results is returned.
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- description: The maximum number of items per bucket per page. This parameter
|
||||
is limited by the configured maximum of items per page.
|
||||
in: query
|
||||
name: per_page
|
||||
type: integer
|
||||
- description: Project ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The list of all webhook targets
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/models.Webhook'
|
||||
type: array
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Get all api webhook targets for the specified project
|
||||
tags:
|
||||
- webhooks
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Create a webhook target which receives POST requests about specified
|
||||
events from a project.
|
||||
parameters:
|
||||
- description: Project ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: The webhook target object with required fields
|
||||
in: body
|
||||
name: webhook
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.Webhook'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The created webhook target.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Webhook'
|
||||
"400":
|
||||
description: Invalid webhook object provided.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Create a webhook target
|
||||
tags:
|
||||
- webhooks
|
||||
/projects/{id}/webhooks/{webhookID}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Delete any of the project's webhook targets.
|
||||
parameters:
|
||||
- description: Project ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: Webhook ID
|
||||
in: path
|
||||
name: webhookID
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully deleted.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
"404":
|
||||
description: The webhok target does not exist.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Deletes an existing webhook target
|
||||
tags:
|
||||
- webhooks
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Change a webhook target's events. You cannot change other values
|
||||
of a webhook.
|
||||
parameters:
|
||||
- description: Project ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: Webhook ID
|
||||
in: path
|
||||
name: webhookID
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Updated webhook target
|
||||
schema:
|
||||
$ref: '#/definitions/models.Webhook'
|
||||
"404":
|
||||
description: The webhok target does not exist
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Change a webhook target's events.
|
||||
tags:
|
||||
- webhooks
|
||||
/projects/{project}/shares:
|
||||
get:
|
||||
consumes:
|
||||
@ -3208,7 +3409,7 @@ paths:
|
||||
- JWTKeyAuth: []
|
||||
summary: Deletes an existing bucket
|
||||
tags:
|
||||
- task
|
||||
- project
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
@ -3253,7 +3454,7 @@ paths:
|
||||
- JWTKeyAuth: []
|
||||
summary: Update an existing bucket
|
||||
tags:
|
||||
- task
|
||||
- project
|
||||
/projects/{projectID}/duplicate:
|
||||
put:
|
||||
consumes:
|
||||
@ -5018,17 +5219,17 @@ paths:
|
||||
- application/json
|
||||
description: Returns all api tokens the current user has created.
|
||||
parameters:
|
||||
- description: The page number for tasks. Used for pagination. If not provided,
|
||||
the first page of results is returned.
|
||||
- description: The page number, used for pagination. If not provided, the first
|
||||
page of results is returned.
|
||||
in: query
|
||||
name: page
|
||||
type: integer
|
||||
- description: The maximum number of tasks per bucket per page. This parameter
|
||||
is limited by the configured maximum of items per page.
|
||||
- description: The maximum number of tokens per page. This parameter is limited
|
||||
by the configured maximum of items per page.
|
||||
in: query
|
||||
name: per_page
|
||||
type: integer
|
||||
- description: Search tasks by task text.
|
||||
- description: Search tokens by their title.
|
||||
in: query
|
||||
name: s
|
||||
type: string
|
||||
@ -5901,6 +6102,30 @@ paths:
|
||||
summary: Get users
|
||||
tags:
|
||||
- user
|
||||
/webhooks/events:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get all possible webhook events to use when creating or updating
|
||||
a webhook target.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The list of all possible webhook events
|
||||
schema:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Get all possible webhook events
|
||||
tags:
|
||||
- webhooks
|
||||
securityDefinitions:
|
||||
BasicAuth:
|
||||
type: basic
|
||||
|
@ -154,7 +154,7 @@ func (u *User) GetID() int64 {
|
||||
}
|
||||
|
||||
// TableName returns the table name for users
|
||||
func (User) TableName() string {
|
||||
func (*User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
@ -353,6 +353,10 @@ func CheckUserCredentials(s *xorm.Session, u *Login) (*User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (u *User) IsLocalUser() bool {
|
||||
return u.Issuer == IssuerLocal
|
||||
}
|
||||
|
||||
func handleFailedPassword(user *User) {
|
||||
key := user.GetFailedPasswordAttemptsKey()
|
||||
err := keyvalue.IncrBy(key, 1)
|
||||
|
27
pkg/utils/normalize_hex.go
Normal file
27
pkg/utils/normalize_hex.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
|
||||
func NormalizeHex(hex string) string {
|
||||
if strings.HasPrefix(hex, "#") {
|
||||
return strings.TrimPrefix(hex, "#")
|
||||
}
|
||||
|
||||
return hex
|
||||
}
|
5
rest/bruno.json
Normal file
5
rest/bruno.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "API-Requests",
|
||||
"type": "collection"
|
||||
}
|
3
rest/environments/local.bru
Normal file
3
rest/environments/local.bru
Normal file
@ -0,0 +1,3 @@
|
||||
vars {
|
||||
host: http://localhost:3456
|
||||
}
|
26
rest/login.bru
Normal file
26
rest/login.bru
Normal file
@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: login
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/v1/login
|
||||
body: json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"username": "{{username}}",
|
||||
"password": "{{password}}"
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
username: test
|
||||
password: 12345678
|
||||
}
|
||||
|
||||
vars:post-response {
|
||||
token: res.body.token
|
||||
}
|
14
rest/mark all notifications as read.bru
Normal file
14
rest/mark all notifications as read.bru
Normal file
@ -0,0 +1,14 @@
|
||||
meta {
|
||||
name: mark all notifications as read
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/v1/notifications
|
||||
body: none
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
14
rest/user info.bru
Normal file
14
rest/user info.bru
Normal file
@ -0,0 +1,14 @@
|
||||
meta {
|
||||
name: user info
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/api/v1/user
|
||||
body: none
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user