Compare commits

..

2 Commits

Author SHA1 Message Date
kolaente 81b3634888
chore(ci): sign drone config 2022-03-16 22:24:49 +01:00
renovate 706d1280d6 chore(deps): update klakegg/hugo docker tag to v0.94.2 2022-03-16 08:00:49 +00:00
103 changed files with 847 additions and 3436 deletions

View File

@ -1,6 +0,0 @@
files/
Dockerfile
docker-manifest.tmpl
docker-manifest-unstable.tmpl
*.db
*.zip

View File

@ -132,14 +132,13 @@ steps:
event: [ push, tag, pull_request ]
- name: lint
image: golang:1.17-alpine
image: vikunja/golang-build:latest
pull: true
environment:
GOPROXY: 'https://goproxy.kolaente.de'
depends_on: [ build ]
commands:
- apk --no-cache add build-base git
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.47.3
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.31.0
- ./mage-static check:all
when:
event: [ push, tag, pull_request ]
@ -153,7 +152,7 @@ steps:
- unzip vikunja-latest.zip vikunja-unstable-linux-amd64
- name: test-migration-sqlite
image: vikunja/golang-build:latest
image: kolaente/toolbox:latest
pull: true
depends_on: [ test-migration-prepare, build ]
environment:
@ -172,7 +171,7 @@ steps:
event: [ push, tag, pull_request ]
- name: test-migration-mysql
image: vikunja/golang-build:latest
image: kolaente/toolbox:latest
pull: true
depends_on: [ test-migration-prepare, build ]
environment:
@ -191,7 +190,7 @@ steps:
event: [ push, tag, pull_request ]
- name: test-migration-psql
image: vikunja/golang-build:latest
image: kolaente/toolbox:latest
pull: true
depends_on: [ test-migration-prepare, build ]
environment:
@ -622,7 +621,7 @@ steps:
- tar -xzf vikunja-theme.tar.gz
- name: build
image: klakegg/hugo:0.93.3
image: klakegg/hugo:0.94.2
pull: true
commands:
- cd docs
@ -663,7 +662,6 @@ steps:
image: docker:git
commands:
- git fetch --tags
- name: docker-arm-unstable
image: plugins/docker:linux-arm
pull: true
@ -674,7 +672,6 @@ steps:
from_secret: docker_password
repo: vikunja/api
tags: unstable-linux-arm
dockerfile: Dockerfile.arm32
depends_on: [ fetch-tags ]
when:
ref:
@ -691,7 +688,6 @@ steps:
repo: vikunja/api
auto_tag: true
auto_tag_suffix: linux-arm
dockerfile: Dockerfile.arm32
depends_on: [ fetch-tags ]
when:
ref:
@ -878,6 +874,6 @@ steps:
- failure
---
kind: signature
hmac: e72b631f902689777e3263ae9527e5aa47738b9021538f7cb5034f95ac265f07
hmac: d72925988e6da0cff96709842344c07f2d82a8df180e3a9cfb1b6cf421ee1451
...

View File

@ -1,44 +0,0 @@
<!--
Please fill out this issue template to report a bug.
If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
-->
**Version information:**
Frontend Version:
API Version:
Browser and OS Version:
**Steps to reproduce:**
<!--
Add clear steps to reproduce the bug. Provide screenshots where applicable.
-->
1.
2.
...
**Expected behavior:**
<!--
Describe what happened.
-->
**Actual behavior:**
<!--
Describe what happened instead.
-->
**Checklist:**
* [ ] I have provided all required information
* [ ] I am using the latest release or the latest unstable build
* [ ] I was able to reproduce the bug on [try](https://try.vikunja.io)

View File

@ -13,11 +13,10 @@ linters:
- goheader
- gofmt
- goimports
- revive
- golint
- misspell
disable:
- scopelint # Obsolete, using exportloopref instead
- durationcheck
presets:
- bugs
- unused
@ -36,7 +35,6 @@ issues:
linters:
- gocyclo
- deadcode
- errorlint
- path: pkg/integrations/*
linters:
- gocyclo
@ -82,9 +80,3 @@ issues:
- text: "Missed string"
linters:
- goheader
- path: pkg/.*/error.go
linters:
- errorlint
- path: pkg/models/favorites\.go
linters:
- nilerr

View File

@ -7,388 +7,6 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
All releases can be found on https://code.vikunja.io/api/releases.
## [0.19.2] - 2022-08-17
### Bug Fixes
* Don't fail a migration if there is no filter saved ([10ded56](10ded56f6697ef47910ec68d37f26ed47cbe9180))
* Don't override saved filters ([beb4d07](beb4d07cf95fc25f7cc5f7471b46bdab49f95fe0))
## [0.19.1] - 2022-08-17
### Bug Fixes
* Prevent moving a list into a pseudo namespace ([3ccc636](3ccc6365a6892f37ee54b0750a34a61e52f6dba1))
* Make sure generating blur hashes for bmp, tiff and webp images works ([8bf0f8b](8bf0f8bb571ddff69a7142be1acaa2e4e0c38e3b))
* Add debian-based docker image for arm 32 builds ([c9e044b](c9e044b3ad60d25e9641d22d84571a7db83a26ac))
* Only list all users when allowed ([9ddd7f4](9ddd7f48895f508539d591aeebde450a86987024))
* Lint ([0c8bed4](0c8bed4054649de8510e5a636d1a14b65d52c402))
### Dependencies
* *(deps)* Update golang.org/x/sys digest to 6e608f9 (#1229)
* *(deps)* Update golang.org/x/sync digest to 886fb93 (#1221)
* *(deps)* Update golang.org/x/sys digest to 8e32c04 (#1230)
* *(deps)* Update golang.org/x/term digest to a9ba230 (#1222)
* *(deps)* Update module github.com/prometheus/client_golang to v1.13.0
* *(deps)* Update module github.com/prometheus/client_golang to v1.13.0 (#1231)
* *(deps)* Update golang.org/x/sys digest to 1c4a2a7
* *(deps)* Update golang.org/x/oauth2 digest to 128564f (#1220)
* *(deps)* Update golang.org/x/image digest to 062f8c9 (#1219)
* *(deps)* Update golang.org/x/crypto digest to 630584e (#1218)
* *(deps)* Update module github.com/labstack/echo/v4 to v4.8.0 (#1233)
* *(deps)* Update golang.org/x/sys digest to fbc7d0a (#1234)
* *(deps)* Update module github.com/wneessen/go-mail to v0.2.6 (#1235)
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.15 (#1238)
### Features
* *(docs)* Add k8s docs* Add openid examples ([dbb0f54](dbb0f5473269fb29c4a484cd233a5b76484c4ca7))
* Search by assignee username instead of id ([7f28865](7f28865903740d6dde15ee005323fbdee3072166))
* Add migration to change user ids to usernames in saved filters ([3047ccf](3047ccfd4af8fee55d9ebff49138911ab80cb3d2))
## [0.19.0] - 2022-08-03
### Bug Fixes
* *(caldav)* Make sure the caldav tokens of non-local accounts are properly checked
* *(caldav)* Properly parse durations when returning VTODOs
* *(caldav)* Make sure description is parsed correctly when multiline
* *(ci)* Sign drone config
* *(ci)* Make sure the linter actually runs
* *(ci)* Install git in lint step
* *(docker)* Switch to debian base image
* *(docker)* Use official go image instead of our own to build
* *(docs)* Update minimum required go version
* *(docs)* Use up-to-date hugo image for building
* *(docs)* Don't use cannonify url
* *(docs)* Image urls in synology setup explanation
* *(docs)* Clarify frontend requirements to use Vikunja
* *(dump)* Don't try to save a config file if none was provided and dump vikunja env variables
* *(mage)* Handle different types of errors
* *(mail)* Don't set a username by default
* *(mail)* Don't try to authenticate against the mail server when no credentials are provided
* *(mail)* Set server name in tls config so that sending mail works with skipTlsVerify set to false
* *(restore)* Properly decode notifications json data
* *(restore)* Make sure to reset sequences after importing a dump when using postgres
* *(restore)* Use the correct initial migration* Generate swagger docs ([4de8ec5](4de8ec56a62caef22c2061376383de1fe53ca4c3))
* Make sure the full task is available in notifications ([c2b6119](c2b6119434e6e806785d2c259c3ca3d25496ec75))
* Don't try to load the namespace of a list if it is a shared list ([d7e47a2](d7e47a28d4bb04d4c7c3ed85a263134180da447a))
* Correctly load and pass the user when deleting it ([50b65a5](50b65a517da6869dc6a48fec40323e254ba4c032))
* Updating a list might remove its background ([cf05de1](cf05de19b317bd99c30de4c6a149a0d8a4ff4f49))
* Sorting for saved filters ([57e5d10](57e5d10eee4c45a04e9e1aaeaf41dd44eb8ce788))
* Importing trello attachments ([c3e0e64](c3e0e6405a634894a30dbf9c0506d1691ae4d443))
* Lint ([0b77625](0b7762590f6a0a82090ef74e9e7e32b37142d343))
* Deleting users with no namespaces ([f8a0a7e](f8a0a7e9539a44b2f790a08eb1b03028b56eaac3))
* Importing tasks from todoist without a due time set ([fd0d462](fd0d462bf4dd8225c67ba34958e5148f6167d264))
* User deletion never happens ([72d3c54](72d3c54efd3dda6ae846a069415688391cb1c9ae))
* User deletion reminder emails counting up ([f581885](f581885e65ada15439ec02f1d18d825b03581523))
* User not actually deleted ([70e005e](70e005e7ce5cf1dd25ec9ddfde3cfbbd258fadb6))
* User deletion schedule ([5c88dfe](5c88dfe88eab442724f22c3b29741e78939deae2))
* Friendly name not getting synced on first login from openid ([190a9f2](190a9f2a4c1a59bc68b839c465bb2536532c0e96))
* Importing archived lists or namespaces ([8bb3f8d](8bb3f8d37c78dc704ff4316c750e143528151b48))
* Lint ([a31086a](a31086a7a9ca7723f61a826bccbea125243478f1))
* Microsoft todo migration not importing all tasks ([43f1daf](43f1daf40c388a0aa40f7fd6a8db4c78308d4efd))
* Clarify which config file is used on startup ([44aaf0a](44aaf0a4eccebb1d1a25f5563e928bd1bb82d351))
* Disabling logging completely now works ([22e3f24](22e3f242a396aa9cf54e9426077816f97a0da36f))
* Restoring dumps with no config file saved in them ([8bf2254](8bf2254f4b87446ab0a39080cb0b7d32ccec7c0a))
* Validate email address when creating a user via cli ([75f74b4](75f74b429eea7ae3a75cb10def1ca658af35086a))
* Checking for error types ([ac6818a](ac6818a4769a162c458553944509fe64357370f9))
* Lint ([7fa0865](7fa086518800243385d8cc4696eeea9bf093e5b3))
* Return BlurHash in unsplash search results ([6b51fae](6b51fae0931308464038f55b25e81e68d014c49c))
* Go mod tidy ([e19ad11](e19ad1184662dc9ac9aa89a44abdffc091e2a1b8))
* Decoding images for blurHash generation ([d3bdafb](d3bdafb717b1ad3e2165097ef0b0c2dd47e1502e))
* Lint ([de97fcb](de97fcbd121b1d56b74175fd79ef594ef34e71c8))
* Broken link (#27) ([96e519e](96e519ea96c9537222d0b455037e11fbe9660c31))
* Add more methods to figure out the current binary location ([9845fcc](9845fcc1708431f8f736d36e7e19a1067b0e0e52))
* Set derived default values only after reading config from file or env ([f5ebada](f5ebada91351faf1e5602f0260908defaaabd810))
* Sort tasks logically and consistent across dbms (#1177) ([e52c45d](e52c45d5aabb74ea7b472e8d5b44491cdd7e9489))
* VIKUNJA_SERVICE_JWT_SECRET should be VIKUNJA_SERVICE_JWTSECRET (#1184) ([172a621](172a6214d7c30278017129b950339c78a6ddb7bc))
* Add missing migration ([d837f8a](d837f8a6248b5ff2700a4bfc300d7f9d466cb918))
* Revert renaming Attachments to Embeds everywhere ([c62e26b](c62e26b6fe9d9f362fcfb1df2d5664d7f6854c31))
* Set the correct go version in go.mod ([bc7f6a8](bc7f6a858693b0e61fff7d03b5c2b40b6ae1a55d))
* Go mod tidy ([7a30294](7a30294407843693f6c3a7414b3b9d7093359194))
* Tests ([d0e09d6](d0e09d69d048e62ee7c5b666c2f56761b03e68e6))
* Go mod tidy ([951d74b](951d74b272b1e881faa10095f47b6598bb076273))
* Prevent logging openid provider errors twice ([25ffa1b](25ffa1bc2e2f1108f20b0336708d2410bb61c9e1))
* Remove credential escaping for postgres connections to allow for passwords with special characters ([230478a](230478aae947c86f4c6f1f251dcb30aeb1293283))
* Cycles in tasks array when memory caching was enabled ([f5a4c13](f5a4c136fbca6fc5770476e6de8d81173f007df2))
* Add missing error check ([5cc4927](5cc4927b9ef97667bf763772beb36225fdbeded8))
* Properly set tls config for mailer ([5743a4a](5743a4afe51de221beeeabe66552ae4d92eed1a6))
* Return 9:00 as default time for reminders if none was set ([79b3167](79b31673e2a79eaa124976840e85757d2bebb887))
* Reset id sequence when importing a dump from postgres ([0f555b7](0f555b7ec74ad493d2f70a4f4040db333943dc1c))
* Add validation for negative repeat after values ([dd46174](dd461746a655d716ef142d96a2bcef5615de3dd9))
* Lint ([1feb62c](1feb62cc458e939d46d16d24347557e7959ddfb9))
* Make sure to use user discoverability settings when searching list users ([382a788](382a7884be1f37da5c8f657c4b17316d8691dd59))
* Properly decode params in url ([8f27e7e](8f27e7e619ac73716211d838f52c73d7d97aead5))
* Return all users on a list when no search param was provided ([c51ee94](c51ee94ad1d552d69c71adfc2180c7ad0d23235d))
* Don't return email addresses from user search results ([3688bbd](3688bbde20e989397353ea4f7e872b00a53099c2))
* Lint ([77fafd5](77fafd5dc32aee464961be40d5d0ccf82490d02a))
* Increase test timeout ([26e2d0b](26e2d0bddeaea902dba055baf7a4c866a44ba7f1))
* Switch to buster for build image ([59796fd](59796fd4905fca74d26c5541878379cda143a30e))
* Use our own build image as base build image ([b6d7323](b6d7323cdfac958c9740feba1342114ab13a0afd))
* Use golang build image to test migrations ([84bcdbf](84bcdbf937c3be7823fcf8d5fef52e3cbb1c9bde))
* Switch back to alpine for everything, disable arm 32 docker builds ([7ffe9b6](7ffe9b625e441202a704db2774dd66fc38244c6d))
### Dependencies
* *(deps)* Update golang.org/x/sys commit hash to a851e7d (#972)
* *(deps)* Update golang.org/x/sys commit hash to aa78b53 (#973)
* *(deps)* Update golang.org/x/sys commit hash to 528a39c (#974)
* *(deps)* Update golang.org/x/sys commit hash to 437939a (#975)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.1 (#976)
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.1.0 (#985)
* *(deps)* Update module github.com/spf13/viper to v1.9.0 (#987)
* *(deps)* Update golang.org/x/crypto commit hash to 089bfa5 (#979)
* *(deps)* Update golang.org/x/term commit hash to 140adaa (#983)
* *(deps)* Update module github.com/labstack/echo/v4 to v4.6.0 (#988)
* *(deps)* Update golang.org/x/sys commit hash to b8560ed (#989)
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.1.0 (#991)
* *(deps)* Update golang.org/x/sys commit hash to 92d5a99 (#992)
* *(deps)* Update module github.com/swaggo/swag to v1.7.3 (#990)
* *(deps)* Update module github.com/labstack/echo/v4 to v4.6.1 (#993)
* *(deps)* Update golang.org/x/sys commit hash to 1cf2251 (#994)
* *(deps)* Update golang.org/x/sys commit hash to 39ccf1d (#995)
* *(deps)* Update golang.org/x/term commit hash to 03fcf44 (#996)
* *(deps)* Update golang.org/x/oauth2 commit hash to 6b3c2da (#1000)
* *(deps)* Update golang.org/x/sys commit hash to 69063c4 (#1001)
* *(deps)* Update module github.com/gabriel-vasile/mimetype to v1.4.0 (#1004)
* *(deps)* Update postgres docker tag to v14 (#1005)
* *(deps)* Update module github.com/go-redis/redis/v8 to v8.11.4 (#1003)
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.9 (#1008)
* *(deps)* Update golang.org/x/sys commit hash to 9d821ac (#1009)
* *(deps)* Update golang.org/x/sys commit hash to 0ec99a6 (#1010)
* *(deps)* Update golang.org/x/sys commit hash to 9d61738 (#1011)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.2 (#1012)
* *(deps)* Update golang.org/x/sys commit hash to 8e51046 (#1016)
* *(deps)* Update golang.org/x/sys commit hash to d6a326f (#1017)
* *(deps)* Update module github.com/swaggo/swag to v1.7.4 (#1018)
* *(deps)* Update golang.org/x/sys commit hash to 711f33c (#1019)
* *(deps)* Update golang.org/x/sys commit hash to 69cdffd (#1020)
* *(deps)* Update golang.org/x/oauth2 commit hash to ba495a6 (#1022)
* *(deps)* Update golang.org/x/image commit hash to 6944b10 (#1023)
* *(deps)* Update golang.org/x/sys commit hash to 6e78728 (#1024)
* *(deps)* Update golang.org/x/sys commit hash to b3129d9 (#1025)
* *(deps)* Update golang.org/x/sys commit hash to 611d5d6 (#1026)
* *(deps)* Update golang.org/x/sys commit hash to 39c9dd3 (#1027)
* *(deps)* Update golang.org/x/sys commit hash to a2f17f7 (#1028)
* *(deps)* Update golang.org/x/sys commit hash to 4dd7244 (#1029)
* *(deps)* Update golang.org/x/sys commit hash to ae416a5 (#1030)
* *(deps)* Update golang.org/x/sys commit hash to 7861aae (#1031)
* *(deps)* Update golang.org/x/oauth2 commit hash to d3ed0bb (#1032)
* *(deps)* Update module github.com/labstack/gommon to v0.3.1 (#1033)
* *(deps)* Update golang.org/x/sys commit hash to c75c477 (#1034)
* *(deps)* Update golang.org/x/sys commit hash to ebca88c (#1035)
* *(deps)* Update golang.org/x/sys commit hash to e0b2ad0 (#1037)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.3 (#1038)
* *(deps)* Update golang.org/x/crypto commit hash to ceb1ce7 (#1041)
* *(deps)* Update module github.com/lib/pq to v1.10.4 (#1040)
* *(deps)* Update golang.org/x/sys commit hash to 51b60fd (#1042)
* *(deps)* Update golang.org/x/sys commit hash to 99a5385 (#1043)
* *(deps)* Update golang.org/x/sys commit hash to f221eed (#1044)
* *(deps)* Update golang.org/x/sys commit hash to 0c823b9 (#1045)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.4 (#1046)
* *(deps)* Update golang.org/x/sys commit hash to 0a5406a (#1048)
* *(deps)* Update golang.org/x/crypto commit hash to b4de73f (#1047)
* *(deps)* Update module github.com/ulule/limiter/v3 to v3.9.0 (#1049)
* *(deps)* Update golang.org/x/crypto commit hash to ae814b3 (#1050)
* *(deps)* Update golang.org/x/sys commit hash to dee7805 (#1051)
* *(deps)* Update golang.org/x/sys commit hash to ef496fb (#1052)
* *(deps)* Update golang.org/x/sys commit hash to fe61309 (#1054)
* *(deps)* Update module github.com/swaggo/swag to v1.7.6 (#1055)
* *(deps)* Update golang.org/x/crypto commit hash to 5770296 (#1056)
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.2.0 (#1057)
* *(deps)* Update golang.org/x/sys commit hash to 94396e4 (#1058)
* *(deps)* Update golang.org/x/sys commit hash to 97ca703 (#1059)
* *(deps)* Update golang.org/x/crypto commit hash to 4570a08 (#1062)
* *(deps)* Update golang.org/x/sys commit hash to 798191b (#1061)
* *(deps)* Update golang.org/x/sys commit hash to af8b642 (#1063)
* *(deps)* Update module github.com/spf13/viper to v1.10.0 (#1064)
* *(deps)* Update golang.org/x/sys commit hash to 03aa0b5 (#1067)
* *(deps)* Update golang.org/x/sys commit hash to 3b038e5 (#1068)
* *(deps)* Update module github.com/spf13/cobra to v1.3.0 (#1070)
* *(deps)* Update golang.org/x/sys commit hash to 4825e8c (#1071)
* *(deps)* Update module github.com/spf13/viper to v1.10.1 (#1072)
* *(deps)* Update golang.org/x/crypto commit hash to e495a2d (#1073)
* *(deps)* Update golang.org/x/sys commit hash to 4abf325 (#1074)
* *(deps)* Update golang.org/x/sys commit hash to 1d35b9e (#1075)
* *(deps)* Update module github.com/magefile/mage to v1.12.0 (#1076)
* *(deps)* Update module github.com/magefile/mage to v1.12.1 (#1077)
* *(deps)* Update module github.com/getsentry/sentry-go to v0.12.0 (#1079)
* *(deps)* Update module github.com/swaggo/swag to v1.7.8 (#1080)
* *(deps)* Update module github.com/spf13/afero to v1.7.0 (#1078)
* *(deps)* Update module github.com/spf13/afero to v1.7.1 (#1081)
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.10 (#1082)
* *(deps)* Update module github.com/spf13/afero to v1.8.0 (#1083)
* *(deps)* Update module github.com/labstack/echo/v4 to v4.6.2 (#1084)
* *(deps)* Update module github.com/labstack/echo/v4 to v4.6.3 (#1089)
* *(deps)* Update golang.org/x/sys commit hash to a018aaa (#1088)
* *(deps)* Update golang.org/x/sys commit hash to 5a964db (#1090)
* *(deps)* Update golang.org/x/crypto commit hash to 5e0467b (#1091)
* *(deps)* Update golang.org/x/sys commit hash to da31bd3 (#1093)
* *(deps)* Update module github.com/prometheus/client_golang to v1.12.0 (#1094)
* *(deps)* Update golang.org/x/crypto commit hash to e04a857 (#1097)
* *(deps)* Update golang.org/x/crypto commit hash to aa10faf (#1098)
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.11 (#1099)
* *(deps)* Update golang.org/x/crypto commit hash to 198e437 (#1100)
* *(deps)* Update golang.org/x/sys commit hash to 99c3d69 (#1101)
* *(deps)* Update module github.com/prometheus/client_golang to v1.12.1 (#1102)
* *(deps)* Update klakegg/hugo docker tag to v0.92.0 (#1103)
* *(deps)* Update klakegg/hugo docker tag to v0.92.1 (#1104)
* *(deps)* Update golang.org/x/crypto commit hash to 30dcbda (#1105)
* *(deps)* Update module github.com/swaggo/swag to v1.7.9 (#1106)
* *(deps)* Update golang.org/x/sys commit hash to 1c1b9b1 (#1107)
* *(deps)* Update module github.com/spf13/afero to v1.8.1 (#1108)
* *(deps)* Update golang.org/x/sys commit hash to 5739886 (#1110)
* *(deps)* Update golang.org/x/crypto commit hash to 20e1d8d (#1111)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.5 (#1112)
* *(deps)* Update golang.org/x/crypto commit hash to bba287d (#1113)
* *(deps)* Update golang.org/x/crypto commit hash to dad3315 (#1114)
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.3.0 (#1117)
* *(deps)* Update golang.org/x/sys commit hash to 3681064 (#1116)
* *(deps)* Update golang.org/x/crypto commit hash to db63837 (#1115)
* *(deps)* Update golang.org/x/crypto commit hash to f4118a5 (#1118)
* *(deps)* Update golang.org/x/crypto commit hash to 8634188 (#1121)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.6 (#1122)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.7 (#1123)
* *(deps)* Update module github.com/swaggo/swag to v1.8.0 (#1124)
* *(deps)* Update golang.org/x/sys commit hash to 0005352 (#1125)
* *(deps)* Update golang.org/x/sys commit hash to f242548 (#1126)
* *(deps)* Update klakegg/hugo docker tag to v0.92.2 (#1127)
* *(deps)* Update golang.org/x/sys commit hash to dbe011f (#1129)
* *(deps)* Update golang.org/x/sys commit hash to 95c6836 (#1130)
* *(deps)* Update golang.org/x/oauth2 commit hash to ee48083 (#1128)
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.12 (#1132)
* *(deps)* Update golang.org/x/sys commit hash to 4e6760a (#1131)
* *(deps)* Update golang.org/x/image commit hash to 723b81c (#1133)
* *(deps)* Update module github.com/labstack/echo/v4 to v4.7.0 (#1134)
* *(deps)* Update klakegg/hugo docker tag to v0.93.0 (#1135)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.8 (#1136)
* *(deps)* Update klakegg/hugo docker tag to v0.93.2 (#1137)
* *(deps)* Update golang.org/x/sys commit hash to 22a9840 (#1138)
* *(deps)* Update golang.org/x/crypto commit hash to efcb850 (#1139)
* *(deps)* Update golang.org/x/oauth2 commit hash to 6242fa9 (#1140)
* *(deps)* Update golang.org/x/sys commit hash to b874c99 (#1141)
* *(deps)* Update klakegg/hugo docker tag to v0.93.3 (#1142)
* *(deps)* Update module github.com/labstack/echo/v4 to v4.7.1 (#1146)
* *(deps)* Update module github.com/stretchr/testify to v1.7.1 (#1148)
* *(deps)* Update module github.com/swaggo/swag to v1.8.1 (#1156)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.11 (#1143)
* *(deps)* Update module github.com/spf13/cobra to v1.4.0 (#1145)
* *(deps)* Update module github.com/lib/pq to v1.10.5 (#1157)
* *(deps)* Update module github.com/spf13/viper to v1.11.0 (#1159)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.12 (#1162)
* *(deps)* Update module github.com/prometheus/client_golang to v1.12.2 (#1166)
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.13 (#1165)
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.2.0 (#1164)
* *(deps)* Update module github.com/swaggo/swag to v1.8.2 (#1167)
* *(deps)* Update module github.com/lib/pq to v1.10.6 (#1169)
* *(deps)* Update module gopkg.in/yaml.v3 to v3.0.1 (#1179)
* *(deps)* Update module github.com/imdario/mergo to v0.3.13 (#1178)
* *(deps)* Update module github.com/stretchr/testify to v1.7.2 (#1182)
* *(deps)* Update module github.com/swaggo/swag to v1.8.3 (#1185)
* *(deps)* Update module github.com/spf13/cobra to v1.5.0 (#1192)
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.4.2 (#1193)
* *(deps)* Update module github.com/stretchr/testify to v1.8.0 (#1191)
* *(deps)* Update module github.com/go-testfixtures/testfixtures/v3 to v3.8.0 (#1168)
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.14 (#1194)
* *(deps)* Update golang.org/x/term digest to 065cf7b (#1207)
* *(deps)* Update golang.org/x/image digest to 41969df (#1203)
* *(deps)* Update module github.com/yuin/goldmark to v1.4.13 (#1209)
* *(deps)* Update golang.org/x/crypto digest to 0559593 (#1202)
* *(deps)* Update module github.com/spf13/afero to v1.9.0 (#1210)
* *(deps)* Update module github.com/gabriel-vasile/mimetype to v1.4.1 (#1208)
* *(deps)* Update golang.org/x/sync digest to 0de741c (#1205)
* *(deps)* Update github.com/c2h5oh/datasize digest to 859f65c (#1201)
* *(deps)* Update golang.org/x/oauth2 digest to 2104d58 (#1204)
* *(deps)* Update golang.org/x/sys digest to c0bba94 (#1206)
* *(deps)* Update golang.org/x/oauth2 digest to c8730f7 (#1214)
* *(deps)* Update module github.com/spf13/afero to v1.9.2 (#1215)
* *(deps)* Update module github.com/swaggo/swag to v1.8.4 (#1216)
* *(deps)* Update module github.com/spf13/viper to v1.12.0 (#1180)
* *(deps)* Update golang.org/x/sys digest to 1609e55 (#1217)
* *(deps)* Update module github.com/go-testfixtures/testfixtures/v3 to v3.8.1 (#1226)
* *(deps)* Update module go to 1.18 (#1225)
### Documentation
* Add docker-compose example with no proxy ([4255bc3](4255bc3a945b6fe4314e3cd3f62908dd1be1ff4a))
* Add another youtube tutorial ([dbd6f36](dbd6f36da6e56355993cc1411379997e26c88b36))
* Fix api url in docker examples without a proxy ([68998e9](68998e90a446569869fb150bd5fc0739f496b066))
* Make sure all links to vikunja pages are https ([cc612d5](cc612d505f22e5d895b6ebda61fe62498634cec5))
* Update backup instructions ([4829c89](4829c899400544ad27cacfb7d19b40988302a413))
* Add postgres to docker-compose examples ([2aea169](2aea1691cf33b7d9e03fbe2c711af7d8f76d9724))
* Improve development docs ([9bf32aa](9bf32aae99a7e69cce0cd4477e8fc8ddcaea25ea))
* Add another tutorial link ([1fa74cb](1fa74cba6407c2b694b14f8439f1492476433d62))
* Improve wording for systemd ([13561f2](13561f211493903b17c856b3010345ea9df725d4))
* Update testing ([da318e3](da318e3db15121ba864db8450a76ba9ed18b9fd5))
* Add guide for Synology NAS ([049ae39](049ae39c62079f77921b7a9fad5023b2c1c0c1c5))
### Features
* *(docs)* Add details of using NGINX Proxy Manager to the Reverse Proxy docs (#13)
* *(docs)* Add versions explanation
* *(mail)* Don't try to authenticate when no username and password was provided* Add better error logs for mage commands ([bb086eb](bb086eb9f87669f844c283d42ea9ca9f3f5a7877))
* Expose if task comments are enabled or not in /info ([ae8db17](ae8db176db57fa6176e00b87924f70352332ca66))
* Improve account deletion email grammar (#1006) ([dcb52c0](dcb52c00f1c6b3217e2b508d7799fc83adb3b055))
* Add more debug logging when deleting users ([8f55af0](8f55af07c936218487ec94e65c6673fbddd0cdb5))
* Don't require a password for data export from users authenticated with third-party auth ([9eca971](9eca971c938699d481915fb6e14c765aea1fa3b5))
* Expose if a user is a local user through its jwt token ([516c812](516c812043e77be7f834ae1326d13d39e156ef77))
* Expose if a user is a local user through the /user endpoint ([2683ef2](2683ef23d538eb846d5d799798fa82cca70dc017))
* Enable rate limit for unauthenticated routes ([093d0c6](093d0c65ca6338358dbd1df904daadd7808f2817))
* Use wallpaper topic for default unsplash background list ([88a2ced](88a2cede19f1844814530af948c3cc5a0b026419))
* Gravatar - Lowercase emails before MD5 hash (#10) ([36bf3d2](36bf3d216a7be28e917e2816a9e5da43439f2c20))
* Add marble avatar (#1060) ([73ee696](73ee696fc3cf941af2d2c2cf81224aa01f93234e))
* Save user language in the settings ([a98119f](a98119f2d670a11efab6008129b767f9208f8113))
* Add time zone setting for reminders (#1092) ([61d49c3](61d49c3a56a59e52ce407b858ddd4aa573dbee9d))
* Add long-lived api tokens (#1085) ([1322cb1](1322cb16d76a40ad90631e3e091da0f0d44957a9))
* Upgrade golangci-lint to 1.45.2 ([5cf263a](5cf263a86f954a38cbfafb6b0857bf591f82a811))
* Add date math for filters (#1086) ([0a1d8c9](0a1d8c940410b03a78016ac6110883ca05484816))
* Add migration to create BlurHash strings for all list backgrounds ([362706b](362706b38d52720b5a1615e185a985b7708168f7))
* Generate a BlurHash when uploading a new image ([f83b09a](f83b09af59ed25425a16824ccf48d903c81e861a))
* Save BlurHash from unsplash when selecting a photo from unsplash ([2ec7d7a](2ec7d7a8a85cc12c07d20cfab9b90a78a7857eb6))
* Return BlurHash for unsplash search results ([6df8658](6df865876df961f2bec476126bf6e7fbe5d43e0e))
* Add caldav tokens (#1065) ([e4b50e8](e4b50e84a44f809cc829c2fdb6f52b03b40a367b))
* Ability to serve static files (#1174) ([acaa850](acaa85083f2bebbc67608ae0f454ed5e9a3ef8a0))
* Restrict max avatar size ([2f25b48](2f25b48869f59256bf7d692c4486c64c30b85e5e))
* Send overdue tasks email notification at 9:00 in the user's time zone ([7eb3b96](7eb3b96a4465ca6648572b07c506c06f2c28c375))
* Add setting to change overdue tasks reminder email time ([8869adf](8869adfc276f674b686bf68f949d7efbb417e55b))
* Allow only the authors of task comments to edit them ([01271c4](01271c4c0111b3b040dcb9a0d502d31078ad6d4b))
* Migrate away from gomail ([30e0e98](30e0e98f7738e36698990523377f47edcbf6806c))
* Embed the vikunja logo as inline attachment ([f4f8450](f4f8450d166f1a836eea202dd0340d2156d3dfe9))
* Use embed fs directly to embed the logo in mails ([73c4c39](73c4c399e5d610bb713f1e9feab543e0425ee959))
* Use actual uuids for tasks ([62325de](62325de9cd5da5b70987081956a28e7baa907081))
* Add issue template ([117f6b3](117f6b38e1d35c09f2657975ea75dcfedcd8425d))
### Miscellaneous Tasks
* *(ci)* Use latest version of s3 plugin
* *(ci)* Sign drone config
* *(docs)* Update docs about compiling from source
* *(docs)* Redirect properly from /docs/docs
* *(docs)* Add new mailer option to docs
* *(docs)* Clarify openid setup with environment variables
* *(docs)* Add frontendurl to all example configs
* *(mage)* Don't set api packages when they are not used* Sign drone config ([1d8d0f1](1d8d0f140e4f2a59947167bd597e5f12b84b009d))
* Cleanup namespace creation ([b60c69c](b60c69c5a8c004a780b989cf0bb8ab6455086b0f))
* Generate swagger docs ([ba2bdff](ba2bdff39109db9ecc4b525e39e2642b41ac03b8))
* Go mod tidy ([726a517](726a517bec731f1af8e3186e280718fef02cadf7))
* Upgrade trello api wrapper and remove fork ([7e99618](7e99618319547c7e7dfa2cc063f654300f7074fb))
* Use our custom build image to build docker image ([251b877](251b877015761fdd2b8dbd18cd8ec696dc374103))
* Update golangci-lint ([430057a](430057a404b04e75c62a15693f479c6fc8e63189))
### Other
* *(other)* Healthcheck endpoint (#998)
* *(other)* Added the ability to configure the JWT expiry date using a new server.jwtttl config parameter. (#999)
* *(other)* Enable a list to be moved across namespaces (#1096)
* *(other)* A bunch of dependency updates at once (#1155)
* *(other)* Add client-cert parameters of the Go pq driver to the Vikunja config (#1161)
* *(other)* Add exec to run script to run app as PID 1 (#1200)
## [0.18.1] - 2021-09-08
### Fixed

View File

@ -1,27 +1,29 @@
##############
# Build stage
FROM golang:1.18-alpine AS build-env
RUN apk --no-cache add build-base git && \
go install github.com/magefile/mage@latest && \
mv /go/bin/mage /usr/local/go/bin
FROM golang:1-alpine3.12 AS build-env
ARG VIKUNJA_VERSION
ENV TAGS "sqlite"
ENV GO111MODULE=on
# Build deps
RUN apk --no-cache add build-base git
# Setup repo
COPY . /go/src/code.vikunja.io/api
WORKDIR /go/src/code.vikunja.io/api
COPY . ${GOPATH}/src/code.vikunja.io/api
WORKDIR ${GOPATH}/src/code.vikunja.io/api
# Checkout version if set
RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi \
&& go install github.com/magefile/mage \
&& mage build:clean build
###################
# The actual image
# Note: I wanted to use the scratch image here, but unfortunatly the go-sqlite bindings require cgo and
# because of this, the container would not start when I compiled the image without cgo.
FROM alpine:3.16
FROM alpine:3.12
LABEL maintainer="maintainers@vikunja.io"
WORKDIR /app/vikunja/
@ -37,7 +39,7 @@ RUN apk --no-cache add shadow && \
chown vikunja -R /app/vikunja
COPY run.sh /run.sh
# Add time zone data
# Fix time zone settings not working
RUN apk --no-cache add tzdata
# Files permissions

View File

@ -1,48 +0,0 @@
##############
# Build stage
FROM golang:1.18-buster AS build-env
RUN go install github.com/magefile/mage@latest && \
mv /go/bin/mage /usr/local/go/bin
ARG VIKUNJA_VERSION
# Setup repo
COPY . /go/src/code.vikunja.io/api
WORKDIR /go/src/code.vikunja.io/api
# Checkout version if set
RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi \
&& mage build:clean build
###################
# The actual image
# Note: I wanted to use the scratch image here, but unfortunatly the go-sqlite bindings require cgo and
# because of this, the container would not start when I compiled the image without cgo.
# We're using debian as a base image here because the latest alpine image does not work with arm.
FROM debian:buster-slim
LABEL maintainer="maintainers@vikunja.io"
WORKDIR /app/vikunja/
COPY --from=build-env /go/src/code.vikunja.io/api/vikunja .
ENV VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/
# Dynamic permission changing stuff
ENV PUID 1000
ENV PGID 1000
RUN addgroup --gid ${PGID} vikunja && \
chown ${PUID} -R /app/vikunja && \
useradd --shell /bin/sh --gid vikunja --uid ${PUID} --home-dir /app/vikunja vikunja
COPY run.sh /run.sh
# Fix time zone settings not working
RUN apt-get update && apt-get install -y tzdata && apt-get clean
# Files permissions
RUN mkdir /app/vikunja/files && \
chown -R vikunja /app/vikunja/files
VOLUME /app/vikunja/files
CMD ["/run.sh"]
EXPOSE 3456

View File

@ -2,7 +2,7 @@
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/api/status.svg)](https://drone.kolaente.de/vikunja/api)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.19.2-brightgreen.svg)](https://dl.vikunja.io)
[![Download](https://img.shields.io/badge/download-v0.18.1-brightgreen.svg)](https://dl.vikunja.io)
[![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/kolaente.dev/vikunja/api)](https://goreportcard.com/report/kolaente.dev/vikunja/api)
@ -56,10 +56,6 @@ See [the roadmap](https://my.vikunja.cloud/share/QFyzYEmEYfSyQfTOmIRSwLUpkFjboaB
Fork -> Push -> Pull-Request. Also see the [dev docs](https://vikunja.io/docs/development/) for more info.
## Sponsors
[![Relm](https://vikunja.io/images/sponsors/relm.png)](https://relm.us)
## License
This project is licensed under the AGPLv3 License. See the [LICENSE](LICENSE) file for the full license text.

View File

@ -1,59 +0,0 @@
[changelog]
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
* *({{commit.scope}})* {{ commit.message | upper_first }}
{%- if commit.breaking %}
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{%- endif -%}
{%- endfor -%}
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
* {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))
{% if commit.breaking -%}
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{% endif -%}
{% endif -%}
{% endfor -%}
{% raw %}\n{% endraw %}\
{% endfor %}\n
"""
#{% for group, commits in commits | group_by(attribute="group") %}
# ### {{ group | upper_first }}
# {% for commit in commits %}\
# - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))
# {% endfor %}\
#{% endfor %}\n
# remove the leading and trailing whitespace from the template
trim = true
[git]
conventional_commits = true
filter_unconventional = false
commit_parsers = [
{ message = ".*(deps).*", group = "Dependencies"},
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
{ message = ".*", group = "Other", default_scope = "other"}, # Everything that's not a conventional commit goes into the "Other" category
]

View File

@ -22,8 +22,6 @@ service:
# 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: <rootpath>
# Path on the file system to serve static files from. Set to the path of the frontend files to host frontend alongside the api.
staticpath: ""
# The max number of items which can be returned per page
maxitemsperpage: 50
# Enable the caldav endpoint, see the docs for more details
@ -56,20 +54,17 @@ service:
# it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
# for user deletion.
enableuserdeletion: true
# The maximum size clients will be able to request for user avatars.
# If clients request a size bigger than this, it will be changed on the fly.
maxavatarsize: 1024
database:
# Database type to use. Supported types are mysql, postgres and sqlite.
type: "sqlite"
# Database user which is used to connect to the database.
user: "vikunja"
# Database password
# Databse password
password: ""
# Database host
# Databse host
host: "localhost"
# Database to use
# Databse to use
database: "vikunja"
# When using sqlite, this is the path where to store the data
path: "./vikunja.db"
@ -82,12 +77,6 @@ database:
# Secure connection mode. Only used with postgres.
# (see https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection_String_Parameters)
sslmode: disable
# The path to the client cert. Only used with postgres.
sslcert: ""
# The path to the client key. Only used with postgres.
sslkey: ""
# The path to the ca cert. Only used with postgres.
sslrootcert: ""
# Enable SSL/TLS for mysql connections. Options: false, true, skip-verify, preferred
tls: false
@ -127,11 +116,8 @@ mailer:
enabled: false
# SMTP Host
host: ""
# SMTP Host port.
# **NOTE:** If you're unable to send mail and the only error you see in the logs is an `EOF`, try setting the port to `25`.
# SMTP Host port
port: 587
# SMTP Auth Type. Can be either `plain`, `login` or `cram-md5`.
authtype: "plain"
# SMTP username
username: "user"
# SMTP password
@ -302,8 +288,6 @@ auth:
enabled: false
# The url to redirect clients to. Defaults to the configured frontend url. If you're using Vikunja with the official
# frontend, you don't need to change this value.
# **Note:** The redirect url must exactly match the configured redirect url with the third party provider.
# This includes all slashes at the end or protocols.
redirecturl: <frontend url>
# A list of enabled providers
providers:

View File

@ -36,9 +36,8 @@ Make sure to check the other doc articles for specific development tasks like [t
## Frontend requirements
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
More instructions can be found in the repo's README.
You need to have [pnpm](https://pnpm.io/) and nodejs in version 16 or 18 installed.
You need to have yarn v1 and nodejs in version 16 installed.
## Git flow

View File

@ -98,12 +98,12 @@ Check out the docs [in the frontend repo](https://kolaente.dev/vikunja/frontend/
To run the frontend unit tests, run
{{< highlight bash >}}
pnpm test:unit
yarn test:unit
{{< /highlight >}}
The frontend also has a watcher available that re-runs all unit tests every time you change something.
To use it, simply run
{{< highlight bash >}}
pnpm test:unit-watch
yarn test:unit-watch
{{< /highlight >}}

View File

@ -20,7 +20,7 @@ The Vikunja API has no other dependencies than go itself.
That means compiling it boils down to these steps:
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.17`.
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
2. Make sure [Mage](https://magefile) is properly installed on your system.
3. Clone the repo with `git clone https://code.vikunja.io/api` and switch into the directory.
3. Run `mage build: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.
@ -38,7 +38,9 @@ More options are available, please refer to the [magefile docs]({{< ref "../deve
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
1. Make sure you have [pnpm](https://pnpm.io/) properly installed on your system.
2. Clone the repo with `git clone https://code.vikunja.io/frontend` and switch into the directory.
3. Install all dependencies with `pnpm install`
4. Build the frontend with `pnpm build`. This will result in a static js bundle in the `dist/` folder which you can deploy.
You need to have yarn v1 and nodejs in version 16 installed.
1. Make sure [yarn v1](https://yarnpkg.com/getting-started/install) is properly installed on your system.
3. Clone the repo with `git clone https://code.vikunja.io/frontend` and switch into the directory.
3. Install all dependencies with `yarn install`
4. Build the frontend with `yarn build`. This will result in a js bundle in the `dist/` folder which you can deploy.

View File

@ -10,9 +10,8 @@ menu:
# Configuration options
You can either use a `config.yml` file in the root directory of vikunja or set almost all config option with
You can either use a `config.yml` file in the root directory of vikunja or set all config option with
environment variables. If you have both, the value set in the config file is used.
Right now it is not possible to configure openid authentication via environment variables.
Variables are nested in the `config.yml`, these nested variables become `VIKUNJA_FIRST_CHILD` when configuring via
environment variables. So setting
@ -77,7 +76,7 @@ Default: `<jwt-secret>`
Full path: `service.JWTSecret`
Environment path: `VIKUNJA_SERVICE_JWTSECRET`
Environment path: `VIKUNJA_SERVICE_JWT_SECRET`
### jwtttl
@ -162,17 +161,6 @@ Full path: `service.rootpath`
Environment path: `VIKUNJA_SERVICE_ROOTPATH`
### staticpath
Path on the file system to serve static files from. Set to the path of the frontend files to host frontend alongside the api.
Default: `<empty>`
Full path: `service.staticpath`
Environment path: `VIKUNJA_SERVICE_STATICPATH`
### maxitemsperpage
The max number of items which can be returned per page
@ -322,18 +310,6 @@ Full path: `service.enableuserdeletion`
Environment path: `VIKUNJA_SERVICE_ENABLEUSERDELETION`
### maxavatarsize
The maximum size clients will be able to request for user avatars.
If clients request a size bigger than this, it will be changed on the fly.
Default: `1024`
Full path: `service.maxavatarsize`
Environment path: `VIKUNJA_SERVICE_MAXAVATARSIZE`
---
## database
@ -364,7 +340,7 @@ Environment path: `VIKUNJA_DATABASE_USER`
### password
Database password
Databse password
Default: `<empty>`
@ -375,7 +351,7 @@ Environment path: `VIKUNJA_DATABASE_PASSWORD`
### host
Database host
Databse host
Default: `localhost`
@ -386,7 +362,7 @@ Environment path: `VIKUNJA_DATABASE_HOST`
### database
Database to use
Databse to use
Default: `vikunja`
@ -451,39 +427,6 @@ Full path: `database.sslmode`
Environment path: `VIKUNJA_DATABASE_SSLMODE`
### sslcert
The path to the client cert. Only used with postgres.
Default: `<empty>`
Full path: `database.sslcert`
Environment path: `VIKUNJA_DATABASE_SSLCERT`
### sslkey
The path to the client key. Only used with postgres.
Default: `<empty>`
Full path: `database.sslkey`
Environment path: `VIKUNJA_DATABASE_SSLKEY`
### sslrootcert
The path to the ca cert. Only used with postgres.
Default: `<empty>`
Full path: `database.sslrootcert`
Environment path: `VIKUNJA_DATABASE_SSLROOTCERT`
### tls
Enable SSL/TLS for mysql connections. Options: false, true, skip-verify, preferred
@ -657,8 +600,7 @@ Environment path: `VIKUNJA_MAILER_HOST`
### port
SMTP Host port.
**NOTE:** If you're unable to send mail and the only error you see in the logs is an `EOF`, try setting the port to `25`.
SMTP Host port
Default: `587`
@ -667,17 +609,6 @@ Full path: `mailer.port`
Environment path: `VIKUNJA_MAILER_PORT`
### authtype
SMTP Auth Type. Can be either `plain`, `login` or `cram-md5`.
Default: `plain`
Full path: `mailer.authtype`
Environment path: `VIKUNJA_MAILER_AUTHTYPE`
### username
SMTP username

View File

@ -50,8 +50,6 @@ services:
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
volumes:
- ./files:/app/vikunja/files
depends_on:

View File

@ -103,8 +103,6 @@ services:
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
VIKUNJA_SERVICE_FRONTENDURL: http://<your public frontend url with slash>/
ports:
- 3456:3456
volumes:
@ -143,8 +141,6 @@ services:
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
volumes:
- ./files:/app/vikunja/files
networks:
@ -203,8 +199,6 @@ services:
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
volumes:
- ./files:/app/vikunja/files
networks:
@ -298,8 +292,6 @@ services:
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
volumes:
- ./files:/app/vikunja/files
depends_on:
@ -358,8 +350,6 @@ services:
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
volumes:
- ./files:/app/vikunja/files
depends_on:
@ -389,7 +379,7 @@ you can prepare 2 proxy rules:
* a redirection rule for vikunja's api (see example screenshot using port 3456)
* a similar redirection rule for vikunja's frontend (using port 4321)
![Synology Proxy Settings](/docs/synology-proxy-1.png)
![Synology Proxy Settings](/synology-proxy-1.png)
You should also add 2 empty folders for mariadb and vikunja inside Synology's
docker main folders:
@ -409,7 +399,7 @@ To do that, you can
2. Give it the name Vikunja and paste the adapted docker compose file
3. Deploy the Stack with the "Delpoy Stack" button:
![Portainer Stack deploy](/docs/synology-proxy-2.png)
![Portainer Stack deploy](/synology-proxy-2.png)
The docker-compose file we're going to use is very similar to the [example without any proxy](#example-without-any-proxy) above:
@ -436,8 +426,6 @@ services:
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
ports:
- 3456:3456
volumes:

View File

@ -149,7 +149,6 @@ services:
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_SERVICE_JWTSECRET: <generated secret>
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
volumes:
- ./files:/app/vikunja/files
db:

View File

@ -11,20 +11,16 @@ menu:
# Installing
Vikunja consists of two parts: [API](https://code.vikunja.io/api) and [frontend](https://code.vikunja.io/frontend).
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.
You will always need to install at least the API.
To actually use Vikunja you'll also need to somehow install a frontend to use it.
You can either:
However, using the web frontend is highly reccommended.
* [Install the web frontend]({{< ref "install-frontend.md">}})
* Use the desktop app, which is essentially a web frontend packaged for easy installation on desktop devices
* Use the mobile app only, but as of right now it only supports the very basic features of Vikunja
Vikunja can be installed in various ways.
Vikunja can be installed in various forms.
This document provides an overview and instructions for the different methods.
* [API]({{< ref "install-backend.md">}})
* [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">}})

View File

@ -1,15 +0,0 @@
---
title: "Hosting Vikunja with k8s"
date: 2022-08-12T13:41:48+02:00
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
---
There are two third-party Helm-Charts which can be used to host Vikunja with k8s:
* [Truecharts](https://truecharts.org/docs/charts/stable/vikunja/)
* [k8s at Home](https://github.com/k8s-at-home/charts)

View File

@ -1,45 +0,0 @@
---
date: "2022-08-09:00:00+02:00"
title: "OpenID example configurations"
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
---
# OpenID example configurations
On this page you will find examples about how to set up Vikunja with a third-party OpenID provider.
To add another example, please [edit this document](https://kolaente.dev/vikunja/api/src/branch/main/docs/content/doc/setup/openid-examples.md) and send a PR.
{{< table_of_contents >}}
## Authelia
Vikunja Config:
```yaml
openid:
enabled: true
redirecturl: https://vikunja.mydomain.com/auth/openid/ <---- slash at the end is important
providers:
- name: Authelia
authurl: https://login.mydomain.com
clientid: <vikunja-id>
clientsecret: <vikunja secret>
```
Authelia config:
```yaml
- id: <vikunja-id>
description: Vikunja
secret: <vikunja secret>
redirect_uris:
- https://vikunja.mydomain.com/auth/openid/ <----- Matching slash at the end
scopes:
- openid
- email
- profile
```

View File

@ -1,39 +0,0 @@
---
title: "Running Vikunja in a subdirectory"
date: 2022-09-23T12:15:04+02:00
draft: false
menu:
sidebar:
parent: "setup"
---
# Running Vikunja in a subdirectory
Running Vikunja in a subdirectory is not supported out of the box.
However, you can still run it in a subdirectory but need to build the frontend yourself.
## Frontend
First, make sure you're able to build the frontend from source.
Check [the guide about building from source]({{< ref "build-from-source.md">}}#frontend) about that.
Then, run
```
pnpm vite build --base=/SUBPATH
pnpm workbox copyLibraries dist/
```
Where `SUBPATH` is the subdirectory you want to run Vikunja on.
Once you have the build files you can deploy them as usual.
Note that when deploying in docker you'll need to put the files in a web container yourself, you
can't use the `Dockerfile` in the repo without modifications.
## API
If you're not using a reverse proxy you're good to go.
Simply configure the api url in the frontend as you normally would.
If you're using a reverse proxy you'll need to adjust the paths so that the api is available at `/SUBPATH/api/v1`.
You can check if everything is working correctly by opening `/SUBPATH/api/v1/info` in a browser.

View File

@ -1,45 +0,0 @@
---
date: "2022-07-07:00:00+02:00"
title: "Versions"
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
---
# Vikunja Versions
The Vikunja api and frontend are available in two different release flavors.
{{< table_of_contents >}}
## Stable
Stable releases have a fixed version number like `0.18.2` and are published at irregular intervals whenever a new version is ready.
They receive few bugfixes and security patches.
We use [Semantic Versioning](#) for these releases.
## Unstable
Unstable versions are build every time a PR is merged or a commit to the main development branch is made.
As such, they contain the current development code and are more likely to have bugs.
There might be multiple new such builds a day.
Versions contain the last stable version, the number of commits since then and the commit the currently running binary was built from.
They look like this: `v0.18.1+269-5cc4927b9e`
The demo instance at [try.vikunja.io](https://try.vikunja.io) automatically updates and always runs the last unstable build.
## Switching between versions
First you should create a backup of your current setup!
Switching between versions is the same process as [upgrading]({{< ref install-backend.md >}}#updating).
Simply replace the stable binary with an unstable one or vice-versa.
For installations using docker, it is as simple as using the `unstable` or `latest` tag to switch between versions.
**Note:** While switching from stable to unstable should work without any problem, switching back might work but is not recommended and might break your instance.
To switch from unstable back to stable the best way is to wait for the next stable release after the used unstable build and then upgrade to that.

View File

@ -4,8 +4,8 @@ title: "Errors"
draft: false
type: "doc"
menu:
sidebar:
parent: "usage"
sidebar:
parent: "usage"
---
# Errors
@ -52,15 +52,14 @@ This document describes the different errors Vikunja can return.
## List
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------|
| 3001 | 404 | The list does not exist. |
| 3004 | 403 | The user needs to have read permissions on that list to perform that action. |
| 3005 | 400 | The list title cannot be empty. |
| 3006 | 404 | The list share does not exist. |
| 3007 | 400 | A list with this identifier already exists. |
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------|
| 3001 | 404 | The list does not exist. |
| 3004 | 403 | The user needs to have read permissions on that list to perform that action. |
| 3005 | 400 | The list title cannot be empty. |
| 3006 | 404 | The list share does not exist. |
| 3007 | 400 | A list with this identifier already exists. |
| 3008 | 412 | The list is archived and can therefore only be accessed read only. This is also true for all tasks associated with this list. |
| 3009 | 412 | The list cannot belong to a dynamically generated namespace like "Favorites". |
## Task

142
go.mod
View File

@ -20,56 +20,56 @@ require (
code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3
gitea.com/xorm/xorm-redis-cache v0.2.0
github.com/ThreeDotsLabs/watermill v1.1.1
github.com/adlio/trello v1.10.0
github.com/arran4/golang-ical v0.0.0-20220517104411-fd89fefb0182
github.com/adlio/trello v1.9.0
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/bbrks/go-blurhash v1.1.1
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
github.com/coreos/go-oidc/v3 v3.2.0
github.com/beevik/etree v1.1.0 // indirect
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
github.com/coreos/go-oidc/v3 v3.1.0
github.com/cweill/gotests v1.6.0
github.com/d4l3k/messagediff v1.2.1
github.com/disintegration/imaging v1.6.2
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
github.com/gabriel-vasile/mimetype v1.4.1
github.com/getsentry/sentry-go v0.13.0
github.com/go-redis/redis/v8 v8.11.5
github.com/gabriel-vasile/mimetype v1.4.0
github.com/getsentry/sentry-go v0.12.0
github.com/go-errors/errors v1.1.1 // indirect
github.com/go-redis/redis/v8 v8.11.4
github.com/go-sql-driver/mysql v1.6.0
github.com/go-testfixtures/testfixtures/v3 v3.8.1
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/go-testfixtures/testfixtures/v3 v3.6.1
github.com/golang-jwt/jwt/v4 v4.3.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/uuid v1.3.0
github.com/golang/snappy v0.0.4 // indirect
github.com/iancoleman/strcase v0.2.0
github.com/imdario/mergo v0.3.13
github.com/jinzhu/copier v0.3.5
github.com/labstack/echo/v4 v4.8.0
github.com/imdario/mergo v0.3.12
github.com/labstack/echo/v4 v4.7.1
github.com/labstack/gommon v0.3.1
github.com/lib/pq v1.10.6
github.com/magefile/mage v1.13.0
github.com/mattn/go-sqlite3 v1.14.15
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef
github.com/lib/pq v1.10.4
github.com/magefile/mage v1.12.1
github.com/mattn/go-sqlite3 v1.14.12
github.com/olekukonko/tablewriter v0.0.5
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pquerna/otp v1.3.0
github.com/prometheus/client_golang v1.13.0
github.com/prometheus/client_golang v1.12.1
github.com/robfig/cron/v3 v3.0.1
github.com/samedi/caldav-go v3.0.0+incompatible
github.com/spf13/afero v1.9.2
github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.0
github.com/swaggo/swag v1.8.4
github.com/spf13/afero v1.8.1
github.com/spf13/cobra v1.3.0
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.1
github.com/swaggo/swag v1.8.0
github.com/tkuchiki/go-timezone v0.2.2
github.com/ulule/limiter/v3 v3.10.0
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93
github.com/wneessen/go-mail v0.2.6
github.com/yuin/goldmark v1.4.13
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539
golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
github.com/ulule/limiter/v3 v3.9.0
github.com/yuin/goldmark v1.4.8
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70
golang.org/x/image v0.0.0-20220302094943-723b81ca9867
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/yaml.v3 v3.0.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
src.techknowlogick.com/xgo v1.4.1-0.20210311222705-d25c33fcd864
src.techknowlogick.com/xormigrate v1.4.0
xorm.io/builder v0.3.9
@ -77,78 +77,8 @@ require (
xorm.io/xorm v1.1.2
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/garyburd/redigo v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-chi/chi v4.0.2+incompatible // indirect
github.com/go-errors/errors v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef // indirect
github.com/lithammer/shortuuid/v3 v3.0.4 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace (
github.com/adlio/trello => github.com/kolaente/trello v1.7.1-0.20201216234312-5c4ef79b531e
github.com/coreos/bbolt => go.etcd.io/bbolt v1.3.4
github.com/coreos/go-systemd => github.com/coreos/go-systemd/v22 v22.0.0
github.com/hpcloud/tail => github.com/jeffbean/tail v1.0.1 // See https://github.com/hpcloud/tail/pull/159
@ -156,4 +86,4 @@ replace (
gopkg.in/fsnotify.v1 => github.com/kolaente/fsnotify v1.4.10-0.20200411160148-1bc3c8ff4048 // See https://github.com/fsnotify/fsnotify/issues/328 and https://github.com/golang/go/issues/26904
)
go 1.19
go 1.15

493
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -79,10 +79,8 @@ func runCmdWithOutput(name string, arg ...string) (output []byte, err error) {
cmd := exec.Command(name, arg...)
output, err = cmd.Output()
if err != nil {
if ee, is := err.(*exec.ExitError); is {
return nil, fmt.Errorf("error running command: %s, %s", string(ee.Stderr), err)
}
return nil, fmt.Errorf("error running command: %s", err)
ee := err.(*exec.ExitError)
return nil, fmt.Errorf("error running command: %s, %s", string(ee.Stderr), err)
}
return output, nil
@ -352,7 +350,7 @@ func (Test) Unit() {
mg.Deps(initVars)
setApiPackages()
// We run everything sequentially and not in parallel to prevent issues with real test databases
args := append([]string{"test", Goflags[0], "-p", "1", "-coverprofile", "cover.out", "-timeout", "45m"}, ApiPackages...)
args := append([]string{"test", Goflags[0], "-p", "1", "-coverprofile", "cover.out", "-timeout", "20m"}, ApiPackages...)
runAndStreamOutput("go", args...)
}
@ -367,7 +365,7 @@ func (Test) Coverage() {
func (Test) Integration() {
mg.Deps(initVars)
// We run everything sequentially and not in parallel to prevent issues with real test databases
runAndStreamOutput("go", "test", Goflags[0], "-p", "1", "-timeout", "45m", PACKAGE+"/pkg/integrations")
runAndStreamOutput("go", "test", Goflags[0], "-p", "1", "-timeout", "20m", PACKAGE+"/pkg/integrations")
}
type Check mg.Namespace
@ -406,7 +404,7 @@ func checkGolangCiLintInstalled() {
mg.Deps(initVars)
if err := exec.Command("golangci-lint").Run(); err != nil && strings.Contains(err.Error(), "executable file not found") {
fmt.Println("Please manually install golangci-lint by running")
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.47.3")
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.31.0")
os.Exit(1)
}
}
@ -559,9 +557,7 @@ func (Release) Compress(ctx context.Context) error {
return nil
}
// No mips or s390x for you today
if strings.Contains(info.Name(), "mips") ||
strings.Contains(info.Name(), "s390x") ||
strings.Contains(info.Name(), "riscv64") { // not supported by upx
if strings.Contains(info.Name(), "mips") || strings.Contains(info.Name(), "s390x") {
return nil
}
@ -1055,7 +1051,7 @@ func printConfig(config []*configOption, level int, parent string) (rendered str
fullPath := parent + "." + option.key
rendered += "Full path: `" + fullPath + "`\n\n"
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(strings.ToUpper(fullPath)) + "`\n\n"
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(fullPath) + "`\n\n"
}
}

View File

@ -17,6 +17,7 @@
package caldav
import (
"fmt"
"regexp"
"strconv"
"strings"
@ -149,15 +150,6 @@ END:VCALENDAR` // Need a line break
return
}
func formatDuration(duration time.Duration) string {
seconds := duration.Seconds() - duration.Minutes()*60
minutes := duration.Minutes() - duration.Hours()*60
return strconv.FormatFloat(duration.Hours(), 'f', 0, 64) + `H` +
strconv.FormatFloat(minutes, 'f', 0, 64) + `M` +
strconv.FormatFloat(seconds, 'f', 0, 64) + `S`
}
// ParseTodos returns a caldav vcalendar string with todos
func ParseTodos(config *Config, todos []*Todo) (caldavtodos string) {
caldavtodos = `BEGIN:VCALENDAR
@ -180,15 +172,11 @@ SUMMARY:` + t.Summary + getCaldavColor(t.Color)
if t.Start.Unix() > 0 {
caldavtodos += `
DTSTART:` + makeCalDavTimeFromTimeStamp(t.Start)
if t.Duration != 0 && t.DueDate.Unix() == 0 {
caldavtodos += `
DURATION:PT` + formatDuration(t.Duration)
}
DTSTART: ` + makeCalDavTimeFromTimeStamp(t.Start)
}
if t.End.Unix() > 0 {
caldavtodos += `
DTEND:` + makeCalDavTimeFromTimeStamp(t.End)
DTEND: ` + makeCalDavTimeFromTimeStamp(t.End)
}
if t.Description != "" {
re := regexp.MustCompile(`\r?\n`)
@ -221,6 +209,11 @@ DUE:` + makeCalDavTimeFromTimeStamp(t.DueDate)
CREATED:` + makeCalDavTimeFromTimeStamp(t.Created)
}
if t.Duration != 0 {
caldavtodos += `
DURATION:PT` + fmt.Sprintf("%.6f", t.Duration.Hours()) + `H` + fmt.Sprintf("%.6f", t.Duration.Minutes()) + `M` + fmt.Sprintf("%.6f", t.Duration.Seconds()) + `S`
}
if t.Priority != 0 {
caldavtodos += `
PRIORITY:` + strconv.Itoa(mapPriorityToCaldav(t.Priority))

View File

@ -23,8 +23,7 @@ import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
ics "github.com/arran4/golang-ical"
"github.com/laurent22/ical-go"
)
func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*models.TaskWithComments) string {
@ -61,15 +60,21 @@ func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*m
}
func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
parsed, err := ics.ParseCalendar(strings.NewReader(content))
parsed, err := ical.ParseCalendar(content)
if err != nil {
return nil, err
}
// We put the task details in a map to be able to handle them more easily
task := make(map[string]string)
for _, c := range parsed.Components[0].UnknownPropertiesIANAProperties() {
task[c.IANAToken] = c.Value
for _, c := range parsed.Children {
if c.Name == "VTODO" {
for _, entry := range c.Children {
task[entry.Name] = entry.Value
}
// Breaking, to only process the first task
break
}
}
// Parse the priority
@ -86,13 +91,10 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
// Parse the enddate
duration, _ := time.ParseDuration(task["DURATION"])
description := strings.ReplaceAll(task["DESCRIPTION"], "\\,", ",")
description = strings.ReplaceAll(description, "\\n", "\n")
vTask = &models.Task{
UID: task["UID"],
Title: task["SUMMARY"],
Description: description,
Description: task["DESCRIPTION"],
Priority: priority,
DueDate: caldavTimeToTimestamp(task["DUE"]),
Updated: caldavTimeToTimestamp(task["DTSTAMP"]),
@ -123,10 +125,6 @@ func caldavTimeToTimestamp(tstring string) time.Time {
format = `20060102T150405Z`
}
if len(tstring) == 8 {
format = `20060102`
}
t, err := time.Parse(format, tstring)
if err != nil {
log.Warningf("Error while parsing caldav time %s to TimeStamp: %s", tstring, err)

View File

@ -21,10 +21,8 @@ import (
"fmt"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"time"
_ "time/tzdata" // Imports time zone data instead of relying on the os
@ -47,9 +45,8 @@ const (
ServiceFrontendurl Key = `service.frontendurl`
ServiceEnableCaldav Key = `service.enablecaldav`
ServiceRootpath Key = `service.rootpath`
ServiceStaticpath Key = `service.staticpath`
ServiceMaxItemsPerPage Key = `service.maxitemsperpage`
// Deprecated: Use metrics.enabled
// Deprecated. Use metrics.enabled
ServiceEnableMetrics Key = `service.enablemetrics`
ServiceMotd Key = `service.motd`
ServiceEnableLinkSharing Key = `service.enablelinksharing`
@ -62,7 +59,6 @@ const (
ServiceTestingtoken Key = `service.testingtoken`
ServiceEnableEmailReminders Key = `service.enableemailreminders`
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
ServiceMaxAvatarSize Key = `service.maxavatarsize`
AuthLocalEnabled Key = `auth.local.enabled`
AuthOpenIDEnabled Key = `auth.openid.enabled`
@ -82,9 +78,6 @@ const (
DatabaseMaxIdleConnections Key = `database.maxidleconnections`
DatabaseMaxConnectionLifetime Key = `database.maxconnectionlifetime`
DatabaseSslMode Key = `database.sslmode`
DatabaseSslCert Key = `database.sslcert`
DatabaseSslKey Key = `database.sslkey`
DatabaseSslRootCert Key = `database.sslrootcert`
DatabaseTLS Key = `database.tls`
CacheEnabled Key = `cache.enabled`
@ -96,7 +89,6 @@ const (
MailerPort Key = `mailer.port`
MailerUsername Key = `mailer.username`
MailerPassword Key = `mailer.password`
MailerAuthType Key = `mailer.authtype`
MailerSkipTLSVerify Key = `mailer.skiptlsverify`
MailerFromEmail Key = `mailer.fromemail`
MailerQueuelength Key = `mailer.queuelength`
@ -225,39 +217,6 @@ func (k Key) setDefault(i interface{}) {
viper.SetDefault(string(k), i)
}
// Tries different methods to figure out the binary folder.
// Copied and adopted from https://github.com/speedata/publisher/commit/3b668668d57edef04ea854d5bbd58f83eb1b799f
func getBinaryDirLocation() string {
// First, check if the standard library gives us the path. This will work 99% of the time.
ex, err := os.Executable()
if err == nil {
return filepath.Dir(ex)
}
// Then check if the binary was run with a full path and use that if that's the case.
if strings.Contains(os.Args[0], "/") {
binDir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
log.Fatal(err)
}
return binDir
}
exeSuffix := ""
if runtime.GOOS == "windows" {
exeSuffix = ".exe"
}
// All else failing, search for a vikunja binary in the current $PATH.
// This can give wrong results.
exeLocation, err := exec.LookPath("vikunja" + exeSuffix)
if err != nil {
log.Fatal(err)
}
return filepath.Dir(exeLocation)
}
// InitDefaultConfig sets default config values
// This is an extra function so we can call it when initializing tests without initializing the full config
func InitDefaultConfig() {
@ -276,8 +235,12 @@ func InitDefaultConfig() {
ServiceFrontendurl.setDefault("")
ServiceEnableCaldav.setDefault(true)
ServiceRootpath.setDefault(getBinaryDirLocation())
ServiceStaticpath.setDefault("")
ex, err := os.Executable()
if err != nil {
panic(err)
}
exPath := filepath.Dir(ex)
ServiceRootpath.setDefault(exPath)
ServiceMaxItemsPerPage.setDefault(50)
ServiceEnableMetrics.setDefault(false)
ServiceMotd.setDefault("")
@ -289,7 +252,6 @@ func InitDefaultConfig() {
ServiceEnableTotp.setDefault(true)
ServiceEnableEmailReminders.setDefault(true)
ServiceEnableUserDeletion.setDefault(true)
ServiceMaxAvatarSize.setDefault(1024)
// Auth
AuthLocalEnabled.setDefault(true)
@ -306,9 +268,6 @@ func InitDefaultConfig() {
DatabaseMaxIdleConnections.setDefault(50)
DatabaseMaxConnectionLifetime.setDefault(10000)
DatabaseSslMode.setDefault("disable")
DatabaseSslCert.setDefault("")
DatabaseSslKey.setDefault("")
DatabaseSslRootCert.setDefault("")
DatabaseTLS.setDefault("false")
// Cacher
@ -319,14 +278,13 @@ func InitDefaultConfig() {
MailerEnabled.setDefault(false)
MailerHost.setDefault("")
MailerPort.setDefault("587")
MailerUsername.setDefault("")
MailerUsername.setDefault("user")
MailerPassword.setDefault("")
MailerSkipTLSVerify.setDefault(false)
MailerFromEmail.setDefault("mail@vikunja")
MailerQueuelength.setDefault(100)
MailerQueueTimeout.setDefault(30)
MailerForceSSL.setDefault(false)
MailerAuthType.setDefault("plain")
// Redis
RedisEnabled.setDefault(false)
RedisHost.setDefault("localhost:6379")
@ -398,18 +356,6 @@ func InitConfig() {
viper.AddConfigPath(".")
viper.SetConfigName("config")
err = viper.ReadInConfig()
if viper.ConfigFileUsed() != "" {
log.Printf("Using config file: %s", viper.ConfigFileUsed())
if err != nil {
log.Println(err.Error())
log.Println("Using default config.")
}
} else {
log.Println("No config file found, using default or config from environment variables.")
}
if CacheType.GetString() == "keyvalue" {
CacheType.Set(KeyvalueType.GetString())
}
@ -442,6 +388,19 @@ func InitConfig() {
log.Println("WARNING: service.enablemetrics is deprecated and will be removed in a future release. Please use metrics.enable.")
MetricsEnabled.Set(true)
}
err = viper.ReadInConfig()
if viper.ConfigFileUsed() != "" {
log.Printf("Using config file: %s", viper.ConfigFileUsed())
if err != nil {
log.Println(err.Error())
log.Println("Using default config.")
return
}
} else {
log.Println("No config file found, using default or config from environment variables.")
}
}
func random(length int) (string, error) {

View File

@ -19,6 +19,7 @@ package db
import (
"encoding/gob"
"fmt"
"net/url"
"os"
"strconv"
"strings"
@ -149,16 +150,13 @@ func parsePostgreSQLHostPort(info string) (string, string) {
func initPostgresEngine() (engine *xorm.Engine, err error) {
host, port := parsePostgreSQLHostPort(config.DatabaseHost.GetString())
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s",
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
host,
port,
config.DatabaseUser.GetString(),
config.DatabasePassword.GetString(),
url.PathEscape(config.DatabaseUser.GetString()),
url.PathEscape(config.DatabasePassword.GetString()),
config.DatabaseDatabase.GetString(),
config.DatabaseSslMode.GetString(),
config.DatabaseSslCert.GetString(),
config.DatabaseSslKey.GetString(),
config.DatabaseSslRootCert.GetString(),
)
engine, err = xorm.NewEngine("postgres", connStr)
@ -188,7 +186,7 @@ func initSqliteEngine() (engine *xorm.Engine, err error) {
}
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0)
if err != nil {
return nil, fmt.Errorf("could not open database file [uid=%d, gid=%d]: %w", os.Getuid(), os.Getgid(), err)
return nil, fmt.Errorf("could not open database file [uid=%d, gid=%d]: %s", os.Getuid(), os.Getgid(), err)
}
_ = file.Close() // We directly close the file because we only want to check if it is writable. It will be reopened lazily later by xorm.

View File

@ -19,8 +19,6 @@ package db
import (
"encoding/json"
"code.vikunja.io/api/pkg/log"
"xorm.io/xorm/schemas"
)
@ -59,15 +57,6 @@ func Restore(table string, contents []map[string]interface{}) (err error) {
}
}
if Type() == schemas.POSTGRES {
idSequence := table + "_id_seq"
_, err = x.Query("SELECT setval('" + idSequence + "', COALESCE(MAX(id), 1) )")
if err != nil {
log.Warningf("Could not reset id sequence for %s: %s", idSequence, err)
err = nil
}
}
return
}

View File

@ -17,7 +17,6 @@
package files
import (
"errors"
"io"
"os"
"strconv"
@ -137,10 +136,9 @@ func (f *File) Delete() (err error) {
err = afs.Remove(f.getFileName())
if err != nil {
var perr *os.PathError
if errors.As(err, &perr) {
if e, is := err.(*os.PathError); is {
// Don't fail when removing the file failed
log.Errorf("Error deleting file %d: %w", err)
log.Errorf("Error deleting file %d: %s", e.Error())
return s.Commit()
}

View File

@ -78,15 +78,15 @@ func FullInit() {
LightInit()
// Initialize the files handler
files.InitFileHandler()
// Run the migrations
migration.Migrate(nil)
// Set Engine
InitEngines()
// Initialize the files handler
files.InitFileHandler()
// Start the mail daemon
mail.StartMailDaemon()

View File

@ -17,7 +17,6 @@
package integrations
import (
"errors"
"net/http"
"net/http/httptest"
"net/url"
@ -175,8 +174,8 @@ func assertHandlerErrorCode(t *testing.T, err error, expectedErrorCode int) {
t.Error("Error is nil")
t.FailNow()
}
var httperr *echo.HTTPError
if !errors.As(err, &httperr) {
httperr, ok := err.(*echo.HTTPError)
if !ok {
t.Error("Error is not *echo.HTTPError")
t.FailNow()
}

View File

@ -113,7 +113,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
@ -123,13 +123,13 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
// should equal duedate asc
t.Run("by due_date", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by duedate desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
@ -140,12 +140,12 @@ func TestTaskCollection(t *testing.T) {
t.Run("by duedate asc without suffix", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by due_date without suffix", func(t *testing.T) {
t.Run("by due_date without suffix", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by duedate desc without suffix", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
@ -155,7 +155,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by duedate asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("invalid sort parameter", func(t *testing.T) {
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
@ -244,37 +244,12 @@ func TestTaskCollection(t *testing.T) {
// the current date.
assert.Equal(t, "[]\n", rec.Body.String())
})
t.Run("unix timestamps", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(
url.Values{
"filter_by": []string{"start_date", "end_date", "due_date"},
"filter_value": []string{"1544500000", "1513164001", "1543500000"},
"filter_comparator": []string{"greater", "less", "greater"},
},
urlParams,
)
assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `task #1`)
assert.NotContains(t, rec.Body.String(), `task #2`)
assert.NotContains(t, rec.Body.String(), `task #3`)
assert.NotContains(t, rec.Body.String(), `task #4`)
assert.Contains(t, rec.Body.String(), `task #5`)
assert.Contains(t, rec.Body.String(), `task #6`)
assert.Contains(t, rec.Body.String(), `task #7`)
assert.NotContains(t, rec.Body.String(), `task #8`)
assert.Contains(t, rec.Body.String(), `task #9`)
assert.NotContains(t, rec.Body.String(), `task #10`)
assert.NotContains(t, rec.Body.String(), `task #11`)
assert.NotContains(t, rec.Body.String(), `task #12`)
assert.NotContains(t, rec.Body.String(), `task #13`)
assert.NotContains(t, rec.Body.String(), `task #14`)
})
})
t.Run("invalid date", func(t *testing.T) {
_, err := testHandler.testReadAllWithUser(
url.Values{
"filter_by": []string{"due_date"},
"filter_value": []string{"invalid"},
"filter_value": []string{"1540000000"},
"filter_comparator": []string{"greater"},
},
nil,
@ -366,7 +341,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
@ -376,13 +351,13 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
// should equal duedate asc
t.Run("by due_date", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by duedate desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil)
@ -392,7 +367,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by duedate asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("invalid parameter", func(t *testing.T) {
// Invalid parameter should not sort at all
@ -476,7 +451,7 @@ func TestTaskCollection(t *testing.T) {
_, err := testHandler.testReadAllWithUser(
url.Values{
"filter_by": []string{"due_date"},
"filter_value": []string{"invalid"},
"filter_value": []string{"1540000000"},
"filter_comparator": []string{"greater"},
},
nil,

View File

@ -63,7 +63,6 @@ func TestTaskComments(t *testing.T) {
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
// Only the own comments can be updated
t.Run("Forbidden", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "14", "commentid": "2"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
@ -75,14 +74,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "16", "commentid": "4"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "16", "commentid": "4"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "17", "commentid": "5"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "17", "commentid": "5"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
@ -91,14 +90,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "19", "commentid": "7"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "19", "commentid": "7"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "20", "commentid": "8"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "20", "commentid": "8"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
@ -107,14 +106,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "22", "commentid": "10"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "22", "commentid": "10"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "23", "commentid": "11"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "23", "commentid": "11"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
@ -123,14 +122,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "25", "commentid": "13"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "25", "commentid": "13"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "26", "commentid": "14"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "26", "commentid": "14"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
})
})
})
@ -146,7 +145,6 @@ func TestTaskComments(t *testing.T) {
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
// Only the own comments can be deleted
t.Run("Forbidden", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "14", "commentid": "2"})
assert.Error(t, err)
@ -158,14 +156,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "16", "commentid": "4"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "16", "commentid": "4"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "17", "commentid": "5"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "17", "commentid": "5"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
@ -174,14 +172,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "19", "commentid": "7"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "19", "commentid": "7"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "20", "commentid": "8"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "20", "commentid": "8"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
@ -190,14 +188,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "22", "commentid": "10"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "22", "commentid": "10"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "23", "commentid": "11"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "23", "commentid": "11"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
@ -206,14 +204,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "25", "commentid": "13"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "25", "commentid": "13"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "26", "commentid": "14"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "26", "commentid": "14"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
})
})

View File

@ -17,67 +17,31 @@
package mail
import (
"context"
"crypto/tls"
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"github.com/wneessen/go-mail"
"gopkg.in/gomail.v2"
)
// Queue is the mail queue
var Queue chan *mail.Msg
var Queue chan *gomail.Message
func getClient() (*mail.Client, error) {
var authType mail.SMTPAuthType
switch config.MailerAuthType.GetString() {
case "plain":
authType = mail.SMTPAuthPlain
case "login":
authType = mail.SMTPAuthLogin
case "cram-md5":
authType = mail.SMTPAuthCramMD5
func getDialer() *gomail.Dialer {
d := gomail.NewDialer(config.MailerHost.GetString(), config.MailerPort.GetInt(), config.MailerUsername.GetString(), config.MailerPassword.GetString())
// #nosec
d.TLSConfig = &tls.Config{
InsecureSkipVerify: config.MailerSkipTLSVerify.GetBool(),
ServerName: config.MailerHost.GetString(),
}
tlsPolicy := mail.TLSOpportunistic
if config.MailerForceSSL.GetBool() {
tlsPolicy = mail.TLSMandatory
}
opts := []mail.Option{
mail.WithPort(config.MailerPort.GetInt()),
mail.WithTLSPolicy(tlsPolicy),
//#nosec G402
mail.WithTLSConfig(&tls.Config{
InsecureSkipVerify: config.MailerSkipTLSVerify.GetBool(),
ServerName: config.MailerHost.GetString(),
}),
}
if config.MailerUsername.GetString() != "" && config.MailerPassword.GetString() != "" {
opts = append(opts, mail.WithSMTPAuth(authType))
}
if config.MailerUsername.GetString() != "" {
opts = append(opts, mail.WithUsername(config.MailerUsername.GetString()))
}
if config.MailerPassword.GetString() != "" {
opts = append(opts, mail.WithPassword(config.MailerPassword.GetString()))
}
return mail.NewClient(
config.MailerHost.GetString(),
opts...,
)
d.SSL = config.MailerForceSSL.GetBool()
return d
}
// StartMailDaemon starts the mail daemon
func StartMailDaemon() {
Queue = make(chan *mail.Msg, config.MailerQueuelength.GetInt())
Queue = make(chan *gomail.Message, config.MailerQueuelength.GetInt())
if !config.MailerEnabled.GetBool() {
return
@ -88,12 +52,10 @@ func StartMailDaemon() {
return
}
c, err := getClient()
if err != nil {
log.Errorf("Could not create mail client: %v", err)
return
}
go func() {
d := getDialer()
var s gomail.SendCloser
var err error
open := false
for {
@ -103,16 +65,14 @@ func StartMailDaemon() {
return
}
if !open {
err = c.DialWithContext(context.Background())
if err != nil {
log.Errorf("Error during connect to smtp server: %s", err)
if s, err = d.Dial(); err != nil {
log.Error("Error during connect to smtp server: %s", err)
break
}
open = true
}
err = c.Send(m)
if err != nil {
log.Errorf("Error when sending mail: %s", err)
if err := gomail.Send(s, m); err != nil {
log.Error("Error when sending mail: %s", err)
break
}
// Close the connection to the SMTP server if no email was sent in
@ -120,14 +80,18 @@ func StartMailDaemon() {
case <-time.After(config.MailerQueueTimeout.GetDuration() * time.Second):
if open {
open = false
err = c.Close()
if err != nil {
log.Errorf("Error closing the mail server connection: %s\n", err)
if err := s.Close(); err != nil {
log.Error("Error closing the mail server connection: %s\n", err)
break
}
log.Info("Closed connection to mail server")
log.Infof("Closed connection to mailserver")
}
}
}
}()
}
// StopMailDaemon closes the mail queue channel, aka stops the daemon
func StopMailDaemon() {
close(Queue)
}

View File

@ -17,14 +17,9 @@
package mail
import (
"embed"
"io"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/version"
"github.com/wneessen/go-mail"
"gopkg.in/gomail.v2"
)
// Opts holds infos for a mail
@ -37,8 +32,6 @@ type Opts struct {
ContentType ContentType
Boundary string
Headers []*header
Embeds map[string]io.Reader
EmbedFS map[string]*embed.FS
}
// ContentType represents mail content types
@ -52,11 +45,11 @@ const (
)
type header struct {
Field mail.Header
Field string
Content string
}
// SendTestMail sends a test mail to a recipient.
// SendTestMail sends a test mail to a receipient.
// It works without a queue.
func SendTestMail(opts *Opts) error {
if config.MailerHost.GetString() == "" {
@ -64,51 +57,39 @@ func SendTestMail(opts *Opts) error {
return nil
}
c, err := getClient()
d := getDialer()
s, err := d.Dial()
if err != nil {
return err
}
defer s.Close()
m := getMessage(opts)
m := sendMail(opts)
return c.DialAndSend(m)
return gomail.Send(s, m)
}
func getMessage(opts *Opts) *mail.Msg {
m := mail.NewMsg()
m.SetUserAgent("Vikunja " + version.Version)
func sendMail(opts *Opts) *gomail.Message {
m := gomail.NewMessage()
if opts.From == "" {
opts.From = "Vikunja <" + config.MailerFromEmail.GetString() + ">"
}
_ = m.From(opts.From)
_ = m.To(opts.To)
m.Subject(opts.Subject)
m.SetHeader("From", opts.From)
m.SetHeader("To", opts.To)
m.SetHeader("Subject", opts.Subject)
for _, h := range opts.Headers {
m.SetHeader(h.Field, h.Content)
}
for name, content := range opts.Embeds {
m.EmbedReader(name, content)
}
for name, fs := range opts.EmbedFS {
err := m.EmbedFromEmbedFS(name, fs)
if err != nil {
log.Errorf("Error embedding %s via embed.FS into mail: %v", err)
}
}
switch opts.ContentType {
case ContentTypePlain:
m.SetBodyString("text/plain", opts.Message)
m.SetBody("text/plain", opts.Message)
case ContentTypeHTML:
m.SetBodyString("text/html", opts.Message)
m.SetBody("text/html", opts.Message)
case ContentTypeMultipart:
m.SetBodyString("text/plain", opts.Message)
m.AddAlternativeString("text/html", opts.HTMLMessage)
m.SetBody("text/plain", opts.Message)
m.AddAlternative("text/html", opts.HTMLMessage)
}
return m
}
@ -119,6 +100,6 @@ func SendMail(opts *Opts) {
return
}
m := getMessage(opts)
m := sendMail(opts)
Queue <- m
}

View File

@ -683,17 +683,17 @@ create unique index UQE_users_namespace_id
sess := tx.NewSession()
if err := sess.Begin(); err != nil {
return fmt.Errorf("unable to open session: %w", err)
return fmt.Errorf("unable to open session: %s", err)
}
for _, s := range sql {
_, err := sess.Exec(s)
if err != nil {
_ = sess.Rollback()
return fmt.Errorf("error executing update data for table %s, column %s: %w", table, column, err)
return fmt.Errorf("error executing update data for table %s, column %s: %s", table, column, err)
}
}
if err := sess.Commit(); err != nil {
return fmt.Errorf("error committing data change: %w", err)
return fmt.Errorf("error committing data change: %s", err)
}
return nil
}

View File

@ -1,99 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"errors"
"image"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"github.com/bbrks/go-blurhash"
"golang.org/x/image/draw"
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type lists20211212210054 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"list"`
BackgroundFileID int64 `xorm:"null" json:"-"`
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
}
func (lists20211212210054) TableName() string {
return "lists"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20211212210054",
Description: "Add blurHash to list backgrounds.",
Migrate: func(tx *xorm.Engine) error {
err := tx.Sync2(lists20211212210054{})
if err != nil {
return err
}
lists := []*lists20211212210054{}
err = tx.Where("background_file_id is not null AND background_file_id != ?", 0).Find(&lists)
if err != nil {
return err
}
log.Infof("Creating BlurHash for %d list backgrounds, this might take a while...", len(lists))
for _, l := range lists {
bgFile := &files.File{
ID: l.BackgroundFileID,
}
if err := bgFile.LoadFileByID(); err != nil {
return err
}
src, _, err := image.Decode(bgFile.File)
if err != nil && !errors.Is(err, image.ErrFormat) {
return err
}
if err != nil && errors.Is(err, image.ErrFormat) {
log.Warningf("Could not generate a blur hash of list %d's background image: %s", l.ID, err)
}
dst := image.NewRGBA(image.Rect(0, 0, 32, 32))
draw.NearestNeighbor.Scale(dst, dst.Rect, src, src.Bounds(), draw.Over, nil)
hash, err := blurhash.Encode(4, 3, dst)
if err != nil {
return err
}
l.BackgroundBlurHash = hash
_, err = tx.Where("id = ?", l.ID).
Cols("background_blur_hash").
Update(l)
if err != nil {
return err
}
log.Debugf("Created BlurHash for list %d", l.ID)
}
return nil
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -1,43 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type users20220616145228 struct {
OverdueTasksRemindersTime string `xorm:"varchar(5) not null default '09:00'" json:"-"`
}
func (users20220616145228) TableName() string {
return "users"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20220616145228",
Description: "Add overdue task summary time field to users",
Migrate: func(tx *xorm.Engine) error {
return tx.Sync2(users20220616145228{})
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -1,108 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"encoding/json"
"strconv"
"strings"
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20220815200851",
Description: "Migrate saved assignee filter to usernames instead of IDs",
Migrate: func(tx *xorm.Engine) error {
filters := []map[string]interface{}{} // not using the type here so that the migration does not depend on it
err := tx.Select("*").
Table("saved_filters").
Find(&filters)
if err != nil {
return err
}
for _, f := range filters {
filter := map[string]interface{}{}
filterJSON, is := f["filters"].(string)
if !is {
continue
}
err = json.Unmarshal([]byte(filterJSON), &filter)
if err != nil {
return err
}
filterBy := filter["filter_by"].([]interface{})
filterValue := filter["filter_value"].([]interface{})
for p, fb := range filterBy {
if fb == "assignees" || fb == "user_id" {
userIDs := []int64{}
for _, sid := range strings.Split(filterValue[p].(string), ",") {
id, err := strconv.ParseInt(sid, 10, 64)
if err != nil {
return err
}
userIDs = append(userIDs, id)
}
usernames := []string{}
err := tx.Select("username").
Table("users").
In("id", userIDs).
Find(&usernames)
if err != nil {
return err
}
userfilter := ""
for i, username := range usernames {
if i > 0 {
userfilter += ","
}
userfilter += username
}
filterValue[p] = userfilter
}
}
filter["filter_value"] = filterValue
filtersJSON, err := json.Marshal(filter)
if err != nil {
return err
}
f["filters"] = string(filtersJSON)
_, err = tx.Where("id = ?", f["id"]).
Cols("filters").
NoAutoCondition().
Table("saved_filters").
Update(f)
if err != nil {
return err
}
}
return nil
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -237,7 +237,7 @@ type ErrListIsArchived struct {
ListID int64
}
// IsErrListIsArchived checks if an error is a list is archived error.
// IsErrListIsArchived checks if an error is a .
func IsErrListIsArchived(err error) bool {
_, ok := err.(ErrListIsArchived)
return ok
@ -255,34 +255,6 @@ func (err ErrListIsArchived) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeListIsArchived, Message: "This list is archived. Editing or creating new tasks is not possible."}
}
// ErrListCannotBelongToAPseudoNamespace represents an error where a list cannot belong to a pseudo namespace
type ErrListCannotBelongToAPseudoNamespace struct {
ListID int64
NamespaceID int64
}
// IsErrListCannotBelongToAPseudoNamespace checks if an error is a list is archived error.
func IsErrListCannotBelongToAPseudoNamespace(err error) bool {
_, ok := err.(*ErrListCannotBelongToAPseudoNamespace)
return ok
}
func (err *ErrListCannotBelongToAPseudoNamespace) Error() string {
return fmt.Sprintf("List cannot belong to a pseudo namespace [ListID: %d, NamespaceID: %d]", err.ListID, err.NamespaceID)
}
// ErrCodeListCannotBelongToAPseudoNamespace holds the unique world-error code of this error
const ErrCodeListCannotBelongToAPseudoNamespace = 3009
// HTTPError holds the http error description
func (err *ErrListCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeListCannotBelongToAPseudoNamespace,
Message: "This list cannot belong a dynamically generated namespace.",
}
}
// ================
// List task errors
// ================

View File

@ -49,7 +49,7 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
// Open zip
dumpFile, err := os.Create(tmpFilename)
if err != nil {
return fmt.Errorf("error opening dump file: %w", err)
return fmt.Errorf("error opening dump file: %s", err)
}
defer dumpFile.Close()

View File

@ -43,7 +43,6 @@ func TestLabelTask_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
},

View File

@ -54,7 +54,6 @@ func TestLabel_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -105,7 +104,6 @@ func TestLabel_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
},
@ -170,7 +168,6 @@ func TestLabel_ReadOne(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -232,7 +229,6 @@ func TestLabel_ReadOne(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
},

View File

@ -59,8 +59,6 @@ type List struct {
BackgroundFileID int64 `xorm:"null" json:"-"`
// Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background
BackgroundInformation interface{} `xorm:"-" json:"background_information"`
// Contains a very small version of the list background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
// True if a list is a favorite. Favorite lists show up in a separate namespace. This value depends on the user making the call to the api.
IsFavorite bool `xorm:"-" json:"is_favorite"`
@ -543,10 +541,6 @@ func (l *List) CheckIsArchived(s *xorm.Session) (err error) {
}
func checkListBeforeUpdateOrDelete(s *xorm.Session, list *List) error {
if list.NamespaceID < 0 {
return &ErrListCannotBelongToAPseudoNamespace{ListID: list.ID, NamespaceID: list.NamespaceID}
}
// Check if the namespace exists
if list.NamespaceID > 0 {
_, err := GetNamespaceByID(s, list.NamespaceID)
@ -644,7 +638,7 @@ func UpdateList(s *xorm.Session, list *List, auth web.Auth, updateListBackground
}
if updateListBackground {
colsToUpdate = append(colsToUpdate, "background_file_id", "background_blur_hash")
colsToUpdate = append(colsToUpdate, "background_file_id")
}
wasFavorite, err := isFavorite(s, list.ID, auth, FavoriteKindList)
@ -805,15 +799,14 @@ func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
}
// SetListBackground sets a background file as list background in the db
func SetListBackground(s *xorm.Session, listID int64, background *files.File, blurHash string) (err error) {
func SetListBackground(s *xorm.Session, listID int64, background *files.File) (err error) {
l := &List{
ID: listID,
BackgroundFileID: background.ID,
BackgroundBlurHash: blurHash,
ID: listID,
BackgroundFileID: background.ID,
}
_, err = s.
Where("id = ?", l.ID).
Cols("background_file_id", "background_blur_hash").
Cols("background_file_id").
Update(l)
return
}

View File

@ -144,7 +144,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
}
}
if err := SetListBackground(s, ld.List.ID, file, ld.List.BackgroundBlurHash); err != nil {
if err := SetListBackground(s, ld.List.ID, file); err != nil {
return err
}
@ -216,7 +216,7 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
// It is used to map old task items to new ones.
taskMap := make(map[int64]int64)
// Create + update all tasks (includes reminders)
oldTaskIDs := make([]int64, 0, len(tasks))
oldTaskIDs := make([]int64, len(tasks))
for _, t := range tasks {
oldID := t.ID
t.ID = 0

View File

@ -221,25 +221,6 @@ func TestList_CreateOrUpdate(t *testing.T) {
assert.False(t, can) // namespace is not writeable by us
_ = s.Close()
})
t.Run("pseudo namespace", func(t *testing.T) {
usr := &user.User{
ID: 6,
Username: "user6",
Email: "user6@example.com",
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
list := List{
ID: 6,
Title: "Test6",
Description: "Lorem Ipsum",
NamespaceID: -1,
}
err := list.Update(s, usr)
assert.Error(t, err)
assert.True(t, IsErrListCannotBelongToAPseudoNamespace(err))
})
})
})
}

View File

@ -151,7 +151,6 @@ func TestListUser_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
},
@ -165,7 +164,6 @@ func TestListUser_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
},

View File

@ -150,7 +150,6 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
},
@ -164,7 +163,6 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
},

View File

@ -131,7 +131,7 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskOptions, err
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
// @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`."
// @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match."
// @Param filter_value query string false "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like `due_date`, `start_date`, `end_date`, etc."
// @Param filter_value query string false "The value to filter for."
// @Param filter_comparator query string false "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals`"
// @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`."
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."

View File

@ -25,7 +25,6 @@ import (
"code.vikunja.io/api/pkg/config"
"github.com/iancoleman/strcase"
"github.com/vectordotdev/go-datemath"
"xorm.io/xorm/schemas"
)
@ -160,14 +159,8 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
value, err = strconv.ParseBool(rawValue)
case reflect.Struct:
if field.Type == schemas.TimeType {
var t datemath.Expression
t, err = datemath.Parse(rawValue)
if err == nil {
value = t.Time(datemath.WithLocation(config.GetTimeZone()))
} else {
value, err = time.Parse(time.RFC3339, rawValue)
value = value.(time.Time).In(config.GetTimeZone())
}
value, err = time.Parse(time.RFC3339, rawValue)
value = value.(time.Time).In(config.GetTimeZone())
}
case reflect.Slice:
// If this is a slice of pointers we're dealing with some property which is a relation
@ -214,12 +207,6 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
return
}
if realFieldName == "Assignees" {
vals := strings.Split(value, ",")
valueSlice := append([]string{}, vals...)
return valueSlice, nil
}
field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(realFieldName)
if !ok {
return nil, ErrInvalidTaskField{TaskField: fieldName}

View File

@ -37,7 +37,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -48,7 +47,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -59,7 +57,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -934,93 +931,25 @@ func TestTaskCollection_ReadAll(t *testing.T) {
wantErr: false,
},
{
name: "filter assignees by username",
fields: fields{
FilterBy: []string{"assignees"},
FilterValue: []string{"user1"},
FilterComparator: []string{"equals"},
},
args: defaultArgs,
want: []*Task{
task30,
},
wantErr: false,
},
{
name: "filter assignees by username with users field name",
fields: fields{
FilterBy: []string{"users"},
FilterValue: []string{"user1"},
FilterComparator: []string{"equals"},
},
args: defaultArgs,
want: nil,
wantErr: true,
},
{
name: "filter assignees by username with user_id field name",
fields: fields{
FilterBy: []string{"user_id"},
FilterValue: []string{"user1"},
FilterComparator: []string{"equals"},
},
args: defaultArgs,
want: nil,
wantErr: true,
},
{
name: "filter assignees by multiple username",
fields: fields{
FilterBy: []string{"assignees", "assignees"},
FilterValue: []string{"user1", "user2"},
FilterComparator: []string{"equals", "equals"},
},
args: defaultArgs,
want: []*Task{
task30,
},
wantErr: false,
},
{
name: "filter assignees by numbers",
name: "filter assignees",
fields: fields{
FilterBy: []string{"assignees"},
FilterValue: []string{"1"},
FilterComparator: []string{"equals"},
},
args: defaultArgs,
want: []*Task{},
args: defaultArgs,
want: []*Task{
task30,
},
wantErr: false,
},
{
name: "filter assignees by name with like",
fields: fields{
FilterBy: []string{"assignees"},
FilterValue: []string{"user"},
FilterComparator: []string{"like"},
},
args: defaultArgs,
want: []*Task{},
wantErr: true,
},
{
name: "filter assignees in by id",
name: "filter assignees in",
fields: fields{
FilterBy: []string{"assignees"},
FilterValue: []string{"1,2"},
FilterComparator: []string{"in"},
},
args: defaultArgs,
want: []*Task{},
wantErr: false,
},
{
name: "filter assignees in by username",
fields: fields{
FilterBy: []string{"assignees"},
FilterValue: []string{"user1,user2"},
FilterComparator: []string{"in"},
},
args: defaultArgs,
want: []*Task{
task30,
@ -1117,9 +1046,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
a: &user.User{ID: 1},
},
want: []*Task{
// The only tasks with a position set
task1,
task2,
// the other ones don't have a position set
task3,
task4,
@ -1150,51 +1076,9 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task31,
task32,
task33,
},
},
{
name: "order by due date",
fields: fields{
SortBy: []string{"due_date", "id"},
OrderBy: []string{"asc", "desc"},
},
args: args{
a: &user.User{ID: 1},
},
want: []*Task{
// The only tasks with a due date
task6,
task5,
// The other ones don't have a due date
task33,
task32,
task31,
task30,
task29,
task28,
task27,
task26,
task25,
task24,
task23,
task22,
task21,
task20,
task19,
task18,
task17,
task16,
task15,
task12,
task11,
task10,
task9,
task8,
task7,
task4,
task3,
task2,
// The only tasks with a position set
task1,
task2,
},
},
{

View File

@ -27,36 +27,16 @@ func (tc *TaskComment) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
return t.CanRead(s, a)
}
func (tc *TaskComment) canUserModifyTaskComment(s *xorm.Session, a web.Auth) (bool, error) {
t := Task{ID: tc.TaskID}
canWriteTask, err := t.CanWrite(s, a)
if err != nil {
return false, err
}
if !canWriteTask {
return false, nil
}
savedComment := &TaskComment{
ID: tc.ID,
TaskID: tc.TaskID,
}
err = getTaskCommentSimple(s, savedComment)
if err != nil {
return false, err
}
return a.GetID() == savedComment.AuthorID, nil
}
// CanDelete checks if a user can delete a comment
func (tc *TaskComment) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
return tc.canUserModifyTaskComment(s, a)
t := Task{ID: tc.TaskID}
return t.CanWrite(s, a)
}
// CanUpdate checks if a user can update a comment
func (tc *TaskComment) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
return tc.canUserModifyTaskComment(s, a)
t := Task{ID: tc.TaskID}
return t.CanWrite(s, a)
}
// CanCreate checks if a user can create a new comment

View File

@ -151,24 +151,6 @@ func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
})
}
func getTaskCommentSimple(s *xorm.Session, tc *TaskComment) error {
exists, err := s.
Where("id = ? and task_id = ?", tc.ID, tc.TaskID).
NoAutoCondition().
Get(tc)
if err != nil {
return err
}
if !exists {
return ErrTaskCommentDoesNotExist{
ID: tc.ID,
TaskID: tc.TaskID,
}
}
return nil
}
// ReadOne handles getting a single comment
// @Summary Remove a task comment
// @Description Remove a task comment. The user doing this need to have at least read access to the task this comment belongs to.
@ -184,9 +166,15 @@ func getTaskCommentSimple(s *xorm.Session, tc *TaskComment) error {
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [get]
func (tc *TaskComment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
err = getTaskCommentSimple(s, tc)
exists, err := s.Get(tc)
if err != nil {
return err
return
}
if !exists {
return ErrTaskCommentDoesNotExist{
ID: tc.ID,
TaskID: tc.TaskID,
}
}
// Get the author

View File

@ -121,16 +121,6 @@ func TestTaskComment_Delete(t *testing.T) {
assert.Error(t, err)
assert.True(t, IsErrTaskCommentDoesNotExist(err))
})
t.Run("not the own comment", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
tc := &TaskComment{ID: 1, TaskID: 1}
can, err := tc.CanDelete(s, &user.User{ID: 2})
assert.NoError(t, err)
assert.False(t, can)
})
}
func TestTaskComment_Update(t *testing.T) {
@ -167,16 +157,6 @@ func TestTaskComment_Update(t *testing.T) {
assert.Error(t, err)
assert.True(t, IsErrTaskCommentDoesNotExist(err))
})
t.Run("not the own comment", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
tc := &TaskComment{ID: 1, TaskID: 1}
can, err := tc.CanUpdate(s, &user.User{ID: 2})
assert.NoError(t, err)
assert.False(t, can)
})
}
func TestTaskComment_ReadOne(t *testing.T) {
@ -187,7 +167,7 @@ func TestTaskComment_ReadOne(t *testing.T) {
s := db.NewSession()
defer s.Close()
tc := &TaskComment{ID: 1, TaskID: 1}
tc := &TaskComment{ID: 1}
err := tc.ReadOne(s, u)
assert.NoError(t, err)
assert.Equal(t, "Lorem Ipsum Dolor Sit Amet", tc.Comment)

View File

@ -19,87 +19,35 @@ package models
import (
"time"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/cron"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/notifications"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
"xorm.io/builder"
"xorm.io/xorm"
)
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[int64]*userWithTasks, err error) {
now = utils.GetTimeWithoutSeconds(now)
nextMinute := now.Add(1 * time.Minute)
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (taskIDs []int64, err error) {
now = utils.GetTimeWithoutNanoSeconds(now)
var tasks []*Task
err = s.
Where("due_date is not null and due_date < ?", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
Where("due_date is not null and due_date < ?", now.Format(dbTimeFormat)).
And("done = false").
Find(&tasks)
if err != nil {
return
}
if len(tasks) == 0 {
return
}
var taskIDs []int64
for _, task := range tasks {
taskIDs = append(taskIDs, task.ID)
}
users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
if err != nil {
return
}
if len(users) == 0 {
return
}
uts := make(map[int64]*userWithTasks)
tzs := make(map[string]*time.Location)
for _, t := range users {
if t.User.Timezone == "" {
t.User.Timezone = config.GetTimeZone().String()
}
tz, exists := tzs[t.User.Timezone]
if !exists {
tz, err = time.LoadLocation(t.User.Timezone)
if err != nil {
return
}
tzs[t.User.Timezone] = tz
}
// If it is time for that current user, add the task to their list of overdue tasks
tm, err := time.Parse("15:04", t.User.OverdueTasksRemindersTime)
if err != nil {
return nil, err
}
overdueMailTime := time.Date(now.Year(), now.Month(), now.Day(), tm.Hour(), tm.Minute(), 0, 0, tz)
isTimeForReminder := overdueMailTime.After(now) || overdueMailTime.Equal(now.In(tz))
wasTimeForReminder := overdueMailTime.Before(nextMinute)
taskIsOverdueInUserTimezone := overdueMailTime.After(t.Task.DueDate.In(tz))
if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone {
_, exists := uts[t.User.ID]
if !exists {
uts[t.User.ID] = &userWithTasks{
user: t.User,
tasks: []*Task{},
}
}
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
}
}
return uts, nil
return
}
type userWithTasks struct {
@ -118,18 +66,36 @@ func RegisterOverdueReminderCron() {
return
}
err := cron.Schedule("* * * * *", func() {
err := cron.Schedule("0 8 * * *", func() {
s := db.NewSession()
defer s.Close()
now := time.Now()
uts, err := getUndoneOverdueTasks(s, now)
taskIDs, err := getUndoneOverdueTasks(s, now)
if err != nil {
log.Errorf("[Undone Overdue Tasks Reminder] Could not get undone overdue tasks in the next minute: %s", err)
log.Errorf("[Undone Overdue Tasks Reminder] Could not get tasks with reminders in the next minute: %s", err)
return
}
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(uts))
users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
if err != nil {
log.Errorf("[Undone Overdue Tasks Reminder] Could not get task users to send them reminders: %s", err)
return
}
uts := make(map[int64]*userWithTasks)
for _, t := range users {
_, exists := uts[t.User.ID]
if !exists {
uts[t.User.ID] = &userWithTasks{
user: t.User,
tasks: []*Task{},
}
}
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
}
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(users))
for _, ut := range uts {
var n notifications.Notification = &UndoneTasksOverdueNotification{
@ -151,6 +117,7 @@ func RegisterOverdueReminderCron() {
}
log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for %d tasks to user %d", len(ut.tasks), ut.user.ID)
continue
}
})
if err != nil {

View File

@ -32,34 +32,21 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z")
assert.NoError(t, err)
tasks, err := getUndoneOverdueTasks(s, now)
taskIDs, err := getUndoneOverdueTasks(s, now)
assert.NoError(t, err)
assert.Len(t, tasks, 0)
assert.Len(t, taskIDs, 0)
})
t.Run("undone overdue", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T09:00:00Z")
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T01:13:00Z")
assert.NoError(t, err)
uts, err := getUndoneOverdueTasks(s, now)
taskIDs, err := getUndoneOverdueTasks(s, now)
assert.NoError(t, err)
assert.Len(t, uts, 1)
assert.Len(t, uts[1].tasks, 2)
// The tasks don't always have the same order, so we only check their presence, not their position.
var task5Present bool
var task6Present bool
for _, t := range uts[1].tasks {
if t.ID == 5 {
task5Present = true
}
if t.ID == 6 {
task6Present = true
}
}
assert.Truef(t, task5Present, "expected task 5 to be present but was not")
assert.Truef(t, task6Present, "expected task 6 to be present but was not")
assert.Len(t, taskIDs, 1)
assert.Equal(t, int64(6), taskIDs[0])
})
t.Run("done overdue", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
@ -68,8 +55,8 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z")
assert.NoError(t, err)
tasks, err := getUndoneOverdueTasks(s, now)
taskIDs, err := getUndoneOverdueTasks(s, now)
assert.NoError(t, err)
assert.Len(t, tasks, 0)
assert.Len(t, taskIDs, 0)
})
}

View File

@ -61,7 +61,7 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
// Get all creators of tasks
creators := make(map[int64]*user.User, len(taskIDs))
err = s.
Select("users.*").
Select("users.id, users.username, users.email, users.name, users.timezone").
Join("LEFT", "tasks", "tasks.created_by_id = users.id").
In("tasks.id", taskIDs).
Where(cond).

View File

@ -29,11 +29,9 @@ import (
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web"
"github.com/google/uuid"
"github.com/imdario/mergo"
"github.com/jinzhu/copier"
"xorm.io/builder"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
@ -66,7 +64,7 @@ type Task struct {
// The list this task belongs to.
ListID int64 `xorm:"bigint INDEX not null" json:"list_id" 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:"bigint INDEX null" json:"repeat_after" valid:"range(0|9223372036854775807)"`
RepeatAfter int64 `xorm:"bigint INDEX null" json:"repeat_after"`
// Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.
RepeatMode TaskRepeatMode `xorm:"not null default 0" json:"repeat_mode"`
// The task priority. Can be anything you want, it is possible to sort by this later.
@ -298,20 +296,17 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
if err := param.validate(); err != nil {
return nil, 0, 0, err
}
// Mysql sorts columns with null values before ones without null value.
// Because it does not have support for NULLS FIRST or NULLS LAST we work around this by
// first sorting for null (or not null) values and then the order we actually want to.
if db.Type() == schemas.MYSQL {
orderby += param.sortBy + " IS NULL, "
}
orderby += param.sortBy + " " + param.orderBy.String()
// Postgres and sqlite allow us to control how columns with null values are sorted.
// Postgres sorts by default entries with null values after ones with values.
// To make that consistent with the sort order we have and other dbms, we're adding a separate clause here.
if db.Type() == schemas.POSTGRES || db.Type() == schemas.SQLITE {
orderby += " NULLS LAST"
if db.Type() == schemas.POSTGRES {
if param.orderBy == orderAscending {
orderby += " NULLS FIRST"
}
if param.orderBy == orderDescending {
orderby += " NULLS LAST"
}
}
if (i + 1) < len(opts.sortby) {
@ -338,11 +333,8 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
continue
}
if f.field == "assignees" {
if f.comparator == taskFilterComparatorLike {
return nil, 0, 0, ErrInvalidTaskFilterValue{Field: f.field, Value: f.value}
}
f.field = "username"
if f.field == "assignees" || f.field == "user_id" {
f.field = "user_id"
filter, err := getFilterCond(f, opts.filterIncludeNulls)
if err != nil {
return nil, 0, 0, err
@ -410,7 +402,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
return nil, 0, 0, err
}
userListIDs := make([]int64, 0, len(userLists))
userListIDs := make([]int64, len(userLists))
for _, l := range userLists {
userListIDs = append(userListIDs, l.ID)
}
@ -433,13 +425,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
}
if len(assigneeFilters) > 0 {
assigneeFilter := []builder.Cond{
builder.In("user_id",
builder.Select("id").
From("users").
Where(builder.Or(assigneeFilters...)),
)}
filters = append(filters, getFilterCondForSeparateTable("task_assignees", opts.filterConcat, assigneeFilter))
filters = append(filters, getFilterCondForSeparateTable("task_assignees", opts.filterConcat, assigneeFilters))
}
if len(labelFilters) > 0 {
@ -687,17 +673,7 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]
continue
}
fullRelatedTasks[rt.OtherTaskID].IsFavorite = taskFavorites[rt.OtherTaskID]
// We're duplicating the other task to avoid cycles as these can't be represented properly in json
// and would thus fail with an error.
otherTask := &Task{}
err = copier.Copy(otherTask, fullRelatedTasks[rt.OtherTaskID])
if err != nil {
log.Errorf("Could not duplicate task object: %v", err)
continue
}
otherTask.RelatedTasks = nil
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], otherTask)
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], fullRelatedTasks[rt.OtherTaskID])
}
return
@ -907,7 +883,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
// Generate a uuid if we don't already have one
if t.UID == "" {
t.UID = uuid.NewString()
t.UID = utils.MakeRandomString(40)
}
// Get the default bucket and move the task there
@ -1289,39 +1265,18 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
}
}
// We want to preserve intervals among the due, start and end dates.
// The due date is used as a reference point for all new dates, so the
// behaviour depends on whether the due date is set at all.
if oldTask.DueDate.IsZero() {
// If a task has no due date, but does have a start and end date, the
// end date should keep the difference to the start date when setting
// them as new
if !oldTask.StartDate.IsZero() && !oldTask.EndDate.IsZero() {
diff := oldTask.EndDate.Sub(oldTask.StartDate)
newTask.StartDate = now.Add(repeatDuration)
newTask.EndDate = now.Add(repeatDuration + diff)
} else {
if !oldTask.StartDate.IsZero() {
newTask.StartDate = now.Add(repeatDuration)
}
if !oldTask.EndDate.IsZero() {
newTask.EndDate = now.Add(repeatDuration)
}
}
// If a task has a start and end date, the end date should keep the difference to the start date when setting them as new
if !oldTask.StartDate.IsZero() && !oldTask.EndDate.IsZero() {
diff := oldTask.EndDate.Sub(oldTask.StartDate)
newTask.StartDate = now.Add(repeatDuration)
newTask.EndDate = now.Add(repeatDuration + diff)
} else {
// If the old task has a start and due date, we set the new start date
// to preserve the interval between them.
if !oldTask.StartDate.IsZero() {
diff := oldTask.DueDate.Sub(oldTask.StartDate)
newTask.StartDate = newTask.DueDate.Add(-diff)
newTask.StartDate = now.Add(repeatDuration)
}
// If the old task has an end and due date, we set the new end date
// to preserve the interval between them.
if !oldTask.EndDate.IsZero() {
diff := oldTask.DueDate.Sub(oldTask.EndDate)
newTask.EndDate = newTask.DueDate.Add(-diff)
newTask.EndDate = now.Add(repeatDuration)
}
}

View File

@ -91,20 +91,33 @@ func ListUsersFromList(s *xorm.Session, l *List, search string) (users []*user.U
uidmap[u.TeamNamespaceUserID] = true
}
uids := make([]int64, 0, len(uidmap))
uids := make([]int64, len(uidmap))
for id := range uidmap {
uids = append(uids, id)
}
var cond builder.Cond
var cond builder.Cond = builder.Like{"username", "%" + search + "%"}
if len(uids) > 0 {
cond = builder.In("id", uids)
cond = builder.And(
builder.In("id", uids),
cond,
)
}
// Get all users
err = s.
Table("users").
Select("*").
Where(cond).
GroupBy("id").
OrderBy("id").
Find(&users)
// Obfuscate all user emails
for _, u := range users {
u.Email = ""
}
users, err = user.ListUsers(s, search, &user.ListUserOpts{
AdditionalCond: cond,
ReturnAllIfNoSearchProvided: true,
})
return
}

View File

@ -32,7 +32,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -43,7 +42,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -54,7 +52,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -66,7 +63,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -78,7 +74,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -89,7 +84,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -101,7 +95,6 @@ func TestListUsersFromList(t *testing.T) {
EmailRemindersEnabled: true,
DiscoverableByEmail: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -112,7 +105,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -123,7 +115,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -134,7 +125,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -146,7 +136,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -159,7 +148,6 @@ func TestListUsersFromList(t *testing.T) {
EmailRemindersEnabled: true,
DiscoverableByName: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -170,7 +158,6 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -214,13 +201,6 @@ func TestListUsersFromList(t *testing.T) {
testuser13, // Shared Via NamespaceUser admin
},
},
{
name: "search for user1",
args: args{l: &List{ID: 19, OwnerID: 7}, search: "user1"},
wantUsers: []*user.User{
testuser1, // Shared Via Team readonly
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -19,7 +19,6 @@ package openid
import (
"context"
"encoding/json"
"errors"
"math/rand"
"net/http"
"time"
@ -105,13 +104,12 @@ func HandleCallback(c echo.Context) error {
// Parse the access & ID token
oauth2Token, err := provider.Oauth2Config.Exchange(context.Background(), cb.Code)
if err != nil {
var rerr *oauth2.RetrieveError
if errors.As(err, &rerr) {
if rerr, is := err.(*oauth2.RetrieveError); is {
log.Error(err)
details := make(map[string]interface{})
if err := json.Unmarshal(rerr.Body, &details); err != nil {
log.Errorf("Error unmarshalling token for provider %s: %v", provider.Name, err)
log.Errorf("Error unmarshaling token for provider %s: %v", provider.Name, err)
return handler.HandleHTTPError(err, c)
}

View File

@ -63,7 +63,6 @@ func GetAllProviders() (providers []*Provider, err error) {
if err != nil {
if provider != nil {
log.Errorf("Error while getting openid provider %s: %s", provider.Name, err)
continue
}
log.Errorf("Error while getting openid provider: %s", err)
continue

View File

@ -24,10 +24,9 @@ import (
// Image represents an image which can be used as a list background
type Image struct {
ID string `json:"id"`
URL string `json:"url"`
Thumb string `json:"thumb,omitempty"`
BlurHash string `json:"blur_hash"`
ID string `json:"id"`
URL string `json:"url"`
Thumb string `json:"thumb,omitempty"`
// This can be used to supply extra information from an image provider to clients
Info interface{} `json:"info,omitempty"`
}

View File

@ -17,36 +17,24 @@
package handler
import (
_ "image/gif" // To make sure the decoder used for generating blurHashes recognizes gifs
_ "image/jpeg" // To make sure the decoder used for generating blurHashes recognizes jpgs
_ "image/png" // To make sure the decoder used for generating blurHashes recognizes pngs
_ "golang.org/x/image/bmp" // To make sure the decoder used for generating blurHashes recognizes bmps
_ "golang.org/x/image/tiff" // To make sure the decoder used for generating blurHashes recognizes tiffs
_ "golang.org/x/image/webp" // To make sure the decoder used for generating blurHashes recognizes tiffs
"image"
"io"
"net/http"
"strconv"
"strings"
"code.vikunja.io/api/pkg/db"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
auth2 "code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/modules/background"
"code.vikunja.io/api/pkg/modules/background/unsplash"
"code.vikunja.io/api/pkg/modules/background/upload"
"code.vikunja.io/web"
"code.vikunja.io/web/handler"
"github.com/bbrks/go-blurhash"
"github.com/gabriel-vasile/mimetype"
"github.com/labstack/echo/v4"
"golang.org/x/image/draw"
"xorm.io/xorm"
)
// BackgroundProvider represents a thing which holds a background provider
@ -146,18 +134,6 @@ func (bp *BackgroundProvider) SetBackground(c echo.Context) error {
return c.JSON(http.StatusOK, list)
}
func CreateBlurHash(srcf io.Reader) (hash string, err error) {
src, _, err := image.Decode(srcf)
if err != nil {
return "", err
}
dst := image.NewRGBA(image.Rect(0, 0, 32, 32))
draw.NearestNeighbor.Scale(dst, dst.Rect, src, src.Bounds(), draw.Over, nil)
return blurhash.Encode(4, 3, dst)
}
// UploadBackground uploads a background and passes the id of the uploaded file as an Image to the Set function of the BackgroundProvider.
func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
s := db.NewSession()
@ -169,21 +145,23 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
return handler.HandleHTTPError(err, c)
}
p := bp.Provider()
// Get + upload the image
file, err := c.FormFile("background")
if err != nil {
_ = s.Rollback()
return err
}
srcf, err := file.Open()
src, err := file.Open()
if err != nil {
_ = s.Rollback()
return err
}
defer srcf.Close()
defer src.Close()
// Validate we're dealing with an image
mime, err := mimetype.DetectReader(srcf)
mime, err := mimetype.DetectReader(src)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
@ -192,8 +170,10 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
_ = s.Rollback()
return c.JSON(http.StatusBadRequest, models.Message{Message: "Uploaded file is no image."})
}
_, _ = src.Seek(0, io.SeekStart)
err = SaveBackgroundFile(s, auth, list, srcf, file.Filename, uint64(file.Size))
// Save the file
f, err := files.CreateWithMime(src, file.Filename, uint64(file.Size), auth, mime.String())
if err != nil {
_ = s.Rollback()
if files.IsErrFileIsTooLarge(err) {
@ -203,6 +183,14 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
return handler.HandleHTTPError(err, c)
}
image := &background.Image{ID: strconv.FormatInt(f.ID, 10)}
err = p.Set(s, image, list, auth)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
if err := s.Commit(); err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
@ -211,27 +199,6 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
return c.JSON(http.StatusOK, list)
}
func SaveBackgroundFile(s *xorm.Session, auth web.Auth, list *models.List, srcf io.ReadSeeker, filename string, filesize uint64) (err error) {
_, _ = srcf.Seek(0, io.SeekStart)
f, err := files.Create(srcf, filename, filesize, auth)
if err != nil {
return err
}
// Generate a blurHash
_, _ = srcf.Seek(0, io.SeekStart)
list.BackgroundBlurHash, err = CreateBlurHash(srcf)
if err != nil {
return err
}
// Save it
p := upload.Provider{}
img := &background.Image{ID: strconv.FormatInt(f.ID, 10)}
err = p.Set(s, img, list, auth)
return err
}
func checkListBackgroundRights(s *xorm.Session, c echo.Context) (list *models.List, auth web.Auth, err error) {
auth, err = auth2.GetAuthFromClaims(c)
if err != nil {
@ -333,7 +300,6 @@ func RemoveListBackground(c echo.Context) error {
list.BackgroundFileID = 0
list.BackgroundInformation = nil
list.BackgroundBlurHash = ""
err = models.UpdateList(s, list, auth, true)
if err != nil {
return err

View File

@ -61,7 +61,6 @@ type Photo struct {
Height int `json:"height"`
Color string `json:"color"`
Description string `json:"description"`
BlurHash string `json:"blur_hash"`
User struct {
Username string `json:"username"`
Name string `json:"name"`
@ -179,9 +178,8 @@ func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []
result = []*background.Image{}
for _, p := range collectionResult {
result = append(result, &background.Image{
ID: p.ID,
URL: getImageID(p.Urls.Raw),
BlurHash: p.BlurHash,
ID: p.ID,
URL: getImageID(p.Urls.Raw),
Info: &models.UnsplashPhoto{
UnsplashID: p.ID,
Author: p.User.Username,
@ -215,9 +213,8 @@ func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []
result = []*background.Image{}
for _, p := range searchResult.Results {
result = append(result, &background.Image{
ID: p.ID,
URL: getImageID(p.Urls.Raw),
BlurHash: p.BlurHash,
ID: p.ID,
URL: getImageID(p.Urls.Raw),
Info: &models.UnsplashPhoto{
UnsplashID: p.ID,
Author: p.User.Username,
@ -318,7 +315,7 @@ func (p *Provider) Set(s *xorm.Session, image *background.Image, list *models.Li
list.BackgroundInformation = unsplashPhoto
// Set it as the list background
return models.SetListBackground(s, list.ID, file, photo.BlurHash)
return models.SetListBackground(s, list.ID, file)
}
// Pingback pings the unsplash api if an unsplash photo has been accessed.

View File

@ -52,7 +52,7 @@ func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []
// @Failure 404 {object} models.Message "The list does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id}/backgrounds/upload [put]
func (p *Provider) Set(s *xorm.Session, img *background.Image, list *models.List, auth web.Auth) (err error) {
func (p *Provider) Set(s *xorm.Session, image *background.Image, list *models.List, auth web.Auth) (err error) {
// Remove the old background if one exists
if list.BackgroundFileID != 0 {
file := files.File{ID: list.BackgroundFileID}
@ -62,12 +62,12 @@ func (p *Provider) Set(s *xorm.Session, img *background.Image, list *models.List
}
file := &files.File{}
file.ID, err = strconv.ParseInt(img.ID, 10, 64)
file.ID, err = strconv.ParseInt(image.ID, 10, 64)
if err != nil {
return
}
list.BackgroundInformation = &models.ListBackgroundType{Type: models.ListBackgroundUpload}
return models.SetListBackground(s, list.ID, file, list.BackgroundBlurHash)
return models.SetListBackground(s, list.ID, file)
}

View File

@ -35,7 +35,7 @@ import (
func Dump(filename string) error {
dumpFile, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error opening dump file: %w", err)
return fmt.Errorf("error opening dump file: %s", err)
}
defer dumpFile.Close()
@ -47,7 +47,7 @@ func Dump(filename string) error {
if viper.ConfigFileUsed() != "" {
err = writeFileToZip(viper.ConfigFileUsed(), dumpWriter)
if err != nil {
return fmt.Errorf("error saving config file: %w", err)
return fmt.Errorf("error saving config file: %s", err)
}
} else {
log.Warning("No config file found, not including one in the dump. This usually happens when environment variables are used for configuration.")
@ -64,7 +64,7 @@ func Dump(filename string) error {
if dotEnv != "" {
err = utils.WriteBytesToZip(".env", []byte(dotEnv), dumpWriter)
if err != nil {
return fmt.Errorf("error saving env file: %w", err)
return fmt.Errorf("error saving env file: %s", err)
}
log.Info("Dumped .env file")
}
@ -73,7 +73,7 @@ func Dump(filename string) error {
log.Info("Start dumping version file...")
err = utils.WriteBytesToZip("VERSION", []byte(version.Version), dumpWriter)
if err != nil {
return fmt.Errorf("error saving version: %w", err)
return fmt.Errorf("error saving version: %s", err)
}
log.Info("Dumped version")
@ -81,12 +81,12 @@ func Dump(filename string) error {
log.Info("Start dumping database...")
data, err := db.Dump()
if err != nil {
return fmt.Errorf("error saving database data: %w", err)
return fmt.Errorf("error saving database data: %s", err)
}
for t, d := range data {
err = utils.WriteBytesToZip("database/"+t+".json", d, dumpWriter)
if err != nil {
return fmt.Errorf("error writing database table %s: %w", t, err)
return fmt.Errorf("error writing database table %s: %s", t, err)
}
}
log.Info("Dumped database")
@ -95,7 +95,7 @@ func Dump(filename string) error {
log.Info("Start dumping files...")
allFiles, err := files.Dump()
if err != nil {
return fmt.Errorf("error saving file: %w", err)
return fmt.Errorf("error saving file: %s", err)
}
err = utils.WriteFilesToZip(allFiles, dumpWriter)

View File

@ -20,7 +20,6 @@ import (
"archive/zip"
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
@ -45,7 +44,7 @@ func Restore(filename string) error {
r, err := zip.OpenReader(filename)
if err != nil {
return fmt.Errorf("could not open zip file: %w", err)
return fmt.Errorf("could not open zip file: %s", err)
}
log.Warning("Restoring a dump will wipe your current installation!")
@ -53,7 +52,7 @@ func Restore(filename string) error {
cr := bufio.NewReader(os.Stdin)
text, err := cr.ReadString('\n')
if err != nil {
return fmt.Errorf("could not read confirmation message: %w", err)
return fmt.Errorf("could not read confirmation message: %s", err)
}
if text != "Yes, I understand\n" {
return fmt.Errorf("invalid confirmation message")
@ -100,7 +99,7 @@ func Restore(filename string) error {
// Restore the db
// Start by wiping everything
if err := db.WipeEverything(); err != nil {
return fmt.Errorf("could not wipe database: %w", err)
return fmt.Errorf("could not wipe database: %s", err)
}
log.Info("Wiped database.")
@ -109,52 +108,36 @@ func Restore(filename string) error {
migrations := dbfiles["migration"]
rc, err := migrations.Open()
if err != nil {
return fmt.Errorf("could not open migrations: %w", err)
return fmt.Errorf("could not open migrations: %s", err)
}
defer rc.Close()
var buf bytes.Buffer
if _, err := buf.ReadFrom(rc); err != nil {
return fmt.Errorf("could not read migrations: %w", err)
return fmt.Errorf("could not read migrations: %s", err)
}
ms := []*xormigrate.Migration{}
if err := json.Unmarshal(buf.Bytes(), &ms); err != nil {
return fmt.Errorf("could not read migrations: %w", err)
return fmt.Errorf("could not read migrations: %s", err)
}
sort.Slice(ms, func(i, j int) bool {
return ms[i].ID < ms[j].ID
return ms[i].ID > ms[j].ID
})
lastMigration := ms[len(ms)-2]
log.Debugf("Last migration: %s", lastMigration.ID)
lastMigration := ms[len(ms)-1]
if err := migration.MigrateTo(lastMigration.ID, nil); err != nil {
return fmt.Errorf("could not create db structure: %w", err)
return fmt.Errorf("could not create db structure: %s", err)
}
delete(dbfiles, "migration")
// Restore all db data
for table, d := range dbfiles {
content, err := unmarshalFileToJSON(d)
if err != nil {
return fmt.Errorf("could not read table %s: %w", table, err)
return fmt.Errorf("could not read table %s: %s", table, err)
}
// FIXME: There has to be a general way to do this but this works for now.
if table == "notifications" {
for i := range content {
decoded, err := base64.StdEncoding.DecodeString(content[i]["notification"].(string))
if err != nil {
return fmt.Errorf("could not decode notification %s: %w", content[i]["notification"], err)
}
content[i]["notification"] = string(decoded)
}
}
if err := db.Restore(table, content); err != nil {
return fmt.Errorf("could not restore table data for table %s: %w", table, err)
return fmt.Errorf("could not restore table data for table %s: %s", table, err)
}
log.Infof("Restored table %s", table)
}
@ -168,18 +151,18 @@ func Restore(filename string) error {
for i, file := range filesFiles {
id, err := strconv.ParseInt(i, 10, 64)
if err != nil {
return fmt.Errorf("could not parse file id %s: %w", i, err)
return fmt.Errorf("could not parse file id %s: %s", i, err)
}
f := &files.File{ID: id}
fc, err := file.Open()
if err != nil {
return fmt.Errorf("could not open file %s: %w", i, err)
return fmt.Errorf("could not open file %s: %s", i, err)
}
if err := f.Save(fc); err != nil {
return fmt.Errorf("could not save file: %w", err)
return fmt.Errorf("could not save file: %s", err)
}
_ = fc.Close()
@ -222,7 +205,7 @@ func restoreConfig(configFile, dotEnvFile *zip.File) error {
outFile, err := os.OpenFile(configFile.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, configFile.Mode())
if err != nil {
return fmt.Errorf("could not open config file for writing: %w", err)
return fmt.Errorf("could not open config file for writing: %s", err)
}
cfgr, err := configFile.Open()
@ -233,7 +216,7 @@ func restoreConfig(configFile, dotEnvFile *zip.File) error {
// #nosec - We eliminated the potential decompression bomb by erroring out above if the file is larger than a threshold.
_, err = io.Copy(outFile, cfgr)
if err != nil {
return fmt.Errorf("could not create config file: %w", err)
return fmt.Errorf("could not create config file: %s", err)
}
_ = cfgr.Close()
@ -242,7 +225,7 @@ func restoreConfig(configFile, dotEnvFile *zip.File) error {
log.Infof("The config file has been restored to '%s'.", configFile.Name)
log.Infof("You can now make changes to it, hit enter when you're done.")
if _, err := bufio.NewReader(os.Stdin).ReadString('\n'); err != nil {
return fmt.Errorf("could not read from stdin: %w", err)
return fmt.Errorf("could not read from stdin: %s", err)
}
return nil
@ -266,7 +249,7 @@ func restoreConfig(configFile, dotEnvFile *zip.File) error {
log.Warning("Make sure your current config matches the following env variables, confirm by pressing enter when done.")
log.Warning("If your config does not match, you'll have to make the changes and restart the restoring process afterwards.")
if _, err := bufio.NewReader(os.Stdin).ReadString('\n'); err != nil {
return fmt.Errorf("could not read from stdin: %w", err)
return fmt.Errorf("could not read from stdin: %s", err)
}
}

View File

@ -20,11 +20,10 @@ import (
"bytes"
"io/ioutil"
"code.vikunja.io/api/pkg/modules/background/handler"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
@ -107,19 +106,21 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas
log.Debugf("[creating structure] Created list %d", l.ID)
bf, is := originalBackgroundInformation.(*bytes.Buffer)
backgroundFile, is := originalBackgroundInformation.(*bytes.Buffer)
if is {
backgroundFile := bytes.NewReader(bf.Bytes())
log.Debugf("[creating structure] Creating a background file for list %d", l.ID)
err = handler.SaveBackgroundFile(s, user, &l.List, backgroundFile, "", uint64(backgroundFile.Len()))
file, err := files.Create(backgroundFile, "", uint64(backgroundFile.Len()), user)
if err != nil {
return err
}
log.Debugf("[creating structure] Created a background file for list %d", l.ID)
err = models.SetListBackground(s, l.ID, file)
if err != nil {
return err
}
log.Debugf("[creating structure] Created a background file as new file %d for list %d", file.ID, l.ID)
}
// Create all buckets

View File

@ -64,7 +64,7 @@ func (v *FileMigrator) Name() string {
func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) error {
r, err := zip.NewReader(file, size)
if err != nil {
return fmt.Errorf("could not open import file: %w", err)
return fmt.Errorf("could not open import file: %s", err)
}
log.Debugf(logPrefix+"Importing a zip file containing %d files", len(r.File))
@ -77,7 +77,7 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
fname := strings.ReplaceAll(f.Name, "files/", "")
id, err := strconv.ParseInt(fname, 10, 64)
if err != nil {
return fmt.Errorf("could not convert file id: %w", err)
return fmt.Errorf("could not convert file id: %s", err)
}
storedFiles[id] = f
log.Debugf(logPrefix + "Found a blob file")
@ -104,18 +104,18 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
// Import the bulk of Vikunja data
df, err := dataFile.Open()
if err != nil {
return fmt.Errorf("could not open data file: %w", err)
return fmt.Errorf("could not open data file: %s", err)
}
defer df.Close()
var bufData bytes.Buffer
if _, err := bufData.ReadFrom(df); err != nil {
return fmt.Errorf("could not read data file: %w", err)
return fmt.Errorf("could not read data file: %s", err)
}
namespaces := []*models.NamespaceWithListsAndTasks{}
if err := json.Unmarshal(bufData.Bytes(), &namespaces); err != nil {
return fmt.Errorf("could not read data: %w", err)
return fmt.Errorf("could not read data: %s", err)
}
for _, n := range namespaces {
@ -123,11 +123,11 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
if b, exists := storedFiles[l.BackgroundFileID]; exists {
bf, err := b.Open()
if err != nil {
return fmt.Errorf("could not open list background file %d for reading: %w", l.BackgroundFileID, err)
return fmt.Errorf("could not open list background file %d for reading: %s", l.BackgroundFileID, err)
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(bf); err != nil {
return fmt.Errorf("could not read list background file %d: %w", l.BackgroundFileID, err)
return fmt.Errorf("could not read list background file %d: %s", l.BackgroundFileID, err)
}
l.BackgroundInformation = &buf
@ -143,11 +143,11 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
for _, attachment := range t.Attachments {
af, err := storedFiles[attachment.File.ID].Open()
if err != nil {
return fmt.Errorf("could not open attachment %d for reading: %w", attachment.ID, err)
return fmt.Errorf("could not open attachment %d for reading: %s", attachment.ID, err)
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(af); err != nil {
return fmt.Errorf("could not read attachment %d: %w", attachment.ID, err)
return fmt.Errorf("could not read attachment %d: %s", attachment.ID, err)
}
attachment.ID = 0
@ -160,7 +160,7 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
err = migration.InsertFromStructure(namespaces, user)
if err != nil {
return fmt.Errorf("could not insert data: %w", err)
return fmt.Errorf("could not insert data: %s", err)
}
if filterFile == nil {
@ -172,18 +172,18 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
// Import filters
ff, err := filterFile.Open()
if err != nil {
return fmt.Errorf("could not open filters file: %w", err)
return fmt.Errorf("could not open filters file: %s", err)
}
defer ff.Close()
var bufFilter bytes.Buffer
if _, err := bufFilter.ReadFrom(ff); err != nil {
return fmt.Errorf("could not read filters file: %w", err)
return fmt.Errorf("could not read filters file: %s", err)
}
filters := []*models.SavedFilter{}
if err := json.Unmarshal(bufFilter.Bytes(), &filters); err != nil {
return fmt.Errorf("could not read filter data: %w", err)
return fmt.Errorf("could not read filter data: %s", err)
}
log.Debugf(logPrefix+"Importing %d saved filters", len(filters))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -18,8 +18,6 @@ package notifications
import (
"bytes"
"embed"
_ "embed"
templatehtml "html/template"
templatetext "text/template"
@ -51,7 +49,7 @@ const mailTemplateHTML = `
<div style="width: 100%; font-family: 'Open Sans', sans-serif; text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; text-align: justify;">
<h1 style="font-size: 30px; text-align: center;">
<img src="cid:logo.png" style="height: 75px;" alt="Vikunja"/>
<img src="{{.FrontendURL}}images/logo-full.svg" style="height: 75px;" alt="Vikunja"/>
</h1>
<div style="border: 1px solid #dbdbdb; -webkit-box-shadow: 0.3em 0.3em 0.8em #e6e6e6; box-shadow: 0.3em 0.3em 0.8em #e6e6e6; color: #4a4a4a; padding: 5px 25px; border-radius: 3px; background: #fff;">
<p>
@ -86,9 +84,6 @@ const mailTemplateHTML = `
</html>
`
//go:embed logo.png
var logo embed.FS
// RenderMail takes a precomposed mail message and renders it into a ready to send mail.Opts object
func RenderMail(m *Mail) (mailOpts *mail.Opts, err error) {
@ -160,9 +155,6 @@ func RenderMail(m *Mail) (mailOpts *mail.Opts, err error) {
Message: plainContent.String(),
HTMLMessage: htmlContent.String(),
Boundary: boundary,
EmbedFS: map[string]*embed.FS{
"logo.png": &logo,
},
}
return mailOpts, nil

View File

@ -127,7 +127,7 @@ And one more, because why not?
<div style="width: 100%; font-family: 'Open Sans', sans-serif; text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; text-align: justify;">
<h1 style="font-size: 30px; text-align: center;">
<img src="cid:logo.png" style="height: 75px;" alt="Vikunja"/>
<img src="images/logo-full.svg" style="height: 75px;" alt="Vikunja"/>
</h1>
<div style="border: 1px solid #dbdbdb; -webkit-box-shadow: 0.3em 0.3em 0.8em #e6e6e6; box-shadow: 0.3em 0.3em 0.8em #e6e6e6; color: #4a4a4a; padding: 5px 25px; border-radius: 3px; background: #fff;">
<p>

View File

@ -17,7 +17,6 @@
package v1
import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
@ -50,7 +49,7 @@ import (
// @tags user
// @Produce octet-stream
// @Param username path string true "The username of the user who's avatar you want to get"
// @Param size query int false "The size of the avatar you want to get. If bigger than the max configured size this will be adjusted to the maximum size."
// @Param size query int false "The size of the avatar you want to get"
// @Success 200 {} blob "The avatar"
// @Failure 404 {object} models.Message "The user does not exist."
// @Failure 500 {object} models.Message "Internal error"
@ -98,9 +97,6 @@ func GetAvatar(c echo.Context) error {
return handler.HandleHTTPError(err, c)
}
}
if sizeInt > config.ServiceMaxAvatarSize.GetInt64() {
sizeInt = config.ServiceMaxAvatarSize.GetInt64()
}
// Get the avatar
a, mimeType, err := avatarProvider.GetAvatar(u, sizeInt)

View File

@ -1,112 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package v1
import (
"net/http"
"strconv"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web/handler"
"github.com/labstack/echo/v4"
)
// GenerateCaldavToken is the handler to create a caldav token
// @Summary Generate a caldav token
// @Description Generates a caldav token which can be used for the caldav api. It is not possible to see the token again after it was generated.
// @tags user
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {object} user.Token
// @Failure 400 {object} web.HTTPError "Something's invalid."
// @Failure 404 {object} web.HTTPError "User does not exist."
// @Failure 500 {object} models.Message "Internal server error."
// @Router /user/settings/token/caldav [put]
func GenerateCaldavToken(c echo.Context) (err error) {
u, err := user.GetCurrentUser(c)
if err != nil {
return handler.HandleHTTPError(err, c)
}
token, err := user.GenerateNewCaldavToken(u)
if err != nil {
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusCreated, token)
}
// GetCaldavTokens is the handler to return a list of all caldav tokens for the current user
// @Summary Returns the caldav tokens for the current user
// @Description Return the IDs and created dates of all caldav tokens for the current user.
// @tags user
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {array} user.Token
// @Failure 400 {object} web.HTTPError "Something's invalid."
// @Failure 404 {object} web.HTTPError "User does not exist."
// @Failure 500 {object} models.Message "Internal server error."
// @Router /user/settings/token/caldav [get]
func GetCaldavTokens(c echo.Context) error {
u, err := user.GetCurrentUser(c)
if err != nil {
return handler.HandleHTTPError(err, c)
}
tokens, err := user.GetCaldavTokens(u)
if err != nil {
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusCreated, tokens)
}
// DeleteCaldavToken is the handler to delete a caldv token
// @Summary Delete a caldav token by id
// @tags user
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "Token ID"
// @Success 200 {object} models.Message
// @Failure 400 {object} web.HTTPError "Something's invalid."
// @Failure 404 {object} web.HTTPError "User does not exist."
// @Failure 500 {object} models.Message "Internal server error."
// @Router /user/settings/token/caldav/{id} [get]
func DeleteCaldavToken(c echo.Context) error {
u, err := user.GetCurrentUser(c)
if err != nil {
return handler.HandleHTTPError(err, c)
}
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
return handler.HandleHTTPError(err, c)
}
err = user.DeleteCaldavTokenByID(u, id)
if err != nil {
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, &models.Message{Message: "The token was deleted successfully."})
}

View File

@ -47,7 +47,7 @@ func UserList(c echo.Context) error {
s := db.NewSession()
defer s.Close()
users, err := user.ListUsers(s, search, nil)
users, err := user.ListUsers(s, search)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)

View File

@ -17,8 +17,6 @@
package v1
import (
"errors"
"fmt"
"net/http"
"github.com/labstack/echo/v4"
@ -48,13 +46,11 @@ type UserSettings struct {
DiscoverableByEmail bool `json:"discoverable_by_email"`
// If enabled, the user will get an email for their overdue tasks each morning.
OverdueTasksRemindersEnabled bool `json:"overdue_tasks_reminders_enabled"`
// The time when the daily summary of overdue tasks will be sent via email.
OverdueTasksRemindersTime string `json:"overdue_tasks_reminders_time" valid:"time,required"`
// If a task is created without a specified list this value should be used. Applies
// to tasks made directly in API and from clients.
DefaultListID int64 `json:"default_list_id"`
// The day when the week starts for this user. 0 = sunday, 1 = monday, etc.
WeekStart int `json:"week_start" valid:"range(0|7)"`
WeekStart int `json:"week_start"`
// The user's language
Language string `json:"language"`
// The user's time zone. Used to send task reminders in the time zone of the user.
@ -162,16 +158,7 @@ func UpdateGeneralUserSettings(c echo.Context) error {
us := &UserSettings{}
err := c.Bind(us)
if err != nil {
var he *echo.HTTPError
if errors.As(err, &he) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided. Error was: %s", he.Message))
}
return echo.NewHTTPError(http.StatusBadRequest, "Invalid model provided.")
}
err = c.Validate(us)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
return echo.NewHTTPError(http.StatusBadRequest, "Bad user name provided.")
}
u, err := user2.GetCurrentUser(c)
@ -197,7 +184,6 @@ func UpdateGeneralUserSettings(c echo.Context) error {
user.WeekStart = us.WeekStart
user.Language = us.Language
user.Timezone = us.Timezone
user.OverdueTasksRemindersTime = us.OverdueTasksRemindersTime
_, err = user2.UpdateUser(s, user)
if err != nil {

View File

@ -75,7 +75,6 @@ func UserShow(c echo.Context) error {
WeekStart: u.WeekStart,
Language: u.Language,
Timezone: u.Timezone,
OverdueTasksRemindersTime: u.OverdueTasksRemindersTime,
},
DeletionScheduledAt: u.DeletionScheduledAt,
IsLocalUser: u.Issuer == user.IssuerLocal,

View File

@ -18,7 +18,6 @@ package v1
import (
"bytes"
"errors"
"fmt"
"image/jpeg"
"net/http"
@ -92,8 +91,7 @@ func UserTOTPEnable(c echo.Context) error {
}
if err := c.Bind(passcode); err != nil {
log.Debugf("Invalid model error. Internal error was: %s", err.Error())
var he *echo.HTTPError
if errors.As(err, &he) {
if he, is := err.(*echo.HTTPError); is {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided. Error was: %s", he.Message))
}
return echo.NewHTTPError(http.StatusBadRequest, "Invalid model provided.")
@ -133,8 +131,7 @@ func UserTOTPDisable(c echo.Context) error {
login := &user.Login{}
if err := c.Bind(login); err != nil {
log.Debugf("Invalid model error. Internal error was: %s", err.Error())
var he *echo.HTTPError
if errors.As(err, &he) {
if he, is := err.(*echo.HTTPError); is {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided. Error was: %s", he.Message))
}
return echo.NewHTTPError(http.StatusBadRequest, "Invalid model provided.")

View File

@ -17,7 +17,6 @@
package v1
import (
"errors"
"fmt"
"net/http"
@ -48,8 +47,7 @@ func UpdateUserEmail(c echo.Context) (err error) {
var emailUpdate = &user.EmailUpdate{}
if err := c.Bind(emailUpdate); err != nil {
log.Debugf("Invalid model error. Internal error was: %s", err.Error())
var he *echo.HTTPError
if errors.As(err, &he) {
if he, is := err.(*echo.HTTPError); is {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided. Error was: %s", he.Message))
}
return echo.NewHTTPError(http.StatusBadRequest, "Invalid model provided.")

View File

@ -1,71 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package caldav
import (
"errors"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
)
func BasicAuth(username, password string, c echo.Context) (bool, error) {
s := db.NewSession()
defer s.Close()
credentials := &user.Login{
Username: username,
Password: password,
}
u, err := user.CheckUserCredentials(s, credentials)
if err != nil && !user.IsErrWrongUsernameOrPassword(err) && !user.IsErrAccountIsNotLocal(err) {
log.Errorf("Error during basic auth for caldav: %v", err)
return false, nil
}
if err == nil {
c.Set("userBasicAuth", u)
return true, nil
}
tokens, err := user.GetCaldavTokens(u)
if err != nil {
log.Errorf("Error while getting tokens for caldav auth: %v", err)
return false, nil
}
// Looping over all tokens until we find one that matches
for _, token := range tokens {
err = bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
continue
}
log.Errorf("Error while verifying tokens for caldav auth: %v", err)
return false, nil
}
c.Set("userBasicAuth", u)
return true, nil
}
return false, nil
}

View File

@ -49,7 +49,6 @@ package routes
import (
"errors"
"fmt"
"net/url"
"strings"
"time"
@ -76,10 +75,12 @@ import (
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
"code.vikunja.io/api/pkg/routes/caldav"
_ "code.vikunja.io/api/pkg/swagger" // To generate swagger docs
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/version"
"code.vikunja.io/web"
"code.vikunja.io/web/handler"
"github.com/asaskevich/govalidator"
"github.com/getsentry/sentry-go"
sentryecho "github.com/getsentry/sentry-go/echo"
"github.com/golang-jwt/jwt/v4"
@ -88,6 +89,31 @@ import (
elog "github.com/labstack/gommon/log"
)
// CustomValidator is a dummy struct to use govalidator with echo
type CustomValidator struct{}
// Validate validates stuff
func (cv *CustomValidator) Validate(i interface{}) error {
if _, err := govalidator.ValidateStruct(i); err != nil {
var errs []string
for field, e := range govalidator.ErrorsByField(err) {
errs = append(errs, field+": "+e)
}
httperr := models.ValidationHTTPError{
HTTPError: web.HTTPError{
Code: models.ErrCodeInvalidData,
Message: "Invalid Data",
},
InvalidFields: errs,
}
return httperr
}
return nil
}
// NewEcho registers a new Echo instance
func NewEcho() *echo.Echo {
e := echo.New()
@ -130,8 +156,7 @@ func NewEcho() *echo.Echo {
e.HTTPErrorHandler = func(err error, c echo.Context) {
// Only capture errors not already handled by echo
var herr *echo.HTTPError
if errors.As(err, &herr) {
if _, ok := err.(*echo.HTTPError); !ok {
hub := sentryecho.GetHubFromContext(c)
if hub != nil {
hub.WithScope(func(scope *sentry.Scope) {
@ -168,7 +193,7 @@ func RegisterRoutes(e *echo.Echo) {
if config.ServiceEnableCaldav.GetBool() {
// Caldav routes
wkg := e.Group("/.well-known")
wkg.Use(middleware.BasicAuth(caldav.BasicAuth))
wkg.Use(middleware.BasicAuth(caldavBasicAuth))
wkg.Any("/caldav", caldav.PrincipalHandler)
wkg.Any("/caldav/", caldav.PrincipalHandler)
c := e.Group("/dav")
@ -178,11 +203,6 @@ func RegisterRoutes(e *echo.Echo) {
// healthcheck
e.GET("/health", HealthcheckHandler)
// static files
if static := config.ServiceStaticpath.GetString(); static != "" {
e.Use(middleware.Static(static))
}
// CORS_SHIT
if config.CorsEnable.GetBool() {
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
@ -210,25 +230,6 @@ func registerAPIRoutes(a *echo.Group) {
n := a.Group("")
setupRateLimit(n, "ip")
// Echo does not unescape url path params by default. To make sure values bound as :param in urls are passed
// properly to handlers, we use this middleware to unescape them.
// See https://kolaente.dev/vikunja/api/issues/1224
// See https://github.com/labstack/echo/issues/766
a.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
params := make([]string, 0, len(c.ParamValues()))
for _, param := range c.ParamValues() {
p, err := url.PathUnescape(param)
if err != nil {
return err
}
params = append(params, p)
}
c.SetParamValues(params...)
return next(c)
}
})
// Docs
n.GET("/docs.json", apiv1.DocsJSON)
n.GET("/docs", apiv1.RedocUI)
@ -321,9 +322,6 @@ func registerAPIRoutes(a *echo.Group) {
u.POST("/export/request", apiv1.RequestUserDataExport)
u.POST("/export/download", apiv1.DownloadUserDataExport)
u.GET("/timezones", apiv1.GetAvailableTimezones)
u.PUT("/settings/token/caldav", apiv1.GenerateCaldavToken)
u.GET("/settings/token/caldav", apiv1.GetCaldavTokens)
u.DELETE("/settings/token/caldav/:id", apiv1.DeleteCaldavToken)
if config.ServiceEnableTotp.GetBool() {
u.GET("/settings/totp", apiv1.UserTOTP)
@ -664,7 +662,7 @@ func registerMigrations(m *echo.Group) {
func registerCalDavRoutes(c *echo.Group) {
// Basic auth middleware
c.Use(middleware.BasicAuth(caldav.BasicAuth))
c.Use(middleware.BasicAuth(caldavBasicAuth))
// THIS is the entry point for caldav clients, otherwise lists will show up double
c.Any("", caldav.EntryHandler)
@ -676,3 +674,26 @@ func registerCalDavRoutes(c *echo.Group) {
c.Any("/lists/:list/", caldav.ListHandler)
c.Any("/lists/:list/:task", caldav.TaskHandler) // Mostly used for editing
}
func caldavBasicAuth(username, password string, c echo.Context) (bool, error) {
creds := &user.Login{
Username: username,
Password: password,
}
s := db.NewSession()
defer s.Close()
u, err := user.CheckUserCredentials(s, creds)
if err != nil {
_ = s.Rollback()
log.Errorf("Error during basic auth for caldav: %v", err)
return false, nil
}
if err := s.Commit(); err != nil {
return false, err
}
// Save the user in echo context for later use
c.Set("userBasicAuth", u)
return true, nil
}

View File

@ -1,55 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package routes
import (
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/web"
"github.com/asaskevich/govalidator"
)
// CustomValidator is a dummy struct to use govalidator with echo
type CustomValidator struct{}
func init() {
govalidator.TagMap["time"] = govalidator.Validator(func(str string) bool {
return govalidator.IsTime(str, "15:04")
})
}
// Validate validates stuff
func (cv *CustomValidator) Validate(i interface{}) error {
if _, err := govalidator.ValidateStruct(i); err != nil {
var errs []string
for field, e := range govalidator.ErrorsByField(err) {
errs = append(errs, field+": "+e)
}
httperr := models.ValidationHTTPError{
HTTPError: web.HTTPError{
Code: models.ErrCodeInvalidData,
Message: "Invalid Data",
},
InvalidFields: errs,
}
return httperr
}
return nil
}

View File

@ -1,10 +1,17 @@
// Package swagger GENERATED BY SWAG; DO NOT EDIT
// Package swagger GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag
package swagger
import "github.com/swaggo/swag"
import (
"bytes"
"encoding/json"
"strings"
"text/template"
const docTemplate = `{
"github.com/swaggo/swag"
)
var doc = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
@ -1988,7 +1995,7 @@ const docTemplate = `{
},
{
"type": "string",
"description": "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like ` + "`" + `due_date` + "`" + `, ` + "`" + `start_date` + "`" + `, ` + "`" + `end_date` + "`" + `, etc.",
"description": "The value to filter for.",
"name": "filter_value",
"in": "query"
},
@ -7019,153 +7026,6 @@ const docTemplate = `{
}
}
},
"/user/settings/token/caldav": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Return the IDs and created dates of all caldav tokens for the current user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Returns the caldav tokens for the current user",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/user.Token"
}
}
},
"400": {
"description": "Something's invalid.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"404": {
"description": "User does not exist.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"500": {
"description": "Internal server error.",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
},
"put": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Generates a caldav token which can be used for the caldav api. It is not possible to see the token again after it was generated.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Generate a caldav token",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/user.Token"
}
},
"400": {
"description": "Something's invalid.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"404": {
"description": "User does not exist.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"500": {
"description": "Internal server error.",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/user/settings/token/caldav/{id}": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Delete a caldav token by id",
"parameters": [
{
"type": "integer",
"description": "Token ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"400": {
"description": "Something's invalid.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"404": {
"description": "User does not exist.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"500": {
"description": "Internal server error.",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/user/settings/totp": {
"get": {
"security": [
@ -7537,7 +7397,7 @@ const docTemplate = `{
},
{
"type": "integer",
"description": "The size of the avatar you want to get. If bigger than the max configured size this will be adjusted to the maximum size.",
"description": "The size of the avatar you want to get",
"name": "size",
"in": "query"
}
@ -7577,9 +7437,6 @@ const docTemplate = `{
"background.Image": {
"type": "object",
"properties": {
"blur_hash": {
"type": "string"
},
"id": {
"type": "string"
},
@ -8044,10 +7901,6 @@ const docTemplate = `{
"models.List": {
"type": "object",
"properties": {
"background_blur_hash": {
"description": "Contains a very small version of the list background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.",
"type": "string"
},
"background_information": {
"description": "Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background"
},
@ -8829,9 +8682,7 @@ const docTemplate = `{
"minLength": 1
},
"right": {
"description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.",
"type": "integer",
"default": 0
"type": "integer"
},
"updated": {
"description": "A timestamp when this relation was last updated. You cannot change this value.",
@ -8862,9 +8713,7 @@ const docTemplate = `{
"type": "string"
},
"right": {
"description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.",
"type": "integer",
"default": 0
"type": "integer"
},
"updated": {
"description": "A timestamp when this task was last updated. You cannot change this value.",
@ -9062,20 +8911,6 @@ const docTemplate = `{
}
}
},
"user.Token": {
"type": "object",
"properties": {
"created": {
"type": "string"
},
"id": {
"type": "integer"
},
"token": {
"type": "string"
}
}
},
"user.User": {
"type": "object",
"properties": {
@ -9184,10 +9019,6 @@ const docTemplate = `{
"description": "If enabled, the user will get an email for their overdue tasks each morning.",
"type": "boolean"
},
"overdue_tasks_reminders_time": {
"description": "The time when the daily summary of overdue tasks will be sent via email.",
"type": "string"
},
"timezone": {
"description": "The user's time zone. Used to send task reminders in the time zone of the user.",
"type": "string"
@ -9337,18 +9168,56 @@ const docTemplate = `{
}
}`
type swaggerInfo struct {
Version string
Host string
BasePath string
Schemes []string
Title string
Description string
}
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "",
Host: "",
BasePath: "/api/v1",
Schemes: []string{},
Title: "Vikunja API",
Description: "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Rights\nAll endpoints which return a single item (list, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.\nThis can be used to show or hide ui elements based on the rights the user has.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer <jwt-token>`-header to authenticate successfully.\n\n**BasicAuth:** Only used when requesting tasks via caldav.\n<!-- ReDoc-Inject: <security-definitions> -->",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
var SwaggerInfo = swaggerInfo{
Version: "",
Host: "",
BasePath: "/api/v1",
Schemes: []string{},
Title: "Vikunja API",
Description: "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Rights\nAll endpoints which return a single item (list, task, namespace, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.\nThis can be used to show or hide ui elements based on the rights the user has.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer <jwt-token>`-header to authenticate successfully.\n\n**BasicAuth:** Only used when requesting tasks via caldav.\n<!-- ReDoc-Inject: <security-definitions> -->",
}
type s struct{}
func (s *s) ReadDoc() string {
sInfo := SwaggerInfo
sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
t, err := template.New("swagger_info").Funcs(template.FuncMap{
"marshal": func(v interface{}) string {
a, _ := json.Marshal(v)
return string(a)
},
"escape": func(v interface{}) string {
// escape tabs
str := strings.Replace(v.(string), "\t", "\\t", -1)
// replace " with \", and if that results in \\", replace that with \\\"
str = strings.Replace(str, "\"", "\\\"", -1)
return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1)
},
}).Parse(doc)
if err != nil {
return doc
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, sInfo); err != nil {
return doc
}
return tpl.String()
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
swag.Register("swagger", &s{})
}

View File

@ -1979,7 +1979,7 @@
},
{
"type": "string",
"description": "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like `due_date`, `start_date`, `end_date`, etc.",
"description": "The value to filter for.",
"name": "filter_value",
"in": "query"
},
@ -7010,153 +7010,6 @@
}
}
},
"/user/settings/token/caldav": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Return the IDs and created dates of all caldav tokens for the current user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Returns the caldav tokens for the current user",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/user.Token"
}
}
},
"400": {
"description": "Something's invalid.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"404": {
"description": "User does not exist.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"500": {
"description": "Internal server error.",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
},
"put": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Generates a caldav token which can be used for the caldav api. It is not possible to see the token again after it was generated.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Generate a caldav token",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/user.Token"
}
},
"400": {
"description": "Something's invalid.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"404": {
"description": "User does not exist.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"500": {
"description": "Internal server error.",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/user/settings/token/caldav/{id}": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Delete a caldav token by id",
"parameters": [
{
"type": "integer",
"description": "Token ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"400": {
"description": "Something's invalid.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"404": {
"description": "User does not exist.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"500": {
"description": "Internal server error.",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/user/settings/totp": {
"get": {
"security": [
@ -7528,7 +7381,7 @@
},
{
"type": "integer",
"description": "The size of the avatar you want to get. If bigger than the max configured size this will be adjusted to the maximum size.",
"description": "The size of the avatar you want to get",
"name": "size",
"in": "query"
}
@ -7568,9 +7421,6 @@
"background.Image": {
"type": "object",
"properties": {
"blur_hash": {
"type": "string"
},
"id": {
"type": "string"
},
@ -8035,10 +7885,6 @@
"models.List": {
"type": "object",
"properties": {
"background_blur_hash": {
"description": "Contains a very small version of the list background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.",
"type": "string"
},
"background_information": {
"description": "Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background"
},
@ -8820,9 +8666,7 @@
"minLength": 1
},
"right": {
"description": "The right this team has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.",
"type": "integer",
"default": 0
"type": "integer"
},
"updated": {
"description": "A timestamp when this relation was last updated. You cannot change this value.",
@ -8853,9 +8697,7 @@
"type": "string"
},
"right": {
"description": "The right this user has. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.",
"type": "integer",
"default": 0
"type": "integer"
},
"updated": {
"description": "A timestamp when this task was last updated. You cannot change this value.",
@ -9053,20 +8895,6 @@
}
}
},
"user.Token": {
"type": "object",
"properties": {
"created": {
"type": "string"
},
"id": {
"type": "integer"
},
"token": {
"type": "string"
}
}
},
"user.User": {
"type": "object",
"properties": {
@ -9175,10 +9003,6 @@
"description": "If enabled, the user will get an email for their overdue tasks each morning.",
"type": "boolean"
},
"overdue_tasks_reminders_time": {
"description": "The time when the daily summary of overdue tasks will be sent via email.",
"type": "string"
},
"timezone": {
"description": "The user's time zone. Used to send task reminders in the time zone of the user.",
"type": "string"

View File

@ -7,8 +7,6 @@ definitions:
type: object
background.Image:
properties:
blur_hash:
type: string
id:
type: string
info:
@ -391,11 +389,6 @@ definitions:
type: object
models.List:
properties:
background_blur_hash:
description: Contains a very small version of the list background to use as
a blurry preview until the actual background is loaded. Check out https://blurha.sh/
to learn how it works.
type: string
background_information:
description: Holds extra information about the background set since some background
providers require attribution or similar. If not null, the background can
@ -1042,9 +1035,6 @@ definitions:
minLength: 1
type: string
right:
default: 0
description: The right this team has. 0 = Read only, 1 = Read & Write, 2 =
Admin. See the docs for more details.
type: integer
updated:
description: A timestamp when this relation was last updated. You cannot change
@ -1070,9 +1060,6 @@ definitions:
description: The full name of the user.
type: string
right:
default: 0
description: The right this user has. 0 = Read only, 1 = Read & Write, 2 =
Admin. See the docs for more details.
type: integer
updated:
description: A timestamp when this task was last updated. You cannot change
@ -1216,15 +1203,6 @@ definitions:
passcode:
type: string
type: object
user.Token:
properties:
created:
type: string
id:
type: integer
token:
type: string
type: object
user.User:
properties:
created:
@ -1310,10 +1288,6 @@ definitions:
description: If enabled, the user will get an email for their overdue tasks
each morning.
type: boolean
overdue_tasks_reminders_time:
description: The time when the daily summary of overdue tasks will be sent
via email.
type: string
timezone:
description: The user's time zone. Used to send task reminders in the time
zone of the user.
@ -1437,8 +1411,7 @@ paths:
name: username
required: true
type: string
- description: The size of the avatar you want to get. If bigger than the max
configured size this will be adjusted to the maximum size.
- description: The size of the avatar you want to get
in: query
name: size
type: integer
@ -2925,10 +2898,7 @@ paths:
in: query
name: filter_by
type: string
- description: The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)-
or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style
relative dates for all date fields like `due_date`, `start_date`, `end_date`,
etc.
- description: The value to filter for.
in: query
name: filter_value
type: string
@ -6099,101 +6069,6 @@ paths:
summary: Change general user settings of the current user.
tags:
- user
/user/settings/token/caldav:
get:
consumes:
- application/json
description: Return the IDs and created dates of all caldav tokens for the current
user.
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/user.Token'
type: array
"400":
description: Something's invalid.
schema:
$ref: '#/definitions/web.HTTPError'
"404":
description: User does not exist.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal server error.
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Returns the caldav tokens for the current user
tags:
- user
put:
consumes:
- application/json
description: Generates a caldav token which can be used for the caldav api.
It is not possible to see the token again after it was generated.
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/user.Token'
"400":
description: Something's invalid.
schema:
$ref: '#/definitions/web.HTTPError'
"404":
description: User does not exist.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal server error.
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Generate a caldav token
tags:
- user
/user/settings/token/caldav/{id}:
get:
consumes:
- application/json
parameters:
- description: Token ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Message'
"400":
description: Something's invalid.
schema:
$ref: '#/definitions/web.HTTPError'
"404":
description: User does not exist.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal server error.
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Delete a caldav token by id
tags:
- user
/user/settings/totp:
get:
consumes:

View File

@ -1,40 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package user
import "code.vikunja.io/api/pkg/db"
func GenerateNewCaldavToken(u *User) (token *Token, err error) {
s := db.NewSession()
defer s.Close()
return generateHashedToken(s, u, TokenCaldavAuth)
}
func GetCaldavTokens(u *User) (tokens []*Token, err error) {
s := db.NewSession()
defer s.Close()
return getTokensForKind(s, u, TokenCaldavAuth)
}
func DeleteCaldavTokenByID(u *User, id int64) error {
s := db.NewSession()
defer s.Close()
return removeTokenByID(s, u, TokenCaldavAuth, id)
}

View File

@ -87,7 +87,7 @@ func notifyUsersScheduledForDeletion() {
// RequestDeletion creates a user deletion confirm token and sends a notification to the user
func RequestDeletion(s *xorm.Session, user *User) (err error) {
token, err := generateToken(s, user, TokenAccountDeletion)
token, err := generateNewToken(s, user, TokenAccountDeletion)
if err != nil {
return err
}

View File

@ -452,30 +452,3 @@ func (err *ErrAccountDisabled) HTTPError() web.HTTPError {
Message: "This account is disabled. Check your emails or ask your administrator.",
}
}
// ErrAccountIsNotLocal represents a "AccountIsNotLocal" kind of error.
type ErrAccountIsNotLocal struct {
UserID int64
}
// IsErrAccountIsNotLocal checks if an error is a ErrAccountIsNotLocal.
func IsErrAccountIsNotLocal(err error) bool {
_, ok := err.(*ErrAccountIsNotLocal)
return ok
}
func (err *ErrAccountIsNotLocal) Error() string {
return "Account is not local"
}
// ErrCodeAccountIsNotLocal holds the unique world-error code of this error
const ErrCodeAccountIsNotLocal = 1021
// HTTPError holds the http error description
func (err *ErrAccountIsNotLocal) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeAccountIsNotLocal,
Message: "This account is managed by a third-party authentication provider.",
}
}

View File

@ -34,19 +34,17 @@ const (
TokenPasswordReset
TokenEmailConfirm
TokenAccountDeletion
TokenCaldavAuth
tokenSize = 64
)
// Token is a token a user can use to do things like verify their email or resetting their password
type Token struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
UserID int64 `xorm:"not null" json:"-"`
Token string `xorm:"varchar(450) not null index" json:"-"`
ClearTextToken string `xorm:"-" json:"token"`
Kind TokenKind `xorm:"not null" json:"-"`
Created time.Time `xorm:"created not null" json:"created"`
ID int64 `xorm:"bigint autoincr not null unique pk"`
UserID int64 `xorm:"not null"`
Token string `xorm:"varchar(450) not null index"`
Kind TokenKind `xorm:"not null"`
Created time.Time `xorm:"created not null"`
}
// TableName returns the real table name for user tokens
@ -54,28 +52,12 @@ func (t *Token) TableName() string {
return "user_tokens"
}
func genToken(u *User, kind TokenKind) *Token {
return &Token{
func generateNewToken(s *xorm.Session, u *User, kind TokenKind) (token *Token, err error) {
token = &Token{
UserID: u.ID,
Kind: kind,
Token: utils.MakeRandomString(tokenSize),
}
}
func generateToken(s *xorm.Session, u *User, kind TokenKind) (token *Token, err error) {
token = genToken(u, kind)
_, err = s.Insert(token)
return
}
func generateHashedToken(s *xorm.Session, u *User, kind TokenKind) (token *Token, err error) {
token = genToken(u, kind)
token.ClearTextToken = token.Token
token.Token, err = HashPassword(token.ClearTextToken)
if err != nil {
return nil, err
}
_, err = s.Insert(token)
return
@ -92,26 +74,12 @@ func getToken(s *xorm.Session, token string, kind TokenKind) (t *Token, err erro
return
}
func getTokensForKind(s *xorm.Session, u *User, kind TokenKind) (tokens []*Token, err error) {
tokens = []*Token{}
err = s.Where("kind = ? AND user_id = ?", kind, u.ID).
Find(&tokens)
return
}
func removeTokens(s *xorm.Session, u *User, kind TokenKind) (err error) {
_, err = s.Where("user_id = ? AND kind = ?", u.ID, kind).
Delete(&Token{})
return
}
func removeTokenByID(s *xorm.Session, u *User, kind TokenKind, id int64) (err error) {
_, err = s.Where("id = ? AND user_id = ? AND kind = ?", id, u.ID, kind).
Delete(&Token{})
return
}
// RegisterTokenCleanupCron registers a cron function to clean up all password reset tokens older than 24 hours
func RegisterTokenCleanupCron() {
const logPrefix = "[User Token Cleanup Cron] "

View File

@ -63,7 +63,7 @@ func UpdateEmail(s *xorm.Session, update *EmailUpdate) (err error) {
}
update.User.Status = StatusEmailConfirmationRequired
token, err := generateToken(s, update.User, TokenEmailConfirm)
token, err := generateNewToken(s, update.User, TokenEmailConfirm)
if err != nil {
return
}

View File

@ -94,7 +94,6 @@ type User struct {
DiscoverableByName bool `xorm:"bool default false index" json:"-"`
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"`
OverdueTasksRemindersTime string `xorm:"varchar(5) not null default '09:00'" json:"-"`
DefaultListID int64 `xorm:"bigint null index" json:"-"`
WeekStart int `xorm:"null" json:"-"`
Language string `xorm:"varchar(50) null" json:"-"`
@ -200,7 +199,7 @@ func (apiUser *APIUserPassword) APIFormat() *User {
// GetUserByID gets informations about a user by its ID
func GetUserByID(s *xorm.Session, id int64) (user *User, err error) {
// Apparently xorm does otherwise look for all users but return only one, which leads to returning one even if the ID is 0
// Apparently xorm does otherwise look for all users but return only one, which leads to returing one even if the ID is 0
if id < 1 {
return &User{}, ErrUserDoesNotExist{}
}
@ -280,10 +279,6 @@ func getUser(s *xorm.Session, user *User, withEmail bool) (userOut *User, err er
userOut.Email = ""
}
if userOut.OverdueTasksRemindersTime == "" {
userOut.OverdueTasksRemindersTime = "9:00"
}
return userOut, err
}
@ -318,10 +313,6 @@ func CheckUserCredentials(s *xorm.Session, u *Login) (*User, error) {
return nil, ErrWrongUsernameOrPassword{}
}
if user.Issuer != IssuerLocal {
return user, &ErrAccountIsNotLocal{UserID: user.ID}
}
// The user is invalid if they need to verify their email address
if user.Status == StatusEmailConfirmationRequired {
return &User{}, ErrEmailNotConfirmed{UserID: user.ID}
@ -333,7 +324,7 @@ func CheckUserCredentials(s *xorm.Session, u *Login) (*User, error) {
if IsErrWrongUsernameOrPassword(err) {
handleFailedPassword(user)
}
return user, err
return nil, err
}
return user, nil
@ -502,7 +493,6 @@ func UpdateUser(s *xorm.Session, user *User) (updatedUser *User, err error) {
"week_start",
"language",
"timezone",
"overdue_tasks_reminders_time",
).
Update(user)
if err != nil {

View File

@ -81,7 +81,7 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {
}
user.Status = StatusEmailConfirmationRequired
token, err := generateToken(s, user, TokenEmailConfirm)
token, err := generateNewToken(s, user, TokenEmailConfirm)
if err != nil {
return nil, err
}

View File

@ -112,7 +112,7 @@ func RequestUserPasswordResetTokenByEmail(s *xorm.Session, tr *PasswordTokenRequ
// RequestUserPasswordResetToken sends a user a password reset email.
func RequestUserPasswordResetToken(s *xorm.Session, user *User) (err error) {
token, err := generateToken(s, user, TokenPasswordReset)
token, err := generateNewToken(s, user, TokenPasswordReset)
if err != nil {
return
}

View File

@ -20,9 +20,7 @@ import (
"testing"
"code.vikunja.io/api/pkg/db"
"github.com/stretchr/testify/assert"
"xorm.io/builder"
)
func TestCreateUser(t *testing.T) {
@ -364,7 +362,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user1", nil)
all, err := ListUsers(s, "user1")
assert.NoError(t, err)
assert.True(t, len(all) > 0)
assert.Equal(t, all[0].Username, "user1")
@ -383,7 +381,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "", nil)
all, err := ListUsers(s, "")
assert.NoError(t, err)
assert.Len(t, all, 0)
})
@ -392,12 +390,11 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user1@example.com", nil)
all, err := ListUsers(s, "user1@example.com")
assert.NoError(t, err)
assert.Len(t, all, 0)
db.AssertExists(t, "users", map[string]interface{}{
"email": "user1@example.com",
"discoverable_by_email": false,
"email": "user1@example.com",
}, false)
})
t.Run("not discoverable by name", func(t *testing.T) {
@ -405,12 +402,11 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "one else", nil)
all, err := ListUsers(s, "one else")
assert.NoError(t, err)
assert.Len(t, all, 0)
db.AssertExists(t, "users", map[string]interface{}{
"name": "Some one else",
"discoverable_by_name": false,
"name": "Some one else",
}, false)
})
t.Run("discoverable by email", func(t *testing.T) {
@ -418,67 +414,20 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user7@example.com", nil)
all, err := ListUsers(s, "user7@example.com")
assert.NoError(t, err)
assert.Len(t, all, 1)
assert.Equal(t, int64(7), all[0].ID)
db.AssertExists(t, "users", map[string]interface{}{
"email": "user7@example.com",
"discoverable_by_email": true,
}, false)
})
t.Run("discoverable by partial name", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "with space", nil)
all, err := ListUsers(s, "with space")
assert.NoError(t, err)
assert.Len(t, all, 1)
assert.Equal(t, int64(12), all[0].ID)
db.AssertExists(t, "users", map[string]interface{}{
"name": "Name with spaces",
"discoverable_by_name": true,
}, false)
})
t.Run("discoverable by email with extra condition", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user7@example.com", &ListUserOpts{AdditionalCond: builder.In("id", 7)})
assert.NoError(t, err)
assert.Len(t, all, 1)
assert.Equal(t, int64(7), all[0].ID)
db.AssertExists(t, "users", map[string]interface{}{
"email": "user7@example.com",
"discoverable_by_email": true,
}, false)
})
t.Run("discoverable by exact username", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user7", nil)
assert.NoError(t, err)
assert.Len(t, all, 1)
assert.Equal(t, int64(7), all[0].ID)
db.AssertExists(t, "users", map[string]interface{}{
"username": "user7",
}, false)
})
t.Run("not discoverable by partial username", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user", nil)
assert.NoError(t, err)
assert.Len(t, all, 0)
db.AssertExists(t, "users", map[string]interface{}{
"username": "user7",
}, false)
})
}

View File

@ -23,58 +23,29 @@ import (
"xorm.io/xorm"
)
type ListUserOpts struct {
AdditionalCond builder.Cond
ReturnAllIfNoSearchProvided bool
}
// ListUsers returns a list with all users, filtered by an optional search string
func ListUsers(s *xorm.Session, search string, opts *ListUserOpts) (users []*User, err error) {
if opts == nil {
opts = &ListUserOpts{}
}
// ListUsers returns a list with all users, filtered by an optional searchstring
func ListUsers(s *xorm.Session, search string) (users []*User, err error) {
// Prevent searching for placeholders
search = strings.ReplaceAll(search, "%", "")
if (search == "" || strings.ReplaceAll(search, " ", "") == "") && !opts.ReturnAllIfNoSearchProvided {
if search == "" || strings.ReplaceAll(search, " ", "") == "" {
return
}
conds := []builder.Cond{}
if search != "" {
for _, queryPart := range strings.Split(search, ",") {
conds = append(conds,
builder.Eq{"username": queryPart},
builder.And(
builder.Eq{"email": queryPart},
builder.Eq{"discoverable_by_email": true},
),
builder.And(
builder.Like{"name", "%" + queryPart + "%"},
builder.Eq{"discoverable_by_name": true},
),
)
}
}
cond := builder.Or(conds...)
if opts.AdditionalCond != nil {
cond = builder.And(
cond,
opts.AdditionalCond,
)
}
err = s.
Where(cond).
Where(builder.Or(
builder.Like{"username", "%" + search + "%"},
builder.And(
builder.Eq{"email": search},
builder.Eq{"discoverable_by_email": true},
),
builder.And(
builder.Like{"name", "%" + search + "%"},
builder.Eq{"discoverable_by_name": true},
),
)).
Find(&users)
for _, u := range users {
u.Email = ""
}
return
}

View File

@ -30,12 +30,3 @@ func GetTimeWithoutNanoSeconds(t time.Time) time.Time {
// so we make sure the time we use to get the reminders don't contain nanoseconds.
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location()).In(tz)
}
// GetTimeWithoutSeconds returns a time.Time with the seconds set to 0.
func GetTimeWithoutSeconds(t time.Time) time.Time {
tz := config.GetTimeZone()
// By default, time.Now() includes nanoseconds which we don't save. That results in getting the wrong dates,
// so we make sure the time we use to get the reminders don't contain nanoseconds.
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location()).In(tz)
}

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