Compare commits

...

51 Commits
v0.6 ... v0.7

Author SHA1 Message Date
kolaente
0ed6c6cc0d updated version in readme 2019-04-05 16:54:41 +02:00
kolaente
93dbaea303 Fixed priority not updating when set to 0 2019-04-05 12:40:40 +02:00
kolaente
55e33c1694 [skip ci] updated todo 2019-04-05 12:16:57 +02:00
kolaente
2d9d27f63c Fixed getting lists by namespace 2019-04-02 10:53:16 +02:00
konrad
87873e53c5 Fix rights checks (#70) 2019-04-01 19:48:48 +00:00
konrad
19faee0102 Hide a users email everywhere (#69) 2019-04-01 18:19:55 +00:00
kolaente
ac5446e4f5 [skip ci] update todo 2019-03-31 22:33:28 +02:00
kolaente
a799527a3c [skip ci] update todo 2019-03-31 22:31:03 +02:00
konrad
2b28ab12f1 Added docs for cli usage and adding new commands (#68) 2019-03-31 19:54:17 +00:00
konrad
be5a17e993 DB Migrations (#67) 2019-03-29 17:54:35 +00:00
kolaente
e21471a193 Update web handler (fixed) 2019-03-29 18:29:44 +01:00
konrad
81f76f09ce Added docs redirect
Updated docs theme
2019-03-24 19:38:12 +01:00
konrad
c27e8fe2f1 updated docs theme 2019-03-24 18:33:42 +01:00
konrad
8d78e473f5 Added cli for general usage of Vikunja (#66) 2019-03-24 17:15:44 +00:00
konrad
5525ee0328 Refactored canRead method to get the list before checking the right (#65) 2019-03-24 13:17:36 +00:00
konrad
47352d3ed4 Let rights methods return errors (#64) 2019-03-24 12:35:50 +00:00
konrad
11e7c071ce Updated handler config (#63) 2019-03-24 09:13:40 +00:00
konrad
1dc14d5ddf Fixed labels not being queried correctly on tasks 2019-03-22 22:03:41 +01:00
kolaente
c42e22f352 Fixed bulk update label tasks 2019-03-21 19:26:04 +01:00
kolaente
72e64f7203 Fixed swagger docs for bulk label tasks 2019-03-21 18:42:21 +01:00
kolaente
25999d9b69 Fixed swagger docs for task labels 2019-03-21 07:40:56 +01:00
konrad
eb4d38b5b8 Fixed rights check on lists and namespaces (#62) 2019-03-08 21:31:37 +00:00
kolaente
65f428fe78 Updated swagger docs 2019-03-08 15:41:46 +01:00
konrad
2cc968ec61 [skip ci] updated todo 2019-03-07 21:25:54 +01:00
kolaente
073aa9940f [skip ci] update todo 2019-03-03 18:18:47 +01:00
kolaente
5506dd1ceb [skip ci] update todo 2019-03-03 17:43:25 +01:00
kolaente
e47313b17e [skip ci] update todo 2019-03-03 15:33:14 +01:00
konrad
7cc172cc0c [skip ci] updated todo 2019-02-24 22:18:20 +01:00
konrad
2bcd6e9cb6 [skip ci] updated todo 2019-02-24 18:35:25 +01:00
konrad
afd55d8cf8 [skip ci] updated todo 2019-02-24 12:06:20 +01:00
konrad
caed219d39 [skip ci] Update todo 2019-02-23 21:05:10 +01:00
konrad
1b84292332 Fix lint errs (#59) 2019-02-18 19:32:41 +00:00
konrad
15ef6deabc Use query params to sort tasks instead of url params (#61) 2019-02-18 18:06:15 +00:00
konrad
a06a5fc4f4 Updated readme and todo 2019-02-17 20:57:28 +01:00
konrad
2d88fad5b1 Huge improvements for docs (#58) 2019-02-17 19:53:04 +00:00
konrad
5e7c9b9eb9 Added possible fix for logging when nothing is set 2019-01-25 21:09:24 +01:00
konrad
9e635ea54e Improve logging handling (#57) 2019-01-25 11:40:54 +00:00
kolaente
d0fa9ddaec Remove debugging 2019-01-23 14:33:44 +01:00
konrad
add491d1e0 moar debugging 2019-01-22 22:35:00 +01:00
konrad
e735378caf added debug + possible fix 2019-01-22 22:15:56 +01:00
konrad
f95aa431b8 added debug + possible fix 2019-01-22 21:50:15 +01:00
kolaente
01e7540530 Fix makefile 2019-01-22 13:13:11 +01:00
konrad
75431e1ca5 Rights performance improvements for lists and namespaces (#54) 2019-01-21 22:08:04 +00:00
konrad
f6c7c764d1 Fix drone move step 2019-01-21 23:03:11 +01:00
konrad
eedc19a49e Build debian packages (#56) 2019-01-21 21:52:26 +00:00
konrad
c07e2b6cd4 remove unused makefile variable 2019-01-20 18:43:38 +01:00
konrad
08cbd18bc5 Added more config paths (#55) 2019-01-20 17:13:21 +00:00
konrad
8362799a93 Merge remote-tracking branch 'origin/master' 2019-01-20 00:56:42 +01:00
konrad
3edc728094 [skip ci] updated todo 2019-01-20 00:56:36 +01:00
konrad
d3975193fe Fix build building infinitly (#53) 2019-01-18 09:24:07 +00:00
konrad
6b00ccc942 updated todo with a shit ton of new features 2019-01-17 00:57:13 +01:00
507 changed files with 58310 additions and 11798 deletions

View File

@@ -35,6 +35,8 @@ steps:
- make ineffassign-check
- make misspell-check
- make goconst-check
- make gocyclo-check
- make static-check
- make build
when:
event: [ push, tag, pull_request ]
@@ -111,7 +113,6 @@ steps:
image: karalabe/xgo-latest:latest
pull: true
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release-windows
@@ -121,7 +122,6 @@ steps:
image: karalabe/xgo-latest:latest
pull: true
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release-linux
@@ -131,7 +131,6 @@ steps:
image: karalabe/xgo-latest:latest
pull: true
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release-darwin
@@ -180,6 +179,49 @@ steps:
target: /master/
depends_on: [ sign-release ]
# Build a debian package and push it to our bucket
- name: build-deb
image: kolaente/fpm
pull: true
commands:
- make build-deb
depends_on: [ static-build-linux ]
- name: deb-structure
image: kolaente/reprepro
pull: true
environment:
GPG_PRIVATE_KEY:
from_secret: gpg_privatekey
commands:
- export GPG_TTY=$(tty)
- gpg -qk
- echo "use-agent" >> ~/.gnupg/gpg.conf
- gpgconf --kill gpg-agent
- echo $GPG_PRIVATE_KEY > ~/frederik.gpg
- gpg --import ~/frederik.gpg
- mkdir debian/conf -p
- cp build/reprepro-dist-conf debian/conf/distributions
- make reprepro
depends_on: [ build-deb ]
# Push the releases to our pseudo-s3-bucket
- name: release-deb
image: plugins/s3:1
pull: true
settings:
bucket: vikunja-deb
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://storage.kolaente.de
path_style: true
strip_prefix: debian
source: debian/*/*/*/*/*
target: /
depends_on: [ deb-structure ]
# Build the docker image and push it to docker hub
- name: docker
image: plugins/docker
@@ -205,6 +247,25 @@ steps:
docker_image: vikunja/api
confirm: true
depends_on: [ docker ]
- name: telegram
image: appleboy/drone-telegram
depends_on:
- rancher
- release-latest
settings:
token:
from_secret: TELEGRAM_TOKEN
to:
from_secret: TELEGRAM_TO
message: >
{{repo.owner}}/{{repo.name}}: \[{{build.status}}] Build {{build.number}}
{{commit.author}} pushed to {{commit.branch}} {{commit.sha}}: `{{commit.message}}`
Build started at {{datetime build.started "2006-Jan-02T15:04:05Z" "GMT+2"}} finished at {{datetime build.finished "2006-Jan-02T15:04:05Z" "GMT+2"}}.
when:
status:
- success
- failure
---
########
# Build a release when tagging
@@ -243,7 +304,6 @@ steps:
image: karalabe/xgo-latest:latest
pull: true
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release-windows
@@ -253,7 +313,6 @@ steps:
image: karalabe/xgo-latest:latest
pull: true
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release-linux
@@ -263,7 +322,6 @@ steps:
image: karalabe/xgo-latest:latest
pull: true
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release-darwin
@@ -312,6 +370,49 @@ steps:
target: /${DRONE_TAG##v}/
depends_on: [ sign-release ]
# Build a debian package and push it to our bucket
- name: build-deb
image: kolaente/fpm
pull: true
commands:
- make build-deb
depends_on: [ static-build-linux ]
- name: deb-structure
image: kolaente/reprepro
pull: true
environment:
GPG_PRIVATE_KEY:
from_secret: gpg_privatekey
commands:
- export GPG_TTY=$(tty)
- gpg -qk
- echo "use-agent" >> ~/.gnupg/gpg.conf
- gpgconf --kill gpg-agent
- echo $GPG_PRIVATE_KEY > ~/frederik.gpg
- gpg --import ~/frederik.gpg
- mkdir debian/conf -p
- cp build/reprepro-dist-conf debian/conf/distributions
- make reprepro
depends_on: [ build-deb ]
# Push the releases to our pseudo-s3-bucket
- name: release-deb
image: plugins/s3:1
pull: true
settings:
bucket: vikunja-deb
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://storage.kolaente.de
path_style: true
strip_prefix: debian
source: debian/*/*/*/*/*
target: /
depends_on: [ deb-structure ]
# Build the docker image and push it to docker hub
- name: docker
image: plugins/docker
@@ -324,34 +425,11 @@ steps:
repo: vikunja/api
auto_tag: true
# Update the instance on try.vikunja.io
- name: rancher
image: peloton/drone-rancher
settings:
url: http://server01.kolaente.de:8080/v1
access_key:
from_secret: RANCHER_ACCESS_KEY
secret_key:
from_secret: RANCHER_SECRET_KEY
service: vikunja-dev/api
docker_image: vikunja/api
confirm: true
depends_on: [ docker ]
---
#############
# Tell people vikunja was updated
#############
kind: pipeline
name: notify
depends_on:
- deploy-version
- deploy-master
steps:
- name: telegram
image: appleboy/drone-telegram
depends_on:
- docker
- release-version
settings:
token:
from_secret: TELEGRAM_TOKEN
@@ -364,4 +442,60 @@ steps:
when:
status:
- success
- failure
- failure
---
kind: pipeline
name: deploy-docs
workspace:
base: /srv/app
path: src/code.vikunja.io/api
clone:
depth: 50
trigger:
event:
- push
branch:
- master
steps:
- name: submodules
image: docker:git
commands:
- git submodule update --init
- git submodule update --recursive --remote
- name: build
image: monachus/hugo:v0.54.0
pull: true
commands:
- cd docs
- hugo
- mv public/docs/* public # Hugo seems to be not capable of setting a different theme for a home page, so we do this ugly hack to fix it.
- name: docker
image: plugins/docker
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/docs
context: docs/
dockerfile: docs/Dockerfile
- name: rancher
image: peloton/drone-rancher
settings:
url: http://server01.kolaente.de:8080/v1
access_key:
from_secret: RANCHER_ACCESS_KEY
secret_key:
from_secret: RANCHER_SECRET_KEY
service: vikunja-website/docs
docker_image: vikunja/docs
confirm: true

View File

@@ -0,0 +1,12 @@
# Description
# Checklist
* [ ] I added or improved tests
* [ ] I pushed new or updated dependencies to the repo using `go mod vendor`
* [ ] I added or improved docs for my feature
* [ ] Swagger (including `make do-the-swag`)
* [ ] Error codes
* [ ] New config options

9
.gitignore vendored
View File

@@ -3,10 +3,17 @@
.idea/httpRequests
config.yml
config.yaml
!docs/config.yml
*.db
Run
dist/
cover.*
/vikunja
Test_*
bin/
bin/
secrets
*.deb
debian/
logs/
docs/public/
docs/resources/

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "docs/themes/vikunja"]
path = docs/themes/vikunja
url = https://git.kolaente.de/vikunja/theme.git

View File

@@ -3,49 +3,43 @@
This is the place where I write down ideas to work on at some point.
Sorry for some of them being in German, I'll tranlate them at some point.
## Feature-Ideen
## Feature Ideas
* [x] Priorities
* [x] Repeating tasks
* [x] Tagesübersicht ("Was ist heute/diese Woche due?") -> Machen letztenendes die Clients, wir brauchen nur nen endpoint, der alle tasks auskotzt, der Client macht dann die Sortierung.
* [x] Get all tasks which are due between two given dates
* [x] Subtasks
## Anderes
* [x] Refactor!!!! Alle Funktionen raus, die nicht mehr grbaucht werden + Funktionen vereinfachen/zusammenführen.
Wenn ein Objekt 5x hin und hergereicht wird, und jedesmal nur geringfügig was dran geändert wird sollte das
doch auch in einer Funktion machbar sein.
* [x] ganz viel in eigene neue Dateien + Packages auslagern, am besten eine package pro model mit allen methoden etc.
* [x] Alle alten dinger die nicht mehr gebraucht werden, weg.
* [x] Die alten handlerfunktionen alle in eine datei packen und erstmal "lagern", erstmal brauchen wir die noch für swagger.
* [x] Drone aufsetzen
* [x] Tests schreiben
* [x] Namen finden
* [x] Alle Packages umziehen
* [x] Swagger UI aufsetzen
+ [x] CORS fixen
* [x] Überall echo.NewHTTPError statt c.JSON(Message{}) benutzen
* [x] Bessere Fehlermeldungen wenn das Model was ankommt falsch ist und nicht geparst werden kann
* [x] Fehlerhandling irgendwie besser machen. Zb mit "World error messages"? Sprich, die Methode ruft einfach auf obs die entsprechende Fehlermeldung gibt und zeigt sonst 500 an.
* [x] Viper für config einbauen und ini rauswerfen
* [x] Docs für installationsanleitung
* [x] Tests für Rechtekram
* [x] "Apiformat" Methoden, damit in der Ausgabe zb kein Passwort drin ist..., oder created/updated von Nutzern... oder ownerID nicht drin ist sondern nur das ownerobject
* [x] Rechte überprüfen:
* [x] Listen erstellen
* [x] Listen bearbeiten (nur eigene im Moment)
* [x] Listenpunkte hinzufügen
* [x] Listenpunkte bearbeiten
* [x] Der -1 namespace sollte auch seperat angesprochen werden können, gibt sonst probleme mit der app.
* [x] Refactor!!!! Delete everything not being used anymore, simplify.
* [x] Drone
* [x] Tests
* [x] Find a nme
* [x] Move packages to a better structure
* [x] Swagger UI
+ [x] Fix CORS
* [x] Use echo.NewHTTPError instead of c.JSON(Message{})
* [x] Better error messages when the model which is sent to the server is wrong
* [x] Better error handling to show useful error messages and status codes
* [x] Viper for config instead of ini
* [x] Docs for installing
* [x] Tests for rights managemnt
* [x] Rights checks:
* [x] Create lists
* [x] Edit lists
* [x] Add tasks
* [x] Edit tasks
* [x] The -1 namespace should also be accessible seperately
### Short Term
* [x] Cacher konfigurierbar
* [x] Wenn die ID bei irgendeiner GetByID... Methode < 1 ist soll ein error not exist geworfen werden
* [x] /users sollte die Rechte mit ausgeben
* [x] Nen endpoint um /teams/members /list/users etc die Rechte updazudaten ohne erst zu löschen und dann neu einzufügen
* [x] namespaces & listen updaten geht nicht, gibt nen 500er zurück
* [x] Logging für alle Fehler irgendwohin, da gibts bestimmt ne coole library für
* [x] Cacher configurable
* [x] Should throw an error when an id < 1
* [x] /users should also return the rights
* [x] Extra endpoint /teams/members /list/users to update rights without needing to remove and re-add them
* [x] namespaces & listen update does not work, returns 500
* [x] Logging for all errors somewhere
* [x] Ne extra funktion für list exists machen, damit die nicht immer über GetListByID gehen, um sql-abfragen zu sparen
* [x] Rausfinden warum xorm teilweise beim einfügen IDs mit einfügen will -> Das schlägt dann wegen duplicate fehl
* [x] Bei den Structs "AfterLoad" raus, das verbraucht bei Gruppenabfragen zu viele SQL-Abfragen -> Die sollen einfach die entsprechenden Read()-Methoden verwenden (Krassestes bsp. ist GET /namespaces mit so ca 50 Abfragen)
@@ -61,6 +55,7 @@ Sorry for some of them being in German, I'll tranlate them at some point.
* [x] Wir brauchen noch ne gute idee, wie man die listen kriegt, auf die man nur so Zugriff hat (ohne namespace)
* Dazu am Besten nen pseudonamespace anlegen (id -1 oder so), der hat das dann alles
* [x] Testing mit locust: https://locust.io/
* [ ] Endpoint to get all users who have access to a list - regardless of via team, user share or via namespace
#### Userstuff
@@ -80,33 +75,45 @@ Sorry for some of them being in German, I'll tranlate them at some point.
* [x] Panic wenn mailer nicht erreichbar -> Als workaround mailer deaktivierbar machen, bzw keine mails verschicken
* [x] "unexpected EOF"
* [x] Beim Login & Password reset gibt die API zurück dass der Nutzer nicht existiert
* [x] Re-check rights checks to see if all information which is compared against is properly read from the db and not only based on user input
* [x] Lists
* [x] List users
* [x] List Teams
* [x] Labels
* [x] Tasks
* [x] Namespaces
* [x] Namespace users
* [x] Namespace teams
* [x] Teams
* [x] Team member handling
* [x] Also check `ReadOne()` for unnessecary database operations since the inital query is already done in `CanRead()`
* [x] Add a `User.AfterLoad()` which obfuscates the email address
* [x] Fix priority not updating to 0
### Docs
* [ ] Readme
* [x] Readme
* [x] Auch noch nen "link" zum Featurecreep
* [ ] ToC
* [ ] Logo
* [ ] How to build -> Docs
* [ ] How to use -> Docs
* [ ] How to dev -> Docs
* [ ] License
* [ ] Contributing
* [x] ToC
* [x] Logo
* [x] How to build -> Docs
* [x] How to dev -> Docs
* [x] License
* [x] Contributing
* [x] Redocs
* [x] Swaggerdocs verbessern
* [x] Descriptions in structs
* [x] Maxlength specify etc. (see swaggo docs)
* [x] Rights
* [x] API
* [ ] Anleitung zum Makefile
* [ ] Struktur erklären
* [ ] How to build from source
* [ ] Dev instructions (tests, makefile, mod, structure, etc.)
* [ ] Deploy in die docs
* [ ] Docker
* [ ] Native (systemd + nginx/apache)
* [ ] Backups
* [ ] Docs aufsetzen
* [x] Anleitung zum Makefile
* [x] How to build from source
* [x] Struktur erklären
* [x] Deploy in die docs
* [x] Docker
* [x] Native (systemd + nginx/apache)
* [x] Backups
* [x] Docs aufsetzen
### Tasks
@@ -127,39 +134,130 @@ Sorry for some of them being in German, I'll tranlate them at some point.
* [ ] Task-Templates innerhalb namespaces und Listen (-> Mehrere, die auswählbar sind)
* [ ] Ein Task muss von mehreren Assignees abgehakt werden bis er als done markiert wird
* [ ] Besseres Rechtesystem, damit man so fine-graded sachen machen kann wie "Der da darf aber nur Tasks hinzufügen, aber keine abhaken"
* [ ] Roles which enable or disable chaning certain fields of a task -> includes custm fields
* [ ] Custom fields: Templates at List > Namespace > Global level, overwriting each other
* [ ] Related tasks -> settable with a "kind" of relation like blocked, or just related or so
* [ ] Description should be longtext
### General features
* [x] Deps nach mod umziehen
* [ ] Performance bei rechtchecks verbessern
* [x] Performance bei rechtchecks verbessern
* User & Teamright sollte sich für n rechte in einer Funktion testen lassen
* [ ] Globale Limits für anlegbare Listen + Namespaces
* [ ] "Smart Lists", Listen nach bestimmten Kriterien gefiltert -> nur UI?
* [ ] Endpoint um die Rechte mit Beschreibung und code zu kriegen
* [ ] "Smart Lists", Listen nach bestimmten Kriterien gefiltert -> speichern und im pseudonamespace
* [ ] "Performance-Statistik" -> Wie viele Tasks man in bestimmten Zeiträumen so geschafft hat etc
* [ ] IMAP-Integration -> Man schickt eine email an Vikunja und es macht daraus dann nen task -> Achtung missbrauchsmöglichkeiten
* [ ] In und Out webhooks, mit Templates vom Payload
* [ ] Reminders via mail
* [ ] Activity Feed, so à la "der und der hat das und das gemacht etc"
* [ ] Per list
* [ ] For the current user
* [ ] ~~Websockets~~ SSE https://github.com/kljensen/golang-html5-sse-example
* User authenticates (with jwt)
* When updating/creating/etc an event struct is sent to the broker
* The broker has a list of subscribed users
* It then checks who is allowed to the see the event it recieved and sends it
* [ ] Mgl., dass die Instanz geschlossen ist, also sich keiner registrieren kann, und man sich einloggen muss
* User authenticates (with jwt)
* When updating/creating/etc an event struct is sent to the broker
* The broker has a list of subscribed users
* It then checks who is allowed to the see the event it recieved and sends it
* [ ] Being able to define filters for notifications or turn them silent completely -> Probably frontend only
* [ ] mgl. zum Emailmaskieren haben (in den Nutzereinstellungen, wenn man seine Email nicht an alle Welt rausposaunen will)
* [ ] Mgl. zum Accountlöschen haben (so richtig krass mit emailverifiezierung und dass alle Privaten Listen gelöscht werden und man alle geteilten entweder wem übertragen muss oder auf privat stellen)
* [ ] /info endpoint, in dem dann zb die limits und version etc steht
* [ ] Deprecate /namespaces/{id}/lists in favour of namespace.ReadOne() <-- should also return the lists
* [ ] Bindata for templates
* [ ] `GetUserByID` and the likes should return pointers
* [ ] Colors for lists and namespaces -> Up to the frontend to implement these
* [ ] Some kind of milestones for tasks
* [ ] Create tasks from a text/markdown file (probably frontend only)
* [ ] Label-view: Get a bunch of tasks by label
* [ ] Better caldav support (VTODO)
* [ ] Debian package should have a service file
* [ ] Downloads should be served via nginx (with theme?), minio should only be used for pushing artifacts.
* [ ] User struct should have a field for the avatar url (-> gravatar md5 calculated by the backend)
* [ ] All `ReadAll` methods should return the number of items per page, the number of items on this page, the total pages and the items
-> Check if there's a way to do that efficently. Maybe only implementing it in the web handler.
### Refactor
* [x] ListTaskRights, sollte überall gleich funktionieren, gibt ja mittlerweile auch eine Methode um liste von nem Task aus zu kriegen oder so
* [x] Re-check all `{List|Namespace}{User|Team}` if really all parameters need to be exposed via json or are overwritten via param anyway.
* [x] Things like list/task order should use queries and not url params
* [x] Fix lint errors
* [ ] Reminders should use an extra table so we can make reverse lookups aka "give me all tasks with reminders in this period" which we'll need for things like email reminders notifications
* [ ] Teams and users should also have uuids (for users these can be the username)
* [ ] When giving a team or user access to a list/namespace, they should be reffered to by uuid, not numeric id
* [ ] Adding users to a team should also use uuid
* [ ] Check if the team/user really exist before updating them on lists/namespaces
* [ ] Check if the email is properly obfuscated everywhere -> alter GetUser() and add a new method GetUserWithEmail
### Linters
* [x] goconst
* [ ] Gosimple -> waiting for mod
* [ ] Staticcheck -> waiting for mod
* [ ] unused -> waiting for mod
* [ ] gosec -> waiting for mod
* [x] Staticcheck
* [x] gocyclo-check
* [ ] gosec-check -> waiting for mod
* [x] goconst-check
* [ ] golangci -> docker in drone, will probably make all other linters obsolete
### More server settings
* [ ] Caldav disable/enable
* [ ] Assignees disable/enable
* [ ] List/Namespace limits
* [ ] Attachements disable/enable
* [ ] Attachements size
* [ ] Templates disable/enable
* [ ] Stats disable/enable
* [ ] Activity notifications disable/enable
* [ ] IMAP integration disable/enable
* [ ] Reminders via mail disable/enable
### Later
* [ ] Plugins
* [ ] Rename Namespaces?
* [ ] Namespaces to collections and n-n (one list can be in multiple collections)?
* [ ] Per-User limits of lists/namespaces
* [ ] Admin-Interface to do stuff like settings and user management
* [ ] Enable/Disable users
* [ ] Better rights, fine-graded
* [ ] Enable/disable allowing user adding to lists/namespaces for specific lists or namespaces
* [ ] Admins should be able to see and mange all the boards
* [ ] Limit registration to users with a defined email domain
* [ ] Close the instance, either no registration or only one with defined email
* [ ] 2fa
* [ ] Custom fields for tasks
* [ ] Sorting lists by members, tasks, teams, last modified, etc
* [ ] "Favourite lists" -> A user can favourize boards which will then show up in a pseudonamespace
* [ ] Public lists
* [ ] Internal lists -> Only registered users can see the list
* [ ] Rights management for both public and internal lists
* [ ] Add new users via to a list which don't have an account yet, they'd get a link to sign up for vikunja.
* [ ] Respect registration email domain limits
* [ ] Export all data from Vikunja to json
* [ ] Watch a (n internal) list -> Will get notification for everything
* [ ] Archive a task instead of deleting
* [ ] Task dependencies
* [ ] Time tracking (possible plugin)
* [ ] IFTTT
* [ ] More sharing features (all of these with the already existing permissions)
* [ ] Invite users per mail
* [ ] Share a link with/without password
* [ ] Comments on tasks
* [ ] @mention users in tasks or comments to get them notified
* [ ] Summary of tasks to do in a configurable interval (every day/week or so)
* [ ] Importer (maybe frontend only)
* [ ] Trello
* [ ] Wunderlist
* [ ] Zenkit
* [ ] Asana
* [ ] Microsoft Todo
* [ ] Nozbe
* [ ] Lanes
* [ ] Nirvana
* [ ] Good ol' Caldav (Tasks)
* [ ] More auth providers
* [ ] LDAP/AD
* [ ] Kerberos
* [ ] SAML (what?)
* [ ] smtp
* [ ] OpenID

View File

@@ -19,15 +19,13 @@ GOFMT ?= gofmt -s
GOFLAGS := -v -mod=vendor
EXTRA_GOFLAGS ?=
LDFLAGS := -X "main.Version=$(shell git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')" -X "main.Tags=$(TAGS)"
LDFLAGS := -X "code.vikunja.io/api/pkg/cmd.Version=$(shell git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')" -X "main.Tags=$(TAGS)"
PACKAGES ?= $(filter-out code.vikunja.io/api/integrations,$(shell go list -mod=vendor ./... | grep -v /vendor/))
SOURCES ?= $(shell find . -name "*.go" -type f)
TAGS ?=
TMPDIR := $(shell mktemp -d 2>/dev/null || mktemp -d -t 'kasino-temp')
ifeq ($(OS), Windows_NT)
EXECUTABLE := vikunja.exe
else
@@ -44,6 +42,18 @@ else
endif
endif
ifeq ($(DRONE_WORKSPACE),'')
BINLOCATION := $(EXECUTABLE)
else
BINLOCATION := $(DIST)/binaries/$(EXECUTABLE)-$(VERSION)-linux-amd64
endif
ifeq ($(VERSION),master)
PKGVERSION := $(shell git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')
else
PKGVERSION := $(VERSION)
endif
VERSION := $(shell echo $(VERSION) | sed 's/\//\-/g')
.PHONY: all
@@ -59,9 +69,6 @@ test:
VIKUNJA_SERVICE_ROOTPATH=$(shell pwd) go test $(GOFLAGS) -cover -coverprofile cover.out $(PACKAGES)
go tool cover -html=cover.out -o cover.html
required-gofmt-version:
@go version | grep -q '\(1.7\|1.8\|1.9\|1.10\|1.11\)' || { echo "We require go version 1.7, 1.8, 1.9, 1.10 or 1.11 to format code" >&2 && exit 1; }
.PHONY: lint
lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
@@ -70,11 +77,11 @@ lint:
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
.PHONY: fmt
fmt: required-gofmt-version
fmt:
$(GOFMT) -w $(GOFILES)
.PHONY: fmt-check
fmt-check: required-gofmt-version
fmt-check:
# get all go files and run go fmt on them
@diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \
@@ -83,10 +90,6 @@ fmt-check: required-gofmt-version
exit 1; \
fi;
.PHONY: install
install: $(wildcard *.go)
go install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
.PHONY: build
build: $(EXECUTABLE)
@@ -148,6 +151,15 @@ release-os-package:
release-zip:
$(foreach file,$(wildcard $(DIST)/release/$(EXECUTABLE)-*),cd $(file); zip -r ../../zip/$(shell basename $(file)).zip *; cd ../../../; )
# Builds a deb package using fpm from a previously created binary (using make build)
.PHONY: build-deb
build-deb:
fpm -s dir -t deb --url https://vikunja.io -n vikunja -v $(PKGVERSION) --license GPLv3 --directories /opt/vikunja --after-install ./build/after-install.sh --description 'Vikunja is an open-source todo application, written in Go. It lets you create lists,tasks and share them via teams or directly between users.' -m maintainers@vikunja.io ./$(BINLOCATION)=/opt/vikunja/vikunja ./templates=/opt/vikunja ./config.yml.sample=/etc/vikunja/config.yml;
.PHONY: reprepro
reprepro:
reprepro_expect debian includedeb strech ./$(EXECUTABLE)_$(PKGVERSION)_amd64.deb
.PHONY: got-swag
got-swag: do-the-swag
@diff=$$(git diff docs/swagger/swagger.json); \
@@ -162,11 +174,13 @@ do-the-swag:
@hash swag > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go install $(GOFLAGS) github.com/swaggo/swag/cmd/swag; \
fi
swag init -g pkg/routes/routes.go;
swag init -g pkg/routes/routes.go -s ./pkg/swagger;
# Fix the generated swagger file, currently a workaround until swaggo can properly use go mod
sed -i '/"definitions": {/a "code.vikunja.io.web.HTTPError": {"type": "object","properties": {"code": {"type": "integer"},"message": {"type": "string"}}},' docs/docs.go;
sed -i 's/code.vikunja.io\/web.HTTPError/code.vikunja.io.web.HTTPError/g' docs/docs.go;
sed -i 's/` + \\"`\\" + `/` + "`" + `/g' docs/docs.go; # Replace replacements
sed -i 's/package\ docs/package\ swagger/g' docs/docs.go;
sed -i 's/` + \\"`\\" + `/` + "`" + `/g' docs/docs.go;
mv ./docs/docs.go ./pkg/swagger/docs.go;
.PHONY: misspell-check
misspell-check:
@@ -185,30 +199,18 @@ ineffassign-check:
.PHONY: gocyclo-check
gocyclo-check:
@hash gocyclo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/fzipp/gocyclo; \
go install $(GOFLAGS) github.com/fzipp/gocyclo; \
fi
for S in $(GOFILES); do gocyclo -over 14 $$S || exit 1; done;
.PHONY: gosimple-check
gosimple-check:
@hash gosimple > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get honnef.co/go/tools/cmd/gosimple; \
fi
for S in $(PACKAGES); do gosimple $$S || exit 1; done;
for S in $(GOFILES); do gocyclo -over 17 $$S || exit 1; done;
.PHONY: static-check
static-check:
@hash gocyclo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get honnef.co/go/tools/cmd/staticcheck; \
@hash staticcheck > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u honnef.co/go/tools; \
go install $(GOFLAGS) honnef.co/go/tools/cmd/staticcheck; \
fi
staticcheck;
.PHONY: unused-check
unused-check:
@hash unused > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get honnef.co/go/tools/cmd/unused; \
fi
unused;
staticcheck $(PACKAGES);
.PHONY: gosec-check
gosec-check:
@@ -220,7 +222,7 @@ gosec-check:
.PHONY: goconst-check
goconst-check:
@hash goconst > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get github.com/jgautheron/goconst/cmd/goconst; \
go get -u github.com/jgautheron/goconst/cmd/goconst; \
go install $(GOFLAGS) github.com/jgautheron/goconst/cmd/goconst; \
fi
for S in $(PACKAGES); do goconst $$S || exit 1; done;

View File

@@ -1,14 +1,24 @@
<img src="https://vikunja.io/images/vikunja-logo.svg" alt="" style="display: block;width: 50%;margin: 0 auto;" width="50%"/>
[![Build Status](https://drone1.kolaente.de/api/badges/vikunja/api/status.svg)](https://drone1.kolaente.de/vikunja/api)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.7-brightgreen.svg)](https://storage.kolaente.de/minio/vikunja/)
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/api.svg)](https://hub.docker.com/r/vikunja/api/)
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/docs)
[![Go Report Card](https://goreportcard.com/badge/git.kolaente.de/vikunja/api)](https://goreportcard.com/report/git.kolaente.de/vikunja/api)
[![cover.run](https://cover.run/go/code.vikunja.io/api.svg?style=flat&tag=golang-1.10)](https://cover.run/go?tag=golang-1.10&repo=code.vikunja.io%2Fapi)
# Vikunja API
> The Todo-app to organize your life.
[![Build Status](https://drone1.kolaente.de/api/badges/vikunja/api/status.svg)](https://drone1.kolaente.de/vikunja/api)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.5-brightgreen.svg)](https://storage.kolaente.de/minio/vikunja/)
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/api.svg)](https://hub.docker.com/r/vikunja/api/)
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/swagger)
[![Go Report Card](https://goreportcard.com/badge/git.kolaente.de/vikunja/api)](https://goreportcard.com/report/git.kolaente.de/vikunja/api)
[![cover.run](https://cover.run/go/code.vikunja.io/api.svg?style=flat&tag=golang-1.10)](https://cover.run/go?tag=golang-1.10&repo=code.vikunja.io%2Fapi)
# Table of contents
* [Features](#features)
* [Docs](#docs)
* [Roadmap](#roadmap)
* [Contributing](#contributing)
* [License](#license)
## Features
@@ -17,7 +27,17 @@
* Namespaces: A "group" which bundels multiple lists
* Share lists and namespaces with teams and users with granular permissions
Try it under [try.vikunja.io](https://try.vikunja.io)!
Try it on [try.vikunja.io](https://try.vikunja.io)!
## Docs
* [Installing](https://vikunja.io/docs/installing/)
* [Build from source](https://vikunja.io/docs/build-from-sources/)
* [Development setup](https://vikunja.io/docs/development/)
* [Makefile](https://vikunja.io/docs/makefile/)
* [Testing](https://vikunja.io/docs/testing/)
All docs can be found on [the vikunja home page](https://vikunja.io/docs/).
### Roadmap
@@ -50,26 +70,10 @@ See [Featurecreep.md](Featurecreep.md) for even more! (mostly ideas, for now)
* [ ] [Mobile apps](https://code.vikunja.io/app) (seperate repo) *In Progress*
* [ ] [Webapp](https://code.vikunja.io/frontend) (seperate repo) *In Progress*
## Development
## Contributing
We use go modules to vendor libraries for Vikunja, so you'll need at least go `1.11`.
Fork -> Push -> Pull-Request. Also see the [dev docs](https://vikunja.io/docs/development/) for more infos.
To contribute to Vikunja, fork the project and work on the master branch.
## License
Some internal packages are referenced using their respective package URL. This can become problematic. To “trick” the Go tool into thinking this is a clone from the official repository, download the source code into `$GOPATH/code.vikunja.io/api`. Fork the Vikunja repository, it should then be possible to switch the source directory on the command line.
```bash
cd $GOPATH/src/code.vikunja.io/api
```
To be able to create pull requests, the forked repository should be added as a remote to the Vikunja sources, otherwise changes cant be pushed.
```bash
git remote rename origin upstream
git remote add origin git@git.kolaente.de:<USERNAME>/api.git
git fetch --all --prune
```
This should provide a working development environment for Vikunja. Take a look at the Makefile to get an overview about the available tasks. The most common tasks should be `make test` which will start our test environment and `make build` which will build a vikunja binary into the working directory. Writing test cases is not mandatory to contribute, but it is highly encouraged and helps developers sleep at night.
Thats it! You are ready to hack on Vikunja. Test changes, push them to the repository, and open a pull request.
This project is licensed under the GPLv3 License. See the [LICENSE](LICENSE) file for the full license text.

View File

@@ -40,7 +40,7 @@ Authorization: Bearer {{auth_token}}
###
# Add a new label to a task
PUT http://localhost:8080/api/v1/tasks/3565/labels
PUT http://localhost:8080/api/v1/tasks/35236365/labels
Authorization: Bearer {{auth_token}}
Content-Type: application/json

View File

@@ -1,5 +1,5 @@
# Get all lists
GET http://localhost:8080/api/v1/namespaces/1
GET http://localhost:8080/api/v1/namespaces/35/lists
Authorization: Bearer {{auth_token}}
###
@@ -11,7 +11,7 @@ Authorization: Bearer {{auth_token}}
###
# Add a new list
PUT http://localhost:8080/api/v1/namespaces/1/lists
PUT http://localhost:8080/api/v1/namespaces/35/lists
Authorization: Bearer {{auth_token}}
Content-Type: application/json
@@ -112,7 +112,7 @@ Authorization: Bearer {{auth_token}}
###
# Get all pending tasks with priorities
GET http://localhost:8080/api/v1/tasks/all/desc
GET http://localhost:8080/api/v1/tasks/all?sort=priorityasc
Authorization: Bearer {{auth_token}}
###
@@ -135,10 +135,7 @@ Authorization: Bearer {{auth_token}}
Content-Type: application/json
{
"labels": [
{"id": 1},
{"id": 2}
]
"priority": 0
}
###

View File

@@ -5,7 +5,7 @@ Authorization: Bearer {{auth_token}}
###
# Get one namespaces
GET http://localhost:8080/api/v1/namespaces/125476
GET http://localhost:8080/api/v1/namespaces/-1
Authorization: Bearer {{auth_token}}
###

8
build/after-install.sh Normal file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
ln -s /opt/vikunja/vikunja /usr/bin/vikunja
# Fix the config to contain proper values
NEW_SECRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
sed -i "s/<jwt-secret>/$NEW_SECRET/g" /etc/vikunja/config.yml
sed -i "s/<rootpath>/\/opt\/vikunja\//g" /etc/vikunja/config.yml
sed -i "s/Path: \"\.\/vikunja.db\"/Path: \"\\/opt\/vikunja\/vikunja.db\"/g" /etc/vikunja/config.yml

8
build/reprepro-dist-conf Normal file
View File

@@ -0,0 +1,8 @@
Origin: dl.vikunja.io
Label: Vikunja
Codename: strech
Architectures: amd64
Components: main
Description: The debian repo for Vikunja builds.
SignWith: yes
Pull: strech

View File

@@ -2,7 +2,7 @@ service:
# This token is used to verify issued JWT tokens.
# Default is a random token which will be generated at each startup of vikunja.
# (This means all already issued tokens will be invalid once you restart vikunja)
JWTSecret: "cei6gaezoosah2bao3ieZohkae5aicah"
JWTSecret: "<jwt-secret>"
# The interface on which to run the webserver
interface: ":3456"
# The URL of the frontend, used to send password reset emails.
@@ -10,7 +10,7 @@ service:
# The base path on the file system where the binary and assets are.
# Vikunja will also look in this path for a config file, so you could provide only this variable to point to a folder
# with a config file which will then be used.
rootpath: <the path of the executable>
rootpath: <rootpath>
# The number of items which gets returned per page
pagecount: 50
# If set to true, enables a /metrics endpoint for prometheus to collect metrics about the system
@@ -30,8 +30,6 @@ database:
database: "vikunja"
# When using sqlite, this is the path where to store the data
Path: "./vikunja.db"
# Whether to show mysql queries or not. Useful for debugging.
showqueries: "false"
# Sets the max open connections to the database. Only used when using mysql.
openconnections: 100
@@ -47,9 +45,9 @@ redis:
# Whether to enable redis or not
enabled: false
# The host of the redis server including its port.
redishost: 'localhost:6379'
host: 'localhost:6379'
# The password used to authenicate against the redis server
redispassword: ''
password: ''
# 0 means default database
db: 0
@@ -71,4 +69,20 @@ mailer:
# The length of the mail queue.
queuelength: 100
# The timeout in seconds after which the current open connection to the mailserver will be closed.
queuetimeout: 30
queuetimeout: 30
log:
# A folder where all the logfiles should go.
path: <rootpath>logs
# Whether to show any logging at all or none
enabled: true
# Where the error log should go. Possible values are stdout, stderr, file or off to disable error logging.
errors: "stdout"
# Where the normal log should go. Possible values are stdout, stderr, file or off to disable standard logging.
standard: "stdout"
# Whether or not to log database queries. Useful for debugging. Possible values are stdout, stderr, file or off to disable database logging.
database: "off"
# Whether to log http requests or not. Possible values are stdout, stderr, file or off to disable http logging.
http: "stdout"
# Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
echo: "off"

3
docs/Dockerfile Normal file
View File

@@ -0,0 +1,3 @@
FROM nginx
ADD public /usr/share/nginx/html/docs
ADD nginx.conf /etc/nginx/conf.d/default.conf

39
docs/config.yml Normal file
View File

@@ -0,0 +1,39 @@
baseurl: https://vikunja.io/docs/
title: Vikunja
theme: vikunja
enableRobotsTXT: true
canonifyURLs: true
pygmentsUseClasses: true
permalinks:
post: /:year/:month/:title/
doc: /:slug/
page: /:slug/
default: /:slug/
params:
description: The to-do app to organize your life
author: The Vikunja Authors
website: https://vikunja.io
fanthomEnabled: false
fathomUrl: fathom.kolaente.de
fathomSiteID: RYKSD
menu:
page:
- name: Home
url: https://vikunja.io/en/
weight: 10
- name: Features
url: https://vikunja.io/en/features
weight: 20
- name: Download
url: https://vikunja.io/en/download
weight: 30
- name: Docs
url: https://vikunja.io/docs
weight: 40
- name: Code
url: https://code.vikunja.io/
weight: 50

View File

@@ -0,0 +1,25 @@
---
date: "2019-02-12:00:00+02:00"
title: "Docs"
draft: false
url: "/docs"
type: "doc"
weight: 10
---
# Documentation
This is the documentation for Vikunja.
You can find available articles in the menu on the left.
## About
To learn more about the what, why and how, take a look at [the features page](https://vikunja.io/en/features).
## Start
A good starting point if you want to install and host Vikunja on your server are [the install documentation](installing)
and [available configuration options](config-options).
## Developing
If you want to start contributing to Vikunja, take a look at [the development docs](development).

View File

@@ -0,0 +1,34 @@
---
date: "2019-03-31:00:00+01:00"
title: "Adding new cli commands"
draft: false
type: "doc"
menu:
sidebar:
parent: "development"
---
# Adding new cli commands
All cli-related functions are located in `pkg/cmd`.
Each cli command usually calls a function in another package.
For example, the `vikunja migrate` command calls `migration.Migrate()`.
Vikunja uses the amazing [cobra](https://github.com/spf13/cobra) library for its cli.
Please refer to its documentation for informations about how to use flags etc.
To add a new cli command, add something like the following:
{{< highlight golang >}}
func init() {
rootCmd.AddCommand(myCmd)
}
var myCmd = &cobra.Command{
Use: "My-command",
Short: "A short description about your command.",
Run: func(cmd *cobra.Command, args []string) {
// Call other functions
},
}
{{</ highlight >}}

View File

@@ -0,0 +1,61 @@
---
date: "2019-02-12:00:00+02:00"
title: "Development"
toc: true
draft: false
type: "doc"
menu:
sidebar:
parent: "development"
name: "Development"
---
# Development
We use go modules to vendor libraries for Vikunja, so you'll need at least go `1.11` to use these.
If you don't intend to add new dependencies, go `1.9` and above should be fine.
To contribute to Vikunja, fork the project and work on the master branch.
A lot of developing tasks are automated using a Makefile, so make sure to [take a look at it]({{< ref "make.md">}}).
## Libraries
We keep all libraries used for Vikunja around in the `vendor/` folder to still be able to build the project even if
some maintainers take their libraries down like [it happened in the past](https://github.com/jteeuwen/go-bindata/issues/5).
## Tests
See [testing]({{< ref "test.md">}}).
#### Development using go modules
If you're able to use go modules, you can clone the project wherever you want to and work from there.
However, when building or running tests, please supply the `-mod=vendor` flag to go so it builds using the
dependencies from the `vendor/` folder.
#### Development-setup without go modules
Some internal packages are referenced using their respective package URL. This can become problematic.
To “trick” the Go tool into thinking this is a clone from the official repository, download the source code
into `$GOPATH/code.vikunja.io/api`. Fork the Vikunja repository, it should then be possible to switch the source directory on the command line.
{{< highlight bash >}}
cd $GOPATH/src/code.vikunja.io/api
{{< /highlight >}}
To be able to create pull requests, the forked repository should be added as a remote to the Vikunja sources, otherwise changes cant be pushed.
{{< highlight bash >}}
git remote rename origin upstream
git remote add origin git@git.kolaente.de:<USERNAME>/api.git
git fetch --all --prune
{{< /highlight >}}
This should provide a working development environment for Vikunja. Take a look at the Makefile to get an overview about
the available tasks. The most common tasks should be `make test` which will start our test environment and `make build`
which will build a vikunja binary into the working directory. Writing test cases is not mandatory to contribute, but it
is highly encouraged and helps developers sleep at night.
Thats it! You are ready to hack on Vikunja. Test changes, push them to the repository, and open a pull request.

View File

@@ -0,0 +1,132 @@
---
date: "2019-02-12:00:00+02:00"
title: "Makefile"
draft: false
type: "doc"
menu:
sidebar:
parent: "development"
---
# Makefile
We scripted a lot of tasks used mostly for developing into the makefile. This documents explains what
taks are available and what they do.
## CI
These tasks are automatically run in our CI every time someone pushes to master or you update a pull request:
* `make lint`
* `make fmt-check`
* `make ineffassign-check`
* `make misspell-check`
* `make goconst-check`
* `make build`
### clean
{{< highlight bash >}}
make clean
{{< /highlight >}}
Clears all builds and binaries.
### test
{{< highlight bash >}}
make test
{{< /highlight >}}
Runs all tests in Vikunja.
### Format the code
{{< highlight bash >}}
make fmt
{{< /highlight >}}
Formats all source code using `go fmt`.
#### Check formatting
{{< highlight bash >}}
make fmt-check
{{< /highlight >}}
Checks if the code needs to be formatted. Fails if it does.
### Build Vikunja
{{< highlight bash >}}
make build
{{< /highlight >}}
Builds a `vikunja`-binary in the root directory of the repo for the platform it is run on.
### Build Releases
{{< highlight bash >}}
make build
{{< /highlight >}}
Builds binaries for all platforms and zips them with a copy of the `templates/` folder.
All built zip files are stored into `dist/zips/`. Binaries are stored in `dist/binaries/`,
binaries bundled with `templates` are stored in `dist/releases/`.
All cross-platform binaries built using this series of commands are built with the help of
[xgo](https://github.com/karalabe/xgo). The make command will automatically install the
binary to be able to use it.
`make release` is actually just a shortcut to execute `make release-dirs release-windows release-linux release-darwin release-copy release-check release-os-package release-zip`.
* `release-dirs` creates all directories needed
* `release-windows`/`release-linux`/`release-darwin` execute xgo to build for their respective platforms
* `release-copy` bundles binaries with a copy of `templates/` to then be zipped
* `release-check` creates sha256 checksums for each binary which will be included in the zip file
* `release-os-package` bundles a binary with a copy of the `templates/` folder, the `sha256` checksum file, a sample `config.yml` and a copy of the license in a folder for each architecture
* `release-zip` makes a zip file for the files created by `release-os-package`
### Build debian packages
{{< highlight bash >}}
make build-deb
{{< /highlight >}}
Will build a `.deb` package into the current folder. You need to have [fpm](https://fpm.readthedocs.io/en/latest/intro.html) installed to be able to do this.
#### Make a debian repo
{{< highlight bash >}}
make reprepro
{{< /highlight >}}
Takes an already built debian package and creates a debian repo structure around it.
Used to be run inside a [docker container](https://git.kolaente.de/konrad/reprepro-docker) in the CI process when releasing.
### Generate swagger definitions from code comments
{{< highlight bash >}}
make do-the-swag
{{< /highlight >}}
Generates swagger definitions from the comments in the code.
#### Check if swagger generation is needed
{{< highlight bash >}}
make got-swag
{{< /highlight >}}
This command is currently more an experiment, use it with caution.
It may bring up wrong results.
### Code-Checks
* `misspell-check`: Checks for commonly misspelled words
* `ineffassign-check`: Checks for ineffectual assignments in the code using [ineffassign](https://github.com/gordonklaus/ineffassign).
* `gocyclo-check`: Calculates cyclomatic complexities of functions using [gocyclo](https://github.com/fzipp/gocyclo).
* `static-check`: Analyzes the code for bugs, improvements and more using [staticcheck](https://staticcheck.io/docs/).
* `gosec-check`: Inspects source code for security problems by scanning the Go AST using the [gosec tool](https://github.com/securego/gosec).
* `goconst-check`: Finds repeated strings that could be replaced by a constant using [goconst](https://github.com/jgautheron/goconst/).

View File

@@ -0,0 +1,71 @@
---
date: "2019-03-29:00:00+02:00"
title: "Database migrations"
draft: false
type: "doc"
menu:
sidebar:
parent: "development"
---
# Database Migrations
Vikunja runs all database migrations automatically on each start if needed.
Additionally, they can also be run directly by using the `migrate` command.
We use [xormigrate](https://github.com/techknowlogick/xormigrate) to handle migrations,
which is based on gormigrate.
## Add a new migration
All migrations are stored in `pkg/migrations` and files should have the same name as their id.
Each migration should have a function to apply and roll it back, as well as a numeric id (the datetime)
and a more in-depth description of what the migration actually does.
To easily get a new id, run the following on any unix system:
{{< highlight bash >}}
date +%Y%m%d%H%M%S
{{< /highlight >}}
New migrations should be added via the `init()` function to the `migrations` variable.
All migrations are sorted before being executed, since `init()` does not guarantee the order.
When you're adding a new struct, you also need to add it to the `models.GetTables()` function
to ensure it will be created on new installations.
### Example
{{< highlight golang >}}
package migration
import (
"github.com/go-xorm/xorm"
"src.techknowlogick.com/xormigrate"
)
// Used for rollback
type teamMembersMigration20190328074430 struct {
Updated int64 `xorm:"updated"`
}
func (teamMembersMigration20190328074430) TableName() string {
return "team_members"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20190328074430",
Description: "Remove updated from team_members",
Migrate: func(tx *xorm.Engine) error {
return dropTableColum(tx, "team_members", "updated")
},
Rollback: func(tx *xorm.Engine) error {
return tx.Sync2(teamMembersMigration20190328074430{})
},
})
}
{{< /highlight >}}
You should always copy the changed parts of the struct you're changing when adding migraitons.

View File

@@ -0,0 +1,152 @@
---
date: "2019-02-12:00:00+02:00"
title: "Project structure"
draft: false
type: "doc"
menu:
sidebar:
parent: "development"
---
# Project structure
In general, this api repo has the following structure:
* `docker`
* `docs`
* `pkg`
* `caldav`
* `config`
* `log`
* `mail`
* `metrics`
* `migration`
* `models`
* `red`
* `routes`
* `api/v1`
* `swagger`
* `utils`
* `REST-Tests`
* `templates`
* `vendor`
This document will explain what these mean and what you can find where.
## Root level
The root directory is where [the config file]({{< ref "../setup/config.md">}}), [Makefile]({{< ref "make.md">}}), license, drone config,
application entry point (`main.go`) and so on are located.
## docker
This directory holds additonal files needed to build and run the docker container, mainly service configuration to properly run Vikunja inside a docker
container.
## pkg
This is where most of the magic happens. Most packages with actual code are located in this folder.
### caldav
This folder holds a simple caldav implementation which is responsible for returning the caldav feature.
### cmd
This package contains all cli-related files and functions.
To learn more about how to add a new command, see [the cli docs]({{< ref "cli.md">}}).
To learn more about how to use this cli, see [the cli usage docs]({{< ref "../usage/cli.md">}}).
### config
This package configures the config. It sets default values and sets up viper and tells it where to look for config files,
how to interpret which env variables for config etc.
If you want to add a new config parameter, you should add default value in this package.
### log
Similar to `config`, this will set up the logging, based on differen logging backends.
This init is called in `main.go` after the config init is done.
### mail
This package handles all mail sending. To learn how to send a mail, see [sending emails]({{< ref "../practical-instructions/mail.md">}}).
### metrics
This package handles all metrics which are exposed to the prometheus endpoint.
To learn how it works and how to add new metrics, take a look at [how metrics work]({{< ref "../practical-instructions/metrics.md">}}).
### migration
This package handles all migrations.
All migrations are stored and executed here.
To learn more, take a look at the [migrations docs]({{< ref "../development/migrations.md">}}).
### models
This is where most of the magic happens.
When adding new features or upgrading existing ones, that most likely happens here.
Because this package is pretty huge, there are several documents and how-to's about it:
* [Adding a feature]({{< ref "../practical-instructions/feature.md">}})
* [Making calls to the database]({{< ref "../practical-instructions/database.md">}})
### red (redis)
This package initializes a connection to a redis server.
This inizialization is automatically done at the startup of vikunja.
It also has a function (`GetRedis()`) which returns a redis client object you can then use in your package
to talk to redis.
It uses the [go-redis](https://github.com/go-redis/redis) library, please see their configuration on how to use it.
### routes
This package defines all routes which are available for vikunja clients to use.
To add a new route, see [adding a new route]({{< ref "../practical-instructions/feature.md">}}).
#### api/v1
This is where all http-handler functions for the api are stored.
Every handler function which does not use the standard web handler should live here.
### swagger
This is where the [generated]({{< ref "make.md#generate-swagger-definitions-from-code-comments">}} [api docs]({{< ref "../usage/api.md">}}) live.
You usually don't need to touch this package.
### utils
A small package, containing some helper functions:
* `MakeRandomString`: Generates a random string of a given length.
* `Sha256`: Calculates a sha256 hash from a given string.
See their function definitions for instructions on how to use them.
## REST-Tests
Holds all kinds of test files to directly test the api from inside of [jetbrains ide's](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html).
These files are currently more an experiment, maybe we will drop them in the future to use something we could integrate in the testing process with drone.
Therefore, this has no claim to be complete yet even working, you're free to change whatever is needed to get it working for you.
## templates
Holds the email templates used to send plain text and html emails for new user registration and password changes.
## vendor
All libraries needed to build Vikunja.
We keep all libraries used for Vikunja around in the `vendor/` folder to still be able to build the project even if
some maintainers take their libraries down like [it happened in the past](https://github.com/jteeuwen/go-bindata/issues/5).
When adding a new dependency, make sure to run `go mod vendor` to put it inside this directory.

View File

@@ -1,10 +1,20 @@
---
date: "2019-02-12:00:00+02:00"
title: "Testing"
draft: false
type: "doc"
menu:
sidebar:
parent: "development"
---
# Testing
You can run unit tests with our `Makefile` with
You can run unit tests with [our `Makefile`]({{< ref "make.md">}}) with
```bash
{{< highlight bash >}}
make test
```
{{< /highlight >}}
### Running tests with config

View File

@@ -0,0 +1,38 @@
---
date: "2019-02-12:00:00+02:00"
title: "Database"
draft: false
type: "doc"
menu:
sidebar:
parent: "practical instructions"
---
# Database
Vikunja uses [xorm](http://xorm.io/) as an abstraction layer to handle the database connection.
Please refer to [their](http://xorm.io/docs/) documentation on how to exactly use it.
Inside the `models` package, a variable `x` is available which contains a pointer to an instance of `xorm.Engine`.
This is used whenever you make a call to the database to get or update data.
This xorm instance is set up and initialized every time vikunja is started.
### Adding new database tables
To add a new table to the database, add a an instance of your struct to the `tables` variable in the
init function in `pkg/models/models.go`. Xorm will sync them automatically.
You also need to add a pointer to the `tablesWithPointer` slice to enable caching for all instances of this struct.
To learn more about how to configure your struct to create "good" tables, refer to [the xorm documentaion](http://xorm.io/docs/).
### Adding data to test fixtures
Adding data for test fixtures is done in via `yaml` files insinde of `pkg/models/fixtures`.
The name of the yaml file should equal the table name in the database.
Adding values to it is done via array definition inside of the yaml file.
**Note**: Table and column names need to be in snake_case as that's what is used internally in the database
and for mapping values from the database to xorm so your structs can use it.

View File

@@ -0,0 +1,72 @@
---
date: "2019-02-12:00:00+02:00"
title: "Custom Errors"
draft: false
type: "doc"
menu:
sidebar:
parent: "practical instructions"
---
# Custom Errors
All custom errors are defined in `pkg/models/errors.go`.
You should add new ones in this file.
Custom errors usually have fields for the http return code, a [vikunja-specific error code]({{< ref "../usage/errors.md">}})
and a human-readable error message about what went wrong.
An error consists of multiple functions and definitions:
{{< highlight golang >}}
// This struct holds any information about this specific error.
// In this case, it contains the user ID of a nonexistand user.
// This type should always be a struct, even if it has no values in it.
// ErrUserDoesNotExist represents a "UserDoesNotExist" kind of error.
type ErrUserDoesNotExist struct {
UserID int64
}
// This function is mostly used in unit tests to check if a returned error is of that type.
// Every error type should have one of these.
// The name should always start with IsErr... followed by the name of the error.
// IsErrUserDoesNotExist checks if an error is a ErrUserDoesNotExist.
func IsErrUserDoesNotExist(err error) bool {
_, ok := err.(ErrUserDoesNotExist)
return ok
}
// This is the definition of the actual error type.
// Your error type is _required_ to implement this in order to be able to be returned as an "error" from functions.
func (err ErrUserDoesNotExist) Error() string {
return fmt.Sprintf("User does not exist [user id: %d]", err.UserID)
}
// This const holds the vikunja error code used to be able to identify this error without having to
// rely on an error string.
// This needs to be unique, so you should check whether the error code exists or not.
// The general convention for error codes is as follows:
// * Every "group" errors lives in a thousend something. For example all user issues are 1000-something, all
// list errors are 3000-something and so on.
// * New error codes should be the current max error code + 1. Don't take free numbers to prevent old errors
// which are depricated and removed from being "new ones". For example, if there are error codes 1001, 1002, 1004,
// a new error should be 1005 and not 1003.
// ErrCodeUserDoesNotExist holds the unique world-error code of this error
const ErrCodeUserDoesNotExist = 1005
// This is the implementation which returns an http error which is then passed to the client.
// Here you define the http status code with which one the error will be returned, the vikunja error code and
// a human-readable error message.
// HTTPError holds the http error description
func (err ErrUserDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusNotFound,
Code: ErrCodeUserDoesNotExist,
Message: "The user does not exist.",
}
}
{{< /highlight >}}

View File

@@ -0,0 +1,33 @@
---
date: "2019-02-12:00:00+02:00"
title: "Add a new api endpoint"
draft: false
type: "doc"
menu:
sidebar:
parent: "practical instructions"
---
# Add a new api endpoint/feature
Most of the api endpoints/features of Vikunja are using the [common web handler](https://code.vikunja.io/web).
This is a library created by Vikunja in an effort to facilitate the creation of REST endpoints.
This works by abstracting the handling of CRUD-Requests, including rights check.
You can learn more about the web handler on [the project's repo](https://code.vikunja.io/web).
### Helper for pagination
Pagination limits can be calculated with a helper function, `getLimitFromPageIndex(pageIndex)`
(only available in the `models` package) from any page number.
It returns the `limit` (max-length) and `offset` parameters needed for SQL-Queries.
You can feed this function directly into xorm's `Limit`-Function like so:
{{< highlight golang >}}
lists := []List{}
err := x.Limit(getLimitFromPageIndex(pageIndex)).Find(&lists)
{{< /highlight >}}
// TODO: Add a full example from start to finish, like a tutorial on how to create a new endpoint?

View File

@@ -0,0 +1,84 @@
---
date: "2019-02-12:00:00+02:00"
title: "Mailer"
draft: false
type: "doc"
menu:
sidebar:
parent: "practical instructions"
---
# Mailer
This document explains how to use the mailer to send emails and what to do to create a new kind of email to be sent.
## Sending emails
**Note:** You should use mail templates whenever possible (see below).
To send an email, use the function `mail.SendMail(options)`. The options are defined as follows:
{{< highlight golang >}}
type Opts struct {
To string // The email address of the recipent
Subject string // The subject of the mail
Message string // The plaintext message in the mail
HTMLMessage string // The html message
ContentType ContentType // The content type of the mail. Can be either mail.ContentTypePlain, mail.ContentTypeHTML, mail.ContentTypeMultipart. You should set this according to the kind of mail you want to send.
Boundary string
Headers []*header // Other headers to set in the mail.
}
{{< /highlight >}}
## Sending emails based on a template
For each mail with a template, there are two email templates: One for plaintext emails, one for html emails.
These are located in the `templates/mail` folder and follow the conventions of `template-name.{plain|hmtl}.tmpl`,
both the plaintext and html templates are in the same folder.
To send a mail based on a template, use the function `mail.SendMailWithTemplate(to, subject, tpl string, data map[string]interface{})`.
`to` and `subject` are pretty much self-explanatory, `tpl` is the name of the template, without `.html.tmpl` or `.plain.tmpl`.
`data` is a map you can pass additional data to your template.
#### Sending a mail with a template
A basic html email template would look like this:
{{< highlight go-html-template >}}
{{template "mail-header.tmpl" .}}
<p>
Hey there!<br/>
This is a minimal html email example.<br/>
{{.Something}}
</p>
{{template "mail-footer.tmpl"}}
{{< /highlight >}}
And the corresponding plaintext template:
{{< highlight go-text-template >}}
Hey there!
This is a minimal html email example.
{{.Something}}
{{< /highlight >}}
You would then call this like so:
{{< highlight golang >}}
data := make(map[string]interface{})
data["Something"] = "I am some computed value"
to := "test@example.com"
subject := "A simple test mail"
tpl := "demo" // Assuming you saved the templates as demo.plain.tmpl and demo.html.tmpl
mail.SendMailWithTemplate(to, subject, tpl, data)
{{< /highlight >}}
The function does not return an error. If an error occures when sending a mail, it is logged but not returned because sending the mail happens asinchrounly.
Notice the `mail-header.tmpl` and `mail-footer.tmpl` in the template. These populate some basic css, a box for your content and the vikunja logo.
All that's left for you is to put the content in, which then will appear in a beautifully-styled box.
Remeber, these are email templates. This is different from normal html/css, you cannot use whatever you want (because most of the clients are wayyy to outdated).

View File

@@ -0,0 +1,46 @@
---
date: "2019-02-12:00:00+02:00"
title: "Metrics"
draft: false
type: "doc"
menu:
sidebar:
parent: "practical instructions"
---
# Metrics
Metrics work by exposing a `/metrics` endpoint which can then be accessed by prometheus.
To keep the load on the database minimal, metrics are stored and updated in redis.
The `metrics` package provides several functions to create and update metrics.
## New metrics
First, define a `const` with the metric key in redis. This is done in `pkg/metrics/metrics.go`.
To expose a new metric, you need to register it in the `init` function inside of the `metrics` package like so:
{{< highlight golang >}}
// Register total user count metric
promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_team_count", // The key of the metric. Must be unique.
Help: "The total number of teams on this instance", // A description about the metric itself.
}, func() float64 {
count, _ := GetCount(TeamCountKey) // TeamCountKey is the const we defined earlier.
return float64(count)
})
{{< /highlight >}}
Then you'll need to set the metrics initial value on every startup of vikunja.
This is done in `pkg/routes/routes.go` to avoid cyclic imports.
If metrics are enabled, it checks if a redis connection is available and then sets the initial values.
A convenience function is available if the metric is based on a database struct.
Because metrics are stored in redis, you are responsible to increase or decrease these based on criteria you define.
To do this, use `metrics.UpdateCount(value, key)` where `value` is the amount you want to cange it (you can pass
negative values to decrease it) and `key` it the redis key used to define the metric.
# Using it
A Prometheus config with a Grafana template is available at [our git repo](https://git.kolaente.de/vikunja/monitoring).

View File

@@ -0,0 +1,31 @@
---
date: "2019-02-12:00:00+02:00"
title: "Adding new config options"
draft: false
type: "doc"
menu:
sidebar:
parent: "practical instructions"
---
# Adding new config options
Vikunja uses [viper](https://github.com/spf13/viper) to handle configuration options.
It handles parsing all different configuration sources.
The configuration is done in sections. These are represented with a `.` in viper.
Take a look at `pkg/config/config.go` to understand how these are set.
To add a new config option, you should add a default value to `pkg/config/config.go`.
Default values should always enable the feature to work somehow, or turn it off completely if it always needs
additional configuration.
Make sure to add the new config option to [the config document]({{< ref "../setup/config.md">}}) and the default config file
(`config.yml.sample` at the root of the repository) to make sure it is well documented.
If you're using a computed value as a default, make sure to update the sample config file and debian
post-install scripts to reflect that.
To get a configured option, use `viper.Get("config.option")`.
Take a look at [viper's documentation](https://github.com/spf13/viper#getting-values-from-viper) to learn of the
different ways available to get config options.

View File

@@ -0,0 +1,47 @@
---
date: "2019-02-12:00:00+02:00"
title: "Modifying swagger api docs"
draft: false
type: "doc"
menu:
sidebar:
parent: "practical instructions"
---
# Adding/editing swagger api docs
The api documentation is generated using [swaggo](https://github.com/swaggo/swag) from comments.
### Documenting structs
You should always comment every field which will be exposed as a json in the api.
These comments will show up in the documentation, it'll make it easier for developers using the api.
As an example, this is the definition of a list with all comments:
{{< highlight golang >}}
// List represents a list of tasks
type List struct {
// The unique, numeric id of this list.
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"list"`
// The title of the list. You'll see this in the namespace overview.
Title string `xorm:"varchar(250)" json:"title" valid:"required,runelength(3|250)" minLength:"3" maxLength:"250"`
// The description of the list.
Description string `xorm:"varchar(1000)" json:"description" valid:"runelength(0|1000)" maxLength:"1000"`
OwnerID int64 `xorm:"int(11) INDEX" json:"-"`
NamespaceID int64 `xorm:"int(11) INDEX" json:"-" param:"namespace"`
// The user who created this list.
Owner User `xorm:"-" json:"owner" valid:"-"`
// An array of tasks which belong to the list.
Tasks []*ListTask `xorm:"-" json:"tasks"`
// A unix timestamp when this list was created. You cannot change this value.
Created int64 `xorm:"created" json:"created"`
// A unix timestamp when this list was last updated. You cannot change this value.
Updated int64 `xorm:"updated" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
{{< /highlight >}}

View File

@@ -0,0 +1,34 @@
---
date: "2019-02-12:00:00+02:00"
title: "What to backup"
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
---
# What to backup
Vikunja does not store any data outside of the database.
So, all you need to backup are the contents of that database and maybe the config file.
## MySQL
To create a backup from mysql use the `mysqldump` command:
{{< highlight bash >}}
mysqldump -u <user> -p -h <db-host> <database> > vkunja-backup.sql
{{< /highlight >}}
You will be prompted for the password of the mysql user.
To restore it, simply pipe it back into the `mysql` command:
{{< highlight bash >}}
mysql -u <user> -p -h <db-host> <database> < vkunja-backup.sql
{{< /highlight >}}
## SQLite
To backup sqllite databases, it is enough to copy the database elsewhere.

View File

@@ -0,0 +1,25 @@
---
date: "2019-02-12:00:00+02:00"
title: "Build from sources"
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
---
# Build Vikunja from source
Vikunja being a go application, has no other dependencies than go itself.
All libraries are bundeled inside the repo in the `vendor/` folder, so all it boils down to are these steps:
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.9`.
2. Make sure [Make](https://www.gnu.org/software/make/) is properly installed on your system.
3. Clone the repo with `git clone https://code.vikunja.io/api`
3. Run `make build` in the source of this repo. This will build a binary in the root of the repo which will be able to run on your system.
# Build for different architectures
To build for other platforms and architectures than the one you're currently on, simply run `make release` or `make release-{linux|windows|darwin}`.
More options are available, please refer to the [makefile docs]({{< ref "../development/make.md">}}) for more details.

View File

@@ -1,3 +1,13 @@
---
date: "2019-02-12:00:00+02:00"
title: "Config options"
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
---
# Configuration options
You can either use a `config.yml` file in the root directory of vikunja or set all config option with
@@ -6,22 +16,31 @@ environment variables. If you have both, the value set in the config file is use
Variables are nested in the `config.yml`, these nested variables become `VIKUNJA_FIRST_CHILD` when configuring via
environment variables. So setting
```bash
{{< highlight bash >}}
export VIKUNJA_FIRST_CHILD=true
```
{{< /highlight >}}
is the same as defining it in a `config.yml` like so:
```yaml
{{< highlight yaml >}}
first:
child: true
```
{{< /highlight >}}
## Config file locations
Vikunja will search on various places for a config file:
* Next to the location of the binary
* In the `service.rootpath` location set in a config (remember you can set config arguments via environment variables)
* In `/etc/vikunja`
* In `~/.config/vikunja`
# Default configuration with explanations
This is the same as the `config.yml.sample` file you'll find in the root of vikunja.
```yaml
{{< highlight yaml >}}
service:
# This token is used to verify issued JWT tokens.
# Default is a random token which will be generated at each startup of vikunja.
@@ -54,8 +73,6 @@ database:
database: "vikunja"
# When using sqlite, this is the path where to store the data
Path: "./vikunja.db"
# Whether to show mysql queries or not. Useful for debugging.
showqueries: "false"
# Sets the max open connections to the database. Only used when using mysql.
openconnections: 100
@@ -71,9 +88,9 @@ redis:
# Whether to enable redis or not
enabled: false
# The host of the redis server including its port.
redishost: 'localhost:6379'
host: 'localhost:6379'
# The password used to authenicate against the redis server
redispassword: ''
password: ''
# 0 means default database
db: 0
@@ -96,4 +113,20 @@ mailer:
queuelength: 100
# The timeout in seconds after which the current open connection to the mailserver will be closed.
queuetimeout: 30
```
log:
# A folder where all the logfiles should go.
path: <rootpath>logs
# Whether to show any logging at all or none
enabled: true
# Where the error log should go. Possible values are stdout, stderr, file or off to disable error logging.
errors: "stdout"
# Where the normal log should go. Possible values are stdout, stderr, file or off to disable standard logging.
standard: "stdout"
# Whether or not to log database queries. Useful for debugging. Possible values are stdout, stderr, file or off to disable database logging.
database: "off"
# Whether to log http requests or not. Possible values are stdout, stderr, file or off to disable http logging.
http: "stdout"
# Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
echo: "off"
{{< /highlight >}}

View File

@@ -0,0 +1,110 @@
---
date: "2019-02-12:00:00+02:00"
title: "Full docker example"
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
---
# Full docker example
This docker compose configuration will run Vikunja with backend and frontend with a mariadb as database.
It uses an nginx container to proxy backend and frontend into a single port.
You'll need to save this nginx configuration on your host under `nginx.conf`
(or elsewhere, but then you'd need to adjust the proxy mount at the bottom of the compose file):
{{< highlight conf >}}
server {
listen 80;
location / {
proxy_pass http://frontend:80;
}
location /api/ {
proxy_pass http://api:3456;
}
}
{{< /highlight >}}
### Without redis
{{< highlight yaml >}}
version: '3'
services:
db:
image: mariadb:10
environment:
MYSQL_ROOT_PASSWORD: supersecret
MYSQL_DATABASE: vikunja
volumes:
- ./db:/var/lib/mysql
api:
image: vikunja/api
environment:
VIKUNJA_DATABASE_HOST: db
VIKUNJA_DATABASE_PASSWORD: supersecret
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: root
VIKUNJA_DATABASE_DATABASE: vikunja
depends_on:
- db
frontend:
image: vikunja/frontend
proxy:
image: nginx
ports:
- 80:80
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
- frontend
{{< /highlight >}}
### With redis
{{< highlight yaml >}}
version: '3'
services:
db:
image: mariadb:10
environment:
MYSQL_ROOT_PASSWORD: supersecret
MYSQL_DATABASE: vikunja
volumes:
- ./db:/var/lib/mysql
redis:
image: redis
api:
image: vikunja/api
environment:
VIKUNJA_DATABASE_HOST: db
VIKUNJA_DATABASE_PASSWORD: supersecret
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: root
VIKUNJA_DATABASE_DATABASE: vikunja
VIKUNJA_REDIS_ENABLED: 1
VIKUNJA_REDIS_HOST: 'redis:6379'
VIKUNJA_CACHE_ENABLED: 1
VIKUNJA_CACHE_TYPE: redis
depends_on:
- db
- redis
frontend:
image: vikunja/frontend
proxy:
image: nginx
ports:
- 80:80
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
- frontend
{{< /highlight >}}

View File

@@ -0,0 +1,160 @@
---
date: "2019-02-12:00:00+02:00"
title: "Install Backend"
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
---
# Backend
## Install from binary
Download a copy of Vikunja from the [download page](https://vikunja.io/en/download/) for your architecture.
{{< highlight bash >}}
wget <download-url>
{{< /highlight >}}
### Verify the GPG signature
Starting with version `0.7`, all releases are signed using pgp.
Releases from `master` will always be signed.
To validate the downloaded zip file use the signiture file `.asc` and the key `FF054DACD908493A`:
{{< highlight bash >}}
gpg --keyserver keyserver.ubuntu.com --recv FF054DACD908493A
gpg --verify vikunja-0.7-linux-amd64-full.zip.asc vikunja-0.7-linux-amd64-full.zip
{{< /highlight >}}
### Set it up
Once you've verified the signature, you need to unzip it and make it executable, you'll also need to
create a symlink to it so you can execute Vikunja by typing `vikunja` on your system.
We'll install vikunja to `/opt/vikunja`, change the path where needed if you want to install it elsewhere.
{{< highlight bash >}}
mkdir -p /opt/vikunja
unzip <vikunja-zip-file> -d /opt/vikunja
chmod +x /opt/vikunja
ln -s /opt/vikunja/vikunja /usr/bin/vikunja
{{< /highlight >}}
### Systemd service
Take the following `service` file and adapt it to your needs:
{{< highlight service >}}
[Unit]
Description=Vikunja
After=syslog.target
After=network.target
# Depending on how you configured Vikunja, you may want to uncomment these:
#Requires=mysql.service
#Requires=mariadb.service
#Requires=redis.service
[Service]
RestartSec=2s
Type=simple
WorkingDirectory=/opt/vikunja
ExecStart=/usr/bin/vikunja
Restart=always
# If you want to bind Vikunja to a port below 1024 uncomment
# the two values below
###
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
#AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
{{< /highlight >}}
If you've installed Vikunja to a directory other than `/opt/vikunja`, you need to adapt `WorkingDirectory` accordingly.
Save the file to `/etc/systemd/system/vikunja.service`
After you made all nessecary modifications, it's time to start the service:
{{< highlight bash >}}
sudo systemctl enable vikunja
sudo systemctl start vikunja
{{< /highlight >}}
### Build from source
To build vikunja from source, see [building from source]({{< ref "build-from-source.md">}}).
### Updating
Simply replace the binary and templates with the new version, then restart Vikunja.
It will automatically run all nessecary database migrations.
**Make sure to take a look at the changelog for the new version to not miss any manual steps the update may involve!**
## Docker
(Note: this assumes some familarity with docker)
Usage with docker is pretty straightforward:
{{< highlight bash >}}
docker run -p 3456:3456 vikunja/api
{{< /highlight >}}
to run with a standard configuration.
This will expose
You can mount a local configuration like so:
{{< highlight bash >}}
docker run -p 3456:3456 -v /path/to/config/on/host.yml:/app/vikunja/config.yml:ro vikunja/api
{{< /highlight >}}
Though it is recommended to use eviroment variables or `.env` files to configure Vikunja in docker.
See [config]({{< ref "config.md">}}) for a list of available configuration options.
### Docker compose
To run the backend with a mariadb database you can use this example [docker-compose](https://docs.docker.com/compose/) file:
{{< highlight yaml >}}
version: '2'
services:
api:
image: vikunja/api:latest
environment:
VIKUNJA_DATABASE_HOST: db
VIKUNJA_DATABASE_PASSWORD: supersecret
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: root
VIKUNJA_SERVICE_JWTSECRET: <generated secret>
db:
image: mariadb:10
environment:
MYSQL_ROOT_PASSWORD: supersecret
MYSQL_DATABASE: vikunja
volumes:
- ./db:/var/lib/mysql
{{< /highlight >}}
See [full docker example]({{< ref "full-docker-example.md">}}) for more varations of this config.
## Debian packages
Since version 0.7 Vikunja is also released as debian packages.
To install these, grab a copy from [the download page](https://vikunja.io/en/download/) and run
{{< highlight bash >}}
dpkg -i vikunja.deb
{{< /highlight >}}
This will install the backend to `/opt/vikunja`.
To configure it, use the config file in `/etc/vikunja/config.yml`.
## Configuration
See [available configuration options]({{< ref "config.md">}}).

View File

@@ -0,0 +1,116 @@
---
date: "2019-02-12:00:00+02:00"
title: "Install Frontend"
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
---
# Frontend
Installing the frontend is just a matter of hosting a bunch of static files somewhere.
With nginx or apache, you have to [download](https://vikunja.io/en/download/) the frontend files first.
Unzip them and store them somewhere your server can access them.
You also need to configure a rewrite condition to internally redirect all requests to `index.html` which handles all urls.
## Docker
The docker image is based on nginx and just contains all nessecary files for the frontend.
To run it, all you need is
{{< highlight bash >}}
docker run -p 80:80 vikunja/frontend
{{< /highlight >}}
which will run the docker image and expose port 80 on the host.
See [full docker example]({{< ref "full-docker-example.md">}}) for more varations of this config.
## NGINX
Below are two example configurations which you can put in your `nginx.conf`:
You may need to adjust `server_name` and `root` accordingly.
After configuring them, you need to reload nginx (`service nginx reload`).
### 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;
}
}
{{< /highlight >}}
### 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;
}
}
{{< /highlight >}}
## Apache
Apache needs to have `mod_rewrite` enabled for this to work properly:
{{< highlight bash >}}
a2enmod rewrite
service apache2 restart
{{< /highlight >}}
Put the following config in `cat /etc/apache2/sites-available/vikunja.conf`:
{{< highlight aconf >}}
<VirtualHost *:80>
ServerName localhost
DocumentRoot /path/to/vikunja/static/frontend/files
RewriteEngine On
RewriteRule ^\/?(config\.json|favicon\.ico|css|fonts|images|img|js) - [L]
RewriteRule ^(.*)$ /index.html [QSA,L]
</VirtualHost>
{{< /highlight >}}
You probably want to adjust `ServerName` and `DocumentRoot`.
Once you've customized your config, you need to enable it:
{{< highlight bash >}}
a2ensite vikunja
service apache2 reload
{{< /highlight >}}
## Updating
To update, it should be enough to download the new files and overwrite the old ones.
The paths contain hashes, so all caches are invalidated automatically.

View File

@@ -0,0 +1,40 @@
---
date: "2019-02-12:00:00+02:00"
title: "Installing"
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
weight: 10
---
# Installing
Vikunja consists of two parts: [Backend](https://code.vikunja.io/api) and [frontend](https://code.vikunja.io/frontend).
While the backend is required, the frontend is not.
You don't neccesarily need to have a web-frontend, using Vikunja via the [mobile app](https://code.vikunja.io/app) is totally fine.
However, using the web frontend is highly reccommended.
Vikunja can be installed in various forms.
This document provides an overview and instructions for the different methods.
* [Backend]({{< ref "install-backend.md">}})
* [Installing from binary]({{< ref "install-backend.md#install-from-binary">}})
* [Verify the GPG signature]({{< ref "install-backend.md#verify-the-gpg-signature">}})
* [Set it up]({{< ref "install-backend.md#set-it-up">}})
* [Systemd service]({{< ref "install-backend.md#systemd-service">}})
* [Updating]({{< ref "install-backend.md#updating">}})
* [Build from source]({{< ref "install-backend.md#build-from-source">}})
* [Docker]({{< ref "install-backend.md#docker">}})
* [Debian packages]({{< ref "install-backend.md#debian-packages">}})
* [Configuration]({{< ref "config.md">}})
* [Frontend]({{< ref "install-frontend.md">}})
* [Docker]({{< ref "install-frontend.md#docker">}})
* [NGINX]({{< ref "install-frontend.md#nginx">}})
* [Apache]({{< ref "install-frontend.md#apache">}})
* [Updating]({{< ref "install-frontend.md#updating">}})
* [Reverse proxies]({{< ref "reverse-proxies.md">}})
* [Full docker example]({{< ref "full-docker-example.md">}})
* [Backups]({{< ref "backups.md">}})

View File

@@ -0,0 +1,95 @@
---
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.
## 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/ {
proxy_pass http://localhost:3456;
}
}
{{< /highlight >}}
### 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/ {
proxy_pass http://localhost:3456;
}
}
{{< /highlight >}}
## 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
DocumentRoot /var/www/html
RewriteEngine On
RewriteRule ^\/?(config\.json|favicon\.ico|css|fonts|images|img|js|api) - [L]
RewriteRule ^(.*)$ /index.html [QSA,L]
</VirtualHost>
{{< /highlight >}}
**Note:** The apache modules `proxy` and `proxy_http` must be enabled for this.
For more details see the [frontend apache configuration]({{< ref "install-frontend.md#apache">}}).

View File

@@ -1,3 +1,13 @@
---
date: "2019-02-12:00:00+02:00"
title: "API Documentation"
draft: false
type: "doc"
menu:
sidebar:
parent: "usage"
---
# API Documentation
You can find the api docs under `http://vikunja.tld/api/v1/docs` of your instance.

View File

@@ -0,0 +1,84 @@
---
date: "2019-03-31:00:00+01:00"
title: "CLI"
draft: false
type: "doc"
menu:
sidebar:
parent: "usage"
---
# Command line interface
You can interact with Vikunja using its `cli` interface.
The following commands are available:
* [help](#help)
* [migrate](#migrate)
* [version](#version)
* [web](#web)
If you don't specify a command, the [`web`](#web) command will be executed.
All commands use the same standard [config file]({{< ref "../setup/config.md">}}).
### `help`
Shows more detailed help about any command.
Usage:
{{< highlight bash >}}
$ vikunja help [command]
{{< /highlight >}}
### `migrate`
Run all database migrations which didn't already run.
Usage:
{{< highlight bash >}}
$ vikunja migrate [flags]
$ vikunja migrate [command]
{{< /highlight >}}
#### `migrate list`
Shows a list with all database migrations.
Usage:
{{< highlight bash >}}
$ vikunja migrate list
{{< /highlight >}}
#### `migrate rollback`
Roll migrations back until a certain point.
Usage:
{{< highlight bash >}}
$ vikunja migrate rollback [flags]
{{< /highlight >}}
Flags:
* `-n`, `--name` string: The id of the migration you want to roll back until.
### `version`
Prints the version of Vikunja.
This is either the semantic version (something like `0.7`) or version + git commit hash.
Usage:
{{< highlight bash >}}
$ vikunja version
{{< /highlight >}}
### `web`
Starts Vikunja's REST api server.
Usage:
{{< highlight bash >}}
$ vikunja web
{{< /highlight >}}

View File

@@ -1,3 +1,13 @@
---
date: "2019-02-12:00:00+02:00"
title: "Errors"
draft: false
type: "doc"
menu:
sidebar:
parent: "usage"
---
# Errors
This document describes the different errors Vikunja can return.
@@ -32,14 +42,13 @@ This document describes the different errors Vikunja can return.
| 5011 | 409 | This user has already access to that namespace. |
| 6001 | 400 | The team name cannot be emtpy. |
| 6002 | 404 | The team does not exist. |
| 6003 | 400 | The provided team right is invalid. |
| 6004 | 409 | The team already has access to that namespace or list. |
| 6005 | 409 | The user is already a member of that team. |
| 6006 | 400 | Cannot delete the last team member. |
| 6007 | 403 | The team does not have access to the list to perform that action. |
| 7001 | 400 | The user right is invalid. |
| 7002 | 409 | The user already has access to that list. |
| 7003 | 403 | The user does not have access to that list. |
| 8001 | 403 | This label already exists on that task. |
| 8002 | 404 | The label does not exist. |
| 8003 | 403 | The user does not have access to this label. |
| 8003 | 403 | The user does not have access to this label. |
| 9001 | 403 | The right is invalid. |

View File

@@ -1,3 +1,13 @@
---
date: "2019-02-12:00:00+02:00"
title: "Rights"
draft: false
type: "doc"
menu:
sidebar:
parent: "usage"
---
# List and namespace rights for teams and users
Whenever you share a list or namespace with a user or team, you can specify a `rights` parameter.

21
docs/nginx.conf Normal file
View File

@@ -0,0 +1,21 @@
server {
listen 80;
server_name localhost;
charset utf-8;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 404 /docs/404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location /docs/contact {
return 301 $scheme://vikunja.io/en/contact;
}
}

1
docs/themes/vikunja vendored Submodule

Submodule docs/themes/vikunja added at 611b91ba5d

33
go.mod
View File

@@ -18,7 +18,7 @@ module code.vikunja.io/api
require (
cloud.google.com/go v0.34.0 // indirect
code.vikunja.io/web v0.0.0-20181130231148-b061c20192fb
code.vikunja.io/web v0.0.0-20190329170935-7dc1f4191c49
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf
@@ -26,45 +26,52 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835
github.com/garyburd/redigo v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/spec v0.17.2 // indirect
github.com/go-openapi/swag v0.17.2 // indirect
github.com/go-redis/redis v6.14.2+incompatible
github.com/go-sql-driver/mysql v1.4.1
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25
github.com/go-xorm/core v0.5.8
github.com/go-xorm/builder v0.3.2
github.com/go-xorm/core v0.6.0
github.com/go-xorm/tests v0.5.6 // indirect
github.com/go-xorm/xorm v0.0.0-20170930012613-29d4a0330a00
github.com/go-xorm/xorm v0.7.1
github.com/go-xorm/xorm-redis-cache v0.0.0-20180727005610-859b313566b2
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc
github.com/imdario/mergo v0.3.6
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb
github.com/karalabe/xgo v0.0.0-20181007145344-72da7d1d3970
github.com/kisielk/gotool v1.0.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/labstack/echo v3.3.5+incompatible
github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.2.8
github.com/mattn/go-colorable v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.7 // indirect
github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6 // indirect
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/mattn/go-sqlite3 v1.10.0
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/olekukonko/tablewriter v0.0.1
github.com/onsi/ginkgo v1.7.0 // indirect
github.com/onsi/gomega v1.4.3 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pkg/errors v0.8.0 // indirect
github.com/prometheus/client_golang v0.9.2
github.com/spf13/cobra v0.0.3
github.com/spf13/viper v1.2.0
github.com/stretchr/testify v1.2.2
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.3.0
github.com/swaggo/swag v1.4.1-0.20181210033626-0e12fd5eb026
github.com/urfave/cli v1.20.0 // indirect
github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
github.com/valyala/fasttemplate v1.0.1 // indirect
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3
golang.org/x/net v0.0.0-20181217023233-e147a9138326 // indirect
golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65 // indirect
golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/testfixtures.v2 v2.5.3
gopkg.in/yaml.v2 v2.2.2 // indirect
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3
honnef.co/go/tools v0.0.0-20190215041234-466a0476246c
src.techknowlogick.com/xormigrate v0.0.0-20190321151057-24497c23c09c
)

77
go.sum
View File

@@ -1,8 +1,20 @@
cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
code.vikunja.io/web v0.0.0-20181130231148-b061c20192fb h1:LROmRUOGTxOpOxKy9S6XONDnT+t0v0j8+MZCedssTCc=
code.vikunja.io/web v0.0.0-20181130231148-b061c20192fb/go.mod h1:PmGEu9qI7nbEKDn38H0SWgCoGO4GLdbjdlnWSzFi2PA=
code.vikunja.io/web v0.0.0-20190123142349-c30ef6073334 h1:a8RDvsjGxDx8w/OsADUpikHYHjZb8CoCiwEOKsQnN4w=
code.vikunja.io/web v0.0.0-20190123142349-c30ef6073334/go.mod h1:PmGEu9qI7nbEKDn38H0SWgCoGO4GLdbjdlnWSzFi2PA=
code.vikunja.io/web v0.0.0-20190324075814-1ddbccd6c866 h1:12F2fgKvTpnREgw+8GNGNQbRRXXDdI6tWg0WUyoHyaU=
code.vikunja.io/web v0.0.0-20190324075814-1ddbccd6c866/go.mod h1:PmGEu9qI7nbEKDn38H0SWgCoGO4GLdbjdlnWSzFi2PA=
code.vikunja.io/web v0.0.0-20190324080654-fcc8b45b7e2c h1:j1VTb5aOEQ3y4Y+u0RzU5lj45PpzQ/oSyVE5n/dPXB8=
code.vikunja.io/web v0.0.0-20190324080654-fcc8b45b7e2c/go.mod h1:PmGEu9qI7nbEKDn38H0SWgCoGO4GLdbjdlnWSzFi2PA=
code.vikunja.io/web v0.0.0-20190324080741-7bd881d9892a h1:nB+kG5/gq0njK9/fEtYgzvLfd+U8i1I4m3CvYC+aN9k=
code.vikunja.io/web v0.0.0-20190324080741-7bd881d9892a/go.mod h1:PmGEu9qI7nbEKDn38H0SWgCoGO4GLdbjdlnWSzFi2PA=
code.vikunja.io/web v0.0.0-20190324105229-0933ac082307 h1:t2E9v+k56RbvM5WNJF5BFFJDZrzM5l1Ua8qWdZYJAdA=
code.vikunja.io/web v0.0.0-20190324105229-0933ac082307/go.mod h1:PmGEu9qI7nbEKDn38H0SWgCoGO4GLdbjdlnWSzFi2PA=
code.vikunja.io/web v0.0.0-20190324123058-62b466dd1311 h1:3VRszH3NCTNUh+8y2ImA50ALJiE1e9KNoowv9y8mzvA=
code.vikunja.io/web v0.0.0-20190324123058-62b466dd1311/go.mod h1:PmGEu9qI7nbEKDn38H0SWgCoGO4GLdbjdlnWSzFi2PA=
code.vikunja.io/web v0.0.0-20190329170935-7dc1f4191c49 h1:onS7evj9KeCnf/3kNGlY1pXCT1BDay3WlbFddH6bwIE=
code.vikunja.io/web v0.0.0-20190329170935-7dc1f4191c49/go.mod h1:PmGEu9qI7nbEKDn38H0SWgCoGO4GLdbjdlnWSzFi2PA=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
@@ -17,7 +29,9 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLM
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cweill/gotests v1.5.2 h1:kKqmKmS2wCV3tuLnfpbiuN8OlkosQZTpCfiqmiuNAsA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f h1:WH0w/R4Yoey+04HhFxqZ6VX6I0d7RMyw5aXQ9UTvQPs=
@@ -43,16 +57,24 @@ github.com/go-openapi/swag v0.17.2 h1:K/ycE/XTUDFltNHSO32cGRUhrVGJD64o8WgAIZNyc3
github.com/go-openapi/swag v0.17.2/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-redis/redis v6.14.2+incompatible h1:UE9pLhzmWf+xHNmZsoccjXosPicuiNaInPgym8nzfg0=
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25 h1:jUX9yw6+iKrs/WuysV2M6ap/ObK/07SE/a7I2uxitwM=
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25/go.mod h1:M+P3wv0K2C+ynucGDEqJCeOTc+6DcAtiiqU8GrCksXY=
github.com/go-xorm/builder v0.3.2 h1:pSsZQRRzJNapKEAEhigw3xLmiLPeAYv5GFlpYZ8+a5I=
github.com/go-xorm/builder v0.3.2/go.mod h1:v8mE3MFBgtL+RGFNfUnAMUqqfk/Y4W5KuwCFQIEpQLk=
github.com/go-xorm/core v0.5.8 h1:vQ0ghlVGnlnFmm4SpHY+xNnPlH810paMcw+Hwz9BCqE=
github.com/go-xorm/core v0.5.8/go.mod h1:d8FJ9Br8OGyQl12MCclmYBuBqqxsyeedpXciV5Myih8=
github.com/go-xorm/core v0.6.0 h1:tp6hX+ku4OD9khFZS8VGBDRY3kfVCtelPfmkgCyHxL0=
github.com/go-xorm/core v0.6.0/go.mod h1:d8FJ9Br8OGyQl12MCclmYBuBqqxsyeedpXciV5Myih8=
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM=
github.com/go-xorm/tests v0.5.6 h1:E4nmVkKfHQAm+i2/pmOJ5JUej6sORVcvwl6/LQybif4=
github.com/go-xorm/tests v0.5.6/go.mod h1:s8J/EnVBcXQR93dN7Jy6Dwlo92HUP5nTgKWF1wGeCDg=
github.com/go-xorm/xorm v0.0.0-20170930012613-29d4a0330a00 h1:jlA1XEj8QHl6my6FUkHwRCGu/J5hQ1zkW7RqULZ2XGc=
github.com/go-xorm/xorm v0.0.0-20170930012613-29d4a0330a00/go.mod h1:i7qRPD38xj/v75UV+a9pEzr5tfRaH2ndJfwt/fGbQhs=
github.com/go-xorm/xorm v0.7.1 h1:Kj7mfuqctPdX60zuxP6EoEut0f3E6K66H6hcoxiHUMc=
github.com/go-xorm/xorm v0.7.1/go.mod h1:EHS1htMQFptzMaIHKyzqpHGw6C9Rtug75nsq6DA9unI=
github.com/go-xorm/xorm-redis-cache v0.0.0-20180727005610-859b313566b2 h1:57QbyUkFcFjipHJQstYR5owRxsQzgD8/OAO/hr4yl/E=
github.com/go-xorm/xorm-redis-cache v0.0.0-20180727005610-859b313566b2/go.mod h1:xxK9FGkFXrau9/vGdDYSOyQfSgKXBV7iHXpQfNuv6B0=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
@@ -67,6 +89,10 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb h1:D5s1HIu80AcMGcqmk7fNIVptmAubVHHaj3v5Upex6Zs=
github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb/go.mod h1:82TxjOpWQiPmywlbIaB2ZkqJoSYJdLGPgAJDvM3PbKc=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
@@ -82,6 +108,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo v3.3.5+incompatible h1:9PfxPUmasKzeJor9uQTaXLT6WUG/r+vSTmvXxvv3JO4=
github.com/labstack/echo v3.3.5+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
@@ -92,19 +120,31 @@ github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-oci8 v0.0.0-20181115070430-6eefff3c767c/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8=
github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6 h1:gheNi9lnffYyVyqQzJqY7lo+M3bCDVw5fLU/jSuCMhc=
github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -126,18 +166,26 @@ github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jO
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.2.0 h1:M4Rzxlu+RgU4pyBRKhKaVN1VeYOm8h2jgyXnAseDgCc=
github.com/spf13/viper v1.2.0/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/swaggo/swag v1.4.1-0.20181210033626-0e12fd5eb026 h1:XAOjF3QgjDUkVrPMO4rYvNptSHQgUlHwQsEdJOTxHQ8=
github.com/swaggo/swag v1.4.1-0.20181210033626-0e12fd5eb026/go.mod h1:hog2WgeMOrQ/LvQ+o1YGTeT+vWVrbi0SiIslBtxKTyM=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
@@ -146,13 +194,19 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9QelBjGE/NkhzYw/mhnr0s7nI=
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b h1:Elez2XeF2p9uyVj0yEUDqQ56NFcDtcBNkYP7yv8YbUE=
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576 h1:aUX/1G2gFSs4AsJJg2cL3HuoRhCSCz733FE5GUSuaT4=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -171,6 +225,14 @@ golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190123074212-c6b37f3e9285 h1:b5t9HsJXzMmseFB6KtTJWSEtPP8SlVI5nFdf4hnoRFY=
golang.org/x/sys v0.0.0-20190123074212-c6b37f3e9285/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65 h1:hOY+O8MxdkPV10pNf7/XEHaySCiPKxixMKUshfHsGn0=
golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081 h1:QJP9sxq2/KbTxFnGduVryxJOt6r/UVGyom3tLaqu7tc=
@@ -187,6 +249,7 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/testfixtures.v2 v2.5.3 h1:P8gDACSLJGxutzBqbzvfiXYgmQ2s00LIr4uAvWBCPAg=
gopkg.in/testfixtures.v2 v2.5.3/go.mod h1:rGPtsOtPcZhs7AsHYf1WmufW1hEsM6DXdLrYz60nrQQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -197,3 +260,9 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3 h1:LyX67rVB0kBUFoROrQfzKwdrYLH1cRzHibxdJW85J1c=
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190128043916-71123fcbb8fe h1:/GZ/onp6W295MEgrIwtlbnxmFSKGavFp7/D7tMVyuaM=
honnef.co/go/tools v0.0.0-20190128043916-71123fcbb8fe/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190215041234-466a0476246c h1:z+UFwlQ7KVwdlQTE5JjvDvfZmyyAVrEiiwau20b7X8k=
honnef.co/go/tools v0.0.0-20190215041234-466a0476246c/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
src.techknowlogick.com/xormigrate v0.0.0-20190321151057-24497c23c09c h1:fTwL7EZ3ouk3xeiPiRBYEjSPWTREb9T57bjzpRBNOpQ=
src.techknowlogick.com/xormigrate v0.0.0-20190321151057-24497c23c09c/go.mod h1:B2NutmcRaDDw4EGe7DoCwyWCELA8W+KxXPhLtgqFUaU=

58
main.go
View File

@@ -16,62 +16,8 @@
package main
import (
"code.vikunja.io/api/docs"
_ "code.vikunja.io/api/pkg/config" // To trigger its init() which initializes the config
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/routes"
"context"
"github.com/spf13/viper"
"os"
"os/signal"
"time"
)
// Version sets the version to be printed to the user. Gets overwritten by "make release" or "make build" with last git commit or tag.
var Version = "0.1"
import "code.vikunja.io/api/pkg/cmd"
func main() {
// Init logging
log.InitLogger()
// Set Engine
err := models.SetEngine()
if err != nil {
log.Log.Fatal(err.Error())
}
// Start the mail daemon
mail.StartMailDaemon()
// Version notification
log.Log.Infof("Vikunja version %s", Version)
// Additional swagger information
docs.SwaggerInfo.Version = Version
// Start the webserver
e := routes.NewEcho()
routes.RegisterRoutes(e)
// Start server
go func() {
if err := e.Start(viper.GetString("service.interface")); err != nil {
e.Logger.Info("shutting down...")
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 10 seconds.
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
log.Log.Infof("Shutting down...")
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
cmd.Execute()
}

54
pkg/cmd/cmd.go Normal file
View File

@@ -0,0 +1,54 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package cmd
import (
"code.vikunja.io/api/pkg/config"
"fmt"
"github.com/spf13/cobra"
"os"
)
// Version sets the version to be printed to the user. Gets overwritten by "make release" or "make build" with last git commit or tag.
var Version = "0.1"
func init() {
cobra.OnInitialize(config.InitConfig)
}
var rootCmd = &cobra.Command{
Use: "vikunja",
Short: "Vikunja is the to-do app to organize your life.",
Long: `Vikunja (/vɪˈkuːnjə/)
The to-do app to organize your life.
Also one of the two wild South American camelids which live in the high
alpine areas of the Andes and a relative of the llama.
Vikunja is a self-hosted To-Do list application with a web app and mobile apps for all platforms. It is licensed under the GPLv3.
Find more info at vikunja.io.`,
Run: webCmd.Run,
}
// Execute starts the application
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

59
pkg/cmd/migrate.go Normal file
View File

@@ -0,0 +1,59 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package cmd
import (
"code.vikunja.io/api/pkg/migration"
"github.com/spf13/cobra"
)
func init() {
migrateCmd.AddCommand(migrateListCmd)
migrationRollbackCmd.Flags().StringVarP(&rollbackUntilFlag, "name", "n", "", "The id of the migration you want to roll back until.")
migrationRollbackCmd.MarkFlagRequired("name")
migrateCmd.AddCommand(migrationRollbackCmd)
rootCmd.AddCommand(migrateCmd)
}
// TODO: add args to run migrations up or down, until a certain point etc
// Rollback until
// list -> Essentially just show the table, maybe with an extra column if the migration did run or not
var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Run all database migrations which didn't already run.",
Run: func(cmd *cobra.Command, args []string) {
migration.Migrate(nil)
},
}
var migrateListCmd = &cobra.Command{
Use: "list",
Short: "Show a list with all database migrations.",
Run: func(cmd *cobra.Command, args []string) {
migration.ListMigrations()
},
}
var rollbackUntilFlag string
var migrationRollbackCmd = &cobra.Command{
Use: "rollback",
Short: "Roll migrations back until a certain point.",
Run: func(cmd *cobra.Command, args []string) {
migration.Rollback(rollbackUntilFlag)
},
}

34
pkg/cmd/version.go Normal file
View File

@@ -0,0 +1,34 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Vikunja",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Vikunja api version " + Version)
},
}

87
pkg/cmd/web.go Normal file
View File

@@ -0,0 +1,87 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package cmd
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/migration"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/routes"
"code.vikunja.io/api/pkg/swagger"
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os"
"os/signal"
"time"
)
func init() {
rootCmd.AddCommand(webCmd)
}
var webCmd = &cobra.Command{
Use: "web",
Short: "Starts the rest api web server",
Run: func(cmd *cobra.Command, args []string) {
// Set logger
log.InitLogger()
// Run the migrations
migration.Migrate(nil)
// Set Engine
err := models.SetEngine()
if err != nil {
log.Log.Fatal(err.Error())
}
// Start the mail daemon
mail.StartMailDaemon()
// Version notification
fmt.Printf("Vikunja version %s\n", Version)
// Additional swagger information
swagger.SwaggerInfo.Version = Version
// Start the webserver
e := routes.NewEcho()
routes.RegisterRoutes(e)
// Start server
go func() {
if err := e.Start(viper.GetString("service.interface")); err != nil {
e.Logger.Info("shutting down...")
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 10 seconds.
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
log.Log.Infof("Shutting down...")
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
},
}

View File

@@ -27,7 +27,7 @@ import (
)
// InitConfig initializes the config, sets defaults etc.
func init() {
func InitConfig() {
// Set defaults
// Service config
@@ -56,7 +56,6 @@ func init() {
viper.SetDefault("database.password", "")
viper.SetDefault("database.database", "vikunja")
viper.SetDefault("database.path", "./vikunja.db")
viper.SetDefault("database.showqueries", false)
viper.SetDefault("database.openconnections", 100)
// Cacher
viper.SetDefault("cache.enabled", false)
@@ -77,6 +76,14 @@ func init() {
viper.SetDefault("redis.host", "localhost:6379")
viper.SetDefault("redis.password", "")
viper.SetDefault("redis.db", 0)
// Logger
viper.SetDefault("log.enabled", true)
viper.SetDefault("log.errors", "stdout")
viper.SetDefault("log.standard", "stdout")
viper.SetDefault("log.database", "off")
viper.SetDefault("log.http", "stdout")
viper.SetDefault("log.echo", "off")
viper.SetDefault("log.path", viper.GetString("service.rootpath")+"/logs")
// Init checking for environment variables
viper.SetEnvPrefix("vikunja")
@@ -84,8 +91,9 @@ func init() {
viper.AutomaticEnv()
// Load the config file
viper.AddConfigPath(exPath)
viper.AddConfigPath(viper.GetString("service.rootpath"))
viper.AddConfigPath("/etc/vikunja/")
viper.AddConfigPath("~/.config/vikunja")
viper.AddConfigPath(".")
viper.SetConfigName("config")
err = viper.ReadInConfig()

78
pkg/db/db.go Normal file
View File

@@ -0,0 +1,78 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package db
import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"fmt"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
"github.com/spf13/viper"
_ "github.com/go-sql-driver/mysql" // Because.
_ "github.com/mattn/go-sqlite3" // Because.
)
// CreateDBEngine initializes a db engine from the config
func CreateDBEngine() (engine *xorm.Engine, err error) {
// If the database type is not set, this likely means we need to initialize the config first
if viper.GetString("database.type") == "" {
config.InitConfig()
}
// Use Mysql if set
if viper.GetString("database.type") == "mysql" {
engine, err = initMysqlEngine()
if err != nil {
return
}
} else {
// Otherwise use sqlite
engine, err = initSqliteEngine()
if err != nil {
return
}
}
engine.SetMapper(core.GonicMapper{})
engine.ShowSQL(viper.GetString("log.database") != "off")
engine.SetLogger(xorm.NewSimpleLogger(log.GetLogWriter("database")))
return
}
func initMysqlEngine() (engine *xorm.Engine, err error) {
connStr := fmt.Sprintf(
"%s:%s@tcp(%s)/%s?charset=utf8&parseTime=true",
viper.GetString("database.user"),
viper.GetString("database.password"),
viper.GetString("database.host"),
viper.GetString("database.database"))
engine, err = xorm.NewEngine("mysql", connStr)
engine.SetMaxOpenConns(viper.GetInt("database.openconnections"))
return
}
func initSqliteEngine() (engine *xorm.Engine, err error) {
path := viper.GetString("database.path")
if path == "" {
path = "./db.db"
}
return xorm.NewEngine("sqlite3", path)
}

View File

@@ -18,19 +18,84 @@ package log
import (
"github.com/op/go-logging"
"github.com/spf13/viper"
"io"
"log"
"os"
"time"
)
// ErrFmt holds the format for all the console logging
const ErrFmt = `${time_rfc3339_nano}: ${level} ` + "\t" + `▶ ${prefix} ${short_file}:${line}`
// WebFmt holds the format for all logging related to web requests
const WebFmt = `${time_rfc3339_nano}: WEB ` + "\t" + `▶ ${remote_ip} ${id} ${method} ${status} ${uri} ${latency_human} - ${user_agent}`
// Fmt is the general log format
const Fmt = `%{color}%{time:` + time.RFC3339Nano + `}: %{level}` + "\t" + `▶ %{shortpkg}/%{shortfunc} %{id:03x}%{color:reset} %{message}`
// Log is the handler for the logger
var Log = logging.MustGetLogger("vikunja")
var format = logging.MustStringFormatter(
`%{color}%{time:2006-01-02 15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
)
// InitLogger initializes the global log handler
func InitLogger() {
backend := logging.NewLogBackend(os.Stderr, "", 0)
backendFormatter := logging.NewBackendFormatter(backend, format)
logging.SetBackend(backendFormatter)
if !viper.GetBool("log.enabled") {
// Disable all logging when loggin in general is disabled, overwriting everything a user might have set.
viper.Set("log.errors", "off")
viper.Set("log.standard", "off")
viper.Set("log.database", "off")
viper.Set("log.http", "off")
viper.Set("log.echo", "off")
return
}
if viper.GetString("log.errors") == "file" || viper.GetString("log.standard") == "file" {
err := os.Mkdir(viper.GetString("log.path"), 0744)
if err != nil && !os.IsExist(err) {
log.Fatal("Could not create log folder: ", err.Error())
}
}
var logBackends []logging.Backend
// We define our two backends
if viper.GetString("log.standard") != "off" {
stdWriter := GetLogWriter("standard")
stdBackend := logging.NewLogBackend(stdWriter, "", 0)
// Set the standard backend
logBackends = append(logBackends, logging.NewBackendFormatter(stdBackend, logging.MustStringFormatter(Fmt+"\n")))
}
if viper.GetString("log.error") != "off" {
errWriter := GetLogWriter("error")
errBackend := logging.NewLogBackend(errWriter, "", 0)
// Only warnings and more severe messages should go to the error backend
errBackendLeveled := logging.AddModuleLevel(errBackend)
errBackendLeveled.SetLevel(logging.WARNING, "")
logBackends = append(logBackends, errBackendLeveled)
}
// Set our backends
logging.SetBackend(logBackends...)
}
// GetLogWriter returns the writer to where the normal log goes, depending on the config
func GetLogWriter(logfile string) (writer io.Writer) {
writer = os.Stderr // Set the default case to prevent nil pointer panics
switch viper.GetString("log." + logfile) {
case "file":
f, err := os.OpenFile(viper.GetString("log.path")+"/"+logfile+".log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
writer = f
case "stderr":
writer = os.Stderr
case "stdout":
default:
writer = os.Stdout
}
return
}

View File

@@ -0,0 +1,44 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"github.com/go-xorm/xorm"
"src.techknowlogick.com/xormigrate"
)
// Used for rollback
type tasksReminderDateMigration20190324205606 struct {
ReminderUnix int64 `xorm:"int(11) INDEX"`
}
func (tasksReminderDateMigration20190324205606) TableName() string {
return "tasks"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20190324205606",
Description: "Remove reminders_unix from tasks",
Migrate: func(tx *xorm.Engine) error {
return dropTableColum(tx, "tasks", "reminders_unix")
},
Rollback: func(tx *xorm.Engine) error {
return tx.Sync2(tasksReminderDateMigration20190324205606{})
},
})
}

View File

@@ -1,5 +1,5 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2018 Vikunja and contributors. All rights reserved.
// Copyright 2019 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 General Public License as published by
@@ -14,30 +14,31 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
package migration
// UserRight defines the rights users can have for lists/namespaces
type UserRight int
// define unknown user right
const (
UserRightUnknown = -1
import (
"github.com/go-xorm/xorm"
"src.techknowlogick.com/xormigrate"
)
// Enumerate all the user rights
const (
// Can read lists in a User
UserRightRead UserRight = iota
// Can write tasks in a User like lists and todo tasks. Cannot create new lists.
UserRightWrite
// Can manage a list/namespace, can do everything
UserRightAdmin
)
func (r UserRight) isValid() error {
if r != UserRightAdmin && r != UserRightRead && r != UserRightWrite {
return ErrInvalidUserRight{r}
}
return nil
// Used for rollback
type teamMembersMigration20190328074430 struct {
Updated int64 `xorm:"updated"`
}
func (teamMembersMigration20190328074430) TableName() string {
return "team_members"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20190328074430",
Description: "Remove updated from team_members",
Migrate: func(tx *xorm.Engine) error {
return dropTableColum(tx, "team_members", "updated")
},
Rollback: func(tx *xorm.Engine) error {
return tx.Sync2(teamMembersMigration20190328074430{})
},
})
}

124
pkg/migration/migration.go Normal file
View File

@@ -0,0 +1,124 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"github.com/go-xorm/xorm"
"github.com/olekukonko/tablewriter"
"github.com/spf13/viper"
"os"
"sort"
"src.techknowlogick.com/xormigrate"
)
// You can get the id string for new migrations by running `date +%Y%m%d%H%M%S` on a unix system.
var migrations []*xormigrate.Migration
// A helper function because we need a migration in various places which we can't really solve with an init() function.
func initMigration(x *xorm.Engine) *xormigrate.Xormigrate {
// Get our own xorm engine if we don't have one
if x == nil {
var err error
x, err = db.CreateDBEngine()
if err != nil {
log.Log.Criticalf("Could not connect to db: %v", err.Error())
return nil
}
}
// Because init() does not guarantee the order in which these are added to the slice,
// we need to sort them to ensure that they are in order
sort.Slice(migrations, func(i, j int) bool {
return migrations[i].ID < migrations[j].ID
})
m := xormigrate.New(x, migrations)
m.NewLogger(log.GetLogWriter("database"))
m.InitSchema(initSchema)
return m
}
// Migrate runs all migrations
func Migrate(x *xorm.Engine) {
m := initMigration(x)
err := m.Migrate()
if err != nil {
log.Log.Fatalf("Migration failed: %v", err)
}
log.Log.Info("Ran all migrations successfully.")
}
// ListMigrations pretty-prints a list with all migrations.
func ListMigrations() {
x, err := db.CreateDBEngine()
if err != nil {
log.Log.Fatalf("Could not connect to db: %v", err.Error())
}
ms := []*xormigrate.Migration{}
err = x.Find(&ms)
if err != nil {
log.Log.Fatalf("Error getting migration table: %v", err.Error())
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Description"})
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor})
for _, m := range ms {
table.Append([]string{m.ID, m.Description})
}
table.Render()
}
// Rollback rolls back all migrations until a certain point.
func Rollback(migrationID string) {
m := initMigration(nil)
err := m.RollbackTo(migrationID)
if err != nil {
log.Log.Fatalf("Could not rollback: %v", err)
}
log.Log.Info("Rolled back successfully.")
}
// Deletes a column from a table. All arguments are strings, to let them be standalone and not depending on any struct.
func dropTableColum(x *xorm.Engine, tableName, col string) error {
switch viper.GetString("database.type") {
case "sqlite":
log.Log.Warning("Unable to drop columns in SQLite")
case "mysql":
_, err := x.Exec("ALTER TABLE " + tableName + " DROP COLUMN " + col)
if err != nil {
return err
}
default:
log.Log.Fatal("Unknown db.")
}
return nil
}
func initSchema(tx *xorm.Engine) error {
return tx.Sync2(
models.GetTables()...,
)
}

View File

@@ -17,7 +17,6 @@
package models
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/web"
"github.com/imdario/mergo"
)
@@ -53,19 +52,16 @@ func (bt *BulkTask) checkIfTasksAreOnTheSameList() (err error) {
}
// CanUpdate checks if a user is allowed to update a task
func (bt *BulkTask) CanUpdate(a web.Auth) bool {
func (bt *BulkTask) CanUpdate(a web.Auth) (bool, error) {
err := bt.checkIfTasksAreOnTheSameList()
if err != nil {
log.Log.Error("Error occurred during CanUpdate for BulkTask: %s", err)
return false
return false, err
}
doer := getUserForRights(a)
// A user can update an task if he has write acces to its list
l := &List{ID: bt.Tasks[0].ListID}
l.ReadOne()
return l.CanWrite(doer)
return l.CanWrite(a)
}
// Update updates a bunch of tasks at once
@@ -109,7 +105,7 @@ func (bt *BulkTask) Update() (err error) {
}
// And because a false is considered to be a null value, we need to explicitly check that case here.
if bt.ListTask.Done == false {
if !bt.ListTask.Done {
oldtask.Done = false
}

View File

@@ -57,7 +57,7 @@ func TestBulkTask_Update(t *testing.T) {
Tasks: tt.fields.Tasks,
ListTask: tt.fields.ListTask,
}
allowed := bt.CanUpdate(tt.fields.User)
allowed, _ := bt.CanUpdate(tt.fields.User)
if !allowed != tt.wantForbidden {
t.Errorf("BulkTask.Update() want forbidden, got %v, want %v", allowed, tt.wantForbidden)
}

View File

@@ -699,29 +699,6 @@ func (err ErrTeamDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."}
}
// ErrInvalidTeamRight represents an error where a team right is invalid
type ErrInvalidTeamRight struct {
Right TeamRight
}
// IsErrInvalidTeamRight checks if an error is ErrInvalidTeamRight.
func IsErrInvalidTeamRight(err error) bool {
_, ok := err.(ErrInvalidTeamRight)
return ok
}
func (err ErrInvalidTeamRight) Error() string {
return fmt.Sprintf("Team right invalid [Right: %d]", err.Right)
}
// ErrCodeInvalidTeamRight holds the unique world-error code of this error
const ErrCodeInvalidTeamRight = 6003
// HTTPError holds the http error description
func (err ErrInvalidTeamRight) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeInvalidTeamRight, Message: "The team right is invalid."}
}
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a list/namespace
type ErrTeamAlreadyHasAccess struct {
TeamID int64
@@ -822,29 +799,6 @@ func (err ErrTeamDoesNotHaveAccessToList) HTTPError() web.HTTPError {
// User <-> List errors
// ====================
// ErrInvalidUserRight represents an error where a user right is invalid
type ErrInvalidUserRight struct {
Right UserRight
}
// IsErrInvalidUserRight checks if an error is ErrInvalidUserRight.
func IsErrInvalidUserRight(err error) bool {
_, ok := err.(ErrInvalidUserRight)
return ok
}
func (err ErrInvalidUserRight) Error() string {
return fmt.Sprintf("User right is invalid [Right: %d]", err.Right)
}
// ErrCodeInvalidUserRight holds the unique world-error code of this error
const ErrCodeInvalidUserRight = 7001
// HTTPError holds the http error description
func (err ErrInvalidUserRight) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeInvalidUserRight, Message: "The user right is invalid."}
}
// ErrUserAlreadyHasAccess represents an error where a user already has access to a list/namespace
type ErrUserAlreadyHasAccess struct {
UserID int64
@@ -979,3 +933,34 @@ func (err ErrUserHasNoAccessToLabel) HTTPError() web.HTTPError {
Message: "You don't have access to this label.",
}
}
// ========
// Rights
// ========
// ErrInvalidRight represents an error where a right is invalid
type ErrInvalidRight struct {
Right Right
}
// IsErrInvalidRight checks if an error is ErrInvalidRight.
func IsErrInvalidRight(err error) bool {
_, ok := err.(ErrInvalidRight)
return ok
}
func (err ErrInvalidRight) Error() string {
return fmt.Sprintf(" right invalid [Right: %d]", err.Right)
}
// ErrCodeInvalidRight holds the unique world-error code of this error
const ErrCodeInvalidRight = 9001
// HTTPError holds the http error description
func (err ErrInvalidRight) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeInvalidRight,
Message: "The right is invalid.",
}
}

View File

@@ -1,3 +1,4 @@
- id: 1
task_id: 1
label_id: 4
label_id: 4
created: 0

View File

@@ -1,12 +1,20 @@
- id: 1
title: 'Label #1'
created_by_id: 1
updated: 0
created: 0
- id: 2
title: 'Label #2'
created_by_id: 1
updated: 0
created: 0
- id: 3
title: 'Label #3 - other user'
created_by_id: 2
updated: 0
created: 0
- id: 4
title: 'Label #4 - visible via other task'
created_by_id: 2
created_by_id: 2
updated: 0
created: 0

View File

@@ -4,27 +4,37 @@
description: Lorem Ipsum
owner_id: 1
namespace_id: 1
updated: 0
created: 0
-
id: 2
title: Test2
description: Lorem Ipsum
owner_id: 3
namespace_id: 1
updated: 0
created: 0
-
id: 3
title: Test3
description: Lorem Ipsum
owner_id: 3
namespace_id: 2
updated: 0
created: 0
-
id: 4
title: Test4
description: Lorem Ipsum
owner_id: 3
namespace_id: 3
updated: 0
created: 0
-
id: 5
title: Test5
description: Lorem Ipsum
owner_id: 5
namespace_id: 5
namespace_id: 5
updated: 0
created: 0

View File

@@ -3,13 +3,19 @@
name: testnamespace
description: Lorem Ipsum
owner_id: 1
updated: 0
created: 0
-
id: 2
name: testnamespace2
description: Lorem Ipsum
owner_id: 2
updated: 0
created: 0
-
id: 3
name: testnamespace3
description: Lorem Ipsum
owner_id: 3
owner_id: 3
updated: 0
created: 0

View File

@@ -1,6 +1,10 @@
- id: 1
team_id: 1
list_id: 3
updated: 0
created: 0
- id: 2
team_id: 2
list_id: 3
updated: 0
created: 0

View File

@@ -2,6 +2,8 @@
team_id: 1
user_id: 1
admin: true
created: 0
-
team_id: 1
user_id: 2
created: 0

View File

@@ -1,6 +1,10 @@
- id: 1
team_id: 1
namespace_id: 3
updated: 0
created: 0
- id: 2
team_id: 2
namespace_id: 3
updated: 0
created: 0

View File

@@ -3,26 +3,36 @@
username: 'user1'
password: '1234'
email: 'user1@example.com'
updated: 0
created: 0
-
id: 2
username: 'user2'
password: '1234'
email: 'user2@example.com'
updated: 0
created: 0
-
id: 3
username: 'user3'
password: '1234'
email: 'user3@example.com'
updated: 0
created: 0
-
id: 4
username: 'user4'
password: '1234'
email: 'user4@example.com'
email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael
updated: 0
created: 0
-
id: 5
username: 'user5'
password: '1234'
email: 'user4@example.com'
email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael
is_active: false
is_active: false
updated: 0
created: 0

View File

@@ -1,6 +1,10 @@
- id: 1
user_id: 1
list_id: 3
updated: 0
created: 0
- id: 2
user_id: 2
list_id: 3
updated: 0
created: 0

View File

@@ -1,6 +1,10 @@
- id: 1
user_id: 1
namespace_id: 3
updated: 0
created: 0
- id: 2
user_id: 2
namespace_id: 3
updated: 0
created: 0

View File

@@ -27,18 +27,18 @@ type Label struct {
// The title of the lable. You'll see this one on tasks associated with it.
Title string `xorm:"varchar(250) not null" json:"title" valid:"runelength(3|250)" minLength:"3" maxLength:"250"`
// The label description.
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)" maxLength:"250"`
Description string `xorm:"varchar(250) null" json:"description" valid:"runelength(0|250)" maxLength:"250"`
// The color this label has
HexColor string `xorm:"varchar(6)" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
CreatedByID int64 `xorm:"int(11) not null" json:"-"`
// The user who created this label
CreatedBy *User `xorm:"-" json:"created_by"`
// A unix timestamp when this label was created. You cannot change this value.
Created int64 `xorm:"created" json:"created"`
Created int64 `xorm:"created not null" json:"created"`
// A unix timestamp when this label was last updated. You cannot change this value.
Updated int64 `xorm:"updated" json:"updated"`
Updated int64 `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`

View File

@@ -17,51 +17,48 @@
package models
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/web"
"github.com/go-xorm/builder"
)
// CanUpdate checks if a user can update a label
func (l *Label) CanUpdate(a web.Auth) bool {
func (l *Label) CanUpdate(a web.Auth) (bool, error) {
return l.isLabelOwner(a) // Only owners should be allowed to update a label
}
// CanDelete checks if a user can delete a label
func (l *Label) CanDelete(a web.Auth) bool {
func (l *Label) CanDelete(a web.Auth) (bool, error) {
return l.isLabelOwner(a) // Only owners should be allowed to delete a label
}
// CanRead checks if a user can read a label
func (l *Label) CanRead(a web.Auth) bool {
func (l *Label) CanRead(a web.Auth) (bool, error) {
return l.hasAccessToLabel(a)
}
// CanCreate checks if the user can create a label
// Currently a dummy.
func (l *Label) CanCreate(a web.Auth) bool {
return true
func (l *Label) CanCreate(a web.Auth) (bool, error) {
return true, nil
}
func (l *Label) isLabelOwner(a web.Auth) bool {
func (l *Label) isLabelOwner(a web.Auth) (bool, error) {
u := getUserForRights(a)
lorig, err := getLabelByIDSimple(l.ID)
if err != nil {
log.Log.Errorf("Error occurred during isLabelOwner for Label: %v", err)
return false
return false, err
}
return lorig.CreatedByID == u.ID
return lorig.CreatedByID == u.ID, nil
}
// Helper method to check if a user can see a specific label
func (l *Label) hasAccessToLabel(a web.Auth) bool {
func (l *Label) hasAccessToLabel(a web.Auth) (bool, error) {
u := getUserForRights(a)
// Get all tasks
taskIDs, err := getUserTaskIDs(u)
if err != nil {
log.Log.Errorf("Error occurred during hasAccessToLabel for Label: %v", err)
return false
return false, err
}
// Get all labels associated with these tasks
@@ -72,12 +69,6 @@ func (l *Label) hasAccessToLabel(a web.Auth) bool {
Where("label_task.label_id != null OR labels.created_by_id = ?", u.ID).
Or(builder.In("label_task.task_id", taskIDs)).
And("labels.id = ?", l.ID).
GroupBy("labels.id").
Exist(&labels)
if err != nil {
log.Log.Errorf("Error occurred during hasAccessToLabel for Label: %v", err)
return false
}
return has
return has, err
}

View File

@@ -29,7 +29,7 @@ type LabelTask struct {
// The label id you want to associate with a task.
LabelID int64 `xorm:"int(11) INDEX not null" json:"label_id" param:"label"`
// A unix timestamp when this task was created. You cannot change this value.
Created int64 `xorm:"created" json:"created"`
Created int64 `xorm:"created not null" json:"created"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
@@ -49,7 +49,7 @@ func (LabelTask) TableName() string {
// @Security JWTKeyAuth
// @Param task path int true "Task ID"
// @Param label path int true "Label ID"
// @Success 200 {object} models.Label "The label was successfully removed."
// @Success 200 {object} models.Message "The label was successfully removed."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to remove the label."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found."
// @Failure 500 {object} models.Message "Internal error"
@@ -67,8 +67,8 @@ func (lt *LabelTask) Delete() (err error) {
// @Produce json
// @Security JWTKeyAuth
// @Param task path int true "Task ID"
// @Param label body models.Label true "The label object"
// @Success 200 {object} models.Label "The created label relation object."
// @Param label body models.LabelTask true "The label object"
// @Success 200 {object} models.LabelTask "The created label relation object."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid label object provided."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to add the label."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "The label does not exist."
@@ -109,12 +109,12 @@ func (lt *LabelTask) ReadAll(search string, a web.Auth, page int) (labels interf
}
// Check if the user has the right to see the task
task, err := GetListTaskByID(lt.TaskID)
task := ListTask{ID: lt.TaskID}
canRead, err := task.CanRead(a)
if err != nil {
return nil, err
}
if !task.CanRead(a) {
if !canRead {
return nil, ErrNoRightToSeeTask{lt.TaskID, u.ID}
}
@@ -153,7 +153,7 @@ func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, err
requestOrNil = "label_task.label_id != null OR labels.created_by_id = ?"
}
// Get all labels associated with these labels
// Get all labels associated with these tasks
var labels []*labelWithTaskID
err = x.Table("labels").
Select("labels.*, label_task.task_id").
@@ -161,7 +161,7 @@ func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, err
Where(requestOrNil, uidOrNil).
Or(builder.In("label_task.task_id", opts.TaskIDs)).
And("labels.title LIKE ?", "%"+opts.Search+"%").
GroupBy("labels.id").
GroupBy("labels.id,label_task.task_id"). // This filters out doubles
Limit(getLimitFromPageIndex(opts.Page)).
Find(&labels)
if err != nil {
@@ -204,10 +204,8 @@ func (t *ListTask) updateTaskLabels(creator web.Auth, labels []*Label) (err erro
// Make a hashmap of the new labels for easier comparison
newLabels := make(map[int64]*Label, len(labels))
var allLabelIDs []int64
for _, newLabel := range labels {
newLabels[newLabel.ID] = newLabel
allLabelIDs = append(allLabelIDs, newLabel.ID)
}
// Get old labels to delete
@@ -258,7 +256,11 @@ func (t *ListTask) updateTaskLabels(creator web.Auth, labels []*Label) (err erro
}
// Check if the user has the rights to see the label he is about to add
if !label.hasAccessToLabel(creator) {
hasAccessToLabel, err := label.hasAccessToLabel(creator)
if err != nil {
return err
}
if !hasAccessToLabel {
user, _ := creator.(*User)
return ErrUserHasNoAccessToLabel{LabelID: l.ID, UserID: user.ID}
}
@@ -275,7 +277,7 @@ func (t *ListTask) updateTaskLabels(creator web.Auth, labels []*Label) (err erro
// LabelTaskBulk is a helper struct to update a bunch of labels at once
type LabelTaskBulk struct {
// All labels you want to update at once. Works exactly like you would update labels while updateing a list.
// All labels you want to update at once.
Labels []*Label `json:"labels"`
TaskID int64 `json:"-" param:"listtask"`
@@ -284,8 +286,8 @@ type LabelTaskBulk struct {
}
// Create updates a bunch of labels on a task at once
// @Summary Add multiple new labels to a task
// @Description Adds multiple new labels to a task.
// @Summary Update all labels on a task.
// @Description Updates all labels on a task. Every label which is not passed but exists on the task will be deleted. Every label which does not exist on the task will be added. All labels which are passed and already exist on the task won't be touched.
// @tags labels
// @Accept json
// @Produce json
@@ -301,5 +303,5 @@ func (ltb *LabelTaskBulk) Create(a web.Auth) (err error) {
if err != nil {
return
}
return task.updateTaskLabels(a, task.Labels)
return task.updateTaskLabels(a, ltb.Labels)
}

View File

@@ -17,51 +17,61 @@
package models
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/web"
)
// CanCreate checks if a user can add a label to a task
func (lt *LabelTask) CanCreate(a web.Auth) bool {
func (lt *LabelTask) CanCreate(a web.Auth) (bool, error) {
label, err := getLabelByIDSimple(lt.LabelID)
if err != nil {
log.Log.Errorf("Error during CanCreate for LabelTask: %v", err)
return false
return false, err
}
return label.hasAccessToLabel(a) && canDoLabelTask(lt.TaskID, a)
hasAccessTolabel, err := label.hasAccessToLabel(a)
if err != nil || !hasAccessTolabel { // If the user doesn't have access to the label, we can error out here
return false, err
}
canDoLabelTask, err := canDoLabelTask(lt.TaskID, a)
if err != nil {
return false, err
}
return hasAccessTolabel && canDoLabelTask, nil
}
// CanDelete checks if a user can delete a label from a task
func (lt *LabelTask) CanDelete(a web.Auth) bool {
if !canDoLabelTask(lt.TaskID, a) {
return false
func (lt *LabelTask) CanDelete(a web.Auth) (bool, error) {
canDoLabelTask, err := canDoLabelTask(lt.TaskID, a)
if err != nil {
return false, err
}
if !canDoLabelTask {
return false, nil
}
// We don't care here if the label exists or not. The only relevant thing here is if the relation already exists,
// throw an error.
exists, err := x.Exist(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
if err != nil {
log.Log.Errorf("Error during CanDelete for LabelTask: %v", err)
return false
return false, err
}
return exists
return exists, err
}
// CanCreate determines if a user can update a labeltask
func (ltb *LabelTaskBulk) CanCreate(a web.Auth) bool {
func (ltb *LabelTaskBulk) CanCreate(a web.Auth) (bool, error) {
return canDoLabelTask(ltb.TaskID, a)
}
// Helper function to check if a user can write to a task
// + is able to see the label
// always the same check for either deleting or adding a label to a task
func canDoLabelTask(taskID int64, a web.Auth) bool {
func canDoLabelTask(taskID int64, a web.Auth) (bool, error) {
// A user can add a label to a task if he can write to the task
task, err := getTaskByIDSimple(taskID)
if err != nil {
log.Log.Error("Error occurred during canDoLabelTask for LabelTask: %v", err)
return false
return false, err
}
return task.CanUpdate(a)
}

View File

@@ -49,7 +49,6 @@ func TestLabelTask_ReadAll(t *testing.T) {
ID: 2,
Username: "user2",
Password: "1234",
Email: "user2@example.com",
},
},
},
@@ -178,7 +177,8 @@ func TestLabelTask_Create(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanCreate(tt.args.a) && !tt.wantForbidden {
allowed, _ := l.CanCreate(tt.args.a)
if !allowed && !tt.wantForbidden {
t.Errorf("LabelTask.CanCreate() forbidden, want %v", tt.wantForbidden)
}
err := l.Create(tt.args.a)
@@ -264,7 +264,8 @@ func TestLabelTask_Delete(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanDelete(tt.auth) && !tt.wantForbidden {
allowed, _ := l.CanDelete(tt.auth)
if !allowed && !tt.wantForbidden {
t.Errorf("LabelTask.CanDelete() forbidden, want %v", tt.wantForbidden)
}
err := l.Delete()

View File

@@ -46,7 +46,6 @@ func TestLabel_ReadAll(t *testing.T) {
ID: 1,
Username: "user1",
Password: "1234",
Email: "user1@example.com",
}
tests := []struct {
name string
@@ -87,7 +86,6 @@ func TestLabel_ReadAll(t *testing.T) {
ID: 2,
Username: "user2",
Password: "1234",
Email: "user2@example.com",
},
},
},
@@ -141,7 +139,6 @@ func TestLabel_ReadOne(t *testing.T) {
ID: 1,
Username: "user1",
Password: "1234",
Email: "user1@example.com",
}
tests := []struct {
name string
@@ -196,7 +193,6 @@ func TestLabel_ReadOne(t *testing.T) {
ID: 2,
Username: "user2",
Password: "1234",
Email: "user2@example.com",
},
},
auth: &User{ID: 1},
@@ -217,7 +213,8 @@ func TestLabel_ReadOne(t *testing.T) {
Rights: tt.fields.Rights,
}
if !l.CanRead(tt.auth) && !tt.wantForbidden {
allowed, _ := l.CanRead(tt.auth)
if !allowed && !tt.wantForbidden {
t.Errorf("Label.CanRead() forbidden, want %v", tt.wantForbidden)
}
err := l.ReadOne()
@@ -283,7 +280,8 @@ func TestLabel_Create(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanCreate(tt.args.a) && !tt.wantForbidden {
allowed, _ := l.CanCreate(tt.args.a)
if !allowed && !tt.wantForbidden {
t.Errorf("Label.CanCreate() forbidden, want %v", tt.wantForbidden)
}
if err := l.Create(tt.args.a); (err != nil) != tt.wantErr {
@@ -364,7 +362,8 @@ func TestLabel_Update(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanUpdate(tt.auth) && !tt.wantForbidden {
allowed, _ := l.CanUpdate(tt.auth)
if !allowed && !tt.wantForbidden {
t.Errorf("Label.CanUpdate() forbidden, want %v", tt.wantForbidden)
}
if err := l.Update(); (err != nil) != tt.wantErr {
@@ -441,7 +440,8 @@ func TestLabel_Delete(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanDelete(tt.auth) && !tt.wantForbidden {
allowed, _ := l.CanDelete(tt.auth)
if !allowed && !tt.wantForbidden {
t.Errorf("Label.CanDelete() forbidden, want %v", tt.wantForbidden)
}
if err := l.Delete(); (err != nil) != tt.wantErr {

View File

@@ -25,11 +25,11 @@ type List struct {
// The unique, numeric id of this list.
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"list"`
// The title of the list. You'll see this in the namespace overview.
Title string `xorm:"varchar(250)" json:"title" valid:"required,runelength(3|250)" minLength:"3" maxLength:"250"`
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(3|250)" minLength:"3" maxLength:"250"`
// The description of the list.
Description string `xorm:"varchar(1000)" json:"description" valid:"runelength(0|1000)" maxLength:"1000"`
OwnerID int64 `xorm:"int(11) INDEX" json:"-"`
NamespaceID int64 `xorm:"int(11) INDEX" json:"-" param:"namespace"`
Description string `xorm:"varchar(1000) null" json:"description" valid:"runelength(0|1000)" maxLength:"1000"`
OwnerID int64 `xorm:"int(11) INDEX not null" json:"-"`
NamespaceID int64 `xorm:"int(11) INDEX not null" json:"-" param:"namespace"`
// The user who created this list.
Owner User `xorm:"-" json:"owner" valid:"-"`
@@ -37,9 +37,9 @@ type List struct {
Tasks []*ListTask `xorm:"-" json:"tasks"`
// A unix timestamp when this list was created. You cannot change this value.
Created int64 `xorm:"created" json:"created"`
Created int64 `xorm:"created not null" json:"created"`
// A unix timestamp when this list was last updated. You cannot change this value.
Updated int64 `xorm:"updated" json:"updated"`
Updated int64 `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
@@ -58,12 +58,12 @@ func GetListsByNamespaceID(nID int64, doer *User) (lists []*List, err error) {
Or("ul.user_id = ?", doer.ID).
GroupBy("l.id").
Find(&lists)
if err != nil {
return nil, err
}
} else {
err = x.Where("namespace_id = ?", nID).Find(&lists)
}
if err != nil {
return nil, err
}
// get more list details
err = AddListDetails(lists)
@@ -113,11 +113,6 @@ func (l *List) ReadAll(search string, a web.Auth, page int) (interface{}, error)
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [get]
func (l *List) ReadOne() (err error) {
err = l.GetSimpleByID()
if err != nil {
return err
}
// Get list tasks
l.Tasks, err = GetTasksByListID(l.ID)
if err != nil {

View File

@@ -37,7 +37,8 @@ func TestList_Create(t *testing.T) {
}
// Check if the user can create
assert.True(t, dummylist.CanCreate(&doer))
allowed, _ := dummylist.CanCreate(&doer)
assert.True(t, allowed)
// Create it
err = dummylist.Create(&doer)
@@ -45,6 +46,9 @@ func TestList_Create(t *testing.T) {
// Get the list
newdummy := List{ID: dummylist.ID}
canRead, err := newdummy.CanRead(&doer)
assert.NoError(t, err)
assert.True(t, canRead)
err = newdummy.ReadOne()
assert.NoError(t, err)
assert.Equal(t, dummylist.Title, newdummy.Title)
@@ -52,16 +56,19 @@ func TestList_Create(t *testing.T) {
assert.Equal(t, dummylist.OwnerID, doer.ID)
// Check if the user can see it
assert.True(t, dummylist.CanRead(&doer))
allowed, _ = dummylist.CanRead(&doer)
assert.True(t, allowed)
// Try updating a list
assert.True(t, dummylist.CanUpdate(&doer))
allowed, _ = dummylist.CanUpdate(&doer)
assert.True(t, allowed)
dummylist.Description = "Lorem Ipsum dolor sit amet."
err = dummylist.Update()
assert.NoError(t, err)
// Delete it
assert.True(t, dummylist.CanDelete(&doer))
allowed, _ = dummylist.CanDelete(&doer)
assert.True(t, allowed)
err = dummylist.Delete()
assert.NoError(t, err)

View File

@@ -48,6 +48,11 @@ func CreateOrUpdateList(list *List) (err error) {
return
}
err = list.GetSimpleByID()
if err != nil {
return
}
err = list.ReadOne()
return

View File

@@ -17,121 +17,143 @@
package models
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/web"
"github.com/go-xorm/builder"
)
// IsAdmin returns whether the user has admin rights on the list or not
func (l *List) IsAdmin(a web.Auth) bool {
u := getUserForRights(a)
// Owners are always admins
if l.OwnerID == u.ID {
return true
}
// Check individual rights
if l.checkListUserRight(u, UserRightAdmin) {
return true
}
return l.checkListTeamRight(u, TeamRightAdmin)
}
// CanWrite return whether the user can write on that list or not
func (l *List) CanWrite(a web.Auth) bool {
func (l *List) CanWrite(a web.Auth) (bool, error) {
// Get the list and check the right
originalList := &List{ID: l.ID}
err := originalList.GetSimpleByID()
if err != nil {
return false, err
}
user := getUserForRights(a)
// Admins always have write access
if l.IsAdmin(user) {
return true
// Check if the user is either owner or can write to the list
if originalList.isOwner(user) {
return true, nil
}
// Check individual rights
if l.checkListUserRight(user, UserRightWrite) {
return true
}
return l.checkListTeamRight(user, TeamRightWrite)
return originalList.checkRight(user, RightWrite, RightAdmin)
}
// CanRead checks if a user has read access to a list
func (l *List) CanRead(a web.Auth) bool {
func (l *List) CanRead(a web.Auth) (bool, error) {
user := getUserForRights(a)
// Admins always have read access
if l.IsAdmin(user) {
return true
// Check if the user is either owner or can read
if err := l.GetSimpleByID(); err != nil {
return false, err
}
// Check individual rights
if l.checkListUserRight(user, UserRightRead) {
return true
if l.isOwner(user) {
return true, nil
}
return l.checkRight(user, RightRead, RightWrite, RightAdmin)
}
if l.checkListTeamRight(user, TeamRightRead) {
return true
}
// Users who are able to write should also be able to read
// CanUpdate checks if the user can update a list
func (l *List) CanUpdate(a web.Auth) (bool, error) {
return l.CanWrite(a)
}
// CanDelete checks if the user can delete a list
func (l *List) CanDelete(a web.Auth) bool {
doer := getUserForRights(a)
return l.IsAdmin(doer)
}
// CanUpdate checks if the user can update a list
func (l *List) CanUpdate(a web.Auth) bool {
doer := getUserForRights(a)
return l.CanWrite(doer)
func (l *List) CanDelete(a web.Auth) (bool, error) {
return l.IsAdmin(a)
}
// CanCreate checks if the user can update a list
func (l *List) CanCreate(a web.Auth) bool {
func (l *List) CanCreate(a web.Auth) (bool, error) {
// A user can create a list if he has write access to the namespace
n, _ := GetNamespaceByID(l.NamespaceID)
n := &Namespace{ID: l.NamespaceID}
return n.CanWrite(a)
}
func (l *List) checkListTeamRight(user *User, r TeamRight) bool {
// IsAdmin returns whether the user has admin rights on the list or not
func (l *List) IsAdmin(a web.Auth) (bool, error) {
user := getUserForRights(a)
originalList := &List{ID: l.ID}
err := originalList.GetSimpleByID()
if err != nil {
return false, err
}
// Check all the things
// Check if the user is either owner or can write to the list
// Owners are always admins
if originalList.isOwner(user) {
return true, nil
}
return originalList.checkRight(user, RightAdmin)
}
// Little helper function to check if a user is list owner
func (l *List) isOwner(u *User) bool {
return l.OwnerID == u.ID
}
// Checks n different rights for any given user
func (l *List) checkRight(user *User, rights ...Right) (bool, error) {
/*
The following loop creates an sql condition like this one:
(ul.user_id = 1 AND ul.right = 1) OR (un.user_id = 1 AND un.right = 1) OR
(tm.user_id = 1 AND tn.right = 1) OR (tm2.user_id = 1 AND tl.right = 1) OR
for each passed right. That way, we can check with a single sql query (instead if 8)
if the user has the right to see the list or not.
*/
var conds []builder.Cond
for _, r := range rights {
// User conditions
// If the list was shared directly with the user and the user has the right
conds = append(conds, builder.And(
builder.Eq{"ul.user_id": user.ID},
builder.Eq{"ul.right": r},
))
// If the namespace this list belongs to was shared directly with the user and the user has the right
conds = append(conds, builder.And(
builder.Eq{"un.user_id": user.ID},
builder.Eq{"un.right": r},
))
// Team rights
// If the list was shared directly with the team and the team has the right
conds = append(conds, builder.And(
builder.Eq{"tm2.user_id": user.ID},
builder.Eq{"tl.right": r},
))
// If the namespace this list belongs to was shared directly with the team and the team has the right
conds = append(conds, builder.And(
builder.Eq{"tm.user_id": user.ID},
builder.Eq{"tn.right": r},
))
}
exists, err := x.Select("l.*").
Table("list").
Alias("l").
// User stuff
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
// Team stuff
Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
Where("((tm.user_id = ? AND tn.right = ?) OR (tm2.user_id = ? AND tl.right = ?)) AND l.id = ?",
user.ID, r, user.ID, r, l.ID).
// The actual condition
Where(builder.And(
builder.Or(
conds...,
),
builder.Eq{"l.id": l.ID},
)).
Exist(&List{})
if err != nil {
log.Log.Error("Error occurred during checkListTeamRight for List: %s", err)
return false
}
return exists
}
func (l *List) checkListUserRight(user *User, r UserRight) bool {
exists, err := x.Select("l.*").
Table("list").
Alias("l").
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
Where("((ul.user_id = ? AND ul.right = ?) "+
"OR (un.user_id = ? AND un.right = ?) "+
"OR n.owner_id = ?)"+
"AND l.id = ?",
user.ID, r, user.ID, r, user.ID, l.ID).
Exist(&List{})
if err != nil {
log.Log.Error("Error occurred during checkListUserRight for List: %s", err)
return false
}
return exists
return exists, err
}

View File

@@ -25,7 +25,7 @@ type ListTaskAssginee struct {
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"-"`
TaskID int64 `xorm:"int(11) INDEX not null" json:"-" param:"listtask"`
UserID int64 `xorm:"int(11) INDEX not null" json:"user_id" param:"user"`
Created int64 `xorm:"created"`
Created int64 `xorm:"created not null"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
@@ -175,7 +175,11 @@ func (t *ListTask) addNewAssigneeByID(newAssigneeID int64, list *List) (err erro
if err != nil {
return err
}
if !list.CanRead(&newAssignee) {
canRead, err := list.CanRead(&newAssignee)
if err != nil {
return err
}
if !canRead {
return ErrUserDoesNotHaveAccessToList{list.ID, newAssigneeID}
}

View File

@@ -17,31 +17,29 @@
package models
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/web"
)
// CanCreate checks if a user can add a new assignee
func (la *ListTaskAssginee) CanCreate(a web.Auth) bool {
func (la *ListTaskAssginee) CanCreate(a web.Auth) (bool, error) {
return canDoListTaskAssingee(la.TaskID, a)
}
// CanCreate checks if a user can add a new assignee
func (ba *BulkAssignees) CanCreate(a web.Auth) bool {
func (ba *BulkAssignees) CanCreate(a web.Auth) (bool, error) {
return canDoListTaskAssingee(ba.TaskID, a)
}
// CanDelete checks if a user can delete an assignee
func (la *ListTaskAssginee) CanDelete(a web.Auth) bool {
func (la *ListTaskAssginee) CanDelete(a web.Auth) (bool, error) {
return canDoListTaskAssingee(la.TaskID, a)
}
func canDoListTaskAssingee(taskID int64, a web.Auth) bool {
func canDoListTaskAssingee(taskID int64, a web.Auth) (bool, error) {
// Check if the current user can edit the list
list, err := GetListSimplByTaskID(taskID)
if err != nil {
log.Log.Errorf("Error during canDoListTaskAssingee for ListTaskAssginee: %v", err)
return false
return false, err
}
return list.CanCreate(a)
}

View File

@@ -23,42 +23,6 @@ const (
SortTasksByPriorityDesc
)
// ReadAllWithPriority gets all tasks for a user, sorted
// @Summary Get tasks sorted
// @Description Returns all tasks on any list the user has access to.
// @tags task
// @Accept json
// @Produce json
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param s query string false "Search tasks by task text."
// @Param sortby path string true "The sorting parameter. Possible values to sort by are priority, prioritydesc, priorityasc, dueadate, dueadatedesc, dueadateasc."
// @Security JWTKeyAuth
// @Success 200 {array} models.List "The tasks"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/all/{sortby} [get]
func dummy() {
// Dummy function for swaggo to pick up the docs comment
}
// ReadAllWithPriorityAndDateRange gets all tasks for a user, sorted
// @Summary Get tasks sorted and within a date range
// @Description Returns all tasks on any list the user has access to.
// @tags task
// @Accept json
// @Produce json
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param s query string false "Search tasks by task text."
// @Param sortby path string true "The sorting parameter. Possible values to sort by are priority, prioritydesc, priorityasc, dueadate, dueadatedesc, dueadateasc."
// @Param startdate path string true "The start date parameter. Expects a unix timestamp."
// @Param enddate path string true "The end date parameter. Expects a unix timestamp."
// @Security JWTKeyAuth
// @Success 200 {array} models.List "The tasks"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/all/{sortby}/{startdate}/{enddate} [get]
func dummy2() {
// Dummy function for swaggo to pick up the docs comment
}
// ReadAll gets all tasks for a user
// @Summary Get tasks
// @Description Returns all tasks on any list the user has access to.
@@ -67,6 +31,9 @@ func dummy2() {
// @Produce json
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param s query string false "Search tasks by task text."
// @Param sort query string false "The sorting parameter. Possible values to sort by are priority, prioritydesc, priorityasc, dueadate, dueadatedesc, dueadateasc."
// @Param startdate query int false "The start date parameter to filter by. Expects a unix timestamp."
// @Param enddate query int false "The end date parameter to filter by. Expects a unix timestamp."
// @Security JWTKeyAuth
// @Success 200 {array} models.List "The tasks"
// @Failure 500 {object} models.Message "Internal error"

View File

@@ -26,44 +26,44 @@ type ListTask struct {
// The unique, numeric id of this task.
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
// The task text. This is what you'll see in the list.
Text string `xorm:"varchar(250)" json:"text" valid:"runelength(3|250)" minLength:"3" maxLength:"250"`
Text string `xorm:"varchar(250) not null" json:"text" valid:"runelength(3|250)" minLength:"3" maxLength:"250"`
// The task description.
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)" maxLength:"250"`
// Whether a task is done or not.
Done bool `xorm:"INDEX" json:"done"`
Done bool `xorm:"INDEX null" json:"done"`
// A unix timestamp when the task is due.
DueDateUnix int64 `xorm:"int(11) INDEX" json:"dueDate"`
DueDateUnix int64 `xorm:"int(11) INDEX null" json:"dueDate"`
// An array of unix timestamps when the user wants to be reminded of the task.
RemindersUnix []int64 `xorm:"JSON TEXT" json:"reminderDates"`
CreatedByID int64 `xorm:"int(11)" json:"-"` // ID of the user who put that task on the list
RemindersUnix []int64 `xorm:"JSON TEXT null" json:"reminderDates"`
CreatedByID int64 `xorm:"int(11) not null" json:"-"` // ID of the user who put that task on the list
// The list this task belongs to.
ListID int64 `xorm:"int(11) INDEX" json:"-" param:"list"`
ListID int64 `xorm:"int(11) INDEX not null" json:"-" param:"list"`
// An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount.
RepeatAfter int64 `xorm:"int(11) INDEX" json:"repeatAfter"`
RepeatAfter int64 `xorm:"int(11) INDEX null" json:"repeatAfter"`
// If the task is a subtask, this is the id of its parent.
ParentTaskID int64 `xorm:"int(11) INDEX" json:"parentTaskID"`
ParentTaskID int64 `xorm:"int(11) INDEX null" json:"parentTaskID"`
// The task priority. Can be anything you want, it is possible to sort by this later.
Priority int64 `xorm:"int(11)" json:"priority"`
Priority int64 `xorm:"int(11) null" json:"priority"`
// When this task starts.
StartDateUnix int64 `xorm:"int(11) INDEX" json:"startDate"`
StartDateUnix int64 `xorm:"int(11) INDEX null" json:"startDate" query:"-"`
// When this task ends.
EndDateUnix int64 `xorm:"int(11) INDEX" json:"endDate"`
EndDateUnix int64 `xorm:"int(11) INDEX null" json:"endDate" query:"-"`
// An array of users who are assigned to this task
Assignees []*User `xorm:"-" json:"assignees"`
// An array of labels which are associated with this task.
Labels []*Label `xorm:"-" json:"labels"`
Sorting string `xorm:"-" json:"-" param:"sort"` // Parameter to sort by
StartDateSortUnix int64 `xorm:"-" json:"-" param:"startdatefilter"`
EndDateSortUnix int64 `xorm:"-" json:"-" param:"enddatefilter"`
Sorting string `xorm:"-" json:"-" query:"sort"` // Parameter to sort by
StartDateSortUnix int64 `xorm:"-" json:"-" query:"startdate"`
EndDateSortUnix int64 `xorm:"-" json:"-" query:"enddate"`
// An array of subtasks.
Subtasks []*ListTask `xorm:"-" json:"subtasks"`
// A unix timestamp when this task was created. You cannot change this value.
Created int64 `xorm:"created" json:"created"`
Created int64 `xorm:"created not null" json:"created"`
// A unix timestamp when this task was last updated. You cannot change this value.
Updated int64 `xorm:"updated" json:"updated"`
Updated int64 `xorm:"updated not null" json:"updated"`
// The user who initially created the task.
CreatedBy User `xorm:"-" json:"createdBy" valid:"-"`

View File

@@ -129,10 +129,15 @@ func (t *ListTask) Update() (err error) {
}
// And because a false is considered to be a null value, we need to explicitly check that case here.
if t.Done == false {
if !t.Done {
ot.Done = false
}
// If the priority is 0, we also need to explicitly check that here
if t.Priority == 0 {
ot.Priority = 0
}
_, err = x.ID(t.ID).
Cols("text",
"description",

View File

@@ -17,63 +17,52 @@
package models
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/web"
)
// CanDelete checks if the user can delete an task
func (t *ListTask) CanDelete(a web.Auth) bool {
func (t *ListTask) CanDelete(a web.Auth) (bool, error) {
return t.canDoListTask(a)
}
// CanUpdate determines if a user has the right to update a list task
func (t *ListTask) CanUpdate(a web.Auth) bool {
func (t *ListTask) CanUpdate(a web.Auth) (bool, error) {
return t.canDoListTask(a)
}
// CanCreate determines if a user has the right to create a list task
func (t *ListTask) CanCreate(a web.Auth) bool {
func (t *ListTask) CanCreate(a web.Auth) (bool, error) {
doer := getUserForRights(a)
// A user can do a task if he has write acces to its list
l := &List{ID: t.ListID}
err := l.GetSimpleByID()
if err != nil {
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
return false
}
return l.CanWrite(doer)
}
// CanRead determines if a user can read a task
func (t *ListTask) CanRead(a web.Auth) bool {
func (t *ListTask) CanRead(a web.Auth) (canRead bool, err error) {
// Get the task, error out if it doesn't exist
*t, err = getTaskByIDSimple(t.ID)
if err != nil {
return
}
// A user can read a task if it has access to the list
list := &List{ID: t.ListID}
err := list.GetSimpleByID()
if err != nil {
log.Log.Error("Error occurred during CanRead for ListTask: %s", err)
return false
}
return list.CanRead(a)
}
// Helper function to check if a user can do stuff on a list task
func (t *ListTask) canDoListTask(a web.Auth) bool {
func (t *ListTask) canDoListTask(a web.Auth) (bool, error) {
doer := getUserForRights(a)
// Get the task
lI, err := getTaskByIDSimple(t.ID)
if err != nil {
log.Log.Error("Error occurred during canDoListTask (getTaskByIDSimple) for ListTask: %s", err)
return false
return false, err
}
// A user can do a task if he has write acces to its list
l := &List{ID: lI.ListID}
err = l.GetSimpleByID()
if err != nil {
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
return false
}
return l.CanWrite(doer)
}

View File

@@ -35,14 +35,16 @@ func TestListTask_Create(t *testing.T) {
doer, err := GetUserByID(1)
assert.NoError(t, err)
assert.True(t, listtask.CanCreate(&doer))
allowed, _ := listtask.CanCreate(&doer)
assert.True(t, allowed)
err = listtask.Create(&doer)
assert.NoError(t, err)
// Update it
listtask.Text = "Test34"
assert.True(t, listtask.CanUpdate(&doer))
allowed, _ = listtask.CanUpdate(&doer)
assert.True(t, allowed)
err = listtask.Update()
assert.NoError(t, err)
@@ -52,7 +54,8 @@ func TestListTask_Create(t *testing.T) {
assert.Equal(t, li.Text, "Test34")
// Delete the task
assert.True(t, listtask.CanDelete(&doer))
allowed, _ = listtask.CanDelete(&doer)
assert.True(t, allowed)
err = listtask.Delete()
assert.NoError(t, err)

View File

@@ -27,12 +27,12 @@ type ListUser struct {
// The list id.
ListID int64 `xorm:"int(11) not null INDEX" json:"-" param:"list"`
// The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
Right UserRight `xorm:"int(11) INDEX" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
Right Right `xorm:"int(11) INDEX null" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A unix timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created" json:"created"`
Created int64 `xorm:"created not null" json:"created"`
// A unix timestamp when this relation was last updated. You cannot change this value.
Updated int64 `xorm:"updated" json:"updated"`
Updated int64 `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
@@ -46,5 +46,5 @@ func (ListUser) TableName() string {
// UserWithRight represents a user in combination with the right it can have on a list/namespace
type UserWithRight struct {
User `xorm:"extends"`
Right UserRight `json:"right"`
Right Right `json:"right"`
}

View File

@@ -33,40 +33,40 @@ import "code.vikunja.io/web"
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id}/users [put]
func (ul *ListUser) Create(a web.Auth) (err error) {
func (lu *ListUser) Create(a web.Auth) (err error) {
// Check if the right is valid
if err := ul.Right.isValid(); err != nil {
if err := lu.Right.isValid(); err != nil {
return err
}
// Check if the list exists
l := &List{ID: ul.ListID}
l := &List{ID: lu.ListID}
if err = l.GetSimpleByID(); err != nil {
return
}
// Check if the user exists
if _, err = GetUserByID(ul.UserID); err != nil {
if _, err = GetUserByID(lu.UserID); err != nil {
return err
}
// Check if the user already has access or is owner of that list
// We explicitly DONT check for teams here
if l.OwnerID == ul.UserID {
return ErrUserAlreadyHasAccess{UserID: ul.UserID, ListID: ul.ListID}
if l.OwnerID == lu.UserID {
return ErrUserAlreadyHasAccess{UserID: lu.UserID, ListID: lu.ListID}
}
exist, err := x.Where("list_id = ? AND user_id = ?", ul.ListID, ul.UserID).Get(&ListUser{})
exist, err := x.Where("list_id = ? AND user_id = ?", lu.ListID, lu.UserID).Get(&ListUser{})
if err != nil {
return
}
if exist {
return ErrUserAlreadyHasAccess{UserID: ul.UserID, ListID: ul.ListID}
return ErrUserAlreadyHasAccess{UserID: lu.UserID, ListID: lu.ListID}
}
// Insert user <-> list relation
_, err = x.Insert(ul)
_, err = x.Insert(lu)
return
}

View File

@@ -32,26 +32,27 @@ import "code.vikunja.io/web"
// @Failure 403 {object} code.vikunja.io/web.HTTPError "No right to see the list."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id}/users [get]
func (ul *ListUser) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
func (lu *ListUser) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
u, err := getUserWithError(a)
if err != nil {
return nil, err
}
// Check if the user has access to the list
l := &List{ID: ul.ListID}
if err := l.GetSimpleByID(); err != nil {
l := &List{ID: lu.ListID}
canRead, err := l.CanRead(u)
if err != nil {
return nil, err
}
if !l.CanRead(u) {
return nil, ErrNeedToHaveListReadAccess{UserID: u.ID, ListID: ul.ListID}
if !canRead {
return nil, ErrNeedToHaveListReadAccess{UserID: u.ID, ListID: lu.ListID}
}
// Get all users
all := []*UserWithRight{}
err = x.
Join("INNER", "users_list", "user_id = users.id").
Where("users_list.list_id = ?", ul.ListID).
Where("users_list.list_id = ?", lu.ListID).
Limit(getLimitFromPageIndex(page)).
Where("users.username LIKE ?", "%"+search+"%").
Find(&all)

View File

@@ -17,45 +17,26 @@
package models
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/web"
)
// CanCreate checks if the user can create a new user <-> list relation
func (lu *ListUser) CanCreate(a web.Auth) bool {
doer := getUserForRights(a)
func (lu *ListUser) CanCreate(a web.Auth) (bool, error) {
// Get the list and check if the user has write access on it
l := List{ID: lu.ListID}
if err := l.GetSimpleByID(); err != nil {
log.Log.Error("Error occurred during CanCreate for ListUser: %s", err)
return false
}
return l.CanWrite(doer)
return l.CanWrite(a)
}
// CanDelete checks if the user can delete a user <-> list relation
func (lu *ListUser) CanDelete(a web.Auth) bool {
doer := getUserForRights(a)
func (lu *ListUser) CanDelete(a web.Auth) (bool, error) {
// Get the list and check if the user has write access on it
l := List{ID: lu.ListID}
if err := l.GetSimpleByID(); err != nil {
log.Log.Error("Error occurred during CanDelete for ListUser: %s", err)
return false
}
return l.CanWrite(doer)
return l.CanWrite(a)
}
// CanUpdate checks if the user can update a user <-> list relation
func (lu *ListUser) CanUpdate(a web.Auth) bool {
doer := getUserForRights(a)
func (lu *ListUser) CanUpdate(a web.Auth) (bool, error) {
// Get the list and check if the user has write access on it
l := List{ID: lu.ListID}
if err := l.GetSimpleByID(); err != nil {
log.Log.Error("Error occurred during CanUpdate for ListUser: %s", err)
return false
}
return l.CanWrite(doer)
return l.CanWrite(a)
}

View File

@@ -27,7 +27,7 @@ func TestListUser_CanDoSomething(t *testing.T) {
ID int64
UserID int64
ListID int64
Right UserRight
Right Right
Created int64
Updated int64
CRUDable web.CRUDable
@@ -85,13 +85,13 @@ func TestListUser_CanDoSomething(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if got := lu.CanCreate(tt.args.a); got != tt.want["CanCreate"] {
if got, _ := lu.CanCreate(tt.args.a); got != tt.want["CanCreate"] {
t.Errorf("ListUser.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
}
if got := lu.CanDelete(tt.args.a); got != tt.want["CanDelete"] {
if got, _ := lu.CanDelete(tt.args.a); got != tt.want["CanDelete"] {
t.Errorf("ListUser.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
}
if got := lu.CanUpdate(tt.args.a); got != tt.want["CanUpdate"] {
if got, _ := lu.CanUpdate(tt.args.a); got != tt.want["CanUpdate"] {
t.Errorf("ListUser.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
}
})

View File

@@ -29,7 +29,7 @@ func TestListUser_Create(t *testing.T) {
ID int64
UserID int64
ListID int64
Right UserRight
Right Right
Created int64
Updated int64
CRUDable web.CRUDable
@@ -69,7 +69,7 @@ func TestListUser_Create(t *testing.T) {
Right: 500,
},
wantErr: true,
errType: IsErrInvalidUserRight,
errType: IsErrInvalidRight,
},
{
name: "ListUsers Create with inexisting list",
@@ -127,7 +127,7 @@ func TestListUser_ReadAll(t *testing.T) {
ID int64
UserID int64
ListID int64
Right UserRight
Right Right
Created int64
Updated int64
CRUDable web.CRUDable
@@ -160,18 +160,16 @@ func TestListUser_ReadAll(t *testing.T) {
ID: 1,
Username: "user1",
Password: "1234",
Email: "user1@example.com",
},
Right: UserRightRead,
Right: RightRead,
},
{
User: User{
ID: 2,
Username: "user2",
Password: "1234",
Email: "user2@example.com",
},
Right: UserRightRead,
Right: RightRead,
},
},
},
@@ -218,7 +216,7 @@ func TestListUser_Update(t *testing.T) {
ID int64
UserID int64
ListID int64
Right UserRight
Right Right
Created int64
Updated int64
CRUDable web.CRUDable
@@ -235,7 +233,7 @@ func TestListUser_Update(t *testing.T) {
fields: fields{
ListID: 3,
UserID: 1,
Right: UserRightAdmin,
Right: RightAdmin,
},
},
{
@@ -243,7 +241,7 @@ func TestListUser_Update(t *testing.T) {
fields: fields{
ListID: 3,
UserID: 1,
Right: UserRightWrite,
Right: RightWrite,
},
},
{
@@ -251,7 +249,7 @@ func TestListUser_Update(t *testing.T) {
fields: fields{
ListID: 3,
UserID: 1,
Right: UserRightRead,
Right: RightRead,
},
},
{
@@ -262,7 +260,7 @@ func TestListUser_Update(t *testing.T) {
Right: 500,
},
wantErr: true,
errType: IsErrInvalidUserRight,
errType: IsErrInvalidRight,
},
}
for _, tt := range tests {
@@ -293,7 +291,7 @@ func TestListUser_Delete(t *testing.T) {
ID int64
UserID int64
ListID int64
Right UserRight
Right Right
Created int64
Updated int64
CRUDable web.CRUDable

View File

@@ -17,10 +17,12 @@
package models
import (
"code.vikunja.io/api/pkg/config"
"github.com/spf13/viper"
"testing"
)
func TestMain(m *testing.M) {
config.InitConfig()
MainTest(m, viper.GetString("service.rootpath"))
}

View File

@@ -17,10 +17,10 @@
package models
import (
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"encoding/gob"
"fmt"
_ "github.com/go-sql-driver/mysql" // Because.
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
xrc "github.com/go-xorm/xorm-redis-cache"
_ "github.com/mattn/go-sqlite3" // Because.
@@ -29,51 +29,11 @@ import (
var (
x *xorm.Engine
tables []interface{}
tablesWithPointer []interface{}
)
func getEngine() (*xorm.Engine, error) {
// Use Mysql if set
if viper.GetString("database.type") == "mysql" {
connStr := fmt.Sprintf(
"%s:%s@tcp(%s)/%s?charset=utf8&parseTime=true",
viper.GetString("database.user"),
viper.GetString("database.password"),
viper.GetString("database.host"),
viper.GetString("database.database"))
e, err := xorm.NewEngine("mysql", connStr)
e.SetMaxOpenConns(viper.GetInt("database.openconnections"))
return e, err
}
// Otherwise use sqlite
path := viper.GetString("database.path")
if path == "" {
path = "./db.db"
}
return xorm.NewEngine("sqlite3", path)
}
func init() {
tables = append(tables,
new(User),
new(List),
new(ListTask),
new(Team),
new(TeamMember),
new(TeamList),
new(TeamNamespace),
new(Namespace),
new(ListUser),
new(NamespaceUser),
new(ListTaskAssginee),
new(Label),
new(LabelTask),
)
tablesWithPointer = append(tables,
// GetTables returns all structs which are also a table.
func GetTables() []interface{} {
return []interface{}{
&User{},
&List{},
&ListTask{},
@@ -87,43 +47,33 @@ func init() {
&ListTaskAssginee{},
&Label{},
&LabelTask{},
)
}
}
// SetEngine sets the xorm.Engine
func SetEngine() (err error) {
x, err = getEngine()
x, err = db.CreateDBEngine()
if err != nil {
return fmt.Errorf("Failed to connect to database: %v", err)
log.Log.Criticalf("Could not connect to db: %v", err.Error())
return
}
// Cache
// We have to initialize the cache here to avoid import cycles
if viper.GetBool("cache.enabled") {
switch viper.GetString("cache.type") {
case "memory":
cacher := xorm.NewLRUCacher(xorm.NewMemoryStore(), viper.GetInt("cache.maxelementsize"))
x.SetDefaultCacher(cacher)
break
case "redis":
cacher := xrc.NewRedisCacher(viper.GetString("redis.host"), viper.GetString("redis.password"), xrc.DEFAULT_EXPIRATION, x.Logger())
x.SetDefaultCacher(cacher)
gob.Register(tables)
gob.Register(tablesWithPointer) // Need to register tables with pointer as well...
break
gob.Register(GetTables())
default:
fmt.Println("Did not find a valid cache type. Caching disabled. Please refer to the docs for poosible cache types.")
log.Log.Info("Did not find a valid cache type. Caching disabled. Please refer to the docs for poosible cache types.")
}
}
x.SetMapper(core.GonicMapper{})
// Sync dat shit
if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil {
return fmt.Errorf("sync database struct error: %v", err)
}
x.ShowSQL(viper.GetBool("database.showqueries"))
return nil
}

View File

@@ -18,6 +18,7 @@ package models
import (
"code.vikunja.io/web"
"github.com/imdario/mergo"
"time"
)
@@ -26,18 +27,18 @@ type Namespace struct {
// The unique, numeric id of this namespace.
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"namespace"`
// The name of this namespace.
Name string `xorm:"varchar(250)" json:"name" valid:"required,runelength(5|250)" minLength:"5" maxLength:"250"`
Name string `xorm:"varchar(250) not null" json:"name" valid:"required,runelength(5|250)" minLength:"5" maxLength:"250"`
// The description of the namespace
Description string `xorm:"varchar(1000)" json:"description" valid:"runelength(0|250)" maxLength:"250"`
Description string `xorm:"varchar(1000) null" json:"description" valid:"runelength(0|250)" maxLength:"250"`
OwnerID int64 `xorm:"int(11) not null INDEX" json:"-"`
// The user who owns this namespace
Owner User `xorm:"-" json:"owner" valid:"-"`
// A unix timestamp when this namespace was created. You cannot change this value.
Created int64 `xorm:"created" json:"created"`
Created int64 `xorm:"created not null" json:"created"`
// A unix timestamp when this namespace was last updated. You cannot change this value.
Updated int64 `xorm:"updated" json:"updated"`
Updated int64 `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
@@ -57,35 +58,46 @@ func (Namespace) TableName() string {
return "namespaces"
}
// GetSimpleByID gets a namespace without things like the owner, it more or less only checks if it exists.
func (n *Namespace) GetSimpleByID() (err error) {
if n.ID == 0 {
return ErrNamespaceDoesNotExist{ID: n.ID}
}
// Get the namesapce with shared lists
if n.ID == -1 {
*n = PseudoNamespace
return
}
namespaceFromDB := &Namespace{}
exists, err := x.Where("id = ?", n.ID).Get(namespaceFromDB)
if err != nil {
return
}
if !exists {
return ErrNamespaceDoesNotExist{ID: n.ID}
}
// We don't want to override the provided user struct because this would break updating, so we have to merge it
if err := mergo.Merge(namespaceFromDB, n, mergo.WithOverride); err != nil {
return err
}
*n = *namespaceFromDB
return
}
// GetNamespaceByID returns a namespace object by its ID
func GetNamespaceByID(id int64) (namespace Namespace, err error) {
if id == 0 {
return namespace, ErrNamespaceDoesNotExist{ID: id}
}
namespace.ID = id
// Get the namesapce with shared lists
if id == -1 {
namespace = PseudoNamespace
return namespace, err
}
exists, err := x.Get(&namespace)
namespace = Namespace{ID: id}
err = namespace.GetSimpleByID()
if err != nil {
return namespace, err
}
if !exists {
return namespace, ErrNamespaceDoesNotExist{ID: id}
return
}
// Get the namespace Owner
namespace.Owner, err = GetUserByID(namespace.OwnerID)
if err != nil {
return namespace, err
}
return namespace, err
return
}
// ReadOne gets one namespace
@@ -101,7 +113,8 @@ func GetNamespaceByID(id int64) (namespace Namespace, err error) {
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id} [get]
func (n *Namespace) ReadOne() (err error) {
*n, err = GetNamespaceByID(n.ID)
// Get the namespace Owner
n.Owner, err = GetUserByID(n.OwnerID)
return
}
@@ -180,7 +193,7 @@ func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, e
// Get all lists
lists := []*List{}
err = x.Table(&lists).
err = x.
In("namespace_id", namespaceids).
Find(&lists)
if err != nil {

View File

@@ -49,6 +49,9 @@ func (n *Namespace) Delete() (err error) {
// Delete all lists with their tasks
lists, err := GetListsByNamespaceID(n.ID, &User{})
if err != nil {
return
}
var listIDs []int64
// We need to do that for here because we need the list ids to delete two times:
// 1) to delete the lists itself

View File

@@ -17,123 +17,98 @@
package models
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/web"
"github.com/go-xorm/builder"
)
// IsAdmin returns true or false if the user is admin on that namespace or not
func (n *Namespace) IsAdmin(a web.Auth) bool {
u := getUserForRights(a)
// Owners always have admin rights
if u.ID == n.Owner.ID {
return true
}
// Check user rights
if n.checkUserRights(u, UserRightAdmin) {
return true
}
// Check if that user is in a team which has admin rights to that namespace
return n.checkTeamRights(u, TeamRightAdmin)
// CanWrite checks if a user has write access to a namespace
func (n *Namespace) CanWrite(a web.Auth) (bool, error) {
return n.checkRight(a, RightWrite, RightAdmin)
}
// CanWrite checks if a user has write access to a namespace
func (n *Namespace) CanWrite(a web.Auth) bool {
u := getUserForRights(a)
// Admins always have write access
if n.IsAdmin(u) {
return true
}
// Check user rights
if n.checkUserRights(u, UserRightWrite) {
return true
}
// Check if that user is in a team which has write rights to that namespace
return n.checkTeamRights(u, TeamRightWrite)
// IsAdmin returns true or false if the user is admin on that namespace or not
func (n *Namespace) IsAdmin(a web.Auth) (bool, error) {
return n.checkRight(a, RightAdmin)
}
// CanRead checks if a user has read access to that namespace
func (n *Namespace) CanRead(a web.Auth) bool {
u := getUserForRights(a)
// Admins always have read access
if n.IsAdmin(u) {
return true
}
// Check user rights
if n.checkUserRights(u, UserRightRead) {
return true
}
// Check if the user is in a team which has access to the namespace
return n.checkTeamRights(u, TeamRightRead)
func (n *Namespace) CanRead(a web.Auth) (bool, error) {
return n.checkRight(a, RightRead, RightWrite, RightAdmin)
}
// CanUpdate checks if the user can update the namespace
func (n *Namespace) CanUpdate(a web.Auth) bool {
u := getUserForRights(a)
nn, err := GetNamespaceByID(n.ID)
if err != nil {
log.Log.Error("Error occurred during CanUpdate for Namespace: %s", err)
return false
}
return nn.IsAdmin(u)
func (n *Namespace) CanUpdate(a web.Auth) (bool, error) {
return n.IsAdmin(a)
}
// CanDelete checks if the user can delete a namespace
func (n *Namespace) CanDelete(a web.Auth) bool {
u := getUserForRights(a)
nn, err := GetNamespaceByID(n.ID)
if err != nil {
log.Log.Error("Error occurred during CanDelete for Namespace: %s", err)
return false
}
return nn.IsAdmin(u)
func (n *Namespace) CanDelete(a web.Auth) (bool, error) {
return n.IsAdmin(a)
}
// CanCreate checks if the user can create a new namespace
func (n *Namespace) CanCreate(a web.Auth) bool {
func (n *Namespace) CanCreate(a web.Auth) (bool, error) {
// This is currently a dummy function, later on we could imagine global limits etc.
return true
return true, nil
}
func (n *Namespace) checkTeamRights(u *User, r TeamRight) bool {
func (n *Namespace) checkRight(a web.Auth, rights ...Right) (bool, error) {
// Get the namespace and check the right
err := n.GetSimpleByID()
if err != nil {
return false, err
}
user := getUserForRights(a)
if user.ID == n.OwnerID {
return true, nil
}
/*
The following loop creates an sql condition like this one:
namespaces.owner_id = 1 OR
(users_namespace.user_id = 1 AND users_namespace.right = 1) OR
(team_members.user_id = 1 AND team_namespaces.right = 1) OR
for each passed right. That way, we can check with a single sql query (instead if 8)
if the user has the right to see the list or not.
*/
var conds []builder.Cond
conds = append(conds, builder.Eq{"namespaces.owner_id": user.ID})
for _, r := range rights {
// User conditions
// If the namespace was shared directly with the user and the user has the right
conds = append(conds, builder.And(
builder.Eq{"users_namespace.user_id": user.ID},
builder.Eq{"users_namespace.right": r},
))
// Team rights
// If the namespace was shared directly with the team and the team has the right
conds = append(conds, builder.And(
builder.Eq{"team_members.user_id": user.ID},
builder.Eq{"team_namespaces.right": r},
))
}
exists, err := x.Select("namespaces.*").
Table("namespaces").
// User stuff
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
// Teams stuff
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
Where("namespaces.id = ? AND ("+
"(team_members.user_id = ? AND team_namespaces.right = ?) "+
"OR namespaces.owner_id = ?)", n.ID, u.ID, r, u.ID).
Get(&Namespace{})
if err != nil {
log.Log.Error("Error occurred during checkTeamRights for Namespace: %s, TeamRight: %d", err, r)
return false
}
return exists
}
func (n *Namespace) checkUserRights(u *User, r UserRight) bool {
exists, err := x.Select("namespaces.*").
Table("namespaces").
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
Where("namespaces.id = ? AND ("+
"(users_namespace.user_id = ? AND users_namespace.right = ?) "+
"OR namespaces.owner_id = ?)", n.ID, u.ID, r, u.ID).
Get(&Namespace{})
if err != nil {
log.Log.Error("Error occurred during checkUserRights for Namespace: %s, UserRight: %d", err, r)
return false
}
return exists
// The actual condition
Where(builder.And(
builder.Or(
conds...,
),
builder.Eq{"namespaces.id": n.ID},
)).
Exist(&List{})
return exists, err
}

View File

@@ -37,16 +37,18 @@ func TestNamespace_Create(t *testing.T) {
assert.NoError(t, err)
// Try creating it
assert.True(t, dummynamespace.CanCreate(&doer))
allowed, _ := dummynamespace.CanCreate(&doer)
assert.True(t, allowed)
err = dummynamespace.Create(&doer)
assert.NoError(t, err)
// check if it really exists
assert.True(t, dummynamespace.CanRead(&doer))
newOne := Namespace{ID: dummynamespace.ID}
err = newOne.ReadOne()
allowed, err = dummynamespace.CanRead(&doer)
assert.NoError(t, err)
assert.Equal(t, newOne.Name, "Test")
assert.True(t, allowed)
err = dummynamespace.ReadOne()
assert.NoError(t, err)
assert.Equal(t, dummynamespace.Name, "Test")
// Try creating one without a name
n2 := Namespace{}
@@ -62,11 +64,23 @@ func TestNamespace_Create(t *testing.T) {
assert.True(t, IsErrUserDoesNotExist(err))
// Update it
assert.True(t, dummynamespace.CanUpdate(&doer))
allowed, err = dummynamespace.CanUpdate(&doer)
assert.NoError(t, err)
assert.True(t, allowed)
dummynamespace.Description = "Dolor sit amet."
err = dummynamespace.Update()
assert.NoError(t, err)
// Check if it was updated
assert.Equal(t, "Dolor sit amet.", dummynamespace.Description)
// Get it and check it again
allowed, err = dummynamespace.CanRead(&doer)
assert.NoError(t, err)
assert.True(t, allowed)
err = dummynamespace.ReadOne()
assert.NoError(t, err)
assert.Equal(t, "Dolor sit amet.", dummynamespace.Description)
// Try updating one with a nonexistant owner
dummynamespace.Owner.ID = 94829838572
err = dummynamespace.Update()
@@ -86,7 +100,9 @@ func TestNamespace_Create(t *testing.T) {
assert.True(t, IsErrNamespaceDoesNotExist(err))
// Delete it
assert.True(t, dummynamespace.CanDelete(&doer))
allowed, err = dummynamespace.CanDelete(&doer)
assert.NoError(t, err)
assert.True(t, allowed)
err = dummynamespace.Delete()
assert.NoError(t, err)
@@ -96,7 +112,8 @@ func TestNamespace_Create(t *testing.T) {
assert.True(t, IsErrNamespaceDoesNotExist(err))
// Check if it was successfully deleted
err = dummynamespace.ReadOne()
allowed, err = dummynamespace.CanRead(&doer)
assert.False(t, allowed)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))

View File

@@ -27,12 +27,12 @@ type NamespaceUser struct {
// The namespace id
NamespaceID int64 `xorm:"int(11) not null INDEX" json:"-" param:"namespace"`
// The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
Right UserRight `xorm:"int(11) INDEX" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
Right Right `xorm:"int(11) INDEX null" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A unix timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created" json:"created"`
Created int64 `xorm:"created not null" json:"created"`
// A unix timestamp when this relation was last updated. You cannot change this value.
Updated int64 `xorm:"updated" json:"updated"`
Updated int64 `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`

View File

@@ -33,42 +33,42 @@ import "code.vikunja.io/web"
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the namespace"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id}/users [put]
func (un *NamespaceUser) Create(a web.Auth) (err error) {
func (nu *NamespaceUser) Create(a web.Auth) (err error) {
// Reset the id
un.ID = 0
nu.ID = 0
// Check if the right is valid
if err := un.Right.isValid(); err != nil {
if err := nu.Right.isValid(); err != nil {
return err
}
// Check if the namespace exists
l, err := GetNamespaceByID(un.NamespaceID)
l, err := GetNamespaceByID(nu.NamespaceID)
if err != nil {
return
}
// Check if the user exists
if _, err = GetUserByID(un.UserID); err != nil {
if _, err = GetUserByID(nu.UserID); err != nil {
return err
}
// Check if the user already has access or is owner of that namespace
// We explicitly DO NOT check for teams here
if l.OwnerID == un.UserID {
return ErrUserAlreadyHasNamespaceAccess{UserID: un.UserID, NamespaceID: un.NamespaceID}
if l.OwnerID == nu.UserID {
return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
}
exist, err := x.Where("namespace_id = ? AND user_id = ?", un.NamespaceID, un.UserID).Get(&NamespaceUser{})
exist, err := x.Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).Get(&NamespaceUser{})
if err != nil {
return
}
if exist {
return ErrUserAlreadyHasNamespaceAccess{UserID: un.UserID, NamespaceID: un.NamespaceID}
return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
}
// Insert user <-> namespace relation
_, err = x.Insert(un)
_, err = x.Insert(nu)
return
}

Some files were not shown because too many files have changed in this diff Show More