Compare commits

...

159 Commits

Author SHA1 Message Date
e42278f93f fix(deps): update module github.com/labstack/echo/v4 to v4.11.0 2023-07-14 21:01:48 +00:00
4c04a7bd5f fix(deps): update module github.com/iancoleman/strcase to v0.3.0 2023-07-13 01:02:01 +00:00
d913edbcb6 fix(deps): update src.techknowlogick.com/xgo digest to 617d3b6 2023-07-11 19:02:02 +00:00
adbc1d2997 fix(deps): update module github.com/wneessen/go-mail to v0.4.0 2023-07-08 12:02:02 +00:00
e5cd4897d6
chore: release preparation 2023-07-07 13:34:22 +02:00
86f25f253b
fix(project): duplicate project into parent project 2023-07-07 12:56:15 +02:00
9f3787bf20 fix(deps): update module golang.org/x/oauth2 to v0.10.0 2023-07-05 22:02:03 +00:00
142dacecc0
chore(docs): update list -> project 2023-07-05 17:50:35 +02:00
9b016c6b07 fix(deps): update module golang.org/x/crypto to v0.11.0 2023-07-05 14:01:58 +00:00
d5e8c586b2 fix(deps): update module golang.org/x/term to v0.10.0 2023-07-05 13:44:28 +00:00
d39af7ac57 fix(deps): update module golang.org/x/image to v0.9.0 2023-07-05 13:01:56 +00:00
e3dc8ac65a fix(deps): update module golang.org/x/sys to v0.10.0 2023-07-04 16:01:50 +00:00
b5194624e0
fix: don't try to load subscriptions for nonexistent projects 2023-07-03 18:18:13 +02:00
32689531ec
chore(docs): move login and register routes to auth category in api docs 2023-07-03 18:10:01 +02:00
117c569721
fix(projects): return subprojects which were shared from another user 2023-07-03 11:45:29 +02:00
24c2fad77f fix(deps): update github.com/dustinkirkland/golang-petname digest to e794b93 2023-06-26 23:01:59 +00:00
ef779e8730
fix: lint 2023-06-26 19:10:32 +02:00
895263f054
fix(filters): sorting tasks from filters
Resolves https://community.vikunja.io/t/filter-table-view-not-sorting/1416
2023-06-21 14:59:40 +02:00
1dc9c50d64 fix(deps): update module github.com/imdario/mergo to v1 (#1559)
Reviewed-on: vikunja/api#1559
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-06-20 10:02:21 +00:00
6fa760545c fix(deps): update github.com/gocarina/gocsv digest to 99d496c 2023-06-16 13:02:28 +00:00
104461c40b fix(deps): update module github.com/getsentry/sentry-go to v0.22.0 2023-06-16 11:02:27 +00:00
0a97a7d862 fix(deps): update module github.com/prometheus/client_golang to v1.16.0 2023-06-15 11:02:26 +00:00
78cd711002 fix(deps): update github.com/gocarina/gocsv digest to 2696de6 2023-06-14 21:21:20 +00:00
e938c3d075 fix(deps): update module golang.org/x/sync to v0.3.0 2023-06-14 16:02:28 +00:00
b68315240a fix(deps): update module golang.org/x/oauth2 to v0.9.0 2023-06-13 15:02:24 +00:00
3aa493d64f fix(deps): update module golang.org/x/crypto to v0.10.0 2023-06-13 12:02:21 +00:00
78866fad45 fix(deps): update module golang.org/x/image to v0.8.0 2023-06-13 07:02:21 +00:00
d27474d525 fix(deps): update module golang.org/x/term to v0.9.0 2023-06-12 19:02:35 +00:00
05998f0cc9 fix(deps): update module golang.org/x/sys to v0.9.0 2023-06-12 18:02:31 +00:00
04e2c51fac
feat: allow saving frontend settings via api 2023-06-11 17:49:14 +02:00
cernst
4a4ba041e0 chore: remove reminderDates after frontend is migrated to reminders (#1448)
Co-authored-by: ce72 <christoph.ernst72@googlemail.com>
Reviewed-on: vikunja/api#1448
Reviewed-by: konrad <k@knt.li>
Co-authored-by: cernst <ce72@noreply.kolaente.de>
Co-committed-by: cernst <ce72@noreply.kolaente.de>
2023-06-10 17:21:36 +00:00
d83e3a0a03
chore: remove cache options
Cache was not working correctly, added more complexity and actually made response times slower. Because of this, I'm removing all cache options until we figure out a better solution.

Resolves vikunja/api#1496
Resolves vikunja/api#907
2023-06-08 17:05:36 +02:00
72e0e22152
feat(kanban): return the total task count per bucket 2023-06-08 16:56:05 +02:00
ef94e0cf86
feat(projects): don't allow deleting or archiving the default project 2023-06-07 21:29:46 +02:00
ad0690369f
fix: lint 2023-06-07 21:00:12 +02:00
ebfb3f9aaa
fix(filter): don't try to get the real subscription for a saved filter project 2023-06-07 20:41:59 +02:00
4ed2d305f0
fix(task): don't build partial task identifier 2023-06-07 20:33:18 +02:00
7b7a914560
fix(test): use correct filter id 2023-06-07 20:30:28 +02:00
43ef5f98d8
fix(projects): don't fail to fetch a task if there's a broken subscription record associated to it 2023-06-07 20:30:14 +02:00
e66344c21e
fix(task): don't try to return a project identifier if there is no project 2023-06-07 20:29:35 +02:00
7755b9cd49
fix(projects): delete project in the correct order 2023-06-07 20:28:36 +02:00
67825425a4
fix(filters): return all filters with all projects, not grouped under a pseudo project 2023-06-07 18:55:36 +02:00
69bd023b62
fix(tasks): return a correct task identifier if the list does not have a good one set 2023-06-07 18:17:08 +02:00
1a840c8b87
fix(tasks): make sure task deleted notification actually has information about the deleted task 2023-06-07 18:14:20 +02:00
456495ec30 chore(deps): update goreleaser/nfpm docker tag to v2.30.1 (#1540)
Reviewed-on: vikunja/api#1540
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-06-07 15:05:05 +00:00
b461ce1443 fix(deps): update src.techknowlogick.com/xgo digest to 494bc06 2023-06-06 19:02:42 +00:00
b03213c19e chore(deps): update klakegg/hugo docker tag to v0.111.3 (#1542)
Reviewed-on: vikunja/api#1542
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-06-06 18:17:47 +00:00
e3842b6df7
fix(projects): reset pagination limit when fetching subprojects 2023-06-06 17:24:27 +02:00
a86518da71 chore(deps): update klakegg/hugo docker tag to v0.111.0 (#1539)
Reviewed-on: vikunja/api#1539
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-06-05 18:20:32 +00:00
e17b63b920
fix: lint 2023-06-05 19:14:26 +02:00
076e857507
chore: disable false-positive linter for generated docs 2023-06-05 18:56:27 +02:00
d758bdc5e2
fix(projects): don't try to share for nonexisting namespace 2023-06-05 18:12:12 +02:00
50b0d3f95c
chore(deps): update golangci 2023-06-05 18:07:12 +02:00
96620ce946 chore(deps): update klakegg/hugo docker tag to v0.110.0 (#1538)
Reviewed-on: vikunja/api#1538
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-06-05 14:23:32 +00:00
374cc02399 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.17 2023-06-01 00:01:57 +00:00
d68338b649
fix(docs): clarify error codes in swagger docs
Resolves vikunja/api#1518
2023-05-30 21:37:40 +02:00
8c3ef34f75 fix(deps): update github.com/vectordotdev/go-datemath digest to 640a500 (#1532)
Reviewed-on: vikunja/api#1532
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-05-30 18:11:31 +00:00
b56e45d743
fix: use rewrite when hosting frontend files via the api
Resolves vikunja/api#1531
2023-05-30 18:56:37 +02:00
adc9998b19 fix(deps): update module github.com/spf13/viper to v1.16.0 2023-05-30 11:02:13 +00:00
4ac4dbdaa9 fix(deps): update module github.com/stretchr/testify to v1.8.4 2023-05-30 10:01:56 +00:00
5f06117167 fix(deps): update module github.com/imdario/mergo to v0.3.16 2023-05-30 07:51:43 +00:00
a82192080b fix(deps): update module github.com/redis/go-redis/v9 to v9.0.5 2023-05-29 07:01:41 +00:00
82beb3bf67 feat: remove namespaces, make projects infinitely nestable (#1362)
Reviewed-on: vikunja/api#1362
2023-05-24 14:14:02 +00:00
e5dde315fb
chore: go mod tidy 2023-05-24 15:53:29 +02:00
a51bbd1159
fix: don't get favorite task projects filter multiple times 2023-05-24 15:52:33 +02:00
4b00f224d9
fix: reminder fixture 2023-05-24 15:52:33 +02:00
db3c7aa8b0
fix: make sure projects are correctly sorted 2023-05-24 15:52:33 +02:00
353279cbff
fix: fetch all tasks for all projects 2023-05-24 15:52:33 +02:00
3b0935d033
docs: remove all traces of namespaces 2023-05-24 15:52:33 +02:00
9011894a29
feat: check for cycles when creating or updating a project's parent 2023-05-24 15:52:33 +02:00
edcb806421
feat: remove ChildProjects project property 2023-05-24 15:52:33 +02:00
f2d943f5c4
fix: add missing error code 2023-05-24 15:52:33 +02:00
35964ce4a6
fix(projects): recalculate project's position after dragging when position would be 0 2023-05-24 15:52:33 +02:00
a8b76772ff
fix(migration): revert wrongly changed url 2023-05-24 15:52:33 +02:00
53b2ade5bb
feat(projects): return a favorites pseudo project when the user has favorite tasks 2023-05-24 15:52:32 +02:00
b482664d82
fix(projects): don't allow making a project child of itself 2023-05-24 15:52:32 +02:00
aafcb0bac4
fix(projects): don't return child projects twice 2023-05-24 15:52:32 +02:00
0110f93313
feat: make the new inbox project the default 2023-05-24 15:52:32 +02:00
9111db2a16
fix: lint config 2023-05-24 15:52:32 +02:00
f1cbe50605
fix: rename project receiver variable 2023-05-24 15:52:32 +02:00
746ac1098f
fix(test): adjust fixture bucket and list ids 2023-05-24 15:52:32 +02:00
d7396fac57
fix(test): adjust fixture id 2023-05-24 15:52:32 +02:00
3b00a5c200
fix(test): fixtures 2023-05-24 15:52:32 +02:00
a21bff3ffb
fix: compile errors 2023-05-24 15:52:32 +02:00
93056da792
chore: go mod tidy 2023-05-24 15:52:30 +02:00
ebc3dd2b3e
fix: lint errors 2023-05-24 15:51:57 +02:00
017f771783
chore(test): show table content when db assertion failed 2023-05-24 15:51:56 +02:00
abe5f72493
fix(migration): enable insert from structure work recursively 2023-05-24 15:51:56 +02:00
4b55e2ce03
fix(migration): make file migration work with new structure 2023-05-24 15:51:56 +02:00
2f81791735
chore(export): remove unused events 2023-05-24 15:51:56 +02:00
8235c63f60
fix(tests): task collection fixtures 2023-05-24 15:51:56 +02:00
03b7fa6dd3
fix(tests): subscription test fixtures 2023-05-24 15:51:56 +02:00
5e6bff20f8
fix(tests): task permissions from parents 2023-05-24 15:51:56 +02:00
8e56fe558a
fix(tests): adjust parent projects 2023-05-24 15:51:56 +02:00
154ac61d7c
fix(projects): properly check if a user or link share is allowed to create a new project 2023-05-24 15:51:56 +02:00
03eb4ecd07
fix(tests): permission tests for parent projects 2023-05-24 15:51:56 +02:00
d4e644e91e
chore(project): fmt 2023-05-24 15:51:56 +02:00
48beb5f382
fix(project): don't allow un-archiving a project when its parent project is archived 2023-05-24 15:51:56 +02:00
0cd633981a
fix(project): recursively get all users from all parent projects 2023-05-24 15:51:56 +02:00
19f69419f7
fix(tasks): task relation test 2023-05-24 15:51:55 +02:00
6bb42ced9d
fix(tasks): read all tests 2023-05-24 15:51:55 +02:00
3b837a472b
fix(migration): remove wunderlist leftovers 2023-05-24 15:51:55 +02:00
537ba60f2d
fix(tasks): get all tasks from parent projects 2023-05-24 15:51:55 +02:00
ceaa9c0e03
feat(subscriptions): make sure all subscriptions are inherited properly 2023-05-24 15:51:55 +02:00
afe756e4c1
fix(tests): make the tests compile again 2023-05-24 15:51:55 +02:00
f4fc431b6f
fix(projects): permission check now works 2023-05-24 15:51:55 +02:00
438f5c8e12
fix(projects): don't check if new projects are archived 2023-05-24 15:51:55 +02:00
3e8d1b3667
chore(projects) use a slice again 2023-05-24 15:51:55 +02:00
01723148e8
chore(docs): update error docs 2023-05-24 15:51:55 +02:00
18b9ff8512
feat(projects): check parent project when checking archived status 2023-05-24 15:51:55 +02:00
5e2567645a
feat(projects): check all parent projects for permissions 2023-05-24 15:51:55 +02:00
d799915e78
feat(projects): get all projects recursively 2023-05-24 15:51:55 +02:00
4c698dc7c7
fix: typo 2023-05-24 15:51:55 +02:00
e93a5ff11f
fix: rename after rebase 2023-05-24 15:51:54 +02:00
d79c393e5b
fix: make it compile again 2023-05-24 15:51:54 +02:00
5d02d93d31
fix(tasks): don't check for namespaces in filters 2023-05-24 15:51:54 +02:00
7c448c88a8
fix(project): don't check for namespaces in overdue reminders 2023-05-24 15:51:54 +02:00
906574adc9
fix(project): remove comments, clarifications, notifications about namespaces 2023-05-24 15:51:54 +02:00
d794a2c5ca
fix(project): remove namespaces checks 2023-05-24 15:51:54 +02:00
8cff813e9f
fix(project): remove namespaces from creating projects 2023-05-24 15:51:54 +02:00
2dcd6451a4
fix(project): remove namespaces from getting projects 2023-05-24 15:51:54 +02:00
ac0d84a7d8
feat(migration): ignore namespace changes 2023-05-24 15:51:54 +02:00
386e218b95
feat(migration): use new structure for migration 2023-05-24 15:51:54 +02:00
fef253312c
feat(projects): cleanup namespace leftovers 2023-05-24 15:51:54 +02:00
16de7cd591
feat(projects): remove namespaces 2023-05-24 15:51:54 +02:00
0795828a9f
feat(projects): add parent project, migrate namespaces 2023-05-24 15:51:54 +02:00
47c2da7f18
feat: rename lists to projects 2023-05-24 15:51:53 +02:00
fc73c84bf2
feat: rename lists to projects 2023-05-24 15:51:53 +02:00
87ed68e4c8
fix(migration): remove wunderlist leftovers 2023-05-24 15:51:53 +02:00
50c922b7d1
fix: lint 2023-05-24 15:51:53 +02:00
96a0f5e169
feat: rename lists to projects 2023-05-24 15:51:53 +02:00
73244e7d85 fix(deps): update module github.com/ulule/limiter/v3 to v3.11.2 2023-05-24 10:21:37 +00:00
f2d5220625 chore(deps): update goreleaser/nfpm docker tag to v2.29.0 (#1528)
Reviewed-on: vikunja/api#1528
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-05-24 10:04:54 +00:00
359d0512cc
fix: upgrade jwt v5 2023-05-23 16:37:16 +02:00
9e5c95fd6d fix(docs): Added Keycloak OpenID example (#1521)
Reviewed-on: vikunja/api#1521
Reviewed-by: konrad <k@knt.li>
Co-authored-by: jontyms <jontyms@protonmail.com>
Co-committed-by: jontyms <jontyms@protonmail.com>
2023-05-23 11:14:35 +00:00
20269de2d4 fix(deps): update module github.com/labstack/echo-jwt/v4 to v4.2.0 2023-05-23 11:13:41 +00:00
e38e6698c5 fix(deps): update module github.com/stretchr/testify to v1.8.3 2023-05-23 11:13:23 +00:00
18ad91e3e2 fix(deps): update module github.com/coreos/go-oidc/v3 to v3.6.0 2023-05-16 19:01:57 +00:00
9cff96204b fix(deps): update github.com/gocarina/gocsv digest to 9ddd7fd 2023-05-13 23:02:05 +00:00
513ccc08e3 fix(deps): update module github.com/magefile/mage to v1.15.0 2023-05-11 16:01:42 +00:00
7d9e8bd150
fix(cli): rename user project command 2023-05-10 22:37:37 +02:00
cade124799 fix(deps): update github.com/gocarina/gocsv digest to 7f30c79 2023-05-10 10:02:06 +00:00
9e5cfb6fd6 chore(deps): update alpine docker tag to v3.18 2023-05-10 00:01:24 +00:00
340c888dc1 fix(deps): update module golang.org/x/crypto to v0.9.0 2023-05-09 08:01:40 +00:00
abd67dc14a fix(deps): update module golang.org/x/oauth2 to v0.8.0 2023-05-09 04:01:56 +00:00
949e52e58c fix(deps): update module github.com/getsentry/sentry-go to v0.21.0 2023-05-08 15:01:49 +00:00
7d8c42ab98
fix(migration): remove unused is_deleted flag from Todoist api response
Related discussion: https://community.vikunja.io/t/importing-tasks-from-todoist/322
2023-05-08 15:32:27 +02:00
9c66b473dd fix(deps): update module golang.org/x/sync to v0.2.0 2023-05-04 21:01:44 +00:00
0000f83592
Revert "fix(deps): update module github.com/swaggo/swag to v1.16.1"
This reverts commit eedc84b5a0.
2023-05-04 22:31:55 +02:00
eedc84b5a0 fix(deps): update module github.com/swaggo/swag to v1.16.1 2023-05-04 20:31:12 +00:00
0c4464ae1e fix(deps): update src.techknowlogick.com/xgo digest to 52d704d 2023-05-04 20:31:04 +00:00
7d78fddeeb fix(deps): update module golang.org/x/term to v0.8.0 2023-05-04 17:01:46 +00:00
f3fb369e6b fix(deps): update module github.com/prometheus/client_golang to v1.15.1 2023-05-03 10:01:42 +00:00
c74bf0d33e fix(deps): update module github.com/go-testfixtures/testfixtures/v3 to v3.9.0 2023-05-02 07:18:27 +00:00
414d827533 fix(deps): update module github.com/redis/go-redis/v9 to v9.0.4 2023-05-02 06:01:37 +00:00
a3dd9db54d fix(deps): update github.com/arran4/golang-ical digest to f69e132 2023-04-26 06:13:24 +00:00
f50b0b342a fix(deps): update src.techknowlogick.com/xgo digest to e65295a 2023-04-26 06:12:18 +00:00
0c3d2d4bf1 fix(deps): update module github.com/lib/pq to v1.10.9 2023-04-26 05:01:44 +00:00
bd046d98ba fix(deps): update module github.com/go-sql-driver/mysql to v1.7.1 2023-04-25 11:01:45 +00:00
134 changed files with 3170 additions and 6033 deletions

View File

@ -152,7 +152,7 @@ steps:
commands:
- export "GOROOT=$(go env GOROOT)"
- apk --no-cache add build-base git
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.52.1
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.2
- ./mage-static check:golangci
when:
event: [ push, tag, pull_request ]
@ -518,7 +518,7 @@ steps:
# Build os packages and push it to our bucket
- name: build-os-packages-unstable
image: goreleaser/nfpm:v2.28.0
image: goreleaser/nfpm:v2.30.1
pull: always
commands:
- apk add git go
@ -534,7 +534,7 @@ steps:
depends_on: [ after-build-compress ]
- name: build-os-packages-version
image: goreleaser/nfpm:v2.28.0
image: goreleaser/nfpm:v2.30.1
pull: always
commands:
- apk add git go
@ -619,7 +619,7 @@ steps:
- tar -xzf vikunja-theme.tar.gz
- name: build
image: klakegg/hugo:0.107.0
image: klakegg/hugo:0.111.3
pull: always
commands:
- cd docs
@ -743,6 +743,6 @@ steps:
- failure
---
kind: signature
hmac: 7bade2fc44072cf5730f7f15b5be18058ce47d216853c8c39c692f967c640e20
hmac: d47bd1cf6f3e9be2ff3eed2039e65c8b6de2b16c1e636699f66382f941277411
...

View File

@ -80,6 +80,7 @@ issues:
linters:
- goheader
- misspell
- gosmopolitan
- text: "Missed string"
linters:
- goheader
@ -89,6 +90,10 @@ issues:
- path: pkg/models/favorites\.go
linters:
- nilerr
- path: pkg/models/project\.go
text: "string `parent_project_id` has 3 occurrences, make it a constant"
- path: pkg/models/events\.go
linters:
- musttag
- path: pkg/models/task_collection.go
text: 'append result not assigned to the same slice'

View File

@ -7,6 +7,275 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
All releases can be found on https://code.vikunja.io/api/releases.
## [0.21.0] - 2023-07-07
### Bug Fixes
* *(CalDAV)* Naming
* *(api)* License (#1457)
* *(build)* Make sure the docker image can access go tools
* *(caldav)* Do not create label if it exists by title (#1444)
* *(caldav)* Incoming tasks do not get correct time zone (#1455)
* *(ci)* Pipeline dependency
* *(cli)* Rename user project command
* *(docker)* Don't chown everything in Vikunja's default root folder
* *(docs)* Added Keycloak OpenID example (#1521)
* *(docs)* Clarify error codes in swagger docs
* *(docs)* Link to usage/api
* *(docs)* Semver link (#1470)
* *(filter)* Don't try to get the real subscription for a saved filter project
* *(filters)* Return all filters with all projects, not grouped under a pseudo project
* *(filters)* Sorting tasks from filters
* *(image)* Json type of struct property (#1469)
* *(import)* Don't try to load a nonexistant attachment file
* *(lint)* Disable misspell linter on redoc
* *(migration)* Don't try to fetch task details of tasks whose projects are deleted
* *(migration)* Enable insert from structure work recursively
* *(migration)* Make file migration work with new structure
* *(migration)* Remove unused is_deleted flag from Todoist api response
* *(migration)* Remove wunderlist leftovers
* *(migration)* Remove wunderlist leftovers
* *(migration)* Remove wunderlist leftovers
* *(migration)* Rename TickTick migration
* *(migration)* Revert wrongly changed url
* *(migration)* Use correct struct
* *(project)* Don't allow un-archiving a project when its parent project is archived
* *(project)* Don't check for namespaces in overdue reminders
* *(project)* Duplicate project into parent project
* *(project)* Recursively get all users from all parent projects
* *(project)* Remove comments, clarifications, notifications about namespaces
* *(project)* Remove namespaces checks
* *(project)* Remove namespaces from creating projects
* *(project)* Remove namespaces from getting projects
* *(projects)* Delete project in the correct order
* *(projects)* Don't allow making a project child of itself
* *(projects)* Don't check if new projects are archived
* *(projects)* Don't fail to fetch a task if there's a broken subscription record associated to it
* *(projects)* Don't return child projects twice
* *(projects)* Don't try to share for nonexisting namespace
* *(projects)* Permission check now works
* *(projects)* Properly check if a user or link share is allowed to create a new project
* *(projects)* Recalculate project's position after dragging when position would be 0
* *(projects)* Reset pagination limit when fetching subprojects
* *(projects)* Return subprojects which were shared from another user
* *(saved filters)* Don't let query parameters override saved sorting parameters
* *(spelling)* In config sample (#1489)
* *(task)* Don't build partial task identifier
* *(task)* Don't try to return a project identifier if there is no project
* *(tasks)* Don't check for namespaces in filters
* *(tasks)* Get all tasks from parent projects
* *(tasks)* Make sure task deleted notification actually has information about the deleted task
* *(tasks)* Read all tests
* *(tasks)* Return a correct task identifier if the list does not have a good one set
* *(tasks)* Sql for overdue reminders
* *(tasks)* Task relation test
* *(test)* Adjust fixture bucket and list ids
* *(test)* Adjust fixture id
* *(test)* Fixtures
* *(test)* Use correct filter id
* *(tests)* Adjust parent projects
* *(tests)* Make the tests compile again
* *(tests)* Permission tests for parent projects
* *(tests)* Subscription test fixtures
* *(tests)* Task collection fixtures
* *(tests)* Task permissions from parents
* Accept for migrations ([8edbca3](8edbca39cf9d771645d6feb05ee94eebc6403cbf))
* Add missing error code ([f2d943f](f2d943f5c4f1b13ef565692b893da05c6669c6d0))
* Add missing license header ([f4e12da](f4e12dab273474c0eb27f59c00faa828bb86522c))
* Align "ID" param for Delete and Update method of Task model ([b6d5605](b6d5605ef6b2799f939d016b1572b3d43e857d4d))
* Align "otherTaskID" param for Delete method of TaskRelation model ([ac377a7](ac377a7a5d708ef7543d99f716ceaa1ee8502649))
* Align namespaceID param ([7ada82e](7ada82ea926556ae39d106dc85d5a05f3c1c8cd3))
* Align task ID param ([f76bb2b](f76bb2b4a9c8a3b53bc73d0913ba94bba350f5da))
* Check if usernames contain spaces when creating a new user ([672fb35](672fb35bcbb47e4c0331813aa837fee28f372471))
* Compile errors ([a21bff3](a21bff3ffb8497d6e1b6c3bb50d9a9b2469f4eb0))
* Correctly pass unix socket to xorm ([7ad256f](7ad256f6cd3e15aeafce2bc29c28c458c3abdc0a))
* Docs auth openID method ([4f7d69a](4f7d69a108a2836e90b3c7ffe7f05247d80bfb85))
* Don't get favorite task projects filter multiple times ([a51bbd1](a51bbd1159fb1ada5980a5b27972ccf1404641af))
* Don't send bad request errors to sentry ([c0c523f](c0c523f0a8c83eb164febbc508ac98142d572d7a))
* Don't try to load subscriptions for nonexistent projects ([b519462](b5194624e021360ccdec20cb58bba57c23028c3f))
* Fetch all tasks for all projects ([353279c](353279cbff8fd6fa6b1bb81a8726a7a5a1b6b623))
* ILIKE helper ([dff4e01](dff4e01327907d42bf0b20a20912e5e9c69dd23e))
* Lint ([50c922b](50c922b7d1135b8f75478b89502fe0bb4c39547f))
* Lint ([ad06903](ad0690369f39dab3683ac5ef7664bd765fa1cb18))
* Lint ([e17b63b](e17b63b9201889946e91e7e295f31a80055c6ae4))
* Lint ([ef779e8](ef779e8730af169101bf1ebffb8d2522e5c6b7bc))
* Lint ([f0dcce7](f0dcce702f03f237ecde107a7ba62f61e2c3e313))
* Lint config ([9111db2](9111db2a16df6a4eec9e3cc2021bc6fdcace9ead))
* Lint errors ([ebc3dd2](ebc3dd2b3e72f56880320480829aead1bf554f67))
* Make it compile again ([d79c393](d79c393e5b4e880b8b09ce5944e8247ae07c4d58))
* Make sure Vikunja is buildable without swagger docs present ([47e4223](47e42238ef47ad6e4e90284593aae278e77c8631))
* Make sure projects are correctly sorted ([db3c7aa](db3c7aa8b04e828fafdf10bcfd5bde8cf19e6f10))
* Provide a proper error message when viewing a link share with an invalid token ([aa43127](aa43127e52aeb7412b13b4aaab091442dad534db))
* Reminder fixture ([4b00f22](4b00f224d92f0c6933f6cba14433538d64545eca))
* Remove old saved openid provider settings from cache when starting Vikunja ([9bf535d](9bf535d06f5b9bb455979b0bf3b6f0942daa1c9e))
* Rename after rebase ([e93a5ff](e93a5ff11fee7adac2897b3251db7abbbad4bcc5))
* Rename incorrectly named ProjectUsers method ([7e53a21](7e53a214070ee9b48fdffffcc42de9250c323e96))
* Rename project receiver variable ([f1cbe50](f1cbe50605b46e506c3233cc8da4b325f5727c87))
* Spelling ([fc2cc4a](fc2cc4a1555ca7e63ff902cde62380035a60ebb8))
* Test fixtures ([06f1d2e](06f1d2e91237195f8e720d4dd55b491b91e6547d))
* Test import ([fb818ea](fb818ea1867f8db813ff52622695fd206c21452e))
* Trello import tests ([61a3380](61a3380a9482312eac56f4cfd436517205f601aa))
* Typo ([4c698dc](4c698dc7c71418239e24b1756604371dcb6a2f74))
* Typo in email template ([2dad404](2dad4042170677af3db7be85cbe978ce6be721aa))
* Update redoc ([8916de0](8916de03666482c2319689e950d30a6fb737f239))
* Update xgo in dockerfile to 1.20.2 ([33f0d0f](33f0d0f85a7fdfd509bc8a4aad26df95c064468c))
* Upgrade jwt v5 ([359d051](359d0512cc7e73cdde9d4dd145332591c6743d11))
* Use rewrite when hosting frontend files via the api ([b56e45d](b56e45d74389d38c747887d3cb2a2b295bb549c7))
* Users_lists name in migration ([0a3fdc0](0a3fdc0344790f059140d8e482b028ffecdb3e4b))
* Using mysql via a socket ([0a6bbc2](0a6bbc2efd6bb4468c72cff2a70cd29350a50b75))
### Dependencies
* *(deps)* Update module github.com/imdario/mergo to v0.3.14
* *(deps)* Update github.com/arran4/golang-ical digest to 19abf92
* *(deps)* Update goreleaser/nfpm docker tag to v2.27.1 (#1438)
* *(deps)* Update module github.com/swaggo/swag to v1.8.11
* *(deps)* Update module github.com/imdario/mergo to v0.3.15 (#1443)
* *(deps)* Update golangci-lint to 1.52.1
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.9
* *(deps)* Update github.com/gocarina/gocsv digest to 9a18a84
* *(deps)* Update module github.com/swaggo/swag to v1.8.12
* *(deps)* Update module github.com/getsentry/sentry-go to v0.20.0
* *(deps)* Update module github.com/redis/go-redis/v9 to v9.0.3
* *(deps)* Update goreleaser/nfpm docker tag to v2.28.0 (#1475)
* *(deps)* Update src.techknowlogick.com/xgo digest to bff48e4 (#1474)
* *(deps)* Update module golang.org/x/sys to v0.7.0
* *(deps)* Update github.com/gocarina/gocsv digest to 6445c2b
* *(deps)* Update module golang.org/x/term to v0.7.0
* *(deps)* Update module github.com/spf13/cobra to v1.7.0
* *(deps)* Update module golang.org/x/image to v0.7.0
* *(deps)* Update module golang.org/x/oauth2 to v0.7.0
* *(deps)* Update module golang.org/x/crypto to v0.8.0
* *(deps)* Update module github.com/prometheus/client_golang to v1.15.0
* *(deps)* Update module github.com/lib/pq to v1.10.8
* *(deps)* Update module github.com/go-sql-driver/mysql to v1.7.1
* *(deps)* Update module github.com/lib/pq to v1.10.9
* *(deps)* Update src.techknowlogick.com/xgo digest to e65295a
* *(deps)* Update github.com/arran4/golang-ical digest to f69e132
* *(deps)* Update module github.com/redis/go-redis/v9 to v9.0.4
* *(deps)* Update module github.com/go-testfixtures/testfixtures/v3 to v3.9.0
* *(deps)* Update module github.com/prometheus/client_golang to v1.15.1
* *(deps)* Update module golang.org/x/term to v0.8.0
* *(deps)* Update src.techknowlogick.com/xgo digest to 52d704d
* *(deps)* Update module github.com/swaggo/swag to v1.16.1
* *(deps)* Update module golang.org/x/sync to v0.2.0
* *(deps)* Update module github.com/getsentry/sentry-go to v0.21.0
* *(deps)* Update module golang.org/x/oauth2 to v0.8.0
* *(deps)* Update module golang.org/x/crypto to v0.9.0
* *(deps)* Update alpine docker tag to v3.18
* *(deps)* Update github.com/gocarina/gocsv digest to 7f30c79
* *(deps)* Update module github.com/magefile/mage to v1.15.0
* *(deps)* Update github.com/gocarina/gocsv digest to 9ddd7fd
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.6.0
* *(deps)* Update module github.com/stretchr/testify to v1.8.3
* *(deps)* Update module github.com/labstack/echo-jwt/v4 to v4.2.0
* *(deps)* Update goreleaser/nfpm docker tag to v2.29.0 (#1528)
* *(deps)* Update module github.com/ulule/limiter/v3 to v3.11.2
* *(deps)* Update module github.com/redis/go-redis/v9 to v9.0.5
* *(deps)* Update module github.com/imdario/mergo to v0.3.16
* *(deps)* Update module github.com/stretchr/testify to v1.8.4
* *(deps)* Update module github.com/spf13/viper to v1.16.0
* *(deps)* Update github.com/vectordotdev/go-datemath digest to 640a500 (#1532)
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.17
* *(deps)* Update klakegg/hugo docker tag to v0.110.0 (#1538)
* *(deps)* Update golangci
* *(deps)* Update klakegg/hugo docker tag to v0.111.0 (#1539)
* *(deps)* Update klakegg/hugo docker tag to v0.111.3 (#1542)
* *(deps)* Update src.techknowlogick.com/xgo digest to 494bc06
* *(deps)* Update goreleaser/nfpm docker tag to v2.30.1 (#1540)
* *(deps)* Update module golang.org/x/sys to v0.9.0
* *(deps)* Update module golang.org/x/term to v0.9.0
* *(deps)* Update module golang.org/x/image to v0.8.0
* *(deps)* Update module golang.org/x/crypto to v0.10.0
* *(deps)* Update module golang.org/x/oauth2 to v0.9.0
* *(deps)* Update module golang.org/x/sync to v0.3.0
* *(deps)* Update github.com/gocarina/gocsv digest to 2696de6
* *(deps)* Update module github.com/prometheus/client_golang to v1.16.0
* *(deps)* Update module github.com/getsentry/sentry-go to v0.22.0
* *(deps)* Update github.com/gocarina/gocsv digest to 99d496c
* *(deps)* Update module github.com/imdario/mergo to v1 (#1559)
* *(deps)* Update github.com/dustinkirkland/golang-petname digest to e794b93
* *(deps)* Update module golang.org/x/sys to v0.10.0
* *(deps)* Update module golang.org/x/image to v0.9.0
* *(deps)* Update module golang.org/x/term to v0.10.0
* *(deps)* Update module golang.org/x/crypto to v0.11.0
* *(deps)* Update module golang.org/x/oauth2 to v0.10.0
### Documentation
* Add docs for installing with sqlite in docker (#70) ([a16fd67](a16fd67b51c02e09ef6709bee9ad2b341d80cd73))
* Add information about our Helm Chart ([22f89c1](22f89c1ccc3a281a75db9e42702604f88eb0568b))
* Fix menu links ([1f13b5d](1f13b5d7b4041042ea3b26ac2a850784b11ac377))
* Remove all traces of namespaces ([3b0935d](3b0935d033c6b5060f18e955acf4a647eb10721b))
* Remove outdated information ([327bb3b](327bb3bed99e0a4c5664251e3af15accf1a13062))
* Update error references to list ([259cf7d](259cf7d25bbb7a289fe9569c81c6f7d3855543bf))
* Update prometheus docs for clarity (#1458)
* Update references to list ([8dc6c95](8dc6c95333b38eb83c8053c628d05599e79dd27e))
### Features
* *(caldav)* Sync Reminders / VALARM (#1415)
* *(docs)* Change order of sections in nav (#1471)
* *(docs)* Various improvements
* *(kanban)* Return the total task count per bucket
* *(migration)* Ignore namespace changes
* *(migration)* Use new structure for migration
* *(projects)* Add parent project, migrate namespaces
* *(projects)* Check all parent projects for permissions
* *(projects)* Check parent project when checking archived status
* *(projects)* Cleanup namespace leftovers
* *(projects)* Don't allow deleting or archiving the default project
* *(projects)* Get all projects recursively
* *(projects)* Remove namespaces
* *(projects)* Return a favorites pseudo project when the user has favorite tasks
* *(subscriptions)* Make sure all subscriptions are inherited properly
* *(users)* Don't hide user email if it was the search request* Rename lists to projects ([349e6a5](349e6a59050a0beba82a7f626c2f72f6b8c88dde))
* Add logging options to mailer settings ([9590b82](9590b82c11852666524eeab562988226574a1b1c))
* Add relative Reminders (#1427) ([3f5252d](3f5252dc24a3dea89b2e049ccb1f9d0a59a89a88))
* Add token example ([4417223](441722372af3349b677dc013b1863e678b0e7158))
* Allow saving frontend settings via api ([04e2c51](04e2c51fac24a045abe1a85c8b661b6bc628686c))
* Allow to find users with access to a project more freely ([a7231e1](a7231e197e3d86d3ef27fad89ae60863d25b5df0))
* Check for cycles when creating or updating a project's parent ([9011894](9011894a2975d9d112dc3db453739e13261c0716))
* Generate swagger docs at build time ([efa24ce](efa24cec44865c5a8ab42a106deeb331ad1bed91))
* Improve relation kinds docs ([b826c13](b826c13f385b24ed1b33b8890cc5cdd5fe8b8f22))
* Make the new inbox project the default ([0110f93](0110f933134af0460d9fed9d652148c98e94b6cd))
* Migrate lists to projects in db identifiers ([2fba7bd](2fba7bdf02983e5cf7def09803def4cbf830f53b))
* Remove ChildProjects project property ([edcb806](edcb806421c2181a8b85aed5b53e8da6350b9630))
* Remove namespaces, make projects infinitely nestable (#1362) ([82beb3b](82beb3bf671ca0670b714160f0b4d9c186dfe120))
* Rename all list files ([8f4abd2](8f4abd2fe86e7a23d80bc5ebc4fc1ae75e1b78fb))
* Rename lists to projects ([47c2da7](47c2da7f1856e95956cdb968fa95295d3441a9f6))
* Rename lists to projects ([96a0f5e](96a0f5e169c9e8f8d20e3fe1d9de5eecead53ac9))
* Rename lists to projects ([fc73c84](fc73c84bf2b9a7cbd2f6cbd2a83ea9ccc3fd58fd))
* Rename lists to projects everywhere (#1318) ([869d4a3](869d4a336cb122df894acf040e02b6b2ba786fdb))
### Miscellaneous Tasks
* *(changelog)* Fix spelling
* *(docs)* Add info about `/buckets` sorting
* *(docs)* Move login and register routes to auth category in api docs
* *(docs)* Update error docs
* *(docs)* Update list -> project
* *(docs/translation)* Remove mention of weblate
* *(export)* Remove unused events
* *(project)* Fmt
* *(projects)* use a slice again ([3e8d1b3](3e8d1b3667ccfb2960650a4506771ec3c9b3a970))
* *(test)* Show table content when db assertion failed
* Cleanup ([7a9611c](7a9611c2daa41ec2da135a2a4e804551e4ab8ff2))
* Disable false-positive linter for generated docs ([076e857](076e857507a4cf59e0b0399a2e51a8d8baa03065))
* Fix comment url ([5856f21](5856f21f31fe7b81e7ffd203f70460785955411c))
* Fix spelling ([cd90db3](cd90db3117a7fa40175ecebd3ca37cc94a46e1ee))
* Generate swagger docs ([55410ea](55410ea73d50f5bc124eaf411c77125024b6fefa))
* Go mod tidy ([93056da](93056da792dafa70f91f7d114669997b3f93f7f1))
* Go mod tidy ([e5dde31](e5dde315fb6a7163546b9f88ebafacc886744db3))
* Remove cache options ([d83e3a0](d83e3a0a037b9a4d40ce22c8c51932eb23963ac2))
* Remove reminderDates after frontend is migrated to reminders (#1448) ([4a4ba04](4a4ba041e0f3e9c71dd4844d5191c9cbe4e4e3b7))
* Rename files (fix typo) ([6aadaaa](6aadaaaffc1fff4a94e35e8fa3f6eab397cbc3ce))
## [0.20.4] - 2023-03-12
### Bug Fixes
@ -1906,4 +2175,3 @@ Misc bugfixes and improvements to the build process
## [0.2] - 2018-10-17
## [0.1] - 2018-09-20

View File

@ -24,7 +24,7 @@ RUN export PATH=$PATH:$GOPATH/bin && \
# 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 AS runner
FROM alpine:3.18 AS runner
LABEL maintainer="maintainers@vikunja.io"
WORKDIR /app/vikunja
ENTRYPOINT [ "/sbin/tini", "-g", "--", "/entrypoint.sh" ]

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.20.4-brightgreen.svg)](https://dl.vikunja.io)
[![Download](https://img.shields.io/badge/download-v0.21.0-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)
@ -26,13 +26,7 @@ If you find any security-related issues you don't want to disclose publicly, ple
## Features
* Create TODO lists with tasks
* Reminder for tasks
* Namespaces: A "group" which bundles multiple lists
* Share lists and namespaces with teams and users with granular permissions
* Plenty of details for tasks
See [the features page](https://vikunja.io/en/features/) on our website for a more exaustive list or
See [the features page](https://vikunja.io/features/) on our website for a more exaustive list or
try it on [try.vikunja.io](https://try.vikunja.io)!
## Docs
@ -58,4 +52,4 @@ Fork -> Push -> Pull-Request. Also see the [dev docs](https://vikunja.io/docs/de
## License
This project is licensed under the AGPLv3 License. See the [LICENSE](LICENSE) file for the full license text.
This project is licensed under the AGPLv3 License. See the [LICENSE](LICENSE) file for the full license text.

View File

@ -91,16 +91,6 @@ database:
# Enable SSL/TLS for mysql connections. Options: false, true, skip-verify, preferred
tls: false
cache:
# If cache is enabled or not
enabled: false
# Cache type. Possible values are "keyvalue", "memory" or "redis".
# When choosing "keyvalue" this setting follows the one configured in the "keyvalue" section.
# When choosing "redis" you will need to configure the redis connection separately.
type: keyvalue
# When using memory this defines the maximum size an element can take
maxelementsize: 1000
redis:
# Whether to enable redis or not
enabled: false

View File

@ -100,9 +100,10 @@ You should also document the routes with [swagger annotations]({{< ref "swagger-
## Insertion helper method
There is a method available in the `migration` package which takes a fully nested Vikunja structure and creates it with all relations.
This means you start by adding a namespace, then add projects inside that namespace, then tasks in the lists and so on.
This means you start by adding a project, then add projects inside that project, then tasks in the lists and so on.
In general, it is reccommended to have one root project with all projects of the other service as child projects.
The root structure must be present as `[]*models.NamespaceWithProjectsAndTasks`. It allows to represent all of Vikunja's hierarchy as a single data structure.
The root structure must be present as `[]*models.ProjectWithTasksAndBuckets`. It allows to represent all of Vikunja's hierarchy as a single data structure.
Then call the method like so:

View File

@ -25,7 +25,7 @@ As an example, this is the definition of a project with all comments:
type Project struct {
// The unique, numeric id of this project.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"project"`
// The title of the project. You'll see this in the namespace overview.
// The title of the project. You'll see this in the overview.
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
// The description of the project.
Description string `xorm:"longtext null" json:"description"`
@ -34,13 +34,14 @@ type Project struct {
// The hex color of this project
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
ParentProject *Project `xorm:"-" json:"-"`
// The user who created this project.
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
// Whether or not a project is archived.
// Whether a project is archived.
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
// The id of the file this project has set as background
@ -50,7 +51,7 @@ type Project struct {
// Contains a very small version of the project 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 project is a favorite. Favorite projects show up in a separate namespace. This value depends on the user making the call to the api.
// True if a project is a favorite. Favorite projects show up in a separate parent project. This value depends on the user making the call to the api.
IsFavorite bool `xorm:"-" json:"is_favorite"`
// The subscription status for the user reading this project. You can only read this property, use the subscription endpoints to modify it.

View File

@ -67,7 +67,6 @@ Beispiel: „Benutzer:in“
| Englisches Original | Verwendung in deutscher Übersetzung |
| ------------------- | -------------------- |
| Bucket | Spalte |
| Namespace | Namespace |
| Link Share | Linkfreigabe |
| Username | Anmeldename |

View File

@ -494,47 +494,6 @@ Full path: `database.tls`
Environment path: `VIKUNJA_DATABASE_TLS`
---
## cache
### enabled
If cache is enabled or not
Default: `false`
Full path: `cache.enabled`
Environment path: `VIKUNJA_CACHE_ENABLED`
### type
Cache type. Possible values are "keyvalue", "memory" or "redis".
When choosing "keyvalue" this setting follows the one configured in the "keyvalue" section.
When choosing "redis" you will need to configure the redis connection separately.
Default: `keyvalue`
Full path: `cache.type`
Environment path: `VIKUNJA_CACHE_TYPE`
### maxelementsize
When using memory this defines the maximum size an element can take
Default: `1000`
Full path: `cache.maxelementsize`
Environment path: `VIKUNJA_CACHE_MAXELEMENTSIZE`
---
## redis

View File

@ -66,3 +66,25 @@ Google config:
- Configure an authorized redirect URI of `https://vikunja.mydomain.com/auth/openid/google`
Note that there currently seems to be no way to stop creation of new users, even when `enableregistration` is `false` in the configuration. This means that this approach works well only with an "Internal Organization" app for Google Workspace, which limits the allowed users to organizational accounts only. External / public applications will potentially allow every Google user to register.
## Keycloak
Vikunja Config:
```yaml
openid:
enabled: true
redirecturl: https://vikunja.mydomain.com/auth/openid/ <---- slash at the end is important
providers:
- name: Keycloak
authurl: https://keycloak.mydomain.com/realms/<relam-name>
logouturl: https://keycloak.mydomain.com/realms/<relam-name>/protocol/openid-connect/logout
clientid: <vikunja-id>
clientsecret: <vikunja secret>
```
Keycloak Config:
- Navigate to the keycloak instance
- Create a new client with the type `OpenID Connect` and a unique ID.
- Set `Client authentication` to On
- Set `Root Url` to `https://vikunja.mydomain.com`
- Set `Valid redirect URIs` to `/auth/openid/keycloak`
- Create the client the navigate to the credentials tab and copy the `Client secret`

View File

@ -26,8 +26,8 @@ Urls are:
* `/principals/<username>/`: Returns urls for project discovery. *Use this url to initially make connections to new clients.*
* `/projects/`: Used to manage projects
* `/projects/<List ID>/`: Used to manage a single project
* `/projects/<List ID>/<Task UID>`: Used to manage a task on a project
* `/projects/<Project ID>/`: Used to manage a single project
* `/projects/<Project ID>/<Task UID>`: Used to manage a task on a project
## Supported properties

View File

@ -54,16 +54,19 @@ This document describes the different errors Vikunja can return.
## Project
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------|
| 3001 | 404 | The project does not exist. |
| 3004 | 403 | The user needs to have read permissions on that project to perform that action. |
| 3005 | 400 | The project title cannot be empty. |
| 3006 | 404 | The project share does not exist. |
| 3007 | 400 | A project with this identifier already exists. |
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| 3001 | 404 | The project does not exist. |
| 3004 | 403 | The user needs to have read permissions on that project to perform that action. |
| 3005 | 400 | The project title cannot be empty. |
| 3006 | 404 | The project share does not exist. |
| 3007 | 400 | A project with this identifier already exists. |
| 3008 | 412 | The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project. |
| 3009 | 412 | The project cannot belong to a dynamically generated namespace like "Favorites". |
| 3010 | 412 | The project must belong to a namespace. |
| 3009 | 412 | The project cannot belong to a dynamically generated parent project like "Favorites". |
| 3010 | 412 | This project cannot be a child of itself. |
| 3011 | 412 | This project cannot have a cyclic relationship to a parent project. |
| 3012 | 412 | This project cannot be deleted because a user has set it as their default project. |
| 3013 | 412 | This project cannot be archived because a user has set it as their default project. |
## Task
@ -92,27 +95,15 @@ This document describes the different errors Vikunja can return.
| 4021 | 400 | This user is already assigned to that task. |
| 4022 | 400 | The task has a relative reminder which does not specify relative to what. |
## Namespace
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------|
| 5001 | 404 | The namespace does not exist. |
| 5003 | 403 | The user does not have access to the specified namespace. |
| 5006 | 400 | The namespace name cannot be empty. |
| 5009 | 403 | The user needs to have namespace read access to perform that action. |
| 5010 | 403 | This team does not have access to that namespace. |
| 5011 | 409 | This user has already access to that namespace. |
| 5012 | 412 | The namespace is archived and can therefore only be accessed read only. |
## Team
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------|
| 6001 | 400 | The team name cannot be empty. |
| 6002 | 404 | The team does not exist. |
| 6004 | 409 | The team already has access to that namespace or project. |
| 6005 | 409 | The user is already a member of that team. |
| 6006 | 400 | Cannot delete the last team member. |
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|----------------------------------------------------------------------|
| 6001 | 400 | The team name cannot be empty. |
| 6002 | 404 | The team does not exist. |
| 6004 | 409 | The team already has access to that project. |
| 6005 | 409 | The user is already a member of that team. |
| 6006 | 400 | Cannot delete the last team member. |
| 6007 | 403 | The team does not have access to the project to perform that action. |
## User Project Access

View File

@ -8,20 +8,20 @@ menu:
parent: "usage"
---
# Project and namespace rights for teams and users
# Project rights for teams and users
Whenever you share a project or namespace with a user or team, you can specify a `rights` parameter.
Whenever you share a project with a user or team, you can specify a `rights` parameter.
This parameter controls the rights that team or user is going to have (or has, if you request the current sharing status).
Rights are being specified using integers.
The following values are possible:
| Right (int) | Meaning |
|-------------|---------------------------------------------------------------------------------------------------------------|
| 0 (Default) | Read only. Anything which is shared with this right cannot be edited. |
| 1 | Read and write. Namespaces or projects shared with this right can be read and written to by the team or user. |
| 2 | Admin. Can do anything like read and write, but can additionally manage sharing options. |
| Right (int) | Meaning |
|-------------|-------------------------------------------------------------------------------------------------|
| 0 (Default) | Read only. Anything which is shared with this right cannot be edited. |
| 1 | Read and write. Projects shared with this right can be read and written to by the team or user. |
| 2 | Admin. Can do anything like read and write, but can additionally manage sharing options. |
## Team admins

90
go.mod
View File

@ -18,70 +18,73 @@ module code.vikunja.io/api
require (
code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3
gitea.com/xorm/xorm-redis-cache v0.2.0
dario.cat/mergo v1.0.0
github.com/ThreeDotsLabs/watermill v1.2.0
github.com/adlio/trello v1.10.0
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc
github.com/arran4/golang-ical v0.0.0-20230425234049-f69e132f2b0c
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.5.0
github.com/coreos/go-oidc/v3 v3.6.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/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49
github.com/gabriel-vasile/mimetype v1.4.2
github.com/getsentry/sentry-go v0.20.0
github.com/go-sql-driver/mysql v1.7.0
github.com/go-testfixtures/testfixtures/v3 v3.8.1
github.com/gocarina/gocsv v0.0.0-20230406101422-6445c2b15027
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/getsentry/sentry-go v0.22.0
github.com/go-sql-driver/mysql v1.7.1
github.com/go-testfixtures/testfixtures/v3 v3.9.0
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/uuid v1.3.0
github.com/iancoleman/strcase v0.2.0
github.com/imdario/mergo v0.3.15
github.com/hashicorp/go-version v1.6.0
github.com/iancoleman/strcase v0.3.0
github.com/jinzhu/copier v0.3.5
github.com/labstack/echo-jwt/v4 v4.1.0
github.com/labstack/echo/v4 v4.10.2
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6
github.com/labstack/echo-jwt/v4 v4.2.0
github.com/labstack/echo/v4 v4.11.0
github.com/labstack/gommon v0.4.0
github.com/lib/pq v1.10.8
github.com/magefile/mage v1.14.0
github.com/mattn/go-sqlite3 v1.14.16
github.com/lib/pq v1.10.9
github.com/magefile/mage v1.15.0
github.com/mattn/go-sqlite3 v1.14.17
github.com/olekukonko/tablewriter v0.0.5
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.15.0
github.com/redis/go-redis/v9 v9.0.3
github.com/prometheus/client_golang v1.16.0
github.com/redis/go-redis/v9 v9.0.5
github.com/robfig/cron/v3 v3.0.1
github.com/samedi/caldav-go v3.0.0+incompatible
github.com/spf13/afero v1.9.5
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.2
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4
github.com/swaggo/swag v1.8.12
github.com/tkuchiki/go-timezone v0.2.2
github.com/ulule/limiter/v3 v3.11.1
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae
github.com/wneessen/go-mail v0.3.9
github.com/ulule/limiter/v3 v3.11.2
github.com/wneessen/go-mail v0.4.0
github.com/yuin/goldmark v1.5.4
golang.org/x/crypto v0.8.0
golang.org/x/image v0.7.0
golang.org/x/oauth2 v0.7.0
golang.org/x/sync v0.1.0
golang.org/x/sys v0.7.0
golang.org/x/term v0.7.0
golang.org/x/crypto v0.11.0
golang.org/x/image v0.9.0
golang.org/x/oauth2 v0.10.0
golang.org/x/sync v0.3.0
golang.org/x/sys v0.10.0
golang.org/x/term v0.10.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/yaml.v3 v3.0.1
src.techknowlogick.com/xgo v1.7.1-0.20230404174715-bff48e481f81
src.techknowlogick.com/xgo v1.7.1-0.20230711181658-617d3b65dd40
src.techknowlogick.com/xormigrate v1.5.0
xorm.io/builder v0.3.12
xorm.io/xorm v1.3.2
)
require (
github.com/ClickHouse/ch-go v0.55.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.9.1 // indirect
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/andybalholm/brotli v1.0.5 // 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
@ -91,15 +94,16 @@ require (
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.6.0 // indirect
github.com/garyburd/redigo v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.6.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // 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/goccy/go-json v0.9.11 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
@ -109,12 +113,13 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef // indirect
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -123,14 +128,18 @@ require (
github.com/oklog/ulid v1.3.1 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.16.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/paulmach/orb v0.9.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
@ -138,13 +147,16 @@ require (
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect
go.opentelemetry.io/otel v1.15.0 // indirect
go.opentelemetry.io/otel/trace v1.15.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

321
go.sum
View File

@ -24,7 +24,6 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@ -39,16 +38,18 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3 h1:MXl7Ff9a/ndTpuEmQKIGhqReE9hWhD4T/+AzK4AXUYc=
code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3/go.mod h1:OgFO06HN1KpA4S7Dw/QAIeygiUPSeGJJn1ykz/sjZdU=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitea.com/xorm/tests v0.7.0 h1:pFcaxTGGAWw3rDuVfhBdyr+mX1uzdTtncyAKxkCQ/IE=
gitea.com/xorm/tests v0.7.0/go.mod h1:ngmhQrSBgihBbOqw1hdReSQJAnTlbStYTn0vruUFwDc=
gitea.com/xorm/xorm-redis-cache v0.2.0 h1:qglRHt6/7vJmDeld6j+n10M9PmruAh+Le2lgNraFu3g=
gitea.com/xorm/xorm-redis-cache v0.2.0/go.mod h1:juYdjkmIKvLbPkdfBVKGVJ2daFQIJAgKsn4mL4ZK8Zk=
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/ch-go v0.55.0 h1:jw4Tpx887YXrkyL5DfgUome/po8MLz92nz2heOQ6RjQ=
github.com/ClickHouse/ch-go v0.55.0/go.mod h1:kQT2f+yp2p+sagQA/7kS6G3ukym+GQ5KAu1kuFAFDiU=
github.com/ClickHouse/clickhouse-go/v2 v2.9.1 h1:IeE2bwVvAba7Yw5ZKu98bKI4NpDmykEy6jUaQdJJCk8=
github.com/ClickHouse/clickhouse-go/v2 v2.9.1/go.mod h1:teXfZNM90iQ99Jnuht+dxQXCuhDZ8nvvMoTJOFrcmcg=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
@ -70,16 +71,16 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0 h1:VVPogIxPiZ6WK5G4Pve5VSQ4HEFiJ8GChpqRjo1gN2c=
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0=
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc h1:up1aDcTCZ3KrL2ukKxNqjMRx/CCaXyn9Wl6N7ea3EWc=
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0=
github.com/arran4/golang-ical v0.0.0-20230425234049-f69e132f2b0c h1:bmHPCBB1T8YZpQI+Ch0RuICrozVFmPAjiBQZvAjtpRI=
github.com/arran4/golang-ical v0.0.0-20230425234049-f69e132f2b0c/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
@ -97,8 +98,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ=
github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdSE0JxlhR2hKnGPcNB35BjQf4RYQDY=
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
@ -120,8 +121,8 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw=
github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM=
github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@ -139,10 +140,9 @@ github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denisenkom/go-mssqldb v0.12.2 h1:1OcPn5GBIobjWNd+8yjfHNIaFX14B1pWI3F9HZy5KXw=
github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
@ -152,6 +152,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY=
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8=
github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49 h1:6SNWi8VxQeCSwmLuTbEvJd7xvPmdS//zvMBWweZLgck=
github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@ -166,28 +168,24 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0=
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
github.com/getsentry/sentry-go v0.19.0 h1:BcCH3CN5tXt5aML+gwmbFwVptLLQA+eT866fCO9wVOM=
github.com/getsentry/sentry-go v0.19.0/go.mod h1:y3+lGEFEFexZtpbG1GUE2WD/f9zGyKYwpEqryTOC/nE=
github.com/getsentry/sentry-go v0.20.0 h1:bwXW98iMRIWxn+4FgPW7vMrjmbym6HblXALmhjHmQaQ=
github.com/getsentry/sentry-go v0.20.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/getsentry/sentry-go v0.22.0 h1:XNX9zKbv7baSEI65l+H1GEJgSeIC1c7EN5kluWaP6dM=
github.com/getsentry/sentry-go v0.22.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -199,6 +197,7 @@ github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgO
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@ -210,34 +209,31 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-testfixtures/testfixtures/v3 v3.8.1 h1:uonwvepqRvSgddcrReZQhojTlWlmOlHkYAb9ZaOMWgU=
github.com/go-testfixtures/testfixtures/v3 v3.8.1/go.mod h1:Kdu7YeMC0KRXVHdaQ91Vmx3pcjoTF63h4f1qTJDdXLA=
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a h1:/5o1ejt5M0fNAN2lU1NBLtPzUSZru689EWJq01ptr+E=
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/gocarina/gocsv v0.0.0-20230325173030-9a18a846a479 h1:KaCpc4e48emF9hYmMB9INyfpGJHAZxEAS9EqWFkpTig=
github.com/gocarina/gocsv v0.0.0-20230325173030-9a18a846a479/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/gocarina/gocsv v0.0.0-20230406101422-6445c2b15027 h1:LCGzZb4kMUUjMUzLxxqSJBwo9szUO0tK8cOxnEOT4Jc=
github.com/gocarina/gocsv v0.0.0-20230406101422-6445c2b15027/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/go-testfixtures/testfixtures/v3 v3.9.0 h1:938g5V+GWLVejm3Hc+nWCuEXRlcglZDDlN/t1gWzcSY=
github.com/go-testfixtures/testfixtures/v3 v3.9.0/go.mod h1:cdsKD2ApFBjdog9jRsz6EJqF+LClq/hrwE9K/1Dzo4s=
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d h1:KbPOUXFUDJxwZ04vbmDOc3yuruGvVO+LOa7cVER3yWw=
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@ -270,7 +266,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@ -291,7 +286,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@ -341,6 +335,8 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -354,17 +350,11 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo=
github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
@ -373,7 +363,6 @@ github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
@ -383,7 +372,7 @@ github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpT
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8=
github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
@ -399,10 +388,10 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y=
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
@ -411,9 +400,7 @@ github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkAL
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE=
github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs=
github.com/jackc/pgx v3.6.0+incompatible h1:bJeo4JdVbDAW8KB2m8XkFeo8CPipREoG37BwEoKGz+Q=
github.com/jackc/pgx v3.6.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
@ -422,7 +409,7 @@ github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=
github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60=
github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y=
github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
@ -432,10 +419,11 @@ github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -443,14 +431,19 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 h1:SwcnSwBR7X/5EHJQlXBockkJVIMRVt5yKaesBPMtyZQ=
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6/go.mod h1:WrYiIuiXUMIvTDAQw97C+9l0CnBmCcvosPjN3XDqS/o=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kolaente/caldav-go v3.0.1-0.20190524174923-9e5cd1688227+incompatible h1:PkEEpmbrFXlMul8cOplR8nkcIM/NDbx+H6fq2+vaKAA=
github.com/kolaente/caldav-go v3.0.1-0.20190524174923-9e5cd1688227+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible h1:q7DbyV+sFjEoTuuUdRDNl2nlyfztkZgxVVCV7JhzIkY=
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -464,11 +457,13 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo-jwt/v4 v4.1.0 h1:eYGBxauPkyzBM78KJbR5OSz5uhKMDkhJZhTTIuoH6Pg=
github.com/labstack/echo-jwt/v4 v4.1.0/go.mod h1:DHSSaL6cTgczdPXjf8qrTHRbrau2flcddV7CPMs2U/Y=
github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/echo/v4 v4.11.0 h1:4Dmi59tmrnFzOchz4EXuGjJhUfcEkU28iDKsiZVOQgw=
github.com/labstack/echo/v4 v4.11.0/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
@ -480,17 +475,15 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.8 h1:3fdt97i/cwSU83+E0hZTC/Xpc9mTZxc6UWSCRcSbxiE=
github.com/lib/pq v1.10.8/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -517,17 +510,17 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-oci8 v0.0.0-20191108001511-cbd8d5bc1da0/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@ -548,7 +541,9 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
@ -589,12 +584,17 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/paulmach/orb v0.9.0 h1:MwA1DqOKtvCgm7u9RZ/pnYejTeDJPnr0+0oFajBbJqk=
github.com/paulmach/orb v0.9.0/go.mod h1:SudmOk85SXtmXAB3sLGyJ6tZy/8pdfrV0o6ef98Xc30=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -611,10 +611,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -625,28 +623,24 @@ github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3d
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE=
github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps=
github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
@ -657,10 +651,13 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20191009025716-f1972eb1d1f5/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
@ -669,15 +666,11 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs=
github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
@ -685,8 +678,8 @@ github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@ -703,26 +696,21 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/swaggo/swag v1.8.11 h1:Fp1dNNtDvbCf+8kvehZbHQnlF6AxHGjmw6H/xAMrZfY=
github.com/swaggo/swag v1.8.11/go.mod h1:2GXgpNI9iy5OdsYWu8zXfRAGnOAPxYxTWTyM0XOTYZQ=
github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w=
github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q=
github.com/tkuchiki/go-timezone v0.2.2/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ulule/limiter/v3 v3.11.0 h1:9hXMyS0K8Z+EYfrtwPMwmWYflPimswsC/EOMsO2sHx4=
github.com/ulule/limiter/v3 v3.11.0/go.mod h1:OiKIiMs9dXLMk5TwtIBZlswhPigov9fGmwO4xYbmFkY=
github.com/ulule/limiter/v3 v3.11.1 h1:wm6YaA2JwIXc0S+z8TK8/neWMOTf4m20I5jL1dwLRcw=
github.com/ulule/limiter/v3 v3.11.1/go.mod h1:4nk/9RHEJthkjD+mmkqYxaPfD4pkB91PTH7k8ozB80g=
github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA=
github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
@ -735,15 +723,17 @@ github.com/valyala/fasttemplate v1.2.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93 h1:bT0ZMfsMi2Xh8dopgxhFT+OJH88QITHpdppdkG1rXJQ=
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ=
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae h1:oyiy3uBj1F4O3AaFh7hUGBrJjAssJhKyAbwxtkslxqo=
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ=
github.com/wneessen/go-mail v0.3.8 h1:ja5D/o/RVwrtRIYFlrO7GmtcjDNeMakGQuwQRZYv0JM=
github.com/wneessen/go-mail v0.3.8/go.mod h1:m25lkU2GYQnlVr6tdwK533/UXxo57V0kLOjaFYmub0E=
github.com/wneessen/go-mail v0.3.9 h1:Q4DbCk3htT5DtDWKeMgNXCiHc4bBY/vv/XQPT6XDXzc=
github.com/wneessen/go-mail v0.3.9/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
github.com/wneessen/go-mail v0.4.0 h1:Oo4HLIV8My7G9JuZkoOX6eipXQD+ACvIqURYeIzUc88=
github.com/wneessen/go-mail v0.4.0/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -755,6 +745,7 @@ github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -763,6 +754,10 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/otel v1.15.0 h1:NIl24d4eiLJPM0vKn4HjLYM+UZf6gSfi9Z+NmCxkWbk=
go.opentelemetry.io/otel v1.15.0/go.mod h1:qfwLEbWhLPk5gyWrne4XnF0lC8wtywbuJbgfAE3zbek=
go.opentelemetry.io/otel/trace v1.15.0 h1:5Fwje4O2ooOxkfyqI/kJwxWotggDLix4BSAvpE1wlpo=
go.opentelemetry.io/otel/trace v1.15.0/go.mod h1:CUsmE2Ht1CRkvE8OsMESvraoZrrcgD1J2W8GV1ev0Y4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@ -794,12 +789,12 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -813,10 +808,10 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg=
golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM=
golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g=
golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -839,7 +834,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@ -887,15 +881,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -905,11 +896,10 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs=
golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -920,9 +910,11 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -989,21 +981,20 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1012,12 +1003,11 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1071,6 +1061,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
@ -1081,10 +1072,10 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
@ -1195,13 +1186,11 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -1231,7 +1220,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@ -1356,21 +1344,16 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
src.techknowlogick.com/xgo v1.7.1-0.20230117190652-94aee174ab86 h1:VybPMHRdCLbdCttI8fMXOaGpoJGSG9+W/5cfRgr1Xjc=
src.techknowlogick.com/xgo v1.7.1-0.20230117190652-94aee174ab86/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.7.1-0.20230214195350-44f7e66f9b20 h1:Wye8Ljlv2AZvYPW1twGbW9sQWGtjurbQECnnkNx6gd0=
src.techknowlogick.com/xgo v1.7.1-0.20230214195350-44f7e66f9b20/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.7.1-0.20230307171022-b60708668fc7 h1:nPPnMdR4wih62PSsnHK/SlYM1lOZk/St0k7DkJadMV4=
src.techknowlogick.com/xgo v1.7.1-0.20230307171022-b60708668fc7/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.7.1-0.20230404174715-bff48e481f81 h1:GNyJiosmWbazA1OYNZ1yF+GWOjIW0ZfJmkwEJDiU18g=
src.techknowlogick.com/xgo v1.7.1-0.20230404174715-bff48e481f81/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.7.1-0.20230606181629-494bc06f804a h1:3UOdsZC8cRR4OVlNZS8YiChlp8g7eiNUYT+DAQmZt20=
src.techknowlogick.com/xgo v1.7.1-0.20230606181629-494bc06f804a/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.7.1-0.20230711181658-617d3b65dd40 h1:elzESSqGnEJoseRrOqqdYcmiZrDo0geMdkMQqb98IaE=
src.techknowlogick.com/xgo v1.7.1-0.20230711181658-617d3b65dd40/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xormigrate v1.5.0 h1:6mWTh8d0sWjMTLUgJqiLe0e0Teu+1j+RgI7ErAeOEV0=
src.techknowlogick.com/xormigrate v1.5.0/go.mod h1:QOCnBeWralVncPn9eZlM4w/rglFK8o1vYpemzPenkBM=
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.0.1/go.mod h1:o4vnEsQ5V2F1/WK6w4XTwmiWJeGj82tqjAnHe44wVHY=
xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=

View File

@ -419,7 +419,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.52.1")
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.2")
os.Exit(1)
}
}

View File

@ -73,7 +73,7 @@ func init() {
// User deletion flags
userDeleteCmd.Flags().BoolVarP(&userFlagDeleteNow, "now", "n", false, "If provided, deletes the user immediately instead of sending them an email first.")
userCmd.AddCommand(userProjectCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd, userDeleteCmd)
userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd, userDeleteCmd)
rootCmd.AddCommand(userCmd)
}
@ -117,9 +117,9 @@ var userCmd = &cobra.Command{
Short: "Manage users locally through the cli.",
}
var userProjectCmd = &cobra.Command{
Use: "project",
Short: "Shows a project of all users.",
var userListCmd = &cobra.Command{
Use: "list",
Short: "Shows a list of all users.",
PreRun: func(cmd *cobra.Command, args []string) {
initialize.FullInit()
},
@ -127,7 +127,7 @@ var userProjectCmd = &cobra.Command{
s := db.NewSession()
defer s.Close()
users, err := user.ProjectAllUsers(s)
users, err := user.ListAllUsers(s)
if err != nil {
_ = s.Rollback()
log.Fatalf("Error getting users: %s", err)
@ -188,10 +188,10 @@ var userCreateCmd = &cobra.Command{
log.Fatalf("Error creating new user: %s", err)
}
err = models.CreateNewNamespaceForUser(s, newUser)
err = models.CreateNewProjectForUser(s, newUser)
if err != nil {
_ = s.Rollback()
log.Fatalf("Error creating new namespace for user: %s", err)
log.Fatalf("Error creating new project for user: %s", err)
}
if err := s.Commit(); err != nil {

View File

@ -87,10 +87,6 @@ const (
DatabaseSslRootCert Key = `database.sslrootcert`
DatabaseTLS Key = `database.tls`
CacheEnabled Key = `cache.enabled`
CacheType Key = `cache.type`
CacheMaxElementSize Key = `cache.maxelementsize`
MailerEnabled Key = `mailer.enabled`
MailerHost Key = `mailer.host`
MailerPort Key = `mailer.port`
@ -321,10 +317,6 @@ func InitDefaultConfig() {
DatabaseSslRootCert.setDefault("")
DatabaseTLS.setDefault("false")
// Cacher
CacheEnabled.setDefault(false)
CacheType.setDefault("memory")
CacheMaxElementSize.setDefault(1000)
// Mailer
MailerEnabled.setDefault(false)
MailerHost.setDefault("")
@ -425,10 +417,6 @@ func InitConfig() {
log.Println("No config file found, using default or config from environment variables.")
}
if CacheType.GetString() == "keyvalue" {
CacheType.Set(KeyvalueType.GetString())
}
if RateLimitStore.GetString() == "keyvalue" {
RateLimitStore.Set(KeyvalueType.GetString())
}

View File

@ -17,7 +17,6 @@
package db
import (
"encoding/gob"
"fmt"
"net/url"
"os"
@ -27,9 +26,7 @@ import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
xrc "gitea.com/xorm/xorm-redis-cache"
"xorm.io/xorm"
"xorm.io/xorm/caches"
"xorm.io/xorm/names"
"xorm.io/xorm/schemas"
@ -85,30 +82,10 @@ func CreateDBEngine() (engine *xorm.Engine, err error) {
logger := log.NewXormLogger("")
engine.SetLogger(logger)
// Cache
// We have to initialize the cache here to avoid import cycles
if config.CacheEnabled.GetBool() {
switch config.CacheType.GetString() {
case "memory":
cacher := caches.NewLRUCacher(caches.NewMemoryStore(), config.CacheMaxElementSize.GetInt())
engine.SetDefaultCacher(cacher)
case "redis":
cacher := xrc.NewRedisCacher(config.RedisHost.GetString(), config.RedisPassword.GetString(), xrc.DEFAULT_EXPIRATION, engine.Logger())
engine.SetDefaultCacher(cacher)
default:
log.Info("Did not find a valid cache type. Caching disabled. Please refer to the docs for poosible cache types.")
}
}
x = engine
return
}
// RegisterTableStructsForCache registers tables in gob encoding for redis cache
func RegisterTableStructsForCache(val interface{}) {
gob.Register(val)
}
func initMysqlEngine() (engine *xorm.Engine, err error) {
// We're using utf8mb here instead of just utf8 because we want to use non-BMP characters.
// See https://stackoverflow.com/a/30074553/10924593 for more info.

View File

@ -220,7 +220,19 @@
updated: 2020-04-18 21:13:52
- id: 36
title: testbucket36
project_id: 26
project_id: 33
created_by_id: 6
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 37
title: testbucket37
project_id: 34
created_by_id: 6
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 38
title: testbucket36
project_id: 36
created_by_id: 15
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52

View File

@ -10,9 +10,6 @@
- entity_id: 34
user_id: 13 # owner
kind: 1
- entity_id: 34
user_id: 1
kind: 1
- entity_id: 23
user_id: 12 # owner
kind: 2

View File

@ -15,6 +15,6 @@
label_id: 4
created: 2018-12-01 15:13:12
- id: 5
task_id: 39
task_id: 40
label_id: 4
created: 2018-12-01 15:13:12

View File

@ -1,96 +0,0 @@
- id: 1
title: testnamespace
description: Lorem Ipsum
owner_id: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 2
title: testnamespace2
description: Lorem Ipsum
owner_id: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 3
title: testnamespace3
description: Lorem Ipsum
owner_id: 3
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 6
title: testnamespace6
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 7
title: testnamespace7
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 8
title: testnamespace8
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 9
title: testnamespace9
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 10
title: testnamespace10
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 11
title: testnamespace11
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 12
title: testnamespace12
description: Lorem Ipsum
owner_id: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 13
title: testnamespace13
description: Lorem Ipsum
owner_id: 7
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 14
title: testnamespace14
description: Lorem Ipsum
owner_id: 7
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 15
title: testnamespace15
description: Lorem Ipsum
owner_id: 13
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 16
title: Archived testnamespace16
owner_id: 1
is_archived: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 17
title: testnamespace17
description: Lorem Ipsum
owner_id: 12
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 18
title: testnamespace18
description: Lorem Ipsum
owner_id: 15
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -4,7 +4,6 @@
description: Lorem Ipsum
identifier: test1
owner_id: 1
namespace_id: 1
position: 3
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -14,7 +13,6 @@
description: Lorem Ipsum
identifier: test2
owner_id: 3
namespace_id: 1
position: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -24,7 +22,6 @@
description: Lorem Ipsum
identifier: test3
owner_id: 3
namespace_id: 2
position: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -34,7 +31,6 @@
description: Lorem Ipsum
identifier: test4
owner_id: 3
namespace_id: 3
position: 4
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -44,7 +40,6 @@
description: Lorem Ipsum
identifier: test5
owner_id: 5
namespace_id: 5
position: 5
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -54,7 +49,6 @@
description: Lorem Ipsum
identifier: test6
owner_id: 6
namespace_id: 6
position: 6
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -64,7 +58,6 @@
description: Lorem Ipsum
identifier: test7
owner_id: 6
namespace_id: 6
position: 7
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -74,7 +67,6 @@
description: Lorem Ipsum
identifier: test8
owner_id: 6
namespace_id: 6
position: 8
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -84,7 +76,6 @@
description: Lorem Ipsum
identifier: test9
owner_id: 6
namespace_id: 6
position: 9
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -94,7 +85,6 @@
description: Lorem Ipsum
identifier: test10
owner_id: 6
namespace_id: 6
position: 10
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -104,7 +94,6 @@
description: Lorem Ipsum
identifier: test11
owner_id: 6
namespace_id: 6
position: 11
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -114,8 +103,8 @@
description: Lorem Ipsum
identifier: test12
owner_id: 6
namespace_id: 7
position: 12
parent_project_id: 27
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
@ -124,8 +113,8 @@
description: Lorem Ipsum
identifier: test13
owner_id: 6
namespace_id: 8
position: 13
parent_project_id: 28
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
@ -134,8 +123,8 @@
description: Lorem Ipsum
identifier: test14
owner_id: 6
namespace_id: 9
position: 14
parent_project_id: 29
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
@ -144,8 +133,8 @@
description: Lorem Ipsum
identifier: test15
owner_id: 6
namespace_id: 10
position: 15
parent_project_id: 32
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
@ -154,8 +143,8 @@
description: Lorem Ipsum
identifier: test16
owner_id: 6
namespace_id: 11
position: 16
parent_project_id: 33
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
@ -164,8 +153,8 @@
description: Lorem Ipsum
identifier: test17
owner_id: 6
namespace_id: 12
position: 17
parent_project_id: 34
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# This project is owned by user 7, and several other users have access to it via different methods.
@ -176,7 +165,6 @@
description: Lorem Ipsum
identifier: test18
owner_id: 7
namespace_id: 13
position: 18
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -186,8 +174,8 @@
description: Lorem Ipsum
identifier: test19
owner_id: 7
namespace_id: 14
position: 19
parent_project_id: 29
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# User 1 does not have access to this project
@ -197,18 +185,17 @@
description: Lorem Ipsum
identifier: test20
owner_id: 13
namespace_id: 15
position: 20
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 21
title: Test21 archived through namespace
title: Test21 archived through parent list
description: Lorem Ipsum
identifier: test21
owner_id: 1
namespace_id: 16
position: 21
parent_project_id: 22
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
@ -217,7 +204,6 @@
description: Lorem Ipsum
identifier: test22
owner_id: 1
namespace_id: 1
is_archived: 1
position: 22
updated: 2018-12-02 15:13:12
@ -228,7 +214,6 @@
description: Lorem Ipsum
identifier: test23
owner_id: 12
namespace_id: 17
position: 23
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
@ -238,28 +223,95 @@
description: Lorem Ipsum
identifier: test6
owner_id: 6
namespace_id: 6
position: 7
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 25
title: Test25 with background
title: Test25
owner_id: 6
parent_project_id: 12
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 26
title: Test26
owner_id: 6
parent_project_id: 25
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 27
title: Test27
owner_id: 6
position: 2700
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 28
title: Test28
owner_id: 6
position: 2800
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 29
title: Test29
owner_id: 6
position: 2900
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 30
title: Test30
owner_id: 6
position: 3000
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 31
title: Test31
owner_id: 6
position: 3100
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 32
title: Test32
owner_id: 6
position: 3200
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 33
title: Test33
owner_id: 6
position: 3300
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 34
title: Test34
owner_id: 6
position: 3400
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 35
title: Test35 with background
description: Lorem Ipsum
identifier: test6
owner_id: 6
namespace_id: 6
background_file_id: 1
position: 8
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 26
title: List 26 for Caldav tests
id: 36
title: Project 36 for Caldav tests
description: Lorem Ipsum
identifier: test26
identifier: test36
owner_id: 15
namespace_id: 18
position: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -3,24 +3,14 @@
entity_id: 2
user_id: 1
created: 2021-02-01 15:13:12
- id: 2
entity_type: 1 # Namespace
entity_id: 6
user_id: 6
created: 2021-02-01 15:13:12
- id: 3
entity_type: 2 # project
entity_id: 12 # belongs to namespace 7
entity_id: 12 # belongs to parent project 7
user_id: 6
created: 2021-02-01 15:13:12
- id: 4
entity_type: 3 # Task
entity_id: 22 # belongs to project 13 which belongs to namespace 8
user_id: 6
created: 2021-02-01 15:13:12
- id: 5
entity_type: 1 # Namespace
entity_id: 8
entity_id: 22 # belongs to project 13
user_id: 6
created: 2021-02-01 15:13:12
- id: 6
@ -33,3 +23,8 @@
entity_id: 26
user_id: 6
created: 2021-02-01 15:13:12
- id: 8
entity_type: 2 # Project
entity_id: 32
user_id: 6
created: 2021-02-01 15:13:12

View File

@ -13,6 +13,6 @@
reminder: 2018-12-01 01:13:44
created: 2018-12-01 01:12:04
- id: 4
task_id: 39
task_id: 40
reminder: 2023-03-04 15:00:00
created: 2018-12-01 01:12:04

View File

@ -193,7 +193,7 @@
title: 'task #21'
done: false
created_by_id: 6
project_id: 12
project_id: 32
index: 1
bucket_id: 12
created: 2018-12-01 01:12:04
@ -202,18 +202,18 @@
title: 'task #22'
done: false
created_by_id: 6
project_id: 13
project_id: 33
index: 1
bucket_id: 13
bucket_id: 36
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
- id: 23
title: 'task #23'
done: false
created_by_id: 6
project_id: 14
project_id: 34
index: 1
bucket_id: 14
bucket_id: 37
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
- id: 24
@ -357,13 +357,19 @@
updated: 2018-12-01 01:12:04
due_date: 2018-10-30 22:25:24
- id: 39
title: 'task #39'
created_by_id: 1
project_id: 25
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
- id: 40
uid: 'uid-caldav-test'
title: 'Title Caldav Test'
description: 'Description Caldav Test'
priority: 3
done: false
created_by_id: 15
project_id: 26
project_id: 36
index: 39
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04

View File

@ -1,52 +0,0 @@
- id: 1
team_id: 1
namespace_id: 3
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 2
team_id: 2
namespace_id: 3
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 3
team_id: 5
namespace_id: 7
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 4
team_id: 6
namespace_id: 8
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 5
team_id: 7
namespace_id: 9
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 6
team_id: 11
namespace_id: 14
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 7
team_id: 12
namespace_id: 14
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 8
team_id: 13
namespace_id: 14
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -52,3 +52,45 @@
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 9
team_id: 1
project_id: 28
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 10
team_id: 11
project_id: 29
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 11
team_id: 12
project_id: 29
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 12
team_id: 13
project_id: 29
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 13
team_id: 1
project_id: 32
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 14
team_id: 1
project_id: 33
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 15
team_id: 1
project_id: 34
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -11,15 +11,6 @@
- id: 4
name: testteam4_admin_on_project8
created_by_id: 1
- id: 5
name: testteam2_read_only_on_namespace7
created_by_id: 1
- id: 6
name: testteam3_write_on_namespace8
created_by_id: 1
- id: 7
name: testteam4_admin_on_namespace9
created_by_id: 1
- id: 8
name: testteam8
created_by_id: 7

View File

@ -12,6 +12,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user2@example.com'
issuer: local
default_project_id: 4
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
@ -20,6 +21,7 @@
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user3@example.com'
issuer: local
default_project_id: 4
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-

View File

@ -1,52 +0,0 @@
- id: 1
user_id: 1
namespace_id: 3
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 2
user_id: 2
namespace_id: 3
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 3
user_id: 1
namespace_id: 10
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 4
user_id: 1
namespace_id: 11
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 5
user_id: 1
namespace_id: 12
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 6
user_id: 11
namespace_id: 14
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 7
user_id: 12
namespace_id: 14
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 8
user_id: 13
namespace_id: 14
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -47,6 +47,54 @@
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 9
user_id: 1
project_id: 27
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 10
user_id: 11
project_id: 29
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 11
user_id: 12
project_id: 29
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 12
user_id: 13
project_id: 29
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 13
user_id: 1
project_id: 30
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 14
user_id: 1
project_id: 31
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 15
user_id: 1
project_id: 28
right: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 16
user_id: 1
project_id: 29
right: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 17
user_id: 15
project_id: 26
right: 0

View File

@ -17,6 +17,7 @@
package db
import (
"encoding/json"
"fmt"
"os"
"testing"
@ -93,7 +94,16 @@ func AssertExists(t *testing.T, table string, values map[string]interface{}, cus
exists, err = x.Table(table).Where(values).Get(&v)
}
assert.NoError(t, err, fmt.Sprintf("Failed to assert entries exist in db, error was: %s", err))
assert.True(t, exists, fmt.Sprintf("Entries %v do not exist in table %s", values, table))
if !exists {
all := []map[string]interface{}{}
err = x.Table(table).Find(&all)
assert.NoError(t, err, fmt.Sprintf("Failed to assert entries exist in db, error was: %s", err))
pretty, err := json.MarshalIndent(all, "", " ")
assert.NoError(t, err, fmt.Sprintf("Failed to assert entries exist in db, error was: %s", err))
t.Errorf(fmt.Sprintf("Entries %v do not exist in table %s\n\nFound entries instead: %v", values, table, string(pretty)))
}
}
// AssertMissing checks and asserts the nonexiste nce of certain entries in the db

View File

@ -17,7 +17,6 @@
package files
import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"xorm.io/xorm"
@ -33,11 +32,6 @@ func SetEngine() (err error) {
return
}
// Cache
if config.CacheEnabled.GetBool() && config.CacheType.GetString() == "redis" {
db.RegisterTableStructsForCache(GetTables())
}
return nil
}

View File

@ -29,8 +29,6 @@ import (
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/auth/openid"
"code.vikunja.io/api/pkg/modules/keyvalue"
migrator "code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/notifications"
"code.vikunja.io/api/pkg/red"
"code.vikunja.io/api/pkg/user"
)
@ -56,22 +54,10 @@ func InitEngines() {
if err != nil {
log.Fatal(err.Error())
}
err = user.InitDB()
if err != nil {
log.Fatal(err.Error())
}
err = files.SetEngine()
if err != nil {
log.Fatal(err.Error())
}
err = migrator.InitDB()
if err != nil {
log.Fatal(err.Error())
}
err = notifications.InitDB()
if err != nil {
log.Fatal(err.Error())
}
}
// FullInit initializes all kinds of things in the right order

View File

@ -17,7 +17,6 @@
package integrations
import (
"net/url"
"testing"
"code.vikunja.io/api/pkg/models"
@ -26,32 +25,27 @@ import (
)
// This tests the following behaviour:
// 1. A namespace should not be editable if it is archived.
// 1. With the exception being to un-archive it.
// 2. A project which belongs to an archived namespace cannot be edited.
// 2. A project which belongs to an archived project cannot be edited.
// 3. An archived project should not be editable.
// 1. Except for un-archiving it.
// 4. It is not possible to un-archive a project individually if its namespace is archived.
// 5. Creating new projects on an archived namespace should not work.
// 4. It is not possible to un-archive a project individually if its parent project is archived.
// 5. Creating new child projects in an archived project should not work.
// 6. Creating new tasks on an archived project should not work.
// 7. Creating new tasks on a project who's namespace is archived should not work.
// 7. Creating new tasks on a project whose parent project is archived should not work.
// 8. Editing tasks on an archived project should not work.
// 9. Editing tasks on a project who's namespace is archived should not work.
// 10. Archived namespaces should not appear in the project with all namespaces.
// 11. Archived projects should not appear in the project with all projects.
// 12. Projects who's namespace is archived should not appear in the project with all projects.
// 9. Editing tasks on a project whose parent project is archived should not work.
// 11. Archived projects should not appear in the list with all projects.
// 12. Projects whose parent project is archived should not appear in the project with all projects.
//
// All of this is tested through integration tests because it's not yet clear if this will be implemented directly
// or with some kind of middleware.
//
// Maybe the inheritance of projects from namespaces could be solved with some kind of is_archived_inherited flag -
// Maybe the inheritance of projects from parents could be solved with some kind of is_archived_inherited flag -
// that way I'd only need to implement the checking on a project level and update the flag for all projects once the
// namespace is archived. The archived flag would then be used to not accedentially unarchive projects which were
// already individually archived when the namespace was archived.
// Should still test it all though.
// project is archived. The archived flag would then be used to not accedentially unarchive projects which were
// already individually archived when the parent project was archived.
//
// Namespace 16 is archived
// Project 21 belongs to namespace 16
// Project 21 belongs to project 16
// Project 22 is archived individually
func TestArchived(t *testing.T) {
@ -62,13 +56,6 @@ func TestArchived(t *testing.T) {
},
t: t,
}
testNamespaceHandler := webHandlerTest{
user: &testuser1,
strFunc: func() handler.CObject {
return &models.Namespace{}
},
t: t,
}
testTaskHandler := webHandlerTest{
user: &testuser1,
strFunc: func() handler.CObject {
@ -105,134 +92,103 @@ func TestArchived(t *testing.T) {
t: t,
}
t.Run("namespace", func(t *testing.T) {
taskTests := func(taskID string, errCode int, t *testing.T) {
t.Run("task", func(t *testing.T) {
t.Run("edit task", func(t *testing.T) {
_, err := testTaskHandler.testUpdateWithUser(nil, map[string]string{"projecttask": taskID}, `{"title":"TestIpsum"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("delete", func(t *testing.T) {
_, err := testTaskHandler.testDeleteWithUser(nil, map[string]string{"projecttask": taskID})
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("add new labels", func(t *testing.T) {
_, err := testLabelHandler.testCreateWithUser(nil, map[string]string{"projecttask": taskID}, `{"label_id":1}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("remove lables", func(t *testing.T) {
_, err := testLabelHandler.testDeleteWithUser(nil, map[string]string{"projecttask": taskID, "label": "4"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("add assignees", func(t *testing.T) {
_, err := testAssigneeHandler.testCreateWithUser(nil, map[string]string{"projecttask": taskID}, `{"user_id":3}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("remove assignees", func(t *testing.T) {
_, err := testAssigneeHandler.testDeleteWithUser(nil, map[string]string{"projecttask": taskID, "user": "2"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("add relation", func(t *testing.T) {
_, err := testRelationHandler.testCreateWithUser(nil, map[string]string{"task": taskID}, `{"other_task_id":1,"relation_kind":"related"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("remove relation", func(t *testing.T) {
_, err := testRelationHandler.testDeleteWithUser(nil, map[string]string{"task": taskID}, `{"other_task_id":2,"relation_kind":"related"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("add comment", func(t *testing.T) {
_, err := testCommentHandler.testCreateWithUser(nil, map[string]string{"task": taskID}, `{"comment":"Lorem"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("remove comment", func(t *testing.T) {
var commentID = "15"
if taskID == "36" {
commentID = "16"
}
_, err := testCommentHandler.testDeleteWithUser(nil, map[string]string{"task": taskID, "commentid": commentID})
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
})
}
// The project belongs to an archived parent project
t.Run("archived parent project", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) {
_, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"TestIpsum","is_archived":true}`)
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"TestIpsum","is_archived":true}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
})
t.Run("no new tasks", func(t *testing.T) {
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"project": "21"}, `{"title":"Lorem"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
})
t.Run("not unarchivable", func(t *testing.T) {
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"LoremIpsum","is_archived":false}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
})
taskTests("35", models.ErrCodeProjectIsArchived, t)
})
// The project itself is archived
t.Run("archived individually", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) {
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"TestIpsum","is_archived":true}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
})
t.Run("no new tasks", func(t *testing.T) {
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"project": "22"}, `{"title":"Lorem"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
})
t.Run("unarchivable", func(t *testing.T) {
rec, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"TestIpsum","is_archived":false}`)
rec, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"LoremIpsum","is_archived":false}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
})
t.Run("no new projects", func(t *testing.T) {
_, err := testProjectHandler.testCreateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"Lorem"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
})
t.Run("should not appear in the project", func(t *testing.T) {
rec, err := testNamespaceHandler.testReadAllWithUser(nil, nil)
assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `"title":"Archived testnamespace16"`)
})
t.Run("should appear in the project if explicitly requested", func(t *testing.T) {
rec, err := testNamespaceHandler.testReadAllWithUser(url.Values{"is_archived": []string{"true"}}, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Archived testnamespace16"`)
})
})
t.Run("project", func(t *testing.T) {
taskTests := func(taskID string, errCode int, t *testing.T) {
t.Run("task", func(t *testing.T) {
t.Run("edit task", func(t *testing.T) {
_, err := testTaskHandler.testUpdateWithUser(nil, map[string]string{"projecttask": taskID}, `{"title":"TestIpsum"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("delete", func(t *testing.T) {
_, err := testTaskHandler.testDeleteWithUser(nil, map[string]string{"projecttask": taskID})
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("add new labels", func(t *testing.T) {
_, err := testLabelHandler.testCreateWithUser(nil, map[string]string{"projecttask": taskID}, `{"label_id":1}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("remove lables", func(t *testing.T) {
_, err := testLabelHandler.testDeleteWithUser(nil, map[string]string{"projecttask": taskID, "label": "4"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("add assignees", func(t *testing.T) {
_, err := testAssigneeHandler.testCreateWithUser(nil, map[string]string{"projecttask": taskID}, `{"user_id":3}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("remove assignees", func(t *testing.T) {
_, err := testAssigneeHandler.testDeleteWithUser(nil, map[string]string{"projecttask": taskID, "user": "2"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("add relation", func(t *testing.T) {
_, err := testRelationHandler.testCreateWithUser(nil, map[string]string{"task": taskID}, `{"other_task_id":1,"relation_kind":"related"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("remove relation", func(t *testing.T) {
_, err := testRelationHandler.testDeleteWithUser(nil, map[string]string{"task": taskID}, `{"other_task_id":2,"relation_kind":"related"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("add comment", func(t *testing.T) {
_, err := testCommentHandler.testCreateWithUser(nil, map[string]string{"task": taskID}, `{"comment":"Lorem"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
t.Run("remove comment", func(t *testing.T) {
var commentID = "15"
if taskID == "36" {
commentID = "16"
}
_, err := testCommentHandler.testDeleteWithUser(nil, map[string]string{"task": taskID, "commentid": commentID})
assert.Error(t, err)
assertHandlerErrorCode(t, err, errCode)
})
})
}
// The project belongs to an archived namespace
t.Run("archived namespace", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) {
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"TestIpsum","is_archived":true}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
})
t.Run("no new tasks", func(t *testing.T) {
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"project": "21"}, `{"title":"Lorem"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
})
t.Run("not unarchivable", func(t *testing.T) {
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"LoremIpsum","is_archived":false}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
})
taskTests("35", models.ErrCodeNamespaceIsArchived, t)
})
// The project itself is archived
t.Run("archived individually", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) {
_, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"TestIpsum","is_archived":true}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
})
t.Run("no new tasks", func(t *testing.T) {
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"project": "22"}, `{"title":"Lorem"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
})
t.Run("unarchivable", func(t *testing.T) {
rec, err := testProjectHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"LoremIpsum","is_archived":false,"namespace_id":1}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
})
taskTests("36", models.ErrCodeProjectIsArchived, t)
})
taskTests("36", models.ErrCodeProjectIsArchived, t)
})
}

View File

@ -28,7 +28,7 @@ const vtodo = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:List 26 for Caldav tests
X-WR-CALNAME:List 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid
@ -46,22 +46,22 @@ END:VCALENDAR`
func TestCaldav(t *testing.T) {
t.Run("Delivers VTODO for project", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "26"})
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "PRODID:-//Vikunja Todo App//EN")
assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:List 26 for Caldav tests")
assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:Project 36 for Caldav tests")
assert.Contains(t, rec.Body.String(), "BEGIN:VTODO")
assert.Contains(t, rec.Body.String(), "END:VTODO")
assert.Contains(t, rec.Body.String(), "END:VCALENDAR")
})
t.Run("Import VTODO", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "26", "task": "uid"})
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "36", "task": "uid"})
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
assert.Equal(t, 201, rec.Result().StatusCode)
})
t.Run("Export VTODO", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid-caldav-test"})
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "SUMMARY:Title Caldav Test")

View File

@ -37,7 +37,7 @@ import (
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"code.vikunja.io/web/handler"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)

View File

@ -115,33 +115,33 @@ func TestBucket(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "12"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "13"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "14"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "15"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "16"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "17"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
@ -198,33 +198,33 @@ func TestBucket(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "12", "bucket": "12"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "13", "bucket": "13"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "14", "bucket": "14"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "15", "bucket": "15"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "16", "bucket": "16"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "17", "bucket": "17"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
@ -281,33 +281,33 @@ func TestBucket(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "12"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "13"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "14"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "15"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "16"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "17"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)

View File

@ -273,10 +273,10 @@ func TestLinkSharing(t *testing.T) {
})
})
// Creating a project should always be forbidden, since users need access to a namespace to create a project
// Creating a project should always be forbidden
t.Run("Create", func(t *testing.T) {
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandlerProjectReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`)
_, err := testHandlerProjectReadOnly.testCreateWithLinkShare(nil, nil, `{"title":"Lorem"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
@ -806,284 +806,4 @@ func TestLinkSharing(t *testing.T) {
})
})
})
t.Run("Namespace", func(t *testing.T) {
testHandlerNamespaceReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.Namespace{}
},
t: t,
}
testHandlerNamespaceWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.Namespace{}
},
t: t,
}
testHandlerNamespaceAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.Namespace{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceReadOnly.testCreateWithLinkShare(nil, nil, `{"title":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceWrite.testCreateWithLinkShare(nil, nil, `{"title":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceAdmin.testCreateWithLinkShare(nil, nil, `{"title":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"title":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"title":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"title":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Right Management", func(t *testing.T) {
t.Run("Users", func(t *testing.T) {
testHandlerNamespaceUserReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.NamespaceUser{}
},
t: t,
}
testHandlerNamespaceUserWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.NamespaceUser{}
},
t: t,
}
testHandlerNamespaceUserAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.NamespaceUser{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceUserReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceUserWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceUserAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceUserReadOnly.testCreateWithLinkShare(nil, nil, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceUserWrite.testCreateWithLinkShare(nil, nil, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceUserAdmin.testCreateWithLinkShare(nil, nil, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceUserReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceUserWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceUserAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"user_id":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceUserReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceUserWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceUserAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
})
t.Run("Teams", func(t *testing.T) {
testHandlerNamespaceTeamReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.TeamNamespace{}
},
t: t,
}
testHandlerNamespaceTeamWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.TeamNamespace{}
},
t: t,
}
testHandlerNamespaceTeamAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.TeamNamespace{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceTeamReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceTeamWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceTeamAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceTeamReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceTeamWrite.testCreateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceTeamAdmin.testCreateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"team_id":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
})
})
})
}

View File

@ -40,10 +40,9 @@ func TestProject(t *testing.T) {
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Test1`)
assert.NotContains(t, rec.Body.String(), `Test2"`)
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_project
assert.Contains(t, rec.Body.String(), `Test4`) // Shared via namespace
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_project
assert.Contains(t, rec.Body.String(), `Test12`) // Shared via parent project
assert.NotContains(t, rec.Body.String(), `Test5`)
assert.NotContains(t, rec.Body.String(), `Test21`) // Archived through namespace
assert.NotContains(t, rec.Body.String(), `Test22`) // Archived directly
})
t.Run("Search", func(t *testing.T) {
@ -60,10 +59,10 @@ func TestProject(t *testing.T) {
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Test1`)
assert.NotContains(t, rec.Body.String(), `Test2"`)
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_project
assert.Contains(t, rec.Body.String(), `Test4`) // Shared via namespace
assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_project
assert.Contains(t, rec.Body.String(), `Test12`) // Shared via parent project
assert.NotContains(t, rec.Body.String(), `Test5`)
assert.Contains(t, rec.Body.String(), `Test21`) // Archived through namespace
assert.Contains(t, rec.Body.String(), `Test21`) // Archived through project
assert.Contains(t, rec.Body.String(), `Test22`) // Archived directly
})
})
@ -76,7 +75,7 @@ func TestProject(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"owner":{"id":1,"name":"","username":"user1",`)
assert.NotContains(t, rec.Body.String(), `"owner":{"id":2,"name":"","username":"user2",`)
assert.NotContains(t, rec.Body.String(), `"tasks":`)
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right")) // User 1 is owner so they should have admin rights.
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right")) // User 1 is owner, so they should have admin rights.
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "9999"})
@ -129,38 +128,38 @@ func TestProject(t *testing.T) {
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "12"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test12"`)
assert.Equal(t, "0", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "13"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test13"`)
assert.Equal(t, "1", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "14"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test14"`)
assert.Equal(t, "2", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "15"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test15"`)
assert.Equal(t, "0", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "16"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test16"`)
assert.Equal(t, "1", rec.Result().Header.Get("x-max-right"))
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "17"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test17"`)
@ -171,7 +170,7 @@ func TestProject(t *testing.T) {
t.Run("Update", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
// Check the project was loaded successfully afterwards, see testReadOneWithUser
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
// The description should not be updated but returned correctly
@ -183,7 +182,7 @@ func TestProject(t *testing.T) {
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Normal with updating the description", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet","namespace_id":1}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`)
@ -211,12 +210,12 @@ func TestProject(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "7"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "7"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "8"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "8"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
@ -227,44 +226,44 @@ func TestProject(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "10"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "10"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "11"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "11"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "12"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "13"}, `{"title":"TestLoremIpsum","namespace_id":8}`)
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "13"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "14"}, `{"title":"TestLoremIpsum","namespace_id":9}`)
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "14"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "15"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "16"}, `{"title":"TestLoremIpsum","namespace_id":11}`)
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "16"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "17"}, `{"title":"TestLoremIpsum","namespace_id":12}`)
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "17"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
@ -320,33 +319,33 @@ func TestProject(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "12"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "13"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "14"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "15"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "16"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "17"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
@ -356,7 +355,7 @@ func TestProject(t *testing.T) {
t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
// Check the project was loaded successfully after update, see testReadOneWithUser
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem"}`)
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":""`)
@ -364,52 +363,50 @@ func TestProject(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `"tasks":`)
})
t.Run("Normal with description", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","description":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
assert.NotContains(t, rec.Body.String(), `"tasks":`)
})
t.Run("Nonexisting Namespace", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`)
t.Run("Nonexisting parent project", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":99999}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Empty title", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":""}`)
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":""}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
})
t.Run("Title too long", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`)
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(1|250)")
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// Owned by user13
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "15"}, `{"title":"Lorem"}`)
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":20}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "7"}, `{"title":"Lorem"}`)
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":32}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "8"}, `{"title":"Lorem"}`)
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":33}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":""`)
assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
assert.NotContains(t, rec.Body.String(), `"tasks":`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "9"}, `{"title":"Lorem"}`)
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":34}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":""`)
@ -417,21 +414,21 @@ func TestProject(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `"tasks":`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "10"}, `{"title":"Lorem"}`)
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":9}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "11"}, `{"title":"Lorem"}`)
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":10}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":""`)
assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
assert.NotContains(t, rec.Body.String(), `"tasks":`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "12"}, `{"title":"Lorem"}`)
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "12"}, `{"title":"Lorem","parent_project_id":11}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":""`)

View File

@ -113,49 +113,49 @@ 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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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"}}]`)
})
t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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":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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_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,"cover_image_attachment_id":0,"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":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
})
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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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"}}]`)
})
// 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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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"}}`)
})
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)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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":6,"title":"task #6 lower due date`)
assert.Contains(t, rec.Body.String(), `[{"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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":6,"title":"task #6 lower due date`)
})
// Due date without unix suffix
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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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"}}`)
})
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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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"}}`)
})
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)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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":6,"title":"task #6 lower due date`)
assert.Contains(t, rec.Body.String(), `[{"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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":6,"title":"task #6 lower due date`)
})
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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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"}}`)
})
t.Run("invalid sort parameter", func(t *testing.T) {
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
@ -171,10 +171,10 @@ func TestTaskCollection(t *testing.T) {
// Invalid parameter should not sort at all
rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, urlParams)
assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`)
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"repeat_after":0,"repeat_mode":0,"priority":1`)
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
})
})
t.Run("Filter", func(t *testing.T) {
@ -366,42 +366,42 @@ 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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_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":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_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":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"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)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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":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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_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,"cover_image_attachment_id":0,"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":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
})
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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_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":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_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":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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":6,"title":"task #6 lower due date`)
assert.Contains(t, rec.Body.String(), `[{"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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":6,"title":"task #6 lower due date`)
})
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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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,"reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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","reminders":null,"project_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,"cover_image_attachment_id":0,"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
rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, nil)
assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`)
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"repeat_after":0,"repeat_mode":0,"priority":1`)
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
})
})
t.Run("Filter", func(t *testing.T) {

View File

@ -101,33 +101,33 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "21", "commentid": "9"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team 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`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
t.Run("Shared Via Parent Project Team 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`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "24", "commentid": "12"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project User 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`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
t.Run("Shared Via Parent Project User 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`)
@ -184,33 +184,33 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "21", "commentid": "9"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team 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`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
t.Run("Shared Via Parent Project Team 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`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "24", "commentid": "12"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project User 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`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
t.Run("Shared Via Parent Project User 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`)
@ -267,33 +267,33 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "21"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "22"}, `{"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) {
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "23"}, `{"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) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "24"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "25"}, `{"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) {
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"task": "26"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)

View File

@ -95,27 +95,6 @@ func TestTask(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"due_date":"0001-01-01T00:00:00Z"`)
assert.NotContains(t, rec.Body.String(), `"due_date":"2020-02-10T10:00:00Z"`)
})
// Deprecated: Remove if ReminderDates is removed
t.Run("ReminderDates", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"reminder_dates": ["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminder_dates":["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]`)
assert.NotContains(t, rec.Body.String(), `"reminder_dates": null`)
})
// Deprecated: Remove if ReminderDates is removed
t.Run("ReminderDates unset to empty array", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminder_dates": []}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminder_dates":null`)
assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`)
})
// Deprecated: Remove if ReminderDates is removed
t.Run("ReminderDates unset to null", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminder_dates": null}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminder_dates":null`)
assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`)
})
t.Run("Reminders", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"reminders": [{"reminder": "2020-02-10T10:00:00Z"},{"reminder": "2020-02-11T10:00:00Z"}]}`)
assert.NoError(t, err)
@ -133,7 +112,7 @@ func TestTask(t *testing.T) {
t.Run("Reminders unset to null", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminders": null}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminder_dates":null`)
assert.Contains(t, rec.Body.String(), `"reminders":null`)
assert.NotContains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"`)
})
t.Run("Repeat after", func(t *testing.T) {
@ -277,33 +256,33 @@ func TestTask(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "21"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "22"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "23"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "24"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "25"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "26"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
@ -395,33 +374,33 @@ func TestTask(t *testing.T) {
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "21"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "22"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "23"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "24"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "25"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"projecttask": "26"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
@ -478,33 +457,33 @@ func TestTask(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "12"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "13"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "14"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "15"}, `{"title":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "16"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "17"}, `{"title":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)

View File

@ -34,9 +34,6 @@ const (
// UserCountKey is the name of the key we use to store total users in redis
UserCountKey = `usercount`
// NamespaceCountKey is the name of the key we use to store the amount of total namespaces in redis
NamespaceCountKey = `namespacecount`
// TaskCountKey is the name of the key we use to store the amount of total tasks in redis
TaskCountKey = `taskcount`
@ -89,18 +86,6 @@ func InitMetrics() {
log.Criticalf("Could not register metrics for %s: %s", UserCountKey, err)
}
// Register total Namespaces count metric
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_namespace_count",
Help: "The total number of namespaces on this instance",
}, func() float64 {
count, _ := GetCount(NamespaceCountKey)
return float64(count)
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", NamespaceCountKey, err)
}
// Register total Tasks count metric
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_task_count",

View File

@ -0,0 +1,310 @@
// 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 (
"time"
"code.vikunja.io/api/pkg/log"
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
type projects20221228112131 struct {
// This is the one new property
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
// Those only exist to make the migration independent of future changes
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"project"`
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
Description string `xorm:"longtext null" json:"description"`
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
Created time.Time `xorm:"created not null" json:"created"`
Updated time.Time `xorm:"updated not null" json:"updated"`
NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
}
func (projects20221228112131) TableName() string {
return "projects"
}
type namespace20221228112131 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
Description string `xorm:"longtext null" json:"description"`
OwnerID int64 `xorm:"bigint not null INDEX" json:"-"`
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
Created time.Time `xorm:"created not null" json:"created"`
Updated time.Time `xorm:"updated not null" json:"updated"`
}
func (namespace20221228112131) TableName() string {
return "namespaces"
}
type teamNamespace20221228112131 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
TeamID int64 `xorm:"bigint not null INDEX" json:"team_id" param:"team"`
NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
Right int `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
Created time.Time `xorm:"created not null" json:"created"`
Updated time.Time `xorm:"updated not null" json:"updated"`
}
func (teamNamespace20221228112131) TableName() string {
return "team_namespaces"
}
type teamProject20221228112131 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
TeamID int64 `xorm:"bigint not null INDEX" json:"team_id" param:"team"`
ProjectID int64 `xorm:"bigint not null INDEX" json:"-" param:"project"`
Right int `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
Created time.Time `xorm:"created not null" json:"created"`
Updated time.Time `xorm:"updated not null" json:"updated"`
}
func (teamProject20221228112131) TableName() string {
return "team_projects"
}
type namespaceUser20221228112131 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
UserID int64 `xorm:"bigint not null INDEX" json:"-"`
NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
Right int `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
Created time.Time `xorm:"created not null" json:"created"`
Updated time.Time `xorm:"updated not null" json:"updated"`
}
func (namespaceUser20221228112131) TableName() string {
return "users_namespaces"
}
type projectUser20221228112131 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
UserID int64 `xorm:"bigint not null INDEX" json:"-"`
ProjectID int64 `xorm:"bigint not null INDEX" json:"-" param:"project"`
Right int `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
Created time.Time `xorm:"created not null" json:"created"`
Updated time.Time `xorm:"updated not null" json:"updated"`
}
func (projectUser20221228112131) TableName() string {
return "users_projects"
}
const sqliteRemoveNamespaceColumn20221228112131 = `
create table projects_dg_tmp
(
id INTEGER not null
primary key autoincrement,
title TEXT not null,
description TEXT,
identifier TEXT,
hex_color TEXT,
owner_id INTEGER not null,
is_archived INTEGER default 0 not null,
background_file_id INTEGER,
background_blur_hash TEXT,
position REAL,
created DATETIME not null,
updated DATETIME not null,
parent_project_id INTEGER
);
insert into projects_dg_tmp(id, title, description, identifier, hex_color, owner_id, is_archived, background_file_id,
background_blur_hash, position, created, updated, parent_project_id)
select id,
title,
description,
identifier,
hex_color,
owner_id,
is_archived,
background_file_id,
background_blur_hash,
position,
created,
updated,
parent_project_id
from projects;
drop table projects;
alter table projects_dg_tmp
rename to projects;
create index IDX_lists_owner_id
on projects (owner_id);
create index IDX_projects_parent_project_id
on projects (parent_project_id);
create unique index UQE_lists_id
on projects (id);
`
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20221228112131",
Description: "make projects nestable",
Migrate: func(tx *xorm.Engine) error {
err := tx.Sync2(projects20221228112131{})
if err != nil {
return err
}
allNamespaces := []*namespace20221228112131{}
err = tx.Find(&allNamespaces)
if err != nil {
return err
}
// namespace id is the key
namespacesToProjects := make(map[int64]*projects20221228112131)
for _, n := range allNamespaces {
p := &projects20221228112131{
Title: n.Title,
Description: n.Description,
OwnerID: n.OwnerID,
HexColor: n.HexColor,
IsArchived: n.IsArchived,
Created: n.Created,
Updated: n.Updated,
}
_, err = tx.Insert(p)
if err != nil {
return err
}
namespacesToProjects[n.ID] = p
}
err = setParentProject(tx, namespacesToProjects)
if err != nil {
return err
}
err = setTeamNamespacesShare(tx, namespacesToProjects)
if err != nil {
return err
}
err = setUserNamespacesShare(tx, namespacesToProjects)
if err != nil {
return err
}
return removeNamespaceLeftovers(tx)
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}
func setParentProject(tx *xorm.Engine, namespacesToProjects map[int64]*projects20221228112131) error {
for namespaceID, project := range namespacesToProjects {
_, err := tx.Where("namespace_id = ?", namespaceID).
Update(&projects20221228112131{
ParentProjectID: project.ID,
})
if err != nil {
return err
}
}
return nil
}
func setTeamNamespacesShare(tx *xorm.Engine, namespacesToProjects map[int64]*projects20221228112131) error {
teamNamespaces := []*teamNamespace20221228112131{}
err := tx.Find(&teamNamespaces)
if err != nil {
return err
}
for _, tn := range teamNamespaces {
if _, exists := namespacesToProjects[tn.NamespaceID]; !exists {
log.Warningf("Namespace %d does not exist but is shared with team %d - this is probably caused by an old share which was not properly deleted.", tn.NamespaceID, tn.TeamID)
continue
}
_, err = tx.Insert(&teamProject20221228112131{
TeamID: tn.TeamID,
Right: tn.Right,
Created: tn.Created,
Updated: tn.Updated,
ProjectID: namespacesToProjects[tn.NamespaceID].ID,
})
if err != nil {
return err
}
}
return nil
}
func setUserNamespacesShare(tx *xorm.Engine, namespacesToProjects map[int64]*projects20221228112131) error {
userNamespace := []*namespaceUser20221228112131{}
err := tx.Find(&userNamespace)
if err != nil {
return err
}
for _, un := range userNamespace {
if _, exists := namespacesToProjects[un.NamespaceID]; !exists {
log.Warningf("Namespace %d does not exist but is shared with user %d - this is probably caused by an old share which was not properly deleted.", un.NamespaceID, un.UserID)
continue
}
_, err = tx.Insert(&projectUser20221228112131{
UserID: un.UserID,
Right: un.Right,
Created: un.Created,
Updated: un.Updated,
ProjectID: namespacesToProjects[un.NamespaceID].ID,
})
if err != nil {
return err
}
}
return nil
}
func removeNamespaceLeftovers(tx *xorm.Engine) error {
err := tx.DropTables("namespaces", "team_namespaces", "users_namespaces")
if err != nil {
return err
}
if tx.Dialect().URI().DBType == schemas.SQLITE {
_, err := tx.Exec(sqliteRemoveNamespaceColumn20221228112131)
return err
}
return dropTableColum(tx, "projects", "namespace_id")
}

View File

@ -14,27 +14,30 @@
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
package migration
import (
"code.vikunja.io/web"
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
// CanCreate checks if one can create a new team <-> namespace relation
func (tn *TeamNamespace) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
n := &Namespace{ID: tn.NamespaceID}
return n.IsAdmin(s, a)
type users20230611170341 struct {
FrontendSettings interface{} `xorm:"json null" json:"-"`
}
// CanDelete checks if a user can remove a team from a namespace. Only namespace admins can do that.
func (tn *TeamNamespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
n := &Namespace{ID: tn.NamespaceID}
return n.IsAdmin(s, a)
func (users20230611170341) TableName() string {
return "users"
}
// CanUpdate checks if a user can update a team from a Only namespace admins can do that.
func (tn *TeamNamespace) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
n := &Namespace{ID: tn.NamespaceID}
return n.IsAdmin(s, a)
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20230611170341",
Description: "",
Migrate: func(tx *xorm.Engine) error {
return tx.Sync2(users20230611170341{})
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -18,7 +18,8 @@ package models
import (
"code.vikunja.io/web"
"github.com/imdario/mergo"
"dario.cat/mergo"
"xorm.io/xorm"
)

View File

@ -19,6 +19,7 @@ package models
import (
"fmt"
"net/http"
"strings"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/web"
@ -255,65 +256,154 @@ func (err ErrProjectIsArchived) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeProjectIsArchived, Message: "This project is archived. Editing or creating new tasks is not possible."}
}
// ErrProjectCannotBelongToAPseudoNamespace represents an error where a project cannot belong to a pseudo namespace
type ErrProjectCannotBelongToAPseudoNamespace struct {
ProjectID int64
NamespaceID int64
// ErrProjectCannotBelongToAPseudoParentProject represents an error where a project cannot belong to a pseudo project
type ErrProjectCannotBelongToAPseudoParentProject struct {
ProjectID int64
ParentProjectID int64
}
// IsErrProjectCannotBelongToAPseudoNamespace checks if an error is a project is archived error.
func IsErrProjectCannotBelongToAPseudoNamespace(err error) bool {
_, ok := err.(*ErrProjectCannotBelongToAPseudoNamespace)
// IsErrProjectCannotBelongToAPseudoParentProject checks if an error is a project is archived error.
func IsErrProjectCannotBelongToAPseudoParentProject(err error) bool {
_, ok := err.(*ErrProjectCannotBelongToAPseudoParentProject)
return ok
}
func (err *ErrProjectCannotBelongToAPseudoNamespace) Error() string {
return fmt.Sprintf("Project cannot belong to a pseudo namespace [ProjectID: %d, NamespaceID: %d]", err.ProjectID, err.NamespaceID)
func (err *ErrProjectCannotBelongToAPseudoParentProject) Error() string {
return fmt.Sprintf("Project cannot belong to a pseudo parent project [ProjectID: %d, ParentProjectID: %d]", err.ProjectID, err.ParentProjectID)
}
// ErrCodeProjectCannotBelongToAPseudoNamespace holds the unique world-error code of this error
const ErrCodeProjectCannotBelongToAPseudoNamespace = 3009
// ErrCodeProjectCannotBelongToAPseudoParentProject holds the unique world-error code of this error
const ErrCodeProjectCannotBelongToAPseudoParentProject = 3009
// HTTPError holds the http error description
func (err *ErrProjectCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
func (err *ErrProjectCannotBelongToAPseudoParentProject) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeProjectCannotBelongToAPseudoNamespace,
Message: "This project cannot belong a dynamically generated namespace.",
Code: ErrCodeProjectCannotBelongToAPseudoParentProject,
Message: "This project cannot belong a dynamically generated project.",
}
}
// ErrProjectMustBelongToANamespace represents an error where a project must belong to a namespace
type ErrProjectMustBelongToANamespace struct {
ProjectID int64
NamespaceID int64
// ErrProjectCannotBeChildOfItself represents an error where a project cannot become a child of its own
type ErrProjectCannotBeChildOfItself struct {
ProjectID int64
}
// IsErrProjectMustBelongToANamespace checks if an error is a project must belong to a namespace error.
func IsErrProjectMustBelongToANamespace(err error) bool {
_, ok := err.(*ErrProjectMustBelongToANamespace)
// IsErrProjectCannotBeChildOfItsOwn checks if an error is a project is archived error.
func IsErrProjectCannotBeChildOfItsOwn(err error) bool {
_, ok := err.(*ErrProjectCannotBeChildOfItself)
return ok
}
func (err *ErrProjectMustBelongToANamespace) Error() string {
return fmt.Sprintf("Project must belong to a namespace [ProjectID: %d, NamespaceID: %d]", err.ProjectID, err.NamespaceID)
func (err *ErrProjectCannotBeChildOfItself) Error() string {
return fmt.Sprintf("Project cannot be made a child of itself [ProjectID: %d]", err.ProjectID)
}
// ErrCodeProjectMustBelongToANamespace holds the unique world-error code of this error
const ErrCodeProjectMustBelongToANamespace = 3010
// ErrCodeProjectCannotBeChildOfItself holds the unique world-error code of this error
const ErrCodeProjectCannotBeChildOfItself = 3010
// HTTPError holds the http error description
func (err *ErrProjectMustBelongToANamespace) HTTPError() web.HTTPError {
func (err *ErrProjectCannotBeChildOfItself) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeProjectMustBelongToANamespace,
Message: "This project must belong to a namespace.",
Code: ErrCodeProjectCannotBeChildOfItself,
Message: "This project cannot be a child of itself.",
}
}
// ================
// Project task errors
// ================
// ErrProjectCannotHaveACyclicRelationship represents an error where a project cannot have a cyclic parent relationship
type ErrProjectCannotHaveACyclicRelationship struct {
ProjectID int64
CycleIDs []int64
}
// IsErrProjectCannotHaveACyclicRelationship checks if an error is a project is archived error.
func IsErrProjectCannotHaveACyclicRelationship(err error) bool {
_, ok := err.(*ErrProjectCannotHaveACyclicRelationship)
return ok
}
func (err *ErrProjectCannotHaveACyclicRelationship) CycleString() string {
var cycle string
for _, projectID := range err.CycleIDs {
cycle += fmt.Sprintf("%d -> ", projectID)
}
return strings.TrimSuffix(cycle, " -> ")
}
func (err *ErrProjectCannotHaveACyclicRelationship) Error() string {
return fmt.Sprintf("Project cannot have a cyclic relationship [ProjectID: %d]", err.ProjectID)
}
// ErrCodeProjectCannotHaveACyclicRelationship holds the unique world-error code of this error
const ErrCodeProjectCannotHaveACyclicRelationship = 3011
// HTTPError holds the http error description
func (err *ErrProjectCannotHaveACyclicRelationship) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeProjectCannotHaveACyclicRelationship,
Message: "This project cannot have a cyclic relationship to a parent project.",
}
}
// ErrCannotDeleteDefaultProject represents an error where the default project is being deleted
type ErrCannotDeleteDefaultProject struct {
ProjectID int64
}
// IsErrCannotDeleteDefaultProject checks if an error is a project is archived error.
func IsErrCannotDeleteDefaultProject(err error) bool {
_, ok := err.(*ErrCannotDeleteDefaultProject)
return ok
}
func (err *ErrCannotDeleteDefaultProject) Error() string {
return fmt.Sprintf("Default project cannot be deleted [ProjectID: %d]", err.ProjectID)
}
// ErrCodeCannotDeleteDefaultProject holds the unique world-error code of this error
const ErrCodeCannotDeleteDefaultProject = 3012
// HTTPError holds the http error description
func (err *ErrCannotDeleteDefaultProject) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeCannotDeleteDefaultProject,
Message: "This project cannot be deleted because it is the default project of a user.",
}
}
// ErrCannotArchiveDefaultProject represents an error where the default project is being deleted
type ErrCannotArchiveDefaultProject struct {
ProjectID int64
}
// IsErrCannotArchiveDefaultProject checks if an error is a project is archived error.
func IsErrCannotArchiveDefaultProject(err error) bool {
_, ok := err.(*ErrCannotArchiveDefaultProject)
return ok
}
func (err *ErrCannotArchiveDefaultProject) Error() string {
return fmt.Sprintf("Default project cannot be archived [ProjectID: %d]", err.ProjectID)
}
// ErrCodeCannotArchiveDefaultProject holds the unique world-error code of this error
const ErrCodeCannotArchiveDefaultProject = 3013
// HTTPError holds the http error description
func (err *ErrCannotArchiveDefaultProject) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeCannotArchiveDefaultProject,
Message: "This project cannot be archived because it is the default project of a user.",
}
}
// ==============
// Task errors
// ==============
// ErrTaskCannotBeEmpty represents a "ErrProjectDoesNotExist" kind of error. Used if the project does not exist.
type ErrTaskCannotBeEmpty struct{}
@ -902,176 +992,6 @@ func (err ErrReminderRelativeToMissing) HTTPError() web.HTTPError {
}
}
// =================
// Namespace errors
// =================
// ErrNamespaceDoesNotExist represents a "ErrNamespaceDoesNotExist" kind of error. Used if the namespace does not exist.
type ErrNamespaceDoesNotExist struct {
ID int64
}
// IsErrNamespaceDoesNotExist checks if an error is a ErrNamespaceDoesNotExist.
func IsErrNamespaceDoesNotExist(err error) bool {
_, ok := err.(ErrNamespaceDoesNotExist)
return ok
}
func (err ErrNamespaceDoesNotExist) Error() string {
return fmt.Sprintf("Namespace does not exist [ID: %d]", err.ID)
}
// ErrCodeNamespaceDoesNotExist holds the unique world-error code of this error
const ErrCodeNamespaceDoesNotExist = 5001
// HTTPError holds the http error description
func (err ErrNamespaceDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeNamespaceDoesNotExist, Message: "Namespace not found."}
}
// ErrUserDoesNotHaveAccessToNamespace represents an error, where the user is not the owner of that namespace (used i.e. when deleting a namespace)
type ErrUserDoesNotHaveAccessToNamespace struct {
NamespaceID int64
UserID int64
}
// IsErrUserDoesNotHaveAccessToNamespace checks if an error is a ErrNamespaceDoesNotExist.
func IsErrUserDoesNotHaveAccessToNamespace(err error) bool {
_, ok := err.(ErrUserDoesNotHaveAccessToNamespace)
return ok
}
func (err ErrUserDoesNotHaveAccessToNamespace) Error() string {
return fmt.Sprintf("User does not have access to the namespace [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
}
// ErrCodeUserDoesNotHaveAccessToNamespace holds the unique world-error code of this error
const ErrCodeUserDoesNotHaveAccessToNamespace = 5003
// HTTPError holds the http error description
func (err ErrUserDoesNotHaveAccessToNamespace) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToNamespace, Message: "This user does not have access to the namespace."}
}
// ErrNamespaceNameCannotBeEmpty represents an error, where a namespace name is empty.
type ErrNamespaceNameCannotBeEmpty struct {
NamespaceID int64
UserID int64
}
// IsErrNamespaceNameCannotBeEmpty checks if an error is a ErrNamespaceDoesNotExist.
func IsErrNamespaceNameCannotBeEmpty(err error) bool {
_, ok := err.(ErrNamespaceNameCannotBeEmpty)
return ok
}
func (err ErrNamespaceNameCannotBeEmpty) Error() string {
return fmt.Sprintf("Namespace name cannot be empty [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
}
// ErrCodeNamespaceNameCannotBeEmpty holds the unique world-error code of this error
const ErrCodeNamespaceNameCannotBeEmpty = 5006
// HTTPError holds the http error description
func (err ErrNamespaceNameCannotBeEmpty) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeNamespaceNameCannotBeEmpty, Message: "The namespace name cannot be empty."}
}
// ErrNeedToHaveNamespaceReadAccess represents an error, where the user is not the owner of that namespace (used i.e. when deleting a namespace)
type ErrNeedToHaveNamespaceReadAccess struct {
NamespaceID int64
UserID int64
}
// IsErrNeedToHaveNamespaceReadAccess checks if an error is a ErrNamespaceDoesNotExist.
func IsErrNeedToHaveNamespaceReadAccess(err error) bool {
_, ok := err.(ErrNeedToHaveNamespaceReadAccess)
return ok
}
func (err ErrNeedToHaveNamespaceReadAccess) Error() string {
return fmt.Sprintf("User does not have access to that namespace [NamespaceID: %d, UserID: %d]", err.NamespaceID, err.UserID)
}
// ErrCodeNeedToHaveNamespaceReadAccess holds the unique world-error code of this error
const ErrCodeNeedToHaveNamespaceReadAccess = 5009
// HTTPError holds the http error description
func (err ErrNeedToHaveNamespaceReadAccess) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeNeedToHaveNamespaceReadAccess, Message: "You need to have namespace read access to do this."}
}
// ErrTeamDoesNotHaveAccessToNamespace represents an error, where the Team is not the owner of that namespace (used i.e. when deleting a namespace)
type ErrTeamDoesNotHaveAccessToNamespace struct {
NamespaceID int64
TeamID int64
}
// IsErrTeamDoesNotHaveAccessToNamespace checks if an error is a ErrNamespaceDoesNotExist.
func IsErrTeamDoesNotHaveAccessToNamespace(err error) bool {
_, ok := err.(ErrTeamDoesNotHaveAccessToNamespace)
return ok
}
func (err ErrTeamDoesNotHaveAccessToNamespace) Error() string {
return fmt.Sprintf("Team does not have access to that namespace [NamespaceID: %d, TeamID: %d]", err.NamespaceID, err.TeamID)
}
// ErrCodeTeamDoesNotHaveAccessToNamespace holds the unique world-error code of this error
const ErrCodeTeamDoesNotHaveAccessToNamespace = 5010
// HTTPError holds the http error description
func (err ErrTeamDoesNotHaveAccessToNamespace) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToNamespace, Message: "You need to have access to this namespace to do this."}
}
// ErrUserAlreadyHasNamespaceAccess represents an error where a user already has access to a namespace
type ErrUserAlreadyHasNamespaceAccess struct {
UserID int64
NamespaceID int64
}
// IsErrUserAlreadyHasNamespaceAccess checks if an error is ErrUserAlreadyHasNamespaceAccess.
func IsErrUserAlreadyHasNamespaceAccess(err error) bool {
_, ok := err.(ErrUserAlreadyHasNamespaceAccess)
return ok
}
func (err ErrUserAlreadyHasNamespaceAccess) Error() string {
return fmt.Sprintf("User already has access to that namespace. [User ID: %d, Namespace ID: %d]", err.UserID, err.NamespaceID)
}
// ErrCodeUserAlreadyHasNamespaceAccess holds the unique world-error code of this error
const ErrCodeUserAlreadyHasNamespaceAccess = 5011
// HTTPError holds the http error description
func (err ErrUserAlreadyHasNamespaceAccess) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeUserAlreadyHasNamespaceAccess, Message: "This user already has access to this namespace."}
}
// ErrNamespaceIsArchived represents an error where a namespace is archived
type ErrNamespaceIsArchived struct {
NamespaceID int64
}
// IsErrNamespaceIsArchived checks if an error is a .
func IsErrNamespaceIsArchived(err error) bool {
_, ok := err.(ErrNamespaceIsArchived)
return ok
}
func (err ErrNamespaceIsArchived) Error() string {
return fmt.Sprintf("Namespace is archived [NamespaceID: %d]", err.NamespaceID)
}
// ErrCodeNamespaceIsArchived holds the unique world-error code of this error
const ErrCodeNamespaceIsArchived = 5012
// HTTPError holds the http error description
func (err ErrNamespaceIsArchived) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeNamespaceIsArchived, Message: "This namespaces is archived. Editing or creating new projects is not possible."}
}
// ============
// Team errors
// ============
@ -1081,7 +1001,7 @@ type ErrTeamNameCannotBeEmpty struct {
TeamID int64
}
// IsErrTeamNameCannotBeEmpty checks if an error is a ErrNamespaceDoesNotExist.
// IsErrTeamNameCannotBeEmpty checks if an error is a ErrTeamNameCannotBeEmpty.
func IsErrTeamNameCannotBeEmpty(err error) bool {
_, ok := err.(ErrTeamNameCannotBeEmpty)
return ok
@ -1122,7 +1042,7 @@ func (err ErrTeamDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "This team does not exist."}
}
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a project/namespace
// ErrTeamAlreadyHasAccess represents an error where a team already has access to a project
type ErrTeamAlreadyHasAccess struct {
TeamID int64
ID int64
@ -1222,7 +1142,7 @@ func (err ErrTeamDoesNotHaveAccessToProject) HTTPError() web.HTTPError {
// User <-> Project errors
// ====================
// ErrUserAlreadyHasAccess represents an error where a user already has access to a project/namespace
// ErrUserAlreadyHasAccess represents an error where a user already has access to a project
type ErrUserAlreadyHasAccess struct {
UserID int64
ProjectID int64

View File

@ -21,16 +21,6 @@ import (
"code.vikunja.io/web"
)
// DataExportRequestEvent represents a DataExportRequestEvent event
type DataExportRequestEvent struct {
User *user.User
}
// Name defines the name for DataExportRequestEvent
func (t *DataExportRequestEvent) Name() string {
return "user.export.request"
}
/////////////////
// Task Events //
/////////////////
@ -176,46 +166,9 @@ func (t *TaskRelationDeletedEvent) Name() string {
return "task.relation.deleted"
}
//////////////////////
// Namespace Events //
//////////////////////
// NamespaceCreatedEvent represents an event where a namespace has been created
type NamespaceCreatedEvent struct {
Namespace *Namespace
Doer web.Auth
}
// Name defines the name for NamespaceCreatedEvent
func (n *NamespaceCreatedEvent) Name() string {
return "namespace.created"
}
// NamespaceUpdatedEvent represents an event where a namespace has been updated
type NamespaceUpdatedEvent struct {
Namespace *Namespace
Doer web.Auth
}
// Name defines the name for NamespaceUpdatedEvent
func (n *NamespaceUpdatedEvent) Name() string {
return "namespace.updated"
}
// NamespaceDeletedEvent represents a NamespaceDeletedEvent event
type NamespaceDeletedEvent struct {
Namespace *Namespace
Doer web.Auth
}
// TopicName defines the name for NamespaceDeletedEvent
func (t *NamespaceDeletedEvent) Name() string {
return "namespace.deleted"
}
/////////////////
////////////////////
// Project Events //
/////////////////
////////////////////
// ProjectCreatedEvent represents an event where a project has been created
type ProjectCreatedEvent struct {
@ -278,30 +231,6 @@ func (l *ProjectSharedWithTeamEvent) Name() string {
return "project.shared.team"
}
// NamespaceSharedWithUserEvent represents an event where a namespace has been shared with a user
type NamespaceSharedWithUserEvent struct {
Namespace *Namespace
User *user.User
Doer web.Auth
}
// Name defines the name for NamespaceSharedWithUserEvent
func (n *NamespaceSharedWithUserEvent) Name() string {
return "namespace.shared.user"
}
// NamespaceSharedWithTeamEvent represents an event where a namespace has been shared with a team
type NamespaceSharedWithTeamEvent struct {
Namespace *Namespace
Team *Team
Doer web.Auth
}
// Name defines the name for NamespaceSharedWithTeamEvent
func (n *NamespaceSharedWithTeamEvent) Name() string {
return "namespace.shared.team"
}
/////////////////
// Team Events //
/////////////////

View File

@ -57,12 +57,12 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
defer dumpWriter.Close()
// Get the data
err = exportProjectsAndTasks(s, u, dumpWriter)
taskIDs, err := exportProjectsAndTasks(s, u, dumpWriter)
if err != nil {
return err
}
// Task attachment files
err = exportTaskAttachments(s, u, dumpWriter)
err = exportTaskAttachments(s, dumpWriter, taskIDs)
if err != nil {
return err
}
@ -121,59 +121,44 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
})
}
func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
namspaces, _, _, err := (&Namespace{IsArchived: true}).ReadAll(s, u, "", -1, 0)
if err != nil {
return err
}
namespaceIDs := []int64{}
namespaces := []*NamespaceWithProjectsAndTasks{}
projectMap := make(map[int64]*ProjectWithTasksAndBuckets)
projectIDs := []int64{}
for _, n := range namspaces.([]*NamespaceWithProjects) {
if n.ID < 1 {
// Don't include filters
continue
}
nn := &NamespaceWithProjectsAndTasks{
Namespace: n.Namespace,
Projects: []*ProjectWithTasksAndBuckets{},
}
for _, l := range n.Projects {
ll := &ProjectWithTasksAndBuckets{
Project: *l,
BackgroundFileID: l.BackgroundFileID,
Tasks: []*TaskWithComments{},
}
nn.Projects = append(nn.Projects, ll)
projectMap[l.ID] = ll
projectIDs = append(projectIDs, l.ID)
}
namespaceIDs = append(namespaceIDs, n.ID)
namespaces = append(namespaces, nn)
}
if len(namespaceIDs) == 0 {
return nil
}
func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (taskIDs []int64, err error) {
// Get all projects
projects, err := getProjectsForNamespaces(s, namespaceIDs, true)
rawProjects, _, _, err := getRawProjectsForUser(
s,
&projectOptions{
search: "",
user: u,
page: 0,
perPage: -1,
getArchived: true,
})
if err != nil {
return err
return taskIDs, err
}
tasks, _, _, err := getTasksForProjects(s, projects, u, &taskOptions{
if len(rawProjects) == 0 {
return
}
projects := []*ProjectWithTasksAndBuckets{}
projectsMap := make(map[int64]*ProjectWithTasksAndBuckets, len(rawProjects))
projectIDs := []int64{}
for _, p := range rawProjects {
pp := &ProjectWithTasksAndBuckets{
Project: *p,
}
projects = append(projects, pp)
projectsMap[p.ID] = pp
projectIDs = append(projectIDs, p.ID)
}
tasks, _, _, err := getTasksForProjects(s, rawProjects, u, &taskOptions{
page: 0,
perPage: -1,
})
if err != nil {
return err
return taskIDs, err
}
taskMap := make(map[int64]*TaskWithComments, len(tasks))
@ -181,11 +166,12 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err
taskMap[t.ID] = &TaskWithComments{
Task: *t,
}
if _, exists := projectMap[t.ProjectID]; !exists {
if _, exists := projectsMap[t.ProjectID]; !exists {
log.Debugf("[User Data Export] Project %d does not exist for task %d, omitting", t.ProjectID, t.ID)
continue
}
projectMap[t.ProjectID].Tasks = append(projectMap[t.ProjectID].Tasks, taskMap[t.ID])
projectsMap[t.ProjectID].Tasks = append(projectsMap[t.ProjectID].Tasks, taskMap[t.ID])
taskIDs = append(taskIDs, t.ID)
}
comments := []*TaskComment{}
@ -212,43 +198,22 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err
}
for _, b := range buckets {
if _, exists := projectMap[b.ProjectID]; !exists {
if _, exists := projectsMap[b.ProjectID]; !exists {
log.Debugf("[User Data Export] Project %d does not exist for bucket %d, omitting", b.ProjectID, b.ID)
continue
}
projectMap[b.ProjectID].Buckets = append(projectMap[b.ProjectID].Buckets, b)
projectsMap[b.ProjectID].Buckets = append(projectsMap[b.ProjectID].Buckets, b)
}
data, err := json.Marshal(namespaces)
data, err := json.Marshal(projects)
if err != nil {
return err
return taskIDs, err
}
return utils.WriteBytesToZip("data.json", data, wr)
return taskIDs, utils.WriteBytesToZip("data.json", data, wr)
}
func exportTaskAttachments(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
projects, _, _, err := getRawProjectsForUser(
s,
&projectOptions{
user: u,
page: -1,
},
)
if err != nil {
return err
}
tasks, _, _, err := getRawTasksForProjects(s, projects, u, &taskOptions{page: -1})
if err != nil {
return err
}
taskIDs := []int64{}
for _, t := range tasks {
taskIDs = append(taskIDs, t.ID)
}
func exportTaskAttachments(s *xorm.Session, wr *zip.Writer, taskIDs []int64) (err error) {
tas, err := getTaskAttachmentsByTaskIDs(s, taskIDs)
if err != nil {
return err

View File

@ -41,6 +41,9 @@ type Bucket struct {
// If this bucket is the "done bucket". All tasks moved into this bucket will automatically marked as done. All tasks marked as done from elsewhere will be moved into this bucket.
IsDoneBucket bool `xorm:"BOOL" json:"is_done_bucket"`
// The number of tasks currently in this bucket
Count int64 `xorm:"-" json:"count"`
// The position this bucket has when querying all buckets. See the tasks.position property on how to use this.
Position float64 `xorm:"double null" json:"position"`
@ -202,11 +205,13 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
opts.filters[bucketFilterIndex].value = id
ts, _, _, err := getRawTasksForProjects(s, []*Project{{ID: bucket.ProjectID}}, auth, opts)
ts, _, total, err := getRawTasksForProjects(s, []*Project{{ID: bucket.ProjectID}}, auth, opts)
if err != nil {
return nil, 0, 0, err
}
bucket.Count = total
tasks = append(tasks, ts...)
}

View File

@ -77,7 +77,7 @@ func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRigh
builder.
Select("id").
From("tasks").
Where(builder.In("project_id", getUserProjectsStatement(u.ID).Select("l.id"))),
Where(builder.In("project_id", getUserProjectsStatement(nil, u.ID, "", false).Select("l.id"))),
)
ll := &LabelTask{}

View File

@ -180,7 +180,7 @@ func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*Lab
builder.
Select("id").
From("tasks").
Where(builder.In("project_id", getUserProjectsStatement(opts.GetForUser).Select("l.id"))),
Where(builder.In("project_id", getUserProjectsStatement(nil, opts.GetForUser, "", false).Select("l.id"))),
), cond)
}
if opts.GetUnusedLabels {

View File

@ -44,6 +44,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,
},
@ -143,7 +144,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
return
}
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("LabelTask.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
t.Errorf("LabelTask.ReadAll() Wrong error type! Error = %v, want = %v, got = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name(), err)
}
if diff, equal := messagediff.PrettyDiff(gotLabels, tt.wantLabels); !equal {
t.Errorf("LabelTask.ReadAll() = %v, want %v, diff: %v", l, tt.wantLabels, diff)

View File

@ -106,6 +106,7 @@ func TestLabel_ReadAll(t *testing.T) {
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,
},
@ -233,6 +234,7 @@ func TestLabel_ReadOne(t *testing.T) {
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,
},

View File

@ -26,7 +26,7 @@ import (
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"xorm.io/builder"
"xorm.io/xorm"

View File

@ -35,8 +35,6 @@ import (
func RegisterListeners() {
events.RegisterListener((&ProjectCreatedEvent{}).Name(), &IncreaseProjectCounter{})
events.RegisterListener((&ProjectDeletedEvent{}).Name(), &DecreaseProjectCounter{})
events.RegisterListener((&NamespaceCreatedEvent{}).Name(), &IncreaseNamespaceCounter{})
events.RegisterListener((&NamespaceDeletedEvent{}).Name(), &DecreaseNamespaceCounter{})
events.RegisterListener((&TaskCreatedEvent{}).Name(), &IncreaseTaskCounter{})
events.RegisterListener((&TaskDeletedEvent{}).Name(), &DecreaseTaskCounter{})
events.RegisterListener((&TeamDeletedEvent{}).Name(), &DecreaseTeamCounter{})
@ -300,7 +298,13 @@ func (s *SendTaskDeletedNotification) Handle(msg *message.Message) (err error) {
sess := db.NewSession()
defer sess.Close()
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityTask, event.Task.ID)
var subscribers []*Subscription
subscribers, err = getSubscribersForEntity(sess, SubscriptionEntityTask, event.Task.ID)
// If the task does not exist and no one has explicitly subscribed to it, we won't find any subscriptions for it.
// Hence, we need to check for subscriptions to the parent project manually.
if err != nil && (IsErrTaskDoesNotExist(err) || IsErrProjectDoesNotExist(err)) {
subscribers, err = getSubscribersForEntity(sess, SubscriptionEntityProject, event.Task.ProjectID)
}
if err != nil {
return err
}
@ -540,37 +544,6 @@ func (s *SendProjectCreatedNotification) Handle(msg *message.Message) (err error
return nil
}
//////
// Namespace events
// IncreaseNamespaceCounter represents a listener
type IncreaseNamespaceCounter struct {
}
// Name defines the name for the IncreaseNamespaceCounter listener
func (s *IncreaseNamespaceCounter) Name() string {
return "namespace.counter.increase"
}
// Hanlde is executed when the event IncreaseNamespaceCounter listens on is fired
func (s *IncreaseNamespaceCounter) Handle(_ *message.Message) (err error) {
return keyvalue.IncrBy(metrics.NamespaceCountKey, 1)
}
// DecreaseNamespaceCounter represents a listener
type DecreaseNamespaceCounter struct {
}
// Name defines the name for the DecreaseNamespaceCounter listener
func (s *DecreaseNamespaceCounter) Name() string {
return "namespace.counter.decrease"
}
// Handle is executed when the event DecreaseNamespaceCounter listens on is fired
func (s *DecreaseNamespaceCounter) Handle(_ *message.Message) (err error) {
return keyvalue.DecrBy(metrics.NamespaceCountKey, 1)
}
///////
// Team Events

View File

@ -44,10 +44,7 @@ func GetTables() []interface{} {
&Team{},
&TeamMember{},
&TeamProject{},
&TeamNamespace{},
&Namespace{},
&ProjectUser{},
&NamespaceUser{},
&TaskAssginee{},
&Label{},
&LabelTask{},
@ -72,11 +69,6 @@ func SetEngine() (err error) {
return
}
// Cache
if config.CacheEnabled.GetBool() && config.CacheType.GetString() == "redis" {
db.RegisterTableStructsForCache(GetTables())
}
return nil
}

View File

@ -1,774 +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 models
import (
"sort"
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/builder"
"xorm.io/xorm"
)
// Namespace holds informations about a namespace
type Namespace struct {
// The unique, numeric id of this namespace.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
// The name of this namespace.
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
// The description of the namespace
Description string `xorm:"longtext null" json:"description"`
OwnerID int64 `xorm:"bigint not null INDEX" json:"-"`
// The hex color of this namespace
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
// Whether or not a namespace is archived.
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
// The user who owns this namespace
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
// The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.
// Will only returned when retreiving one namespace.
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
// A timestamp when this namespace was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
// A timestamp when this namespace was last updated. You cannot change this value.
Updated time.Time `xorm:"updated not null" json:"updated"`
// If set to true, will only return the namespaces, not their projects.
NamespacesOnly bool `xorm:"-" json:"-" query:"namespaces_only"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
// SharedProjectsPseudoNamespace is a pseudo namespace used to hold shared projects
var SharedProjectsPseudoNamespace = Namespace{
ID: -1,
Title: "Shared Projects",
Description: "Projects of other users shared with you via teams or directly.",
Created: time.Now(),
Updated: time.Now(),
}
// FavoritesPseudoNamespace is a pseudo namespace used to hold favorited projects and tasks
var FavoritesPseudoNamespace = Namespace{
ID: -2,
Title: "Favorites",
Description: "Favorite projects and tasks.",
Created: time.Now(),
Updated: time.Now(),
}
// SavedFiltersPseudoNamespace is a pseudo namespace used to hold saved filters
var SavedFiltersPseudoNamespace = Namespace{
ID: -3,
Title: "Filters",
Description: "Saved filters.",
Created: time.Now(),
Updated: time.Now(),
}
// TableName makes beautiful table names
func (Namespace) TableName() string {
return "namespaces"
}
// GetSimpleByID gets a namespace without things like the owner, it more or less only checks if it exists.
func getNamespaceSimpleByID(s *xorm.Session, id int64) (namespace *Namespace, err error) {
if id == 0 {
return nil, ErrNamespaceDoesNotExist{ID: id}
}
// Get the namesapce with shared projects
if id == -1 {
return &SharedProjectsPseudoNamespace, nil
}
if id == FavoritesPseudoNamespace.ID {
return &FavoritesPseudoNamespace, nil
}
if id == SavedFiltersPseudoNamespace.ID {
return &SavedFiltersPseudoNamespace, nil
}
namespace = &Namespace{}
exists, err := s.Where("id = ?", id).Get(namespace)
if err != nil {
return
}
if !exists {
return nil, ErrNamespaceDoesNotExist{ID: id}
}
return
}
// GetNamespaceByID returns a namespace object by its ID
func GetNamespaceByID(s *xorm.Session, id int64) (namespace *Namespace, err error) {
namespace, err = getNamespaceSimpleByID(s, id)
if err != nil {
return
}
// Get the namespace Owner
namespace.Owner, err = user.GetUserByID(s, namespace.OwnerID)
return
}
// CheckIsArchived returns an ErrNamespaceIsArchived if the namepace is archived.
func (n *Namespace) CheckIsArchived(s *xorm.Session) error {
exists, err := s.
Where("id = ? AND is_archived = true", n.ID).
Exist(&Namespace{})
if err != nil {
return err
}
if exists {
return ErrNamespaceIsArchived{NamespaceID: n.ID}
}
return nil
}
// ReadOne gets one namespace
// @Summary Gets one namespace
// @Description Returns a namespace by its ID.
// @tags namespace
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "Namespace ID"
// @Success 200 {object} models.Namespace "The Namespace"
// @Failure 403 {object} web.HTTPError "The user does not have access to that namespace."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id} [get]
func (n *Namespace) ReadOne(s *xorm.Session, a web.Auth) (err error) {
nn, err := GetNamespaceByID(s, n.ID)
if err != nil {
return err
}
*n = *nn
n.Subscription, err = GetSubscription(s, SubscriptionEntityNamespace, n.ID, a)
return
}
// NamespaceWithProjects represents a namespace with project meta informations
type NamespaceWithProjects struct {
Namespace `xorm:"extends"`
Projects []*Project `xorm:"-" json:"projects"`
}
type NamespaceWithProjectsAndTasks struct {
Namespace
Projects []*ProjectWithTasksAndBuckets `xorm:"-" json:"projects"`
}
func makeNamespaceSlice(namespaces map[int64]*NamespaceWithProjects, userMap map[int64]*user.User, subscriptions map[int64]*Subscription) []*NamespaceWithProjects {
all := make([]*NamespaceWithProjects, 0, len(namespaces))
for _, n := range namespaces {
n.Owner = userMap[n.OwnerID]
n.Subscription = subscriptions[n.ID]
all = append(all, n)
for _, l := range n.Projects {
if n.Subscription != nil && l.Subscription == nil {
l.Subscription = n.Subscription
}
}
}
sort.Slice(all, func(i, j int) bool {
return all[i].ID < all[j].ID
})
return all
}
func getNamespaceFilterCond(search string) (filterCond builder.Cond) {
filterCond = db.ILIKE("namespaces.title", search)
if search == "" {
return
}
vals := strings.Split(search, ",")
if len(vals) == 0 {
return
}
ids := []int64{}
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
log.Debugf("Namespace search string part '%s' is not a number: %s", val, err)
continue
}
ids = append(ids, v)
}
if len(ids) > 0 {
filterCond = builder.In("namespaces.id", ids)
}
return
}
func getNamespaceArchivedCond(archived bool) builder.Cond {
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
var isArchivedCond builder.Cond = builder.Eq{"1": 1}
if !archived {
isArchivedCond = builder.And(
builder.Eq{"namespaces.is_archived": false},
)
}
return isArchivedCond
}
func getNamespacesWithProjects(s *xorm.Session, namespaces *map[int64]*NamespaceWithProjects, search string, isArchived bool, page, perPage int, userID int64) (numberOfTotalItems int64, err error) {
isArchivedCond := getNamespaceArchivedCond(isArchived)
filterCond := getNamespaceFilterCond(search)
limit, start := getLimitFromPageIndex(page, perPage)
query := s.Select("namespaces.*").
Table("namespaces").
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
Where("team_members.user_id = ?", userID).
Or("namespaces.owner_id = ?", userID).
Or("users_namespaces.user_id = ?", userID).
GroupBy("namespaces.id").
Where(filterCond).
Where(isArchivedCond)
if limit > 0 {
query = query.Limit(limit, start)
}
err = query.Find(namespaces)
if err != nil {
return 0, err
}
numberOfTotalItems, err = s.
Table("namespaces").
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
Where("team_members.user_id = ?", userID).
Or("namespaces.owner_id = ?", userID).
Or("users_namespaces.user_id = ?", userID).
And("namespaces.is_archived = false").
GroupBy("namespaces.id").
Where(filterCond).
Where(isArchivedCond).
Count(&NamespaceWithProjects{})
return numberOfTotalItems, err
}
func getNamespaceOwnerIDs(namespaces map[int64]*NamespaceWithProjects) (namespaceIDs, ownerIDs []int64) {
for _, nsp := range namespaces {
namespaceIDs = append(namespaceIDs, nsp.ID)
ownerIDs = append(ownerIDs, nsp.OwnerID)
}
return
}
func getNamespaceSubscriptions(s *xorm.Session, namespaceIDs []int64, userID int64) (map[int64]*Subscription, error) {
subscriptionsMap := make(map[int64]*Subscription)
if len(namespaceIDs) == 0 {
return subscriptionsMap, nil
}
subscriptions := []*Subscription{}
err := s.
Where("entity_type = ? AND user_id = ?", SubscriptionEntityNamespace, userID).
In("entity_id", namespaceIDs).
Find(&subscriptions)
if err != nil {
return nil, err
}
for _, sub := range subscriptions {
sub.Entity = sub.EntityType.String()
subscriptionsMap[sub.EntityID] = sub
}
return subscriptionsMap, err
}
func getProjectsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool) ([]*Project, error) {
projects := []*Project{}
projectQuery := s.
OrderBy("position").
In("namespace_id", namespaceIDs)
if !archived {
projectQuery.And("is_archived = false")
}
err := projectQuery.Find(&projects)
return projects, err
}
func getSharedProjectsInNamespace(s *xorm.Session, archived bool, doer *user.User) (sharedProjectsNamespace *NamespaceWithProjects, err error) {
// Create our pseudo namespace to hold the shared projects
sharedProjectsPseudonamespace := SharedProjectsPseudoNamespace
sharedProjectsPseudonamespace.OwnerID = doer.ID
sharedProjectsNamespace = &NamespaceWithProjects{
sharedProjectsPseudonamespace,
[]*Project{},
}
// Get all projects individually shared with our user (not via a namespace)
individualProjects := []*Project{}
iProjectQuery := s.Select("l.*").
Table("projects").
Alias("l").
Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id").
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tl.team_id").
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id").
Where(builder.And(
builder.Eq{"tm.user_id": doer.ID},
builder.Neq{"l.owner_id": doer.ID},
)).
Or(builder.And(
builder.Eq{"ul.user_id": doer.ID},
builder.Neq{"l.owner_id": doer.ID},
)).
GroupBy("l.id")
if !archived {
iProjectQuery.And("l.is_archived = false")
}
err = iProjectQuery.Find(&individualProjects)
if err != nil {
return
}
// Make the namespace -1 so we now later which one it was
// + Append it to all projects we already have
for _, l := range individualProjects {
l.NamespaceID = sharedProjectsNamespace.ID
}
sharedProjectsNamespace.Projects = individualProjects
// Remove the sharedProjectsPseudonamespace if we don't have any shared projects
if len(individualProjects) == 0 {
sharedProjectsNamespace = nil
}
return
}
func getFavoriteProjects(s *xorm.Session, projects []*Project, namespaceIDs []int64, doer *user.User) (favoriteNamespace *NamespaceWithProjects, err error) {
// Create our pseudo namespace with favorite projects
pseudoFavoriteNamespace := FavoritesPseudoNamespace
pseudoFavoriteNamespace.OwnerID = doer.ID
favoriteNamespace = &NamespaceWithProjects{
Namespace: pseudoFavoriteNamespace,
Projects: []*Project{{}},
}
*favoriteNamespace.Projects[0] = FavoritesPseudoProject // Copying the project to be able to modify it later
favoriteNamespace.Projects[0].Owner = doer
for _, project := range projects {
if !project.IsFavorite {
continue
}
favoriteNamespace.Projects = append(favoriteNamespace.Projects, project)
}
// Check if we have any favorites or favorited projects and remove the favorites namespace from the project if not
cond := builder.
Select("tasks.id").
From("tasks").
Join("INNER", "projects", "tasks.project_id = projects.id").
Join("INNER", "namespaces", "projects.namespace_id = namespaces.id").
Where(builder.In("namespaces.id", namespaceIDs))
var favoriteCount int64
favoriteCount, err = s.
Where(builder.And(
builder.Eq{"user_id": doer.ID},
builder.Eq{"kind": FavoriteKindTask},
builder.In("entity_id", cond),
)).
Count(&Favorite{})
if err != nil {
return
}
// If we don't have any favorites in the favorites pseudo project, remove that pseudo project from the namespace
if favoriteCount == 0 {
for in, l := range favoriteNamespace.Projects {
if l.ID == FavoritesPseudoProject.ID {
favoriteNamespace.Projects = append(favoriteNamespace.Projects[:in], favoriteNamespace.Projects[in+1:]...)
break
}
}
}
// If we don't have any favorites in the namespace, remove it
if len(favoriteNamespace.Projects) == 0 {
return nil, nil
}
return
}
func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *NamespaceWithProjects, err error) {
savedFilters, err := getSavedFiltersForUser(s, doer)
if err != nil {
return
}
if len(savedFilters) == 0 {
return nil, nil
}
savedFiltersPseudoNamespace := SavedFiltersPseudoNamespace
savedFiltersPseudoNamespace.OwnerID = doer.ID
savedFiltersNamespace = &NamespaceWithProjects{
Namespace: savedFiltersPseudoNamespace,
Projects: make([]*Project, 0, len(savedFilters)),
}
for _, filter := range savedFilters {
filterProject := filter.toProject()
filterProject.NamespaceID = savedFiltersNamespace.ID
filterProject.Owner = doer
savedFiltersNamespace.Projects = append(savedFiltersNamespace.Projects, filterProject)
}
return
}
// ReadAll gets all namespaces a user has access to
// @Summary Get all namespaces a user has access to
// @Description Returns all namespaces a user has access to.
// @tags namespace
// @Accept json
// @Produce json
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search namespaces by name."
// @Param is_archived query bool false "If true, also returns all archived namespaces."
// @Param namespaces_only query bool false "If true, also returns only namespaces without their projects."
// @Security JWTKeyAuth
// @Success 200 {array} models.NamespaceWithProjects "The Namespaces."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces [get]
//
//nolint:gocyclo
func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
if _, is := a.(*LinkSharing); is {
return nil, 0, 0, ErrGenericForbidden{}
}
// This map will hold all namespaces and their projects. The key is usually the id of the namespace.
// We're using a map here because it makes a few things like adding projects or removing pseudo namespaces easier.
namespaces := make(map[int64]*NamespaceWithProjects)
//////////////////////////////
// Projects with their namespaces
doer, err := user.GetFromAuth(a)
if err != nil {
return nil, 0, 0, err
}
numberOfTotalItems, err = getNamespacesWithProjects(s, &namespaces, search, n.IsArchived, page, perPage, doer.ID)
if err != nil {
return nil, 0, 0, err
}
namespaceIDs, ownerIDs := getNamespaceOwnerIDs(namespaces)
if len(namespaceIDs) == 0 {
return nil, 0, 0, nil
}
subscriptionsMap, err := getNamespaceSubscriptions(s, namespaceIDs, doer.ID)
if err != nil {
return nil, 0, 0, err
}
ownerMap, err := user.GetUsersByIDs(s, ownerIDs)
if err != nil {
return nil, 0, 0, err
}
ownerMap[doer.ID] = doer
if n.NamespacesOnly {
all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
return all, len(all), numberOfTotalItems, nil
}
// Get all projects
projects, err := getProjectsForNamespaces(s, namespaceIDs, n.IsArchived)
if err != nil {
return nil, 0, 0, err
}
///////////////
// Shared Projects
sharedProjectsNamespace, err := getSharedProjectsInNamespace(s, n.IsArchived, doer)
if err != nil {
return nil, 0, 0, err
}
if sharedProjectsNamespace != nil {
namespaces[sharedProjectsNamespace.ID] = sharedProjectsNamespace
projects = append(projects, sharedProjectsNamespace.Projects...)
}
/////////////////
// Saved Filters
savedFiltersNamespace, err := getSavedFilters(s, doer)
if err != nil {
return nil, 0, 0, err
}
if savedFiltersNamespace != nil {
namespaces[savedFiltersNamespace.ID] = savedFiltersNamespace
projects = append(projects, savedFiltersNamespace.Projects...)
}
/////////////////
// Add project details (favorite state, among other things)
err = addProjectDetails(s, projects, a)
if err != nil {
return
}
/////////////////
// Favorite projects
favoritesNamespace, err := getFavoriteProjects(s, projects, namespaceIDs, doer)
if err != nil {
return nil, 0, 0, err
}
if favoritesNamespace != nil {
namespaces[favoritesNamespace.ID] = favoritesNamespace
}
//////////////////////
// Put it all together
for _, project := range projects {
if project.NamespaceID == SharedProjectsPseudoNamespace.ID || project.NamespaceID == SavedFiltersPseudoNamespace.ID {
// Shared projects and filtered projects are already in the namespace
continue
}
namespaces[project.NamespaceID].Projects = append(namespaces[project.NamespaceID].Projects, project)
}
all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
return all, len(all), numberOfTotalItems, err
}
// Create implements the creation method via the interface
// @Summary Creates a new namespace
// @Description Creates a new namespace.
// @tags namespace
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param namespace body models.Namespace true "The namespace you want to create."
// @Success 201 {object} models.Namespace "The created namespace."
// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces [put]
func (n *Namespace) Create(s *xorm.Session, a web.Auth) (err error) {
// Check if we have at least a title
if n.Title == "" {
return ErrNamespaceNameCannotBeEmpty{NamespaceID: 0, UserID: a.GetID()}
}
n.Owner, err = user.GetUserByID(s, a.GetID())
if err != nil {
return
}
n.OwnerID = n.Owner.ID
if _, err = s.Insert(n); err != nil {
return err
}
err = events.Dispatch(&NamespaceCreatedEvent{
Namespace: n,
Doer: a,
})
if err != nil {
return err
}
return
}
// CreateNewNamespaceForUser creates a new namespace for a user. To prevent import cycles, we can't do that
// directly in the user.Create function.
func CreateNewNamespaceForUser(s *xorm.Session, user *user.User) (err error) {
newN := &Namespace{
Title: user.Username,
Description: user.Username + "'s namespace.",
}
return newN.Create(s, user)
}
// Delete deletes a namespace
// @Summary Deletes a namespace
// @Description Delets a namespace
// @tags namespace
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "Namespace ID"
// @Success 200 {object} models.Message "The namespace was successfully deleted."
// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id} [delete]
func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
return deleteNamespace(s, n, a, true)
}
func deleteNamespace(s *xorm.Session, n *Namespace, a web.Auth, withProjects bool) (err error) {
// Check if the namespace exists
_, err = GetNamespaceByID(s, n.ID)
if err != nil {
return
}
// Delete the namespace
_, err = s.ID(n.ID).Delete(&Namespace{})
if err != nil {
return
}
namespaceDeleted := &NamespaceDeletedEvent{
Namespace: n,
Doer: a,
}
if !withProjects {
return events.Dispatch(namespaceDeleted)
}
// Delete all projects with their tasks
projects, err := GetProjectsByNamespaceID(s, n.ID, &user.User{})
if err != nil {
return
}
if len(projects) == 0 {
return events.Dispatch(namespaceDeleted)
}
// Looping over all projects to let the project handle properly cleaning up the tasks and everything else associated with it.
for _, project := range projects {
err = project.Delete(s, a)
if err != nil {
return err
}
}
return events.Dispatch(namespaceDeleted)
}
// Update implements the update method via the interface
// @Summary Updates a namespace
// @Description Updates a namespace.
// @tags namespace
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "Namespace ID"
// @Param namespace body models.Namespace true "The namespace with updated values you want to update."
// @Success 200 {object} models.Namespace "The updated namespace."
// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespace/{id} [post]
func (n *Namespace) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if we have at least a name
if n.Title == "" {
return ErrNamespaceNameCannotBeEmpty{NamespaceID: n.ID}
}
// Check if the namespace exists
currentNamespace, err := GetNamespaceByID(s, n.ID)
if err != nil {
return
}
// Check if the namespace is archived and the update is not un-archiving it
if currentNamespace.IsArchived && n.IsArchived {
return ErrNamespaceIsArchived{NamespaceID: n.ID}
}
// Check if the (new) owner exists
if n.Owner != nil {
n.OwnerID = n.Owner.ID
if currentNamespace.OwnerID != n.OwnerID {
n.Owner, err = user.GetUserByID(s, n.OwnerID)
if err != nil {
return
}
}
}
// We need to specify the cols we want to update here to be able to un-archive projects
colsToUpdate := []string{
"title",
"is_archived",
"hex_color",
}
if n.Description != "" {
colsToUpdate = append(colsToUpdate, "description")
}
// Do the actual update
_, err = s.
ID(currentNamespace.ID).
Cols(colsToUpdate...).
Update(n)
if err != nil {
return err
}
return events.Dispatch(&NamespaceUpdatedEvent{
Namespace: n,
Doer: a,
})
}

View File

@ -1,145 +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 models
import (
"code.vikunja.io/web"
"xorm.io/builder"
"xorm.io/xorm"
)
// CanWrite checks if a user has write access to a namespace
func (n *Namespace) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
can, _, err := n.checkRight(s, a, RightWrite, RightAdmin)
return can, err
}
// IsAdmin returns true or false if the user is admin on that namespace or not
func (n *Namespace) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
is, _, err := n.checkRight(s, a, RightAdmin)
return is, err
}
// CanRead checks if a user has read access to that namespace
func (n *Namespace) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
return n.checkRight(s, a, RightRead, RightWrite, RightAdmin)
}
// CanUpdate checks if the user can update the namespace
func (n *Namespace) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
return n.IsAdmin(s, a)
}
// CanDelete checks if the user can delete a namespace
func (n *Namespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
return n.IsAdmin(s, a)
}
// CanCreate checks if the user can create a new namespace
func (n *Namespace) CanCreate(_ *xorm.Session, a web.Auth) (bool, error) {
if _, is := a.(*LinkSharing); is {
return false, nil
}
// This is currently a dummy function, later on we could imagine global limits etc.
return true, nil
}
func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
// If the auth is a link share, don't do anything
if _, is := a.(*LinkSharing); is {
return false, 0, nil
}
// Get the namespace and check the right
nn, err := getNamespaceSimpleByID(s, n.ID)
if err != nil {
return false, 0, err
}
if a.GetID() == nn.OwnerID ||
nn.ID == SharedProjectsPseudoNamespace.ID ||
nn.ID == FavoritesPseudoNamespace.ID ||
nn.ID == SavedFiltersPseudoNamespace.ID {
return true, int(RightAdmin), nil
}
/*
The following loop creates an sql condition like this one:
namespaces.owner_id = 1 OR
(users_namespaces.user_id = 1 AND users_namespaces.right = 1) OR
(team_members.user_id = 1 AND team_namespaces.right = 1) OR
for each passed right. That way, we can check with a single sql query (instead if 8)
if the user has the right to see the project or not.
*/
var conds []builder.Cond
conds = append(conds, builder.Eq{"namespaces.owner_id": a.GetID()})
for _, r := range rights {
// User conditions
// If the namespace was shared directly with the user and the user has the right
conds = append(conds, builder.And(
builder.Eq{"users_namespaces.user_id": a.GetID()},
builder.Eq{"users_namespaces.right": r},
))
// Team rights
// If the namespace was shared directly with the team and the team has the right
conds = append(conds, builder.And(
builder.Eq{"team_members.user_id": a.GetID()},
builder.Eq{"team_namespaces.right": r},
))
}
type allRights struct {
UserNamespace NamespaceUser `xorm:"extends"`
TeamNamespace TeamNamespace `xorm:"extends"`
}
var maxRights = 0
r := &allRights{}
exists, err := s.
Select("*").
Table("namespaces").
// User stuff
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
// Teams stuff
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
// The actual condition
Where(builder.And(
builder.Or(
conds...,
),
builder.Eq{"namespaces.id": n.ID},
)).
Exist(r)
// Figure out the max right and return it
if int(r.UserNamespace.Right) > maxRights {
maxRights = int(r.UserNamespace.Right)
}
if int(r.TeamNamespace.Right) > maxRights {
maxRights = int(r.TeamNamespace.Right)
}
return exists, maxRights, err
}

View File

@ -1,244 +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 models
import (
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/web"
"xorm.io/xorm"
)
// TeamNamespace defines the relationship between a Team and a Namespace
type TeamNamespace struct {
// The unique, numeric id of this namespace <-> team relation.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
// The team id.
TeamID int64 `xorm:"bigint not null INDEX" json:"team_id" param:"team"`
// The namespace id.
NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
// The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A timestamp when this relation was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
// A timestamp when this relation was last updated. You cannot change this value.
Updated time.Time `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
// TableName makes beautiful table names
func (TeamNamespace) TableName() string {
return "team_namespaces"
}
// Create creates a new team <-> namespace relation
// @Summary Add a team to a namespace
// @Description Gives a team access to a namespace.
// @tags sharing
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "Namespace ID"
// @Param namespace body models.TeamNamespace true "The team you want to add to the namespace."
// @Success 201 {object} models.TeamNamespace "The created team<->namespace relation."
// @Failure 400 {object} web.HTTPError "Invalid team namespace object provided."
// @Failure 404 {object} web.HTTPError "The team does not exist."
// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id}/teams [put]
func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
// Check if the rights are valid
if err = tn.Right.isValid(); err != nil {
return
}
// Check if the team exists
team, err := GetTeamByID(s, tn.TeamID)
if err != nil {
return err
}
// Check if the namespace exists
namespace, err := GetNamespaceByID(s, tn.NamespaceID)
if err != nil {
return err
}
// Check if the team already has access to the namespace
exists, err := s.
Where("team_id = ?", tn.TeamID).
And("namespace_id = ?", tn.NamespaceID).
Get(&TeamNamespace{})
if err != nil {
return
}
if exists {
return ErrTeamAlreadyHasAccess{tn.TeamID, tn.NamespaceID}
}
// Insert the new team
_, err = s.Insert(tn)
if err != nil {
return err
}
return events.Dispatch(&NamespaceSharedWithTeamEvent{
Namespace: namespace,
Team: team,
Doer: a,
})
}
// Delete deletes a team <-> namespace relation based on the namespace & team id
// @Summary Delete a team from a namespace
// @Description Delets a team from a namespace. The team won't have access to the namespace anymore.
// @tags sharing
// @Produce json
// @Security JWTKeyAuth
// @Param namespaceID path int true "Namespace ID"
// @Param teamID path int true "team ID"
// @Success 200 {object} models.Message "The team was successfully deleted."
// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace"
// @Failure 404 {object} web.HTTPError "team or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/teams/{teamID} [delete]
func (tn *TeamNamespace) Delete(s *xorm.Session, _ web.Auth) (err error) {
// Check if the team exists
_, err = GetTeamByID(s, tn.TeamID)
if err != nil {
return
}
// Check if the team has access to the namespace
has, err := s.
Where("team_id = ? AND namespace_id = ?", tn.TeamID, tn.NamespaceID).
Get(&TeamNamespace{})
if err != nil {
return
}
if !has {
return ErrTeamDoesNotHaveAccessToNamespace{TeamID: tn.TeamID, NamespaceID: tn.NamespaceID}
}
// Delete the relation
_, err = s.
Where("team_id = ?", tn.TeamID).
And("namespace_id = ?", tn.NamespaceID).
Delete(TeamNamespace{})
return
}
// ReadAll implements the method to read all teams of a namespace
// @Summary Get teams on a namespace
// @Description Returns a namespace with all teams which have access on a given namespace.
// @tags sharing
// @Accept json
// @Produce json
// @Param id path int true "Namespace ID"
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search teams by its name."
// @Security JWTKeyAuth
// @Success 200 {array} models.TeamWithRight "The teams with the right they have."
// @Failure 403 {object} web.HTTPError "No right to see the namespace."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id}/teams [get]
func (tn *TeamNamespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
// Check if the user can read the namespace
n := Namespace{ID: tn.NamespaceID}
canRead, _, err := n.CanRead(s, a)
if err != nil {
return nil, 0, 0, err
}
if !canRead {
return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{NamespaceID: tn.NamespaceID, UserID: a.GetID()}
}
// Get the teams
all := []*TeamWithRight{}
limit, start := getLimitFromPageIndex(page, perPage)
query := s.
Table("teams").
Join("INNER", "team_namespaces", "team_id = teams.id").
Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
Where(db.ILIKE("teams.name", search))
if limit > 0 {
query = query.Limit(limit, start)
}
err = query.Find(&all)
if err != nil {
return nil, 0, 0, err
}
teams := []*Team{}
for _, t := range all {
teams = append(teams, &t.Team)
}
err = addMoreInfoToTeams(s, teams)
if err != nil {
return
}
numberOfTotalItems, err = s.
Table("teams").
Join("INNER", "team_namespaces", "team_id = teams.id").
Where("team_namespaces.namespace_id = ?", tn.NamespaceID).
Where("teams.name LIKE ?", "%"+search+"%").
Count(&TeamWithRight{})
return all, len(all), numberOfTotalItems, err
}
// Update updates a team <-> namespace relation
// @Summary Update a team <-> namespace relation
// @Description Update a team <-> namespace relation. Mostly used to update the right that team has.
// @tags sharing
// @Accept json
// @Produce json
// @Param namespaceID path int true "Namespace ID"
// @Param teamID path int true "Team ID"
// @Param namespace body models.TeamNamespace true "The team you want to update."
// @Security JWTKeyAuth
// @Success 200 {object} models.TeamNamespace "The updated team <-> namespace relation."
// @Failure 403 {object} web.HTTPError "The team does not have admin-access to the namespace"
// @Failure 404 {object} web.HTTPError "Team or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/teams/{teamID} [post]
func (tn *TeamNamespace) Update(s *xorm.Session, _ web.Auth) (err error) {
// Check if the right is valid
if err := tn.Right.isValid(); err != nil {
return err
}
_, err = s.
Where("namespace_id = ? AND team_id = ?", tn.NamespaceID, tn.TeamID).
Cols("right").
Update(tn)
return
}

View File

@ -1,107 +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 models
import (
"testing"
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
)
func TestTeamNamespace_CanDoSomething(t *testing.T) {
type fields struct {
ID int64
TeamID int64
NamespaceID int64
Right Right
Created time.Time
Updated time.Time
CRUDable web.CRUDable
Rights web.Rights
}
type args struct {
a web.Auth
}
tests := []struct {
name string
fields fields
args args
want map[string]bool
}{
{
name: "CanDoSomething Normally",
fields: fields{
NamespaceID: 3,
},
args: args{
a: &user.User{ID: 3},
},
want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true},
},
{
name: "CanDoSomething for a nonexistant namespace",
fields: fields{
NamespaceID: 300,
},
args: args{
a: &user.User{ID: 3},
},
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
},
{
name: "CanDoSomething where the user does not have the rights",
fields: fields{
NamespaceID: 3,
},
args: args{
a: &user.User{ID: 4},
},
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
tn := &TeamNamespace{
ID: tt.fields.ID,
TeamID: tt.fields.TeamID,
NamespaceID: tt.fields.NamespaceID,
Right: tt.fields.Right,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if got, _ := tn.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] {
t.Errorf("TeamNamespace.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
}
if got, _ := tn.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] {
t.Errorf("TeamNamespace.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
}
if got, _ := tn.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] {
t.Errorf("TeamNamespace.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
}
_ = s.Close()
})
}
}

View File

@ -1,298 +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 models
import (
"reflect"
"runtime"
"testing"
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"github.com/stretchr/testify/assert"
)
func TestTeamNamespace_ReadAll(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
tn := TeamNamespace{
NamespaceID: 3,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
teams, _, _, err := tn.ReadAll(s, u, "", 1, 50)
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
ts := reflect.ValueOf(teams)
assert.Equal(t, ts.Len(), 2)
_ = s.Close()
})
t.Run("nonexistant namespace", func(t *testing.T) {
tn := TeamNamespace{
NamespaceID: 9999,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
_, _, _, err := tn.ReadAll(s, u, "", 1, 50)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))
_ = s.Close()
})
t.Run("no right for namespace", func(t *testing.T) {
tn := TeamNamespace{
NamespaceID: 17,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
_, _, _, err := tn.ReadAll(s, u, "", 1, 50)
assert.Error(t, err)
assert.True(t, IsErrNeedToHaveNamespaceReadAccess(err))
_ = s.Close()
})
t.Run("search", func(t *testing.T) {
tn := TeamNamespace{
NamespaceID: 3,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
teams, _, _, err := tn.ReadAll(s, u, "READ_only_on_project6", 1, 50)
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
ts := teams.([]*TeamWithRight)
assert.Len(t, ts, 1)
assert.Equal(t, int64(2), ts[0].ID)
_ = s.Close()
})
}
func TestTeamNamespace_Create(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
tn := TeamNamespace{
TeamID: 1,
NamespaceID: 1,
Right: RightAdmin,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
allowed, _ := tn.CanCreate(s, u)
assert.True(t, allowed)
err := tn.Create(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "team_namespaces", map[string]interface{}{
"team_id": 1,
"namespace_id": 1,
"right": RightAdmin,
}, false)
})
t.Run("team already has access", func(t *testing.T) {
tn := TeamNamespace{
TeamID: 1,
NamespaceID: 3,
Right: RightRead,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := tn.Create(s, u)
assert.Error(t, err)
assert.True(t, IsErrTeamAlreadyHasAccess(err))
_ = s.Close()
})
t.Run("invalid team right", func(t *testing.T) {
tn := TeamNamespace{
TeamID: 1,
NamespaceID: 3,
Right: RightUnknown,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := tn.Create(s, u)
assert.Error(t, err)
assert.True(t, IsErrInvalidRight(err))
_ = s.Close()
})
t.Run("nonexistant team", func(t *testing.T) {
tn := TeamNamespace{
TeamID: 9999,
NamespaceID: 1,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := tn.Create(s, u)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotExist(err))
_ = s.Close()
})
t.Run("nonexistant namespace", func(t *testing.T) {
tn := TeamNamespace{
TeamID: 1,
NamespaceID: 9999,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := tn.Create(s, u)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))
_ = s.Close()
})
}
func TestTeamNamespace_Delete(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
tn := TeamNamespace{
TeamID: 7,
NamespaceID: 9,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
allowed, _ := tn.CanDelete(s, u)
assert.True(t, allowed)
err := tn.Delete(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertMissing(t, "team_namespaces", map[string]interface{}{
"team_id": 7,
"namespace_id": 9,
})
})
t.Run("nonexistant team", func(t *testing.T) {
tn := TeamNamespace{
TeamID: 9999,
NamespaceID: 3,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := tn.Delete(s, u)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotExist(err))
_ = s.Close()
})
t.Run("nonexistant namespace", func(t *testing.T) {
tn := TeamNamespace{
TeamID: 1,
NamespaceID: 9999,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := tn.Delete(s, u)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotHaveAccessToNamespace(err))
_ = s.Close()
})
}
func TestTeamNamespace_Update(t *testing.T) {
type fields struct {
ID int64
TeamID int64
NamespaceID int64
Right Right
Created time.Time
Updated time.Time
CRUDable web.CRUDable
Rights web.Rights
}
tests := []struct {
name string
fields fields
wantErr bool
errType func(err error) bool
}{
{
name: "Test Update Normally",
fields: fields{
NamespaceID: 3,
TeamID: 1,
Right: RightAdmin,
},
},
{
name: "Test Update to write",
fields: fields{
NamespaceID: 3,
TeamID: 1,
Right: RightWrite,
},
},
{
name: "Test Update to Read",
fields: fields{
NamespaceID: 3,
TeamID: 1,
Right: RightRead,
},
},
{
name: "Test Update with invalid right",
fields: fields{
NamespaceID: 3,
TeamID: 1,
Right: 500,
},
wantErr: true,
errType: IsErrInvalidRight,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
tl := &TeamNamespace{
ID: tt.fields.ID,
TeamID: tt.fields.TeamID,
NamespaceID: tt.fields.NamespaceID,
Right: tt.fields.Right,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
err := tl.Update(s, &user.User{ID: 1})
if (err != nil) != tt.wantErr {
t.Errorf("TeamNamespace.Update() error = %v, wantErr %v", err, tt.wantErr)
}
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("TeamNamespace.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
err = s.Commit()
assert.NoError(t, err)
if !tt.wantErr {
db.AssertExists(t, "team_namespaces", map[string]interface{}{
"team_id": tt.fields.TeamID,
"namespace_id": tt.fields.NamespaceID,
"right": tt.fields.Right,
}, false)
}
})
}
}

View File

@ -1,372 +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 models
import (
"testing"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/user"
"github.com/stretchr/testify/assert"
)
func TestNamespace_Create(t *testing.T) {
// Dummy namespace
dummynamespace := Namespace{
Title: "Test",
Description: "Lorem Ipsum",
}
user1 := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := dummynamespace.Create(s, user1)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "namespaces", map[string]interface{}{
"title": "Test",
"description": "Lorem Ipsum",
}, false)
})
t.Run("no title", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
n2 := Namespace{}
err := n2.Create(s, user1)
assert.Error(t, err)
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
_ = s.Close()
})
t.Run("nonexistant user", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
nUser := &user.User{ID: 9482385}
dnsp2 := dummynamespace
err := dnsp2.Create(s, nUser)
assert.Error(t, err)
assert.True(t, user.IsErrUserDoesNotExist(err))
_ = s.Close()
})
}
func TestNamespace_ReadOne(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
n := &Namespace{ID: 1}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
err := n.ReadOne(s, u)
assert.NoError(t, err)
assert.Equal(t, n.Title, "testnamespace")
})
t.Run("nonexistant", func(t *testing.T) {
n := &Namespace{ID: 99999}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
err := n.ReadOne(s, u)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))
})
t.Run("with subscription", func(t *testing.T) {
n := &Namespace{ID: 8}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
err := n.ReadOne(s, &user.User{ID: 6})
assert.NoError(t, err)
assert.NotNil(t, n.Subscription)
})
}
func TestNamespace_Update(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
n := &Namespace{
ID: 1,
Title: "Lorem Ipsum",
}
err := n.Update(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "namespaces", map[string]interface{}{
"id": 1,
"title": "Lorem Ipsum",
}, false)
})
t.Run("nonexisting", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
n := &Namespace{
ID: 99999,
Title: "Lorem Ipsum",
}
err := n.Update(s, u)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))
_ = s.Close()
})
t.Run("nonexisting owner", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
n := &Namespace{
ID: 1,
Title: "Lorem Ipsum",
Owner: &user.User{ID: 99999},
}
err := n.Update(s, u)
assert.Error(t, err)
assert.True(t, user.IsErrUserDoesNotExist(err))
_ = s.Close()
})
t.Run("no title", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
n := &Namespace{
ID: 1,
}
err := n.Update(s, u)
assert.Error(t, err)
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
_ = s.Close()
})
}
func TestNamespace_Delete(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
n := &Namespace{
ID: 1,
}
err := n.Delete(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertMissing(t, "namespaces", map[string]interface{}{
"id": 1,
})
})
t.Run("nonexisting", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
n := &Namespace{
ID: 9999,
}
err := n.Delete(s, u)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))
_ = s.Close()
})
}
func TestNamespace_ReadAll(t *testing.T) {
user1 := &user.User{ID: 1}
user6 := &user.User{ID: 6}
user7 := &user.User{ID: 7}
user11 := &user.User{ID: 11}
user12 := &user.User{ID: 12}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 11) // Total of 11 including shared, favorites and saved filters
assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with saved filters
assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites
assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces
// Ensure every project and namespace are not archived
for _, namespace := range namespaces {
assert.False(t, namespace.IsArchived)
for _, project := range namespace.Projects {
assert.False(t, project.IsArchived)
}
}
})
t.Run("no own shared projects", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user6, "", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Equal(t, int64(-1), namespaces[1].ID) // The third one should be the one with the shared namespaces
sharedProjectOccurences := make(map[int64]int64)
for _, project := range namespaces[1].Projects {
assert.NotEqual(t, user1.ID, project.OwnerID)
sharedProjectOccurences[project.ID]++
}
for projectID, occ := range sharedProjectOccurences {
assert.Equal(t, int64(1), occ, "shared project %d is present %d times, should be 1", projectID, occ)
}
})
t.Run("namespaces only", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{
NamespacesOnly: true,
}
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 8) // Total of 8 - excluding shared, favorites and saved filters (normally 11)
// Ensure every namespace does not contain projects
for _, namespace := range namespaces {
assert.Nil(t, namespace.Projects)
}
})
t.Run("ids only", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{
NamespacesOnly: true,
}
nn, _, _, err := n.ReadAll(s, user7, "13,14", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 2)
assert.Equal(t, int64(13), namespaces[0].ID)
assert.Equal(t, int64(14), namespaces[1].ID)
})
t.Run("ids only but ids with other people's namespace", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{
NamespacesOnly: true,
}
nn, _, _, err := n.ReadAll(s, user1, "1,w", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 1)
assert.Equal(t, int64(1), namespaces[0].ID)
})
t.Run("archived", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{
IsArchived: true,
}
nn, _, _, err := n.ReadAll(s, user1, "", 1, -1)
namespaces := nn.([]*NamespaceWithProjects)
assert.NoError(t, err)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 12) // Total of 12 including shared & favorites, one is archived
assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with shared filters
assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites
assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces
})
t.Run("no favorites", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user11, "", 1, -1)
namespaces := nn.([]*NamespaceWithProjects)
assert.NoError(t, err)
// Assert the first namespace is not the favorites namespace
assert.NotEqual(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
})
t.Run("no favorite tasks but namespace", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user12, "", 1, -1)
namespaces := nn.([]*NamespaceWithProjects)
assert.NoError(t, err)
// Assert the first namespace is the favorites namespace and contains projects
assert.Equal(t, FavoritesPseudoNamespace.ID, namespaces[0].ID)
assert.NotEqual(t, 0, namespaces[0].Projects)
})
t.Run("no saved filters", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user11, "", 1, -1)
namespaces := nn.([]*NamespaceWithProjects)
assert.NoError(t, err)
// Assert the first namespace is not the favorites namespace
assert.NotEqual(t, SavedFiltersPseudoNamespace.ID, namespaces[0].ID)
})
t.Run("no results", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user1, "some search string which will never return results", 1, -1)
assert.NoError(t, err)
assert.Nil(t, nn)
})
t.Run("search", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
n := &Namespace{}
nn, _, _, err := n.ReadAll(s, user6, "NamespACE7", 1, -1)
assert.NoError(t, err)
namespaces := nn.([]*NamespaceWithProjects)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 2)
assert.Equal(t, int64(7), namespaces[1].ID)
})
}

View File

@ -1,251 +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 models
import (
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
user2 "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/xorm"
)
// NamespaceUser represents a namespace <-> user relation
type NamespaceUser struct {
// The unique, numeric id of this namespace <-> user relation.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
// The username.
Username string `xorm:"-" json:"user_id" param:"user"`
UserID int64 `xorm:"bigint not null INDEX" json:"-"`
// The namespace id
NamespaceID int64 `xorm:"bigint not null INDEX" json:"-" param:"namespace"`
// The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A timestamp when this relation was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
// A timestamp when this relation was last updated. You cannot change this value.
Updated time.Time `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
// TableName is the table name for NamespaceUser
func (NamespaceUser) TableName() string {
return "users_namespaces"
}
// Create creates a new namespace <-> user relation
// @Summary Add a user to a namespace
// @Description Gives a user access to a namespace.
// @tags sharing
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "Namespace ID"
// @Param namespace body models.NamespaceUser true "The user you want to add to the namespace."
// @Success 201 {object} models.NamespaceUser "The created user<->namespace relation."
// @Failure 400 {object} web.HTTPError "Invalid user namespace object provided."
// @Failure 404 {object} web.HTTPError "The user does not exist."
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id}/users [put]
func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
// Reset the id
nu.ID = 0
// Check if the right is valid
if err := nu.Right.isValid(); err != nil {
return err
}
// Check if the namespace exists
n, err := GetNamespaceByID(s, nu.NamespaceID)
if err != nil {
return
}
// Check if the user exists
user, err := user2.GetUserByUsername(s, nu.Username)
if err != nil {
return err
}
nu.UserID = user.ID
// Check if the user already has access or is owner of that namespace
// We explicitly DO NOT check for teams here
if n.OwnerID == nu.UserID {
return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
}
exist, err := s.
Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).
Get(&NamespaceUser{})
if err != nil {
return
}
if exist {
return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
}
// Insert user <-> namespace relation
_, err = s.Insert(nu)
if err != nil {
return err
}
return events.Dispatch(&NamespaceSharedWithUserEvent{
Namespace: n,
User: user,
Doer: a,
})
}
// Delete deletes a namespace <-> user relation
// @Summary Delete a user from a namespace
// @Description Delets a user from a namespace. The user won't have access to the namespace anymore.
// @tags sharing
// @Produce json
// @Security JWTKeyAuth
// @Param namespaceID path int true "Namespace ID"
// @Param userID path int true "user ID"
// @Success 200 {object} models.Message "The user was successfully deleted."
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
// @Failure 404 {object} web.HTTPError "user or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/users/{userID} [delete]
func (nu *NamespaceUser) Delete(s *xorm.Session, _ web.Auth) (err error) {
// Check if the user exists
user, err := user2.GetUserByUsername(s, nu.Username)
if err != nil {
return
}
nu.UserID = user.ID
// Check if the user has access to the namespace
has, err := s.
Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
Get(&NamespaceUser{})
if err != nil {
return
}
if !has {
return ErrUserDoesNotHaveAccessToNamespace{NamespaceID: nu.NamespaceID, UserID: nu.UserID}
}
_, err = s.
Where("user_id = ? AND namespace_id = ?", nu.UserID, nu.NamespaceID).
Delete(&NamespaceUser{})
return
}
// ReadAll gets all users who have access to a namespace
// @Summary Get users on a namespace
// @Description Returns a namespace with all users which have access on a given namespace.
// @tags sharing
// @Accept json
// @Produce json
// @Param id path int true "Namespace ID"
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search users by its name."
// @Security JWTKeyAuth
// @Success 200 {array} models.UserWithRight "The users with the right they have."
// @Failure 403 {object} web.HTTPError "No right to see the namespace."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id}/users [get]
func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
// Check if the user has access to the namespace
l := Namespace{ID: nu.NamespaceID}
canRead, _, err := l.CanRead(s, a)
if err != nil {
return nil, 0, 0, err
}
if !canRead {
return nil, 0, 0, ErrNeedToHaveNamespaceReadAccess{}
}
// Get all users
all := []*UserWithRight{}
limit, start := getLimitFromPageIndex(page, perPage)
query := s.
Join("INNER", "users_namespaces", "user_id = users.id").
Where("users_namespaces.namespace_id = ?", nu.NamespaceID).
Where(db.ILIKE("users.username", search))
if limit > 0 {
query = query.Limit(limit, start)
}
err = query.Find(&all)
if err != nil {
return nil, 0, 0, err
}
// Obfuscate all user emails
for _, u := range all {
u.Email = ""
}
numberOfTotalItems, err = s.
Join("INNER", "users_namespaces", "user_id = users.id").
Where("users_namespaces.namespace_id = ?", nu.NamespaceID).
Where("users.username LIKE ?", "%"+search+"%").
Count(&UserWithRight{})
return all, len(all), numberOfTotalItems, err
}
// Update updates a user <-> namespace relation
// @Summary Update a user <-> namespace relation
// @Description Update a user <-> namespace relation. Mostly used to update the right that user has.
// @tags sharing
// @Accept json
// @Produce json
// @Param namespaceID path int true "Namespace ID"
// @Param userID path int true "User ID"
// @Param namespace body models.NamespaceUser true "The user you want to update."
// @Security JWTKeyAuth
// @Success 200 {object} models.NamespaceUser "The updated user <-> namespace relation."
// @Failure 403 {object} web.HTTPError "The user does not have admin-access to the namespace"
// @Failure 404 {object} web.HTTPError "User or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/users/{userID} [post]
func (nu *NamespaceUser) Update(s *xorm.Session, _ web.Auth) (err error) {
// Check if the right is valid
if err := nu.Right.isValid(); err != nil {
return err
}
// Check if the user exists
user, err := user2.GetUserByUsername(s, nu.Username)
if err != nil {
return err
}
nu.UserID = user.ID
_, err = s.
Where("namespace_id = ? AND user_id = ?", nu.NamespaceID, nu.UserID).
Cols("right").
Update(nu)
return
}

View File

@ -1,42 +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 models
import (
"code.vikunja.io/web"
"xorm.io/xorm"
)
// CanCreate checks if the user can create a new user <-> namespace relation
func (nu *NamespaceUser) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
return nu.canDoNamespaceUser(s, a)
}
// CanDelete checks if the user can delete a user <-> namespace relation
func (nu *NamespaceUser) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
return nu.canDoNamespaceUser(s, a)
}
// CanUpdate checks if the user can update a user <-> namespace relation
func (nu *NamespaceUser) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
return nu.canDoNamespaceUser(s, a)
}
func (nu *NamespaceUser) canDoNamespaceUser(s *xorm.Session, a web.Auth) (bool, error) {
n := &Namespace{ID: nu.NamespaceID}
return n.IsAdmin(s, a)
}

View File

@ -1,107 +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 models
import (
"testing"
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
)
func TestNamespaceUser_CanDoSomething(t *testing.T) {
type fields struct {
ID int64
UserID int64
NamespaceID int64
Right Right
Created time.Time
Updated time.Time
CRUDable web.CRUDable
Rights web.Rights
}
type args struct {
a web.Auth
}
tests := []struct {
name string
fields fields
args args
want map[string]bool
}{
{
name: "CanDoSomething Normally",
fields: fields{
NamespaceID: 3,
},
args: args{
a: &user.User{ID: 3},
},
want: map[string]bool{"CanCreate": true, "CanDelete": true, "CanUpdate": true},
},
{
name: "CanDoSomething for a nonexistant namespace",
fields: fields{
NamespaceID: 300,
},
args: args{
a: &user.User{ID: 3},
},
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
},
{
name: "CanDoSomething where the user does not have the rights",
fields: fields{
NamespaceID: 3,
},
args: args{
a: &user.User{ID: 4},
},
want: map[string]bool{"CanCreate": false, "CanDelete": false, "CanUpdate": false},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
nu := &NamespaceUser{
ID: tt.fields.ID,
UserID: tt.fields.UserID,
NamespaceID: tt.fields.NamespaceID,
Right: tt.fields.Right,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if got, _ := nu.CanCreate(s, tt.args.a); got != tt.want["CanCreate"] {
t.Errorf("NamespaceUser.CanCreate() = %v, want %v", got, tt.want["CanCreate"])
}
if got, _ := nu.CanDelete(s, tt.args.a); got != tt.want["CanDelete"] {
t.Errorf("NamespaceUser.CanDelete() = %v, want %v", got, tt.want["CanDelete"])
}
if got, _ := nu.CanUpdate(s, tt.args.a); got != tt.want["CanUpdate"] {
t.Errorf("NamespaceUser.CanUpdate() = %v, want %v", got, tt.want["CanUpdate"])
}
})
}
}

View File

@ -1,436 +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 models
import (
"reflect"
"runtime"
"testing"
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"github.com/stretchr/testify/assert"
"gopkg.in/d4l3k/messagediff.v1"
)
func TestNamespaceUser_Create(t *testing.T) {
type fields struct {
ID int64
Username string
UserID int64
NamespaceID int64
Right Right
Created time.Time
Updated time.Time
CRUDable web.CRUDable
Rights web.Rights
}
type args struct {
a web.Auth
}
tests := []struct {
name string
fields fields
args args
wantErr bool
errType func(err error) bool
}{
{
name: "NamespaceUsers Create normally",
fields: fields{
Username: "user1",
UserID: 1,
NamespaceID: 2,
},
},
{
name: "NamespaceUsers Create for duplicate",
fields: fields{
Username: "user1",
NamespaceID: 3,
},
wantErr: true,
errType: IsErrUserAlreadyHasNamespaceAccess,
},
{
name: "NamespaceUsers Create with invalid right",
fields: fields{
Username: "user1",
NamespaceID: 2,
Right: 500,
},
wantErr: true,
errType: IsErrInvalidRight,
},
{
name: "NamespaceUsers Create with inexisting project",
fields: fields{
Username: "user1",
NamespaceID: 2000,
},
wantErr: true,
errType: IsErrNamespaceDoesNotExist,
},
{
name: "NamespaceUsers Create with inexisting user",
fields: fields{
Username: "user500",
NamespaceID: 2,
},
wantErr: true,
errType: user.IsErrUserDoesNotExist,
},
{
name: "NamespaceUsers Create with the owner as shared user",
fields: fields{
Username: "user1",
NamespaceID: 1,
},
wantErr: true,
errType: IsErrUserAlreadyHasNamespaceAccess,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
un := &NamespaceUser{
ID: tt.fields.ID,
Username: tt.fields.Username,
NamespaceID: tt.fields.NamespaceID,
Right: tt.fields.Right,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
err := un.Create(s, tt.args.a)
if (err != nil) != tt.wantErr {
t.Errorf("NamespaceUser.Create() error = %v, wantErr %v", err, tt.wantErr)
}
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("NamespaceUser.Create() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
err = s.Commit()
assert.NoError(t, err)
if !tt.wantErr {
db.AssertExists(t, "users_namespaces", map[string]interface{}{
"user_id": tt.fields.UserID,
"namespace_id": tt.fields.NamespaceID,
}, false)
}
})
}
}
func TestNamespaceUser_ReadAll(t *testing.T) {
user1 := &UserWithRight{
User: user.User{
ID: 1,
Username: "user1",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
},
Right: RightRead,
}
user2 := &UserWithRight{
User: user.User{
ID: 2,
Username: "user2",
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
},
Right: RightRead,
}
type fields struct {
ID int64
UserID int64
NamespaceID int64
Right Right
Created time.Time
Updated time.Time
CRUDable web.CRUDable
Rights web.Rights
}
type args struct {
search string
a web.Auth
page int
}
tests := []struct {
name string
fields fields
args args
want interface{}
wantErr bool
errType func(err error) bool
}{
{
name: "Test readall normal",
fields: fields{
NamespaceID: 3,
},
args: args{
a: &user.User{ID: 3},
},
want: []*UserWithRight{
user1,
user2,
},
},
{
name: "Test ReadAll by a user who does not have access to the project",
fields: fields{
NamespaceID: 3,
},
args: args{
a: &user.User{ID: 4},
},
wantErr: true,
errType: IsErrNeedToHaveNamespaceReadAccess,
},
{
name: "Search",
fields: fields{
NamespaceID: 3,
},
args: args{
a: &user.User{ID: 3},
search: "usER2",
},
want: []*UserWithRight{
user2,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
un := &NamespaceUser{
ID: tt.fields.ID,
UserID: tt.fields.UserID,
NamespaceID: tt.fields.NamespaceID,
Right: tt.fields.Right,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
got, _, _, err := un.ReadAll(s, tt.args.a, tt.args.search, tt.args.page, 50)
if (err != nil) != tt.wantErr {
t.Errorf("NamespaceUser.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
return
}
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("NamespaceUser.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
t.Errorf("NamespaceUser.ReadAll() = %v, want %v, diff: %v", got, tt.want, diff)
}
})
}
}
func TestNamespaceUser_Update(t *testing.T) {
type fields struct {
ID int64
Username string
UserID int64
NamespaceID int64
Right Right
Created time.Time
Updated time.Time
CRUDable web.CRUDable
Rights web.Rights
}
tests := []struct {
name string
fields fields
wantErr bool
errType func(err error) bool
}{
{
name: "Test Update Normally",
fields: fields{
NamespaceID: 3,
Username: "user1",
UserID: 1,
Right: RightAdmin,
},
},
{
name: "Test Update to write",
fields: fields{
NamespaceID: 3,
Username: "user1",
UserID: 1,
Right: RightWrite,
},
},
{
name: "Test Update to Read",
fields: fields{
NamespaceID: 3,
Username: "user1",
UserID: 1,
Right: RightRead,
},
},
{
name: "Test Update with invalid right",
fields: fields{
NamespaceID: 3,
Username: "user1",
Right: 500,
},
wantErr: true,
errType: IsErrInvalidRight,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
nu := &NamespaceUser{
ID: tt.fields.ID,
Username: tt.fields.Username,
NamespaceID: tt.fields.NamespaceID,
Right: tt.fields.Right,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
err := nu.Update(s, &user.User{ID: 1})
if (err != nil) != tt.wantErr {
t.Errorf("NamespaceUser.Update() error = %v, wantErr %v", err, tt.wantErr)
}
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("NamespaceUser.Update() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
err = s.Commit()
assert.NoError(t, err)
if !tt.wantErr {
db.AssertExists(t, "users_namespaces", map[string]interface{}{
"user_id": tt.fields.UserID,
"namespace_id": tt.fields.NamespaceID,
"right": tt.fields.Right,
}, false)
}
})
}
}
func TestNamespaceUser_Delete(t *testing.T) {
type fields struct {
ID int64
Username string
UserID int64
NamespaceID int64
Right Right
Created time.Time
Updated time.Time
CRUDable web.CRUDable
Rights web.Rights
}
tests := []struct {
name string
fields fields
wantErr bool
errType func(err error) bool
}{
{
name: "Try deleting some unexistant user",
fields: fields{
Username: "user1000",
NamespaceID: 2,
},
wantErr: true,
errType: user.IsErrUserDoesNotExist,
},
{
name: "Try deleting a user which does not has access but exists",
fields: fields{
Username: "user1",
NamespaceID: 4,
},
wantErr: true,
errType: IsErrUserDoesNotHaveAccessToNamespace,
},
{
name: "Try deleting normally",
fields: fields{
Username: "user1",
UserID: 1,
NamespaceID: 3,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
nu := &NamespaceUser{
ID: tt.fields.ID,
Username: tt.fields.Username,
NamespaceID: tt.fields.NamespaceID,
Right: tt.fields.Right,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
err := nu.Delete(s, &user.User{ID: 1})
if (err != nil) != tt.wantErr {
t.Errorf("NamespaceUser.Delete() error = %v, wantErr %v", err, tt.wantErr)
}
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("NamespaceUser.Delete() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
err = s.Commit()
assert.NoError(t, err)
if !tt.wantErr {
db.AssertMissing(t, "users_namespaces", map[string]interface{}{
"user_id": tt.fields.UserID,
"namespace_id": tt.fields.NamespaceID,
})
}
})
}
}

View File

@ -136,8 +136,8 @@ type TaskDeletedNotification struct {
// ToMail returns the mail notification for TaskDeletedNotification
func (n *TaskDeletedNotification) ToMail() *notifications.Mail {
return notifications.NewMail().
Subject(n.Task.Title + "(" + n.Task.GetFullIdentifier() + ")" + " has been delete").
Line(n.Doer.GetName() + " has deleted the task " + n.Task.Title + "(" + n.Task.GetFullIdentifier() + ")")
Subject(n.Task.Title + " (" + n.Task.GetFullIdentifier() + ")" + " has been deleted").
Line(n.Doer.GetName() + " has deleted the task " + n.Task.Title + " (" + n.Task.GetFullIdentifier() + ")")
}
// ToDB returns the TaskDeletedNotification notification in a format which can be saved in the db

View File

@ -17,6 +17,7 @@
package models
import (
"math"
"strconv"
"strings"
"time"
@ -37,7 +38,7 @@ import (
type Project struct {
// The unique, numeric id of this project.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"project"`
// The title of the project. You'll see this in the namespace overview.
// The title of the project. You'll see this in the overview.
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
// The description of the project.
Description string `xorm:"longtext null" json:"description"`
@ -46,13 +47,14 @@ type Project struct {
// The hex color of this project
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
ParentProject *Project `xorm:"-" json:"-"`
// The user who created this project.
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
// Whether or not a project is archived.
// Whether a project is archived.
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
// The id of the file this project has set as background
@ -62,7 +64,7 @@ type Project struct {
// Contains a very small version of the project 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 project is a favorite. Favorite projects show up in a separate namespace. This value depends on the user making the call to the api.
// True if a project is a favorite. Favorite projects show up in a separate parent project. This value depends on the user making the call to the api.
IsFavorite bool `xorm:"-" json:"is_favorite"`
// The subscription status for the user reading this project. You can only read this property, use the subscription endpoints to modify it.
@ -83,6 +85,8 @@ type Project struct {
type ProjectWithTasksAndBuckets struct {
Project
ChildProjects []*ProjectWithTasksAndBuckets `xorm:"-" json:"child_projects"`
// An array of tasks which belong to the project.
Tasks []*TaskWithComments `xorm:"-" json:"tasks"`
// Only used for migration.
@ -91,7 +95,7 @@ type ProjectWithTasksAndBuckets struct {
}
// TableName returns a better name for the projects table
func (l *Project) TableName() string {
func (p *Project) TableName() string {
return "projects"
}
@ -108,67 +112,12 @@ var FavoritesPseudoProject = Project{
ID: -1,
Title: "Favorites",
Description: "This project has all tasks marked as favorites.",
NamespaceID: FavoritesPseudoNamespace.ID,
IsFavorite: true,
Position: -1,
Created: time.Now(),
Updated: time.Now(),
}
// GetProjectsByNamespaceID gets all projects in a namespace
func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (projects []*Project, err error) {
switch nID {
case SharedProjectsPseudoNamespace.ID:
nnn, err := getSharedProjectsInNamespace(s, false, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Projects != nil {
projects = nnn.Projects
}
case FavoritesPseudoNamespace.ID:
namespaces := make(map[int64]*NamespaceWithProjects)
_, err := getNamespacesWithProjects(s, &namespaces, "", false, 0, -1, doer.ID)
if err != nil {
return nil, err
}
namespaceIDs, _ := getNamespaceOwnerIDs(namespaces)
ls, err := getProjectsForNamespaces(s, namespaceIDs, false)
if err != nil {
return nil, err
}
nnn, err := getFavoriteProjects(s, ls, namespaceIDs, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Projects != nil {
projects = nnn.Projects
}
case SavedFiltersPseudoNamespace.ID:
nnn, err := getSavedFilters(s, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Projects != nil {
projects = nnn.Projects
}
default:
err = s.Select("l.*").
Alias("l").
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
Where("l.is_archived = false").
Where("n.is_archived = false OR n.is_archived IS NULL").
Where("namespace_id = ?", nID).
Find(&projects)
}
if err != nil {
return nil, err
}
// get more project details
err = addProjectDetails(s, projects, doer)
return projects, err
}
// ReadAll gets all projects a user has access to
// @Summary Get all projects a user has access to
// @Description Returns all projects a user has access to.
@ -184,7 +133,7 @@ func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (proj
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects [get]
func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
@ -197,22 +146,47 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
return projects, 0, 0, err
}
projects, resultCount, totalItems, err := getRawProjectsForUser(
doer, err := user.GetFromAuth(a)
if err != nil {
return nil, 0, 0, err
}
prs, resultCount, totalItems, err := getRawProjectsForUser(
s,
&projectOptions{
search: search,
user: &user.User{ID: a.GetID()},
page: page,
perPage: perPage,
isArchived: l.IsArchived,
search: search,
user: doer,
page: page,
perPage: perPage,
getArchived: p.IsArchived,
})
if err != nil {
return nil, 0, 0, err
}
// Add more project details
err = addProjectDetails(s, projects, a)
return projects, resultCount, totalItems, err
/////////////////
// Saved Filters
savedFiltersProject, err := getSavedFilterProjects(s, doer)
if err != nil {
return nil, 0, 0, err
}
if len(savedFiltersProject) > 0 {
prs = append(prs, savedFiltersProject...)
}
/////////////////
// Add project details (favorite state, among other things)
err = addProjectDetails(s, prs, a)
if err != nil {
return
}
//////////////////////////
// Putting it all together
return prs, resultCount, totalItems, err
}
// ReadOne gets one project by its ID
@ -227,61 +201,65 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id} [get]
func (l *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
// Already "built" the project in CanRead
return nil
}
// Check for saved filters
if getSavedFilterIDFromProjectID(l.ID) > 0 {
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(l.ID))
filterID := getSavedFilterIDFromProjectID(p.ID)
isFilter := filterID > 0
if isFilter {
sf, err := getSavedFilterSimpleByID(s, filterID)
if err != nil {
return err
}
l.Title = sf.Title
l.Description = sf.Description
l.Created = sf.Created
l.Updated = sf.Updated
l.OwnerID = sf.OwnerID
p.Title = sf.Title
p.Description = sf.Description
p.Created = sf.Created
p.Updated = sf.Updated
p.OwnerID = sf.OwnerID
}
// Get project owner
l.Owner, err = user.GetUserByID(s, l.OwnerID)
p.Owner, err = user.GetUserByID(s, p.OwnerID)
if err != nil {
return err
}
// Check if the namespace is archived and set the namespace to archived if it is not already archived individually.
if !l.IsArchived {
err = l.CheckIsArchived(s)
// Check if the project is archived and set it to archived if it is not already archived individually.
if !p.IsArchived && !isFilter {
err = p.CheckIsArchived(s)
if err != nil {
if !IsErrNamespaceIsArchived(err) && !IsErrProjectIsArchived(err) {
return
}
l.IsArchived = true
p.IsArchived = true
}
}
// Get any background information if there is one set
if l.BackgroundFileID != 0 {
if p.BackgroundFileID != 0 {
// Unsplash image
l.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, l.BackgroundFileID)
p.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, p.BackgroundFileID)
if err != nil && !files.IsErrFileIsNotUnsplashFile(err) {
return
}
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
}
}
l.IsFavorite, err = isFavorite(s, l.ID, a, FavoriteKindProject)
p.IsFavorite, err = isFavorite(s, p.ID, a, FavoriteKindProject)
if err != nil {
return
}
l.Subscription, err = GetSubscription(s, SubscriptionEntityProject, l.ID, a)
p.Subscription, err = GetSubscription(s, SubscriptionEntityProject, p.ID, a)
if err != nil && IsErrProjectDoesNotExist(err) && isFilter {
return nil
}
return
}
@ -344,62 +322,31 @@ func GetProjectsByIDs(s *xorm.Session, projectIDs []int64) (projects map[int64]*
}
type projectOptions struct {
search string
user *user.User
page int
perPage int
isArchived bool
search string
user *user.User
page int
perPage int
getArchived bool
}
func getUserProjectsStatement(userID int64) *builder.Builder {
func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search string, getArchived bool) *builder.Builder {
dialect := config.DatabaseType.GetString()
if dialect == "sqlite" {
dialect = builder.SQLITE
}
return builder.Dialect(dialect).
Select("l.*").
From("projects", "l").
Join("INNER", "namespaces n", "l.namespace_id = n.id").
Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id").
Join("LEFT", "team_members tm", "tm.team_id = tn.team_id").
Join("LEFT", "team_projects tl", "l.id = tl.project_id").
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
Join("LEFT", "users_projects ul", "ul.project_id = l.id").
Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id").
Where(builder.Or(
builder.Eq{"tm.user_id": userID},
builder.Eq{"tm2.user_id": userID},
builder.Eq{"ul.user_id": userID},
builder.Eq{"un.user_id": userID},
builder.Eq{"l.owner_id": userID},
)).
OrderBy("position").
GroupBy("l.id")
}
// Gets the projects only, without any tasks or so
func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*Project, resultCount int, totalItems int64, err error) {
fullUser, err := user.GetUserByID(s, opts.user.ID)
if err != nil {
return nil, 0, 0, err
}
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
var isArchivedCond builder.Cond = builder.Eq{"1": 1}
if !opts.isArchived {
isArchivedCond = builder.And(
var getArchivedCond builder.Cond = builder.Eq{"1": 1}
if !getArchived {
getArchivedCond = builder.And(
builder.Eq{"l.is_archived": false},
builder.Eq{"n.is_archived": false},
)
}
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
var filterCond builder.Cond
ids := []int64{}
if opts.search != "" {
vals := strings.Split(opts.search, ",")
if search != "" {
vals := strings.Split(search, ",")
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
@ -410,32 +357,168 @@ func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*P
}
}
filterCond = db.ILIKE("l.title", opts.search)
filterCond = db.ILIKE("l.title", search)
if len(ids) > 0 {
filterCond = builder.In("l.id", ids)
}
// Gets all Projects where the user is either owner or in a team which has access to the project
// Or in a team which has namespace read access
var parentCondition builder.Cond
parentCondition = builder.Or(
builder.IsNull{"l.parent_project_id"},
builder.Eq{"l.parent_project_id": 0},
// else check for shared sub projects with a parent
builder.And(
builder.Or(
builder.NotNull{"tm2.user_id"},
builder.NotNull{"ul.user_id"},
),
builder.NotNull{"l.parent_project_id"},
),
)
projectCol := "id"
if len(parentProjectIDs) > 0 {
parentCondition = builder.In("l.parent_project_id", parentProjectIDs)
projectCol = "parent_project_id"
}
query := getUserProjectsStatement(fullUser.ID).
Where(filterCond).
Where(isArchivedCond)
return builder.Dialect(dialect).
Select("l.*").
From("projects", "l").
Join("LEFT", "team_projects tl", "tl.project_id = l."+projectCol).
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
Join("LEFT", "users_projects ul", "ul.project_id = l."+projectCol).
Where(builder.And(
builder.Or(
builder.Eq{"tm2.user_id": userID},
builder.Eq{"ul.user_id": userID},
builder.Eq{"l.owner_id": userID},
),
filterCond,
getArchivedCond,
parentCondition,
)).
OrderBy("position").
GroupBy("l.id")
}
func getAllProjectsForUser(s *xorm.Session, userID int64, parentProjectIDs []int64, opts *projectOptions, projects *[]*Project, oldTotalCount int64) (resultCount int, totalCount int64, err error) {
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
query := getUserProjectsStatement(parentProjectIDs, userID, opts.search, opts.getArchived)
if limit > 0 {
query = query.Limit(limit, start)
}
err = s.SQL(query).Find(&projects)
currentProjects := []*Project{}
err = s.SQL(query).Find(&currentProjects)
if err != nil {
return 0, 0, err
}
if len(currentProjects) == 0 {
return 0, oldTotalCount, err
}
query = getUserProjectsStatement(parentProjectIDs, userID, opts.search, opts.getArchived)
totalCount, err = s.
SQL(query.Select("count(*)")).
Count(&Project{})
if err != nil {
return 0, 0, err
}
parentIDsMap := make(map[int64]bool, len(parentProjectIDs))
for _, id := range parentProjectIDs {
parentIDsMap[id] = true
}
newParentIDs := []int64{}
for _, project := range currentProjects {
// Filter out parent project ids which we're not looking for to avoid leaking
// information about parent projects
if !parentIDsMap[project.ParentProjectID] {
project.ParentProjectID = 0
}
newParentIDs = append(newParentIDs, project.ID)
}
*projects = append(*projects, currentProjects...)
// If we don't reset the limit for subprojects, it will be impossible to fetch all subprojects.
opts.page = -1
return getAllProjectsForUser(s, userID, newParentIDs, opts, projects, oldTotalCount+totalCount)
}
// Gets the projects with their children without any tasks
func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*Project, resultCount int, totalItems int64, err error) {
fullUser, err := user.GetUserByID(s, opts.user.ID)
if err != nil {
return nil, 0, 0, err
}
query = getUserProjectsStatement(fullUser.ID).
Where(filterCond).
Where(isArchivedCond)
totalItems, err = s.
SQL(query.Select("count(*)")).
Count(&Project{})
return projects, len(projects), totalItems, err
allProjects := []*Project{}
resultCount, totalItems, err = getAllProjectsForUser(s, fullUser.ID, nil, opts, &allProjects, 0)
if err != nil {
return
}
favoriteCount, err := s.
Where(builder.And(
builder.Eq{"user_id": opts.user.ID},
builder.Eq{"kind": FavoriteKindTask},
)).
Count(&Favorite{})
if err != nil {
return
}
if favoriteCount > 0 {
favoritesProject := &Project{}
*favoritesProject = FavoritesPseudoProject
allProjects = append(allProjects, favoritesProject)
}
if len(allProjects) == 0 {
return nil, 0, totalItems, nil
}
return allProjects, len(allProjects), totalItems, err
}
func getSavedFilterProjects(s *xorm.Session, doer *user.User) (savedFiltersProjects []*Project, err error) {
savedFilters, err := getSavedFiltersForUser(s, doer)
if err != nil {
return
}
if len(savedFilters) == 0 {
return nil, nil
}
for _, filter := range savedFilters {
filterProject := filter.toProject()
filterProject.Owner = doer
savedFiltersProjects = append(savedFiltersProjects, filterProject)
}
return
}
// GetAllParentProjects returns all parents of a given project
func (p *Project) GetAllParentProjects(s *xorm.Session) (err error) {
if p.ParentProjectID == 0 {
return
}
parent, err := GetProjectSimpleByID(s, p.ParentProjectID)
if err != nil {
return err
}
p.ParentProject = parent
return parent.GetAllParentProjects(s)
}
// addProjectDetails adds owner user objects and project tasks to all projects in the slice
@ -445,30 +528,17 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
}
var ownerIDs []int64
for _, l := range projects {
ownerIDs = append(ownerIDs, l.OwnerID)
}
// Get all project owners
owners := map[int64]*user.User{}
if len(ownerIDs) > 0 {
err = s.In("id", ownerIDs).Find(&owners)
if err != nil {
return
}
}
var fileIDs []int64
var projectIDs []int64
for _, l := range projects {
projectIDs = append(projectIDs, l.ID)
if o, exists := owners[l.OwnerID]; exists {
l.Owner = o
}
if l.BackgroundFileID != 0 {
l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
}
fileIDs = append(fileIDs, l.BackgroundFileID)
var fileIDs []int64
for _, p := range projects {
ownerIDs = append(ownerIDs, p.OwnerID)
projectIDs = append(projectIDs, p.ID)
fileIDs = append(fileIDs, p.BackgroundFileID)
}
owners, err := user.GetUsersByIDs(s, ownerIDs)
if err != nil {
return err
}
favs, err := getFavorites(s, projectIDs, a, FavoriteKindProject)
@ -478,19 +548,26 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
subscriptions, err := GetSubscriptions(s, SubscriptionEntityProject, projectIDs, a)
if err != nil {
log.Errorf("An error occurred while getting project subscriptions for a namespace item: %s", err.Error())
subscriptions = make(map[int64]*Subscription)
log.Errorf("An error occurred while getting project subscriptions for a project: %s", err.Error())
subscriptions = make(map[int64][]*Subscription)
}
for _, project := range projects {
for _, p := range projects {
if o, exists := owners[p.OwnerID]; exists {
p.Owner = o
}
if p.BackgroundFileID != 0 {
p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
}
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
if project.IsFavorite {
if p.IsFavorite {
continue
}
project.IsFavorite = favs[project.ID]
p.IsFavorite = favs[p.ID]
if subscription, exists := subscriptions[project.ID]; exists {
project.Subscription = subscription
if subscription, exists := subscriptions[p.ID]; exists && len(subscription) > 0 {
p.Subscription = subscription[0]
}
}
@ -520,49 +597,70 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
return
}
// NamespaceProject is a meta type to be able to join a project with its namespace
type NamespaceProject struct {
Project Project `xorm:"extends"`
Namespace Namespace `xorm:"extends"`
}
// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or its namespace is archived.
func (l *Project) CheckIsArchived(s *xorm.Session) (err error) {
// When creating a new project, we check if the namespace is archived
if l.ID == 0 {
n := &Namespace{ID: l.NamespaceID}
return n.CheckIsArchived(s)
// CheckIsArchived returns an ErrProjectIsArchived if the project or any of its parent projects is archived.
func (p *Project) CheckIsArchived(s *xorm.Session) (err error) {
if p.ParentProjectID > 0 {
p := &Project{ID: p.ParentProjectID}
return p.CheckIsArchived(s)
}
nl := &NamespaceProject{}
exists, err := s.
Table("projects").
Join("LEFT", "namespaces", "projects.namespace_id = namespaces.id").
Where("projects.id = ? AND (projects.is_archived = true OR namespaces.is_archived = true)", l.ID).
Get(nl)
if p.ID == 0 { // don't check new projects
return nil
}
project, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return
return err
}
if exists && nl.Project.ID != 0 && nl.Project.IsArchived {
return ErrProjectIsArchived{ProjectID: l.ID}
}
if exists && nl.Namespace.ID != 0 && nl.Namespace.IsArchived {
return ErrNamespaceIsArchived{NamespaceID: nl.Namespace.ID}
if project.IsArchived {
return ErrProjectIsArchived{ProjectID: p.ID}
}
return nil
}
func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) error {
if project.NamespaceID < 0 {
return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.NamespaceID}
func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) (err error) {
if project.ParentProjectID < 0 {
return &ErrProjectCannotBelongToAPseudoParentProject{ProjectID: project.ID, ParentProjectID: project.ParentProjectID}
}
// Check if the namespace exists
if project.NamespaceID > 0 {
_, err := GetNamespaceByID(s, project.NamespaceID)
// Check if the parent project exists
if project.ParentProjectID > 0 {
if project.ParentProjectID == project.ID {
return &ErrProjectCannotBeChildOfItself{
ProjectID: project.ID,
}
}
var parent *Project
parent, err = GetProjectSimpleByID(s, project.ParentProjectID)
if err != nil {
return err
}
// Check if there's a cycle in the parent relation
parentsVisited := make(map[int64]bool)
parentsVisited[project.ID] = true
for {
if parent.ParentProjectID == 0 {
break
}
// FIXME: Can we do this with better performance?
parent, err = GetProjectSimpleByID(s, parent.ParentProjectID)
if err != nil {
return err
}
if parentsVisited[parent.ID] {
return &ErrProjectCannotHaveACyclicRelationship{
ProjectID: project.ID,
}
}
parentsVisited[parent.ID] = true
}
}
// Check if the identifier is unique and not empty
@ -595,7 +693,6 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth) (err error)
project.OwnerID = doer.ID
project.Owner = doer
project.ID = 0 // Otherwise only the first time a new project would be created
err = checkProjectBeforeUpdateOrDelete(s, project)
if err != nil {
@ -634,16 +731,40 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth) (err error)
})
}
// CreateNewProjectForUser creates a new inbox project for a user. To prevent import cycles, we can't do that
// directly in the user.Create function.
func CreateNewProjectForUser(s *xorm.Session, u *user.User) (err error) {
p := &Project{
Title: "Inbox",
}
err = p.Create(s, u)
if err != nil {
return err
}
if u.DefaultProjectID != 0 {
return err
}
u.DefaultProjectID = p.ID
_, err = user.UpdateUser(s, u, false)
return err
}
func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProjectBackground bool) (err error) {
err = checkProjectBeforeUpdateOrDelete(s, project)
if err != nil {
return
}
if project.NamespaceID == 0 {
return &ErrProjectMustBelongToANamespace{
ProjectID: project.ID,
NamespaceID: project.NamespaceID,
if project.IsArchived {
isDefaultProject, err := project.isDefaultProject(s)
if err != nil {
return err
}
if isDefaultProject {
return &ErrCannotArchiveDefaultProject{ProjectID: project.ID}
}
}
@ -653,7 +774,7 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
"is_archived",
"identifier",
"hex_color",
"namespace_id",
"parent_project_id",
"position",
}
if project.Description != "" {
@ -664,6 +785,13 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
colsToUpdate = append(colsToUpdate, "background_file_id", "background_blur_hash")
}
if project.Position < 0.1 {
err = recalculateProjectPositions(s, project.ParentProjectID)
if err != nil {
return err
}
}
wasFavorite, err := isFavorite(s, project.ID, auth, FavoriteKindProject)
if err != nil {
return err
@ -706,6 +834,34 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
return
}
func recalculateProjectPositions(s *xorm.Session, parentProjectID int64) (err error) {
allProjects := []*Project{}
err = s.
Where("parent_project_id = ?", parentProjectID).
OrderBy("position asc").
Find(&allProjects)
if err != nil {
return
}
maxPosition := math.Pow(2, 32)
for i, project := range allProjects {
currentPosition := maxPosition / float64(len(allProjects)) * (float64(i + 1))
_, err = s.Cols("position").
Where("id = ?", project.ID).
Update(&Project{Position: currentPosition})
if err != nil {
return
}
}
return
}
// Update implements the update method of CRUDable
// @Summary Updates a project
// @Description Updates a project. This does not include adding a task (see below).
@ -720,27 +876,27 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id} [post]
func (l *Project) Update(s *xorm.Session, a web.Auth) (err error) {
fid := getSavedFilterIDFromProjectID(l.ID)
func (p *Project) Update(s *xorm.Session, a web.Auth) (err error) {
fid := getSavedFilterIDFromProjectID(p.ID)
if fid > 0 {
f, err := getSavedFilterSimpleByID(s, fid)
if err != nil {
return err
}
f.Title = l.Title
f.Description = l.Description
f.IsFavorite = l.IsFavorite
f.Title = p.Title
f.Description = p.Description
f.IsFavorite = p.IsFavorite
err = f.Update(s, a)
if err != nil {
return err
}
*l = *f.toProject()
*p = *f.toProject()
return nil
}
return UpdateProject(s, l, a, false)
return UpdateProject(s, p, a, false)
}
func updateProjectLastUpdated(s *xorm.Session, project *Project) error {
@ -760,25 +916,30 @@ func updateProjectByTaskID(s *xorm.Session, taskID int64) (err error) {
// Create implements the create method of CRUDable
// @Summary Creates a new project
// @Description Creates a new project in a given namespace. The user needs write-access to the namespace.
// @Description Creates a new project. If a parent project is provided the user needs to have write access to that project.
// @tags project
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param namespaceID path int true "Namespace ID"
// @Param project body models.Project true "The project you want to create."
// @Success 201 {object} models.Project "The created project."
// @Failure 400 {object} web.HTTPError "Invalid project object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/projects [put]
func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) {
err = CreateProject(s, l, a)
// @Router /projects [put]
func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) {
err = CreateProject(s, p, a)
if err != nil {
return
}
return l.ReadOne(s, a)
return p.ReadOne(s, a)
}
func (p *Project) isDefaultProject(s *xorm.Session) (is bool, err error) {
return s.
Where("default_project_id = ?", p.ID).
Exist(&user.User{})
}
// Delete implements the delete method of CRUDable
@ -793,22 +954,19 @@ func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id} [delete]
func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
fullList, err := GetProjectSimpleByID(s, l.ID)
isDefaultProject, err := p.isDefaultProject(s)
if err != nil {
return
return err
}
// Delete the project
_, err = s.ID(l.ID).Delete(&Project{})
if err != nil {
return
if isDefaultProject {
return &ErrCannotDeleteDefaultProject{ProjectID: p.ID}
}
// Delete all tasks on that project
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
tasks, _, _, err := getRawTasksForProjects(s, []*Project{l}, a, &taskOptions{})
tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskOptions{})
if err != nil {
return
}
@ -820,25 +978,36 @@ func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
}
}
err = fullList.DeleteBackgroundFileIfExists()
fullProject, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return
}
err = fullProject.DeleteBackgroundFileIfExists()
if err != nil {
return
}
// Delete the project
_, err = s.ID(p.ID).Delete(&Project{})
if err != nil {
return
}
return events.Dispatch(&ProjectDeletedEvent{
Project: l,
Project: fullProject,
Doer: a,
})
}
// DeleteBackgroundFileIfExists deletes the list's background file from the db and the filesystem,
// if one exists
func (l *Project) DeleteBackgroundFileIfExists() (err error) {
if l.BackgroundFileID == 0 {
func (p *Project) DeleteBackgroundFileIfExists() (err error) {
if p.BackgroundFileID == 0 {
return
}
file := files.File{ID: l.BackgroundFileID}
file := files.File{ID: p.BackgroundFileID}
return file.Delete()
}

View File

@ -28,97 +28,102 @@ import (
type ProjectDuplicate struct {
// The project id of the project to duplicate
ProjectID int64 `json:"-" param:"projectid"`
// The target namespace ID
NamespaceID int64 `json:"namespace_id,omitempty"`
// The target parent project
ParentProjectID int64 `json:"parent_project_id,omitempty"`
// The copied project
Project *Project `json:",omitempty"`
Project *Project `json:"duplicated_project,omitempty"`
web.Rights `json:"-"`
web.CRUDable `json:"-"`
}
// CanCreate checks if a user has the right to duplicate a project
func (ld *ProjectDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool, err error) {
func (pd *ProjectDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool, err error) {
// Project Exists + user has read access to project
ld.Project = &Project{ID: ld.ProjectID}
canRead, _, err := ld.Project.CanRead(s, a)
pd.Project = &Project{ID: pd.ProjectID}
canRead, _, err := pd.Project.CanRead(s, a)
if err != nil || !canRead {
return canRead, err
}
// Namespace exists + user has write access to is (-> can create new projects)
ld.Project.NamespaceID = ld.NamespaceID
return ld.Project.CanCreate(s, a)
if pd.ParentProjectID == 0 { // no parent project
return canRead, err
}
// Parent project exists + user has write access to is (-> can create new projects)
parent := &Project{ID: pd.ParentProjectID}
return parent.CanCreate(s, a)
}
// Create duplicates a project
// @Summary Duplicate an existing project
// @Description Copies the project, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one project to a new namespace. The user needs read access in the project and write access in the namespace of the new project.
// @Description Copies the project, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one project to a new one. The user needs read access in the project and write access in the parent of the new project.
// @tags project
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param projectID path int true "The project ID to duplicate"
// @Param project body models.ProjectDuplicate true "The target namespace which should hold the copied project."
// @Param project body models.ProjectDuplicate true "The target parent project which should hold the copied project."
// @Success 201 {object} models.ProjectDuplicate "The created project."
// @Failure 400 {object} web.HTTPError "Invalid project duplicate object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the project or namespace"
// @Failure 403 {object} web.HTTPError "The user does not have access to the project or its parent."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{projectID}/duplicate [put]
//
//nolint:gocyclo
func (ld *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
log.Debugf("Duplicating project %d", ld.ProjectID)
log.Debugf("Duplicating project %d", pd.ProjectID)
ld.Project.ID = 0
ld.Project.Identifier = "" // Reset the identifier to trigger regenerating a new one
pd.Project.ID = 0
pd.Project.Identifier = "" // Reset the identifier to trigger regenerating a new one
pd.Project.ParentProjectID = pd.ParentProjectID
// Set the owner to the current user
ld.Project.OwnerID = doer.GetID()
if err := CreateProject(s, ld.Project, doer); err != nil {
pd.Project.OwnerID = doer.GetID()
if err := CreateProject(s, pd.Project, doer); err != nil {
// If there is no available unique project identifier, just reset it.
if IsErrProjectIdentifierIsNotUnique(err) {
ld.Project.Identifier = ""
pd.Project.Identifier = ""
} else {
return err
}
}
log.Debugf("Duplicated project %d into new project %d", ld.ProjectID, ld.Project.ID)
log.Debugf("Duplicated project %d into new project %d", pd.ProjectID, pd.Project.ID)
// Duplicate kanban buckets
// Old bucket ID as key, new id as value
// Used to map the newly created tasks to their new buckets
bucketMap := make(map[int64]int64)
buckets := []*Bucket{}
err = s.Where("project_id = ?", ld.ProjectID).Find(&buckets)
err = s.Where("project_id = ?", pd.ProjectID).Find(&buckets)
if err != nil {
return
}
for _, b := range buckets {
oldID := b.ID
b.ID = 0
b.ProjectID = ld.Project.ID
b.ProjectID = pd.Project.ID
if err := b.Create(s, doer); err != nil {
return err
}
bucketMap[oldID] = b.ID
}
log.Debugf("Duplicated all buckets from project %d into %d", ld.ProjectID, ld.Project.ID)
log.Debugf("Duplicated all buckets from project %d into %d", pd.ProjectID, pd.Project.ID)
err = duplicateTasks(s, doer, ld, bucketMap)
err = duplicateTasks(s, doer, pd, bucketMap)
if err != nil {
return
}
// Background files + unsplash info
if ld.Project.BackgroundFileID != 0 {
if pd.Project.BackgroundFileID != 0 {
log.Debugf("Duplicating background %d from project %d into %d", ld.Project.BackgroundFileID, ld.ProjectID, ld.Project.ID)
log.Debugf("Duplicating background %d from project %d into %d", pd.Project.BackgroundFileID, pd.ProjectID, pd.Project.ID)
f := &files.File{ID: ld.Project.BackgroundFileID}
f := &files.File{ID: pd.Project.BackgroundFileID}
if err := f.LoadFileMetaByID(); err != nil {
return err
}
@ -133,7 +138,7 @@ func (ld *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
}
// Get unsplash info if applicable
up, err := GetUnsplashPhotoByFileID(s, ld.Project.BackgroundFileID)
up, err := GetUnsplashPhotoByFileID(s, pd.Project.BackgroundFileID)
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
return err
}
@ -145,38 +150,38 @@ func (ld *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
}
}
if err := SetProjectBackground(s, ld.Project.ID, file, ld.Project.BackgroundBlurHash); err != nil {
if err := SetProjectBackground(s, pd.Project.ID, file, pd.Project.BackgroundBlurHash); err != nil {
return err
}
log.Debugf("Duplicated project background from project %d into %d", ld.ProjectID, ld.Project.ID)
log.Debugf("Duplicated project background from project %d into %d", pd.ProjectID, pd.Project.ID)
}
// Rights / Shares
// To keep it simple(r) we will only copy rights which are directly used with the project, no namespace changes.
// To keep it simple(r) we will only copy rights which are directly used with the project, not the parent
users := []*ProjectUser{}
err = s.Where("project_id = ?", ld.ProjectID).Find(&users)
err = s.Where("project_id = ?", pd.ProjectID).Find(&users)
if err != nil {
return
}
for _, u := range users {
u.ID = 0
u.ProjectID = ld.Project.ID
u.ProjectID = pd.Project.ID
if _, err := s.Insert(u); err != nil {
return err
}
}
log.Debugf("Duplicated user shares from project %d into %d", ld.ProjectID, ld.Project.ID)
log.Debugf("Duplicated user shares from project %d into %d", pd.ProjectID, pd.Project.ID)
teams := []*TeamProject{}
err = s.Where("project_id = ?", ld.ProjectID).Find(&teams)
err = s.Where("project_id = ?", pd.ProjectID).Find(&teams)
if err != nil {
return
}
for _, t := range teams {
t.ID = 0
t.ProjectID = ld.Project.ID
t.ProjectID = pd.Project.ID
if _, err := s.Insert(t); err != nil {
return err
}
@ -184,20 +189,20 @@ func (ld *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
// Generate new link shares if any are available
linkShares := []*LinkSharing{}
err = s.Where("project_id = ?", ld.ProjectID).Find(&linkShares)
err = s.Where("project_id = ?", pd.ProjectID).Find(&linkShares)
if err != nil {
return
}
for _, share := range linkShares {
share.ID = 0
share.ProjectID = ld.Project.ID
share.ProjectID = pd.Project.ID
share.Hash = utils.MakeRandomString(40)
if _, err := s.Insert(share); err != nil {
return err
}
}
log.Debugf("Duplicated all link shares from project %d into %d", ld.ProjectID, ld.Project.ID)
log.Debugf("Duplicated all link shares from project %d into %d", pd.ProjectID, pd.Project.ID)
return
}

View File

@ -37,8 +37,7 @@ func TestProjectDuplicate(t *testing.T) {
}
l := &ProjectDuplicate{
ProjectID: 1,
NamespaceID: 1,
ProjectID: 1,
}
can, err := l.CanCreate(s, u)
assert.NoError(t, err)

View File

@ -17,6 +17,8 @@
package models
import (
"errors"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/builder"
@ -24,15 +26,15 @@ import (
)
// CanWrite return whether the user can write on that project or not
func (l *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
func (p *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
// The favorite project can't be edited
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
return false, nil
}
// Get the project and check the right
originalProject, err := GetProjectSimpleByID(s, l.ID)
originalProject, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, err
}
@ -67,66 +69,66 @@ func (l *Project) CanWrite(s *xorm.Session, a web.Auth) (bool, error) {
}
// CanRead checks if a user has read access to a project
func (l *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
// The favorite project needs a special treatment
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
owner, err := user.GetFromAuth(a)
if err != nil {
return false, 0, err
}
*l = FavoritesPseudoProject
l.Owner = owner
*p = FavoritesPseudoProject
p.Owner = owner
return true, int(RightRead), nil
}
// Saved Filter Projects need a special case
if getSavedFilterIDFromProjectID(l.ID) > 0 {
sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(l.ID)}
if getSavedFilterIDFromProjectID(p.ID) > 0 {
sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(p.ID)}
return sf.CanRead(s, a)
}
// Check if the user is either owner or can read
var err error
originalProject, err := GetProjectSimpleByID(s, l.ID)
originalProject, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, 0, err
}
*l = *originalProject
*p = *originalProject
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
return l.ID == shareAuth.ProjectID &&
return p.ID == shareAuth.ProjectID &&
(shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), int(shareAuth.Right), nil
}
if l.isOwner(&user.User{ID: a.GetID()}) {
if p.isOwner(&user.User{ID: a.GetID()}) {
return true, int(RightAdmin), nil
}
return l.checkRight(s, a, RightRead, RightWrite, RightAdmin)
return p.checkRight(s, a, RightRead, RightWrite, RightAdmin)
}
// CanUpdate checks if the user can update a project
func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error) {
// The favorite project can't be edited
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
return false, nil
}
// Get the project
ol, err := GetProjectSimpleByID(s, l.ID)
ol, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, err
}
// Check if we're moving the project into a different namespace.
// Check if we're moving the project to a different parent project.
// If that is the case, we need to verify permissions to do so.
if l.NamespaceID != 0 && l.NamespaceID != ol.NamespaceID {
newNamespace := &Namespace{ID: l.NamespaceID}
can, err := newNamespace.CanWrite(s, a)
if p.ParentProjectID != 0 && p.ParentProjectID != ol.ParentProjectID {
newProject := &Project{ID: p.ParentProjectID}
can, err := newProject.CanWrite(s, a)
if err != nil {
return false, err
}
@ -135,7 +137,7 @@ func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
}
}
fid := getSavedFilterIDFromProjectID(l.ID)
fid := getSavedFilterIDFromProjectID(p.ID)
if fid > 0 {
sf, err := getSavedFilterSimpleByID(s, fid)
if err != nil {
@ -145,34 +147,43 @@ func (l *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
return sf.CanUpdate(s, a)
}
canUpdate, err = l.CanWrite(s, a)
canUpdate, err = p.CanWrite(s, a)
// If the project is archived and the user tries to un-archive it, let the request through
if IsErrProjectIsArchived(err) && !l.IsArchived {
archivedErr := ErrProjectIsArchived{}
is := errors.As(err, &archivedErr)
if is && !p.IsArchived && archivedErr.ProjectID == p.ID {
err = nil
}
return canUpdate, err
}
// CanDelete checks if the user can delete a project
func (l *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
return l.IsAdmin(s, a)
func (p *Project) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
return p.IsAdmin(s, a)
}
// CanCreate checks if the user can create a project
func (l *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
// A user can create a project if they have write access to the namespace
n := &Namespace{ID: l.NamespaceID}
return n.CanWrite(s, a)
func (p *Project) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
if p.ParentProjectID != 0 {
parent := &Project{ID: p.ParentProjectID}
return parent.CanWrite(s, a)
}
// Check if we're dealing with a share auth
_, is := a.(*LinkSharing)
if is {
return false, nil
}
return true, nil
}
// IsAdmin returns whether the user has admin rights on the project or not
func (l *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
func (p *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
// The favorite project can't be edited
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
return false, nil
}
originalProject, err := GetProjectSimpleByID(s, l.ID)
originalProject, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return false, err
}
@ -194,22 +205,12 @@ func (l *Project) IsAdmin(s *xorm.Session, a web.Auth) (bool, error) {
}
// Little helper function to check if a user is project owner
func (l *Project) isOwner(u *user.User) bool {
return l.OwnerID == u.ID
func (p *Project) isOwner(u *user.User) bool {
return p.OwnerID == u.ID
}
// Checks n different rights for any given user
func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
/*
The following loop creates a sql condition like this one:
(ul.user_id = 1 AND ul.right = 1) OR (un.user_id = 1 AND un.right = 1) OR
(tm.user_id = 1 AND tn.right = 1) OR (tm2.user_id = 1 AND tl.right = 1) OR
for each passed right. That way, we can check with a single sql query (instead if 8)
if the user has the right to see the project or not.
*/
func (p *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
var conds []builder.Cond
for _, r := range rights {
@ -219,11 +220,6 @@ func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool
builder.Eq{"ul.user_id": a.GetID()},
builder.Eq{"ul.right": r},
))
// If the namespace this project belongs to was shared directly with the user and the user has the right
conds = append(conds, builder.And(
builder.Eq{"un.user_id": a.GetID()},
builder.Eq{"un.right": r},
))
// Team rights
// If the project was shared directly with the team and the team has the right
@ -231,66 +227,50 @@ func (l *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool
builder.Eq{"tm2.user_id": a.GetID()},
builder.Eq{"tl.right": r},
))
// If the namespace this project belongs to was shared directly with the team and the team has the right
conds = append(conds, builder.And(
builder.Eq{"tm.user_id": a.GetID()},
builder.Eq{"tn.right": r},
))
}
// If the user is the owner of a namespace, it has any right, all the time
conds = append(conds, builder.Eq{"n.owner_id": a.GetID()})
type allProjectRights struct {
UserNamespace *NamespaceUser `xorm:"extends"`
UserProject *ProjectUser `xorm:"extends"`
TeamNamespace *TeamNamespace `xorm:"extends"`
TeamProject *TeamProject `xorm:"extends"`
NamespaceOwnerID int64 `xorm:"namespaces_owner_id"`
UserProject *ProjectUser `xorm:"extends"`
TeamProject *TeamProject `xorm:"extends"`
}
r := &allProjectRights{}
var maxRight = 0
exists, err := s.
Select("l.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id").
Select("p.*, ul.right, tl.right").
Table("projects").
Alias("l").
Alias("p").
// User stuff
Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = l.namespace_id").
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id").
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = p.id").
// Team stuff
Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id").
Join("LEFT", []string{"team_projects", "tl"}, "p.id = tl.project_id").
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
// The actual condition
Where(builder.And(
builder.Or(
conds...,
),
builder.Eq{"l.id": l.ID},
builder.Eq{"p.id": p.ID},
)).
Get(r)
// Figure out the max right and return it
if int(r.UserNamespace.Right) > maxRight {
maxRight = int(r.UserNamespace.Right)
// If there's noting shared for this project, and it has a parent, go up the tree
if !exists && p.ParentProjectID > 0 {
parent, err := GetProjectSimpleByID(s, p.ParentProjectID)
if err != nil {
return false, 0, err
}
return parent.checkRight(s, a, rights...)
}
// Figure out the max right and return it
if int(r.UserProject.Right) > maxRight {
maxRight = int(r.UserProject.Right)
}
if int(r.TeamNamespace.Right) > maxRight {
maxRight = int(r.TeamNamespace.Right)
}
if int(r.TeamProject.Right) > maxRight {
maxRight = int(r.TeamProject.Right)
}
if r.NamespaceOwnerID == a.GetID() {
maxRight = int(RightAdmin)
}
return exists, maxRight, err
}

View File

@ -182,7 +182,7 @@ func (tl *TeamProject) Delete(s *xorm.Session, _ web.Auth) (err error) {
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id}/teams [get]
func (tl *TeamProject) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
// Check if the user can read the namespace
// Check if the user can read the project
l := &Project{ID: tl.ProjectID}
canRead, _, err := l.CanRead(s, a)
if err != nil {

View File

@ -56,18 +56,6 @@ func TestTeamProject_ReadAll(t *testing.T) {
assert.True(t, IsErrProjectDoesNotExist(err))
_ = s.Close()
})
t.Run("namespace owner", func(t *testing.T) {
tl := TeamProject{
TeamID: 1,
ProjectID: 2,
Right: RightAdmin,
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
_, _, _, err := tl.ReadAll(s, u, "", 1, 50)
assert.NoError(t, err)
_ = s.Close()
})
t.Run("no access", func(t *testing.T) {
tl := TeamProject{
TeamID: 1,

View File

@ -40,30 +40,29 @@ func TestProject_CreateOrUpdate(t *testing.T) {
project := Project{
Title: "test",
Description: "Lorem Ipsum",
NamespaceID: 1,
}
err := project.Create(s, usr)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "projects", map[string]interface{}{
"id": project.ID,
"title": project.Title,
"description": project.Description,
"namespace_id": project.NamespaceID,
"id": project.ID,
"title": project.Title,
"description": project.Description,
"parent_project_id": 0,
}, false)
})
t.Run("nonexistant namespace", func(t *testing.T) {
t.Run("nonexistant parent project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
Title: "test",
Description: "Lorem Ipsum",
NamespaceID: 999999,
Title: "test",
Description: "Lorem Ipsum",
ParentProjectID: 999999,
}
err := project.Create(s, usr)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))
assert.True(t, IsErrProjectDoesNotExist(err))
_ = s.Close()
})
t.Run("nonexistant owner", func(t *testing.T) {
@ -73,7 +72,6 @@ func TestProject_CreateOrUpdate(t *testing.T) {
project := Project{
Title: "test",
Description: "Lorem Ipsum",
NamespaceID: 1,
}
err := project.Create(s, usr)
assert.Error(t, err)
@ -87,7 +85,6 @@ func TestProject_CreateOrUpdate(t *testing.T) {
Title: "test",
Description: "Lorem Ipsum",
Identifier: "test1",
NamespaceID: 1,
}
err := project.Create(s, usr)
assert.Error(t, err)
@ -100,17 +97,15 @@ func TestProject_CreateOrUpdate(t *testing.T) {
project := Project{
Title: "приффки фсем",
Description: "Lorem Ipsum",
NamespaceID: 1,
}
err := project.Create(s, usr)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "projects", map[string]interface{}{
"id": project.ID,
"title": project.Title,
"description": project.Description,
"namespace_id": project.NamespaceID,
"id": project.ID,
"title": project.Title,
"description": project.Description,
}, false)
})
})
@ -123,7 +118,6 @@ func TestProject_CreateOrUpdate(t *testing.T) {
ID: 1,
Title: "test",
Description: "Lorem Ipsum",
NamespaceID: 1,
}
project.Description = "Lorem Ipsum dolor sit amet."
err := project.Update(s, usr)
@ -131,19 +125,17 @@ func TestProject_CreateOrUpdate(t *testing.T) {
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "projects", map[string]interface{}{
"id": project.ID,
"title": project.Title,
"description": project.Description,
"namespace_id": project.NamespaceID,
"id": project.ID,
"title": project.Title,
"description": project.Description,
}, false)
})
t.Run("nonexistant", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 99999999,
Title: "test",
NamespaceID: 1,
ID: 99999999,
Title: "test",
}
err := project.Update(s, usr)
assert.Error(t, err)
@ -158,14 +150,13 @@ func TestProject_CreateOrUpdate(t *testing.T) {
Title: "test",
Description: "Lorem Ipsum",
Identifier: "test1",
NamespaceID: 1,
}
err := project.Create(s, usr)
assert.Error(t, err)
assert.True(t, IsErrProjectIdentifierIsNotUnique(err))
_ = s.Close()
})
t.Run("change namespace", func(t *testing.T) {
t.Run("change parent project", func(t *testing.T) {
t.Run("own", func(t *testing.T) {
usr := &user.User{
ID: 6,
@ -176,10 +167,10 @@ func TestProject_CreateOrUpdate(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 6,
Title: "Test6",
Description: "Lorem Ipsum",
NamespaceID: 7, // from 6
ID: 6,
Title: "Test6",
Description: "Lorem Ipsum",
ParentProjectID: 7, // from 6
}
can, err := project.CanUpdate(s, usr)
assert.NoError(t, err)
@ -189,41 +180,26 @@ func TestProject_CreateOrUpdate(t *testing.T) {
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "projects", map[string]interface{}{
"id": project.ID,
"title": project.Title,
"description": project.Description,
"namespace_id": project.NamespaceID,
"id": project.ID,
"title": project.Title,
"description": project.Description,
"parent_project_id": project.ParentProjectID,
}, false)
})
// FIXME: The check for whether the namespace is archived is missing in namespace.CanWrite
// t.Run("archived own", func(t *testing.T) {
// db.LoadAndAssertFixtures(t)
// s := db.NewSession()
// project := Project{
// ID: 1,
// Title: "Test1",
// Description: "Lorem Ipsum",
// NamespaceID: 16, // from 1
// }
// can, err := project.CanUpdate(s, usr)
// assert.NoError(t, err)
// assert.False(t, can) // namespace is archived and thus not writeable
// _ = s.Close()
// })
t.Run("others", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 1,
Title: "Test1",
Description: "Lorem Ipsum",
NamespaceID: 2, // from 1
ID: 1,
Title: "Test1",
Description: "Lorem Ipsum",
ParentProjectID: 2, // from 1
}
can, _ := project.CanUpdate(s, usr)
assert.False(t, can) // namespace is not writeable by us
assert.False(t, can) // project is not writeable by us
_ = s.Close()
})
t.Run("pseudo namespace", func(t *testing.T) {
t.Run("pseudo project", func(t *testing.T) {
usr := &user.User{
ID: 6,
Username: "user6",
@ -233,16 +209,38 @@ func TestProject_CreateOrUpdate(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 6,
Title: "Test6",
Description: "Lorem Ipsum",
NamespaceID: -1,
ID: 6,
Title: "Test6",
Description: "Lorem Ipsum",
ParentProjectID: -1,
}
err := project.Update(s, usr)
assert.Error(t, err)
assert.True(t, IsErrProjectCannotBelongToAPseudoNamespace(err))
assert.True(t, IsErrProjectCannotBelongToAPseudoParentProject(err))
})
})
t.Run("archive default project of the same user", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 4,
IsArchived: true,
}
err := project.Update(s, &user.User{ID: 3})
assert.Error(t, err)
assert.True(t, IsErrCannotArchiveDefaultProject(err))
})
t.Run("archive default project of another user", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 4,
IsArchived: true,
}
err := project.Update(s, &user.User{ID: 2})
assert.Error(t, err)
assert.True(t, IsErrCannotArchiveDefaultProject(err))
})
})
}
@ -266,19 +264,39 @@ func TestProject_Delete(t *testing.T) {
files.InitTestFileFixtures(t)
s := db.NewSession()
project := Project{
ID: 25,
ID: 35,
}
err := project.Delete(s, &user.User{ID: 6})
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertMissing(t, "projects", map[string]interface{}{
"id": 25,
"id": 35,
})
db.AssertMissing(t, "files", map[string]interface{}{
"id": 1,
})
})
t.Run("default project of the same user", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 4,
}
err := project.Delete(s, &user.User{ID: 3})
assert.Error(t, err)
assert.True(t, IsErrCannotDeleteDefaultProject(err))
})
t.Run("default project of a different user", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
project := Project{
ID: 4,
}
err := project.Delete(s, &user.User{ID: 2})
assert.Error(t, err)
assert.True(t, IsErrCannotDeleteDefaultProject(err))
})
}
func TestProject_DeleteBackgroundFileIfExists(t *testing.T) {
@ -321,15 +339,18 @@ func TestProject_DeleteBackgroundFileIfExists(t *testing.T) {
}
func TestProject_ReadAll(t *testing.T) {
t.Run("all in namespace", func(t *testing.T) {
t.Run("all", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
// Get all projects for our namespace
projects, err := GetProjectsByNamespaceID(s, 1, &user.User{})
projects := []*Project{}
_, _, err := getAllProjectsForUser(s, 1, nil, &projectOptions{}, &projects, 0)
assert.NoError(t, err)
assert.Equal(t, len(projects), 2)
assert.Equal(t, 25, len(projects))
_ = s.Close()
})
t.Run("only child projects for one project", func(t *testing.T) {
// TODO
})
t.Run("all projects for user", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -340,10 +361,12 @@ func TestProject_ReadAll(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(projects3).Kind(), reflect.Slice)
ls := projects3.([]*Project)
assert.Equal(t, 16, len(ls))
assert.Equal(t, 27, len(ls))
assert.Equal(t, int64(3), ls[0].ID) // Project 3 has a position of 1 and should be sorted first
assert.Equal(t, int64(1), ls[1].ID)
assert.Equal(t, int64(4), ls[2].ID)
assert.Equal(t, int64(6), ls[2].ID)
assert.Equal(t, int64(-1), ls[25].ID)
assert.Equal(t, int64(-2), ls[26].ID)
_ = s.Close()
})
t.Run("projects for nonexistant user", func(t *testing.T) {
@ -365,8 +388,10 @@ func TestProject_ReadAll(t *testing.T) {
assert.NoError(t, err)
ls := projects3.([]*Project)
assert.Equal(t, 1, len(ls))
assert.Equal(t, 3, len(ls))
assert.Equal(t, int64(10), ls[0].ID)
assert.Equal(t, int64(-1), ls[1].ID)
assert.Equal(t, int64(-2), ls[2].ID)
_ = s.Close()
})
}

View File

@ -31,7 +31,7 @@ import (
// ProjectUser represents a project <-> user relation
type ProjectUser struct {
// The unique, numeric id of this project <-> user relation.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"namespace"`
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
// The username.
Username string `xorm:"-" json:"user_id" param:"user"`
// Used internally to reference the user
@ -55,7 +55,7 @@ func (ProjectUser) TableName() string {
return "users_projects"
}
// UserWithRight represents a user in combination with the right it can have on a project/namespace
// UserWithRight represents a user in combination with the right it can have on a project
type UserWithRight struct {
user.User `xorm:"extends"`
Right Right `json:"right"`

View File

@ -52,14 +52,14 @@ func TestProjectUser_Create(t *testing.T) {
errType func(err error) bool
}{
{
name: "ProjectUsers Create normally",
name: "ListUsers Create normally",
fields: fields{
Username: "user1",
ProjectID: 2,
},
},
{
name: "ProjectUsers Create for duplicate",
name: "ListUsers Create for duplicate",
fields: fields{
Username: "user1",
ProjectID: 3,
@ -68,7 +68,7 @@ func TestProjectUser_Create(t *testing.T) {
errType: IsErrUserAlreadyHasAccess,
},
{
name: "ProjectUsers Create with invalid right",
name: "ListUsers Create with invalid right",
fields: fields{
Username: "user1",
ProjectID: 2,
@ -78,7 +78,7 @@ func TestProjectUser_Create(t *testing.T) {
errType: IsErrInvalidRight,
},
{
name: "ProjectUsers Create with inexisting project",
name: "ListUsers Create with inexisting project",
fields: fields{
Username: "user1",
ProjectID: 2000,
@ -87,7 +87,7 @@ func TestProjectUser_Create(t *testing.T) {
errType: IsErrProjectDoesNotExist,
},
{
name: "ProjectUsers Create with inexisting user",
name: "ListUsers Create with inexisting user",
fields: fields{
Username: "user500",
ProjectID: 2,
@ -96,7 +96,7 @@ func TestProjectUser_Create(t *testing.T) {
errType: user.IsErrUserDoesNotExist,
},
{
name: "ProjectUsers Create with the owner as shared user",
name: "ListUsers Create with the owner as shared user",
fields: fields{
Username: "user1",
ProjectID: 1,
@ -166,6 +166,7 @@ func TestProjectUser_ReadAll(t *testing.T) {
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,
},

View File

@ -16,7 +16,7 @@
package models
// Right defines the rights users/teams can have for projects/namespaces
// Right defines the rights users/teams can have for projects
type Right int
// define unknown right
@ -30,7 +30,7 @@ const (
RightRead Right = iota
// Can write in a like projects and tasks. Cannot create new projects.
RightWrite
// Can manage a project/namespace, can do everything
// Can manage a project, can do everything
RightAdmin
)

View File

@ -39,7 +39,7 @@ type SavedFilter struct {
// The user who owns this filter
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
// True if the filter is a favorite. Favorite filters show up in a separate namespace together with favorite projects.
// True if the filter is a favorite. Favorite filters show up in a separate parent project together with favorite projects.
IsFavorite bool `xorm:"default false" json:"is_favorite"`
// A timestamp when this filter was created. You cannot change this value.
@ -102,7 +102,6 @@ func (sf *SavedFilter) toProject() *Project {
Created: sf.Created,
Updated: sf.Updated,
Owner: sf.Owner,
NamespaceID: SavedFiltersPseudoNamespace.ID,
}
}

View File

@ -30,16 +30,15 @@ import (
type SubscriptionEntityType int
const (
SubscriptionEntityUnknown = iota
SubscriptionEntityNamespace
SubscriptionEntityUnknown = iota
SubscriptionEntityNamespace // Kept even though not used anymore since we don't want to manually change all ids
SubscriptionEntityProject
SubscriptionEntityTask
)
const (
entityNamespace = `namespace`
entityProject = `project`
entityTask = `task`
entityProject = `project`
entityTask = `task`
)
// Subscription represents a subscription for an entity
@ -70,8 +69,6 @@ func (sb *Subscription) TableName() string {
func getEntityTypeFromString(entityType string) SubscriptionEntityType {
switch entityType {
case entityNamespace:
return SubscriptionEntityNamespace
case entityProject:
return SubscriptionEntityProject
case entityTask:
@ -84,8 +81,6 @@ func getEntityTypeFromString(entityType string) SubscriptionEntityType {
// String returns a human-readable string of an entity
func (et SubscriptionEntityType) String() string {
switch et {
case SubscriptionEntityNamespace:
return entityNamespace
case SubscriptionEntityProject:
return entityProject
case SubscriptionEntityTask:
@ -96,8 +91,7 @@ func (et SubscriptionEntityType) String() string {
}
func (et SubscriptionEntityType) validate() error {
if et == SubscriptionEntityNamespace ||
et == SubscriptionEntityProject ||
if et == SubscriptionEntityProject ||
et == SubscriptionEntityTask {
return nil
}
@ -112,7 +106,7 @@ func (et SubscriptionEntityType) validate() error {
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param entity path string true "The entity the user subscribes to. Can be either `namespace`, `project` or `task`."
// @Param entity path string true "The entity the user subscribes to. Can be either `project` or `task`."
// @Param entityID path string true "The numeric id of the entity to subscribe to."
// @Success 201 {object} models.Subscription "The subscription"
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
@ -153,7 +147,7 @@ func (sb *Subscription) Create(s *xorm.Session, auth web.Auth) (err error) {
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param entity path string true "The entity the user subscribed to. Can be either `namespace`, `project` or `task`."
// @Param entity path string true "The entity the user subscribed to. Can be either `project` or `task`."
// @Param entityID path string true "The numeric id of the subscribed entity to."
// @Success 200 {object} models.Subscription "The subscription"
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
@ -169,51 +163,26 @@ func (sb *Subscription) Delete(s *xorm.Session, auth web.Auth) (err error) {
return
}
func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int64) (cond builder.Cond) {
if entityType == SubscriptionEntityNamespace {
cond = builder.And(
builder.Eq{"entity_id": entityID},
builder.Eq{"entity_type": SubscriptionEntityNamespace},
)
}
func getSubscriberCondForEntities(entityType SubscriptionEntityType, entityIDs []int64) (cond builder.Cond) {
if entityType == SubscriptionEntityProject {
cond = builder.Or(
builder.And(
builder.Eq{"entity_id": entityID},
builder.Eq{"entity_type": SubscriptionEntityProject},
),
builder.And(
builder.Eq{"entity_id": builder.
Select("namespace_id").
From("projects").
Where(builder.Eq{"id": entityID}),
},
builder.Eq{"entity_type": SubscriptionEntityNamespace},
),
return builder.And(
builder.In("entity_id", entityIDs),
builder.Eq{"entity_type": SubscriptionEntityProject},
)
}
if entityType == SubscriptionEntityTask {
cond = builder.Or(
return builder.Or(
builder.And(
builder.Eq{"entity_id": entityID},
builder.In("entity_id", entityIDs),
builder.Eq{"entity_type": SubscriptionEntityTask},
),
builder.And(
builder.Eq{"entity_id": builder.
Select("namespace_id").
From("projects").
Join("INNER", "tasks", "projects.id = tasks.project_id").
Where(builder.Eq{"tasks.id": entityID}),
},
builder.Eq{"entity_type": SubscriptionEntityNamespace},
),
builder.And(
builder.Eq{"entity_id": builder.
Select("project_id").
From("tasks").
Where(builder.Eq{"id": entityID}),
Where(builder.In("id", entityIDs)),
// TODO parent project
},
builder.Eq{"entity_type": SubscriptionEntityProject},
),
@ -225,74 +194,183 @@ func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int6
// GetSubscription returns a matching subscription for an entity and user.
// It will return the next parent of a subscription. That means for tasks, it will first look for a subscription for
// that task, if there is none it will look for a subscription on the project the task belongs to and if that also
// doesn't exist it will check for a subscription for the namespace the project is belonging to.
// that task, if there is none it will look for a subscription on the project the task belongs to.
func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) {
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, a)
if err != nil || len(subs) == 0 {
return nil, err
}
if sub, exists := subs[entityID]; exists {
return sub, nil // Take exact match first, if available
if sub, exists := subs[entityID]; exists && len(sub) > 0 {
return sub[0], nil // Take exact match first, if available
}
for _, sub := range subs {
return sub, nil // For parents, take next available
if len(sub) > 0 {
return sub[0], nil // For parents, take next available
}
}
return nil, nil
}
// GetSubscriptions returns a map of subscriptions to a set of given entity IDs
func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (projectsToSubscriptions map[int64]*Subscription, err error) {
func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (projectsToSubscriptions map[int64][]*Subscription, err error) {
u, is := a.(*user.User)
if !is {
if u != nil && !is {
return
}
if err := entityType.validate(); err != nil {
return nil, err
}
var entitiesFilter builder.Cond
for _, eID := range entityIDs {
if entitiesFilter == nil {
entitiesFilter = getSubscriberCondForEntity(entityType, eID)
switch entityType {
case SubscriptionEntityProject:
return getSubscriptionsForProjects(s, entityIDs, u)
case SubscriptionEntityTask:
subs, err := getSubscriptionsForTasks(s, entityIDs, u)
if err != nil {
return nil, err
}
// If the task does not have a subscription directly or from its project, get the one
// from the parent and return it instead.
for _, eID := range entityIDs {
if _, has := subs[eID]; has {
continue
}
task, err := GetTaskByIDSimple(s, eID)
if err != nil {
return nil, err
}
projectSubscriptions, err := getSubscriptionsForProjects(s, []int64{task.ProjectID}, u)
if err != nil {
return nil, err
}
for _, subscription := range projectSubscriptions {
subs[eID] = subscription // The first project subscription is the subscription we're looking for
break
}
}
return subs, nil
}
return
}
func getSubscriptionsForProjects(s *xorm.Session, projectIDs []int64, u *user.User) (projectsToSubscriptions map[int64][]*Subscription, err error) {
origEntityIDs := projectIDs
var ps = make(map[int64]*Project)
for _, eID := range projectIDs {
if eID < 1 {
continue
}
entitiesFilter = entitiesFilter.Or(getSubscriberCondForEntity(entityType, eID))
ps[eID], err = GetProjectSimpleByID(s, eID)
if err != nil {
return nil, err
}
err = ps[eID].GetAllParentProjects(s)
if err != nil {
return nil, err
}
parentIDs := []int64{}
var parent = ps[eID].ParentProject
for parent != nil {
parentIDs = append(parentIDs, parent.ID)
parent = parent.ParentProject
}
// Now we have all parent ids
projectIDs = append(projectIDs, parentIDs...) // the child project id is already in there
}
var subscriptions []*Subscription
err = s.
Where("user_id = ?", u.ID).
And(entitiesFilter).
Find(&subscriptions)
if u != nil {
err = s.
Where("user_id = ?", u.ID).
And(getSubscriberCondForEntities(SubscriptionEntityProject, projectIDs)).
Find(&subscriptions)
} else {
err = s.
And(getSubscriberCondForEntities(SubscriptionEntityProject, projectIDs)).
Find(&subscriptions)
}
if err != nil {
return nil, err
}
projectsToSubscriptions = make(map[int64]*Subscription)
projectsToSubscriptions = make(map[int64][]*Subscription)
for _, sub := range subscriptions {
sub.Entity = sub.EntityType.String()
projectsToSubscriptions[sub.EntityID] = sub
projectsToSubscriptions[sub.EntityID] = append(projectsToSubscriptions[sub.EntityID], sub)
}
// Rearrange so that subscriptions trickle down
for _, eID := range origEntityIDs {
// If the current project does not have a subscription, climb up the tree until a project has one,
// then use that subscription for all child projects
_, has := projectsToSubscriptions[eID]
_, hasProject := ps[eID]
if !has && hasProject {
var parent = ps[eID].ParentProject
for parent != nil {
sub, has := projectsToSubscriptions[parent.ID]
projectsToSubscriptions[eID] = sub
parent = parent.ParentProject
if has { // reached the top of the tree
break
}
}
}
}
return projectsToSubscriptions, nil
}
func getSubscriptionsForTasks(s *xorm.Session, taskIDs []int64, u *user.User) (projectsToSubscriptions map[int64][]*Subscription, err error) {
var subscriptions []*Subscription
if u != nil {
err = s.
Where("user_id = ?", u.ID).
And(getSubscriberCondForEntities(SubscriptionEntityTask, taskIDs)).
Find(&subscriptions)
} else {
err = s.
And(getSubscriberCondForEntities(SubscriptionEntityTask, taskIDs)).
Find(&subscriptions)
}
if err != nil {
return nil, err
}
projectsToSubscriptions = make(map[int64][]*Subscription)
for _, sub := range subscriptions {
sub.Entity = sub.EntityType.String()
projectsToSubscriptions[sub.EntityID] = append(projectsToSubscriptions[sub.EntityID], sub)
}
return
}
func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType, entityID int64) (subscriptions []*Subscription, err error) {
if err := entityType.validate(); err != nil {
return nil, err
}
cond := getSubscriberCondForEntity(entityType, entityID)
err = s.
Where(cond).
Find(&subscriptions)
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, nil)
if err != nil {
return
}
userIDs := []int64{}
for _, subscription := range subscriptions {
userIDs = append(userIDs, subscription.UserID)
subscriptions = make([]*Subscription, 0, len(subs))
for _, subss := range subs {
for _, subscription := range subss {
userIDs = append(userIDs, subscription.UserID)
subscriptions = append(subscriptions, subscription)
}
}
users, err := user.GetUsersByIDs(s, userIDs)

View File

@ -30,9 +30,6 @@ func (sb *Subscription) CanCreate(s *xorm.Session, a web.Auth) (can bool, err er
sb.EntityType = getEntityTypeFromString(sb.Entity)
switch sb.EntityType {
case SubscriptionEntityNamespace:
n := &Namespace{ID: sb.EntityID}
can, _, err = n.CanRead(s, a)
case SubscriptionEntityProject:
l := &Project{ID: sb.EntityID}
can, _, err = l.CanRead(s, a)

View File

@ -25,10 +25,6 @@ import (
)
func TestSubscriptionGetTypeFromString(t *testing.T) {
t.Run("namespace", func(t *testing.T) {
entityType := getEntityTypeFromString("namespace")
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityNamespace), entityType)
})
t.Run("project", func(t *testing.T) {
entityType := getEntityTypeFromString("project")
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityProject), entityType)
@ -88,22 +84,6 @@ func TestSubscription_Create(t *testing.T) {
assert.Error(t, err)
assert.False(t, can)
})
t.Run("noneixsting namespace", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
sb := &Subscription{
Entity: "namespace",
EntityID: 99999999,
UserID: u.ID,
}
can, err := sb.CanCreate(s, u)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))
assert.False(t, can)
})
t.Run("noneixsting project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -136,21 +116,6 @@ func TestSubscription_Create(t *testing.T) {
assert.True(t, IsErrTaskDoesNotExist(err))
assert.False(t, can)
})
t.Run("no rights to see namespace", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
sb := &Subscription{
Entity: "namespace",
EntityID: 6,
UserID: u.ID,
}
can, err := sb.CanCreate(s, u)
assert.NoError(t, err)
assert.False(t, can)
})
t.Run("no rights to see project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -268,16 +233,6 @@ func TestSubscriptionGet(t *testing.T) {
u := &user.User{ID: 6}
t.Run("test each individually", func(t *testing.T) {
t.Run("namespace", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
sub, err := GetSubscription(s, SubscriptionEntityNamespace, 6, u)
assert.NoError(t, err)
assert.NotNil(t, sub)
assert.Equal(t, int64(2), sub.ID)
})
t.Run("project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -300,38 +255,51 @@ func TestSubscriptionGet(t *testing.T) {
})
})
t.Run("inherited", func(t *testing.T) {
t.Run("project from namespace", func(t *testing.T) {
t.Run("project from parent", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Project 6 belongs to namespace 6 where user 6 has subscribed to
sub, err := GetSubscription(s, SubscriptionEntityProject, 6, u)
// Project 25 belongs to project 12 where user 6 has subscribed to
sub, err := GetSubscription(s, SubscriptionEntityProject, 25, u)
assert.NoError(t, err)
assert.NotNil(t, sub)
assert.Equal(t, int64(2), sub.ID)
assert.Equal(t, int64(12), sub.EntityID)
assert.Equal(t, int64(3), sub.ID)
})
t.Run("task from namespace", func(t *testing.T) {
t.Run("project from parent's parent", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Task 20 belongs to project 11 which belongs to namespace 6 where the user has subscribed
sub, err := GetSubscription(s, SubscriptionEntityTask, 20, u)
// Project 26 belongs to project 25 which belongs to project 12 where user 6 has subscribed to
sub, err := GetSubscription(s, SubscriptionEntityProject, 26, u)
assert.NoError(t, err)
assert.NotNil(t, sub)
assert.Equal(t, int64(2), sub.ID)
assert.Equal(t, int64(12), sub.EntityID)
assert.Equal(t, int64(3), sub.ID)
})
t.Run("task from parent", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Task 39 belongs to project 25 which belongs to project 12 where the user has subscribed
sub, err := GetSubscription(s, SubscriptionEntityTask, 39, u)
assert.NoError(t, err)
assert.NotNil(t, sub)
// assert.Equal(t, int64(2), sub.ID) TODO
})
t.Run("task from project", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Task 21 belongs to project 12 which the user has subscribed to
// Task 21 belongs to project 32 which the user has subscribed to
sub, err := GetSubscription(s, SubscriptionEntityTask, 21, u)
assert.NoError(t, err)
assert.NotNil(t, sub)
assert.Equal(t, int64(3), sub.ID)
assert.Equal(t, int64(8), sub.ID)
})
})
t.Run("invalid type", func(t *testing.T) {

View File

@ -24,8 +24,7 @@ import (
// TaskCollection is a struct used to hold filter details and not clutter the Task struct with information not related to actual tasks.
type TaskCollection struct {
ProjectID int64 `param:"project" json:"-"`
Projects []*Project `json:"-"`
ProjectID int64 `param:"project" json:"-"`
// The query parameter to sort by. This is for ex. done, priority, etc.
SortBy []string `query:"sort_by" json:"sort_by"`
@ -150,13 +149,21 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
return nil, 0, 0, err
}
sf.Filters.SortByArr = append(sf.Filters.SortByArr, tf.SortByArr...)
sf.Filters.SortBy = append(sf.Filters.SortBy, tf.SortBy...)
if len(sf.Filters.OrderBy) > len(sf.Filters.SortBy) {
sf.Filters.OrderBy = sf.Filters.OrderBy[:len(sf.Filters.SortBy)]
}
sf.Filters.OrderBy = append(sf.Filters.OrderBy, tf.OrderBy...)
sf.Filters.OrderByArr = tf.OrderByArr
// By prepending sort options before the saved ones from the filter, we make sure the supplied sort
// options via query take precedence over the rest.
sortby := append(tf.SortBy, tf.SortByArr...)
sortby = append(sortby, sf.Filters.SortBy...)
sortby = append(sortby, sf.Filters.SortByArr...)
orderby := append(tf.OrderBy, tf.OrderByArr...)
orderby = append(orderby, sf.Filters.OrderBy...)
orderby = append(orderby, sf.Filters.OrderByArr...)
sf.Filters.SortBy = sortby
sf.Filters.SortByArr = nil
sf.Filters.OrderBy = orderby
sf.Filters.OrderByArr = nil
return sf.getTaskCollection().ReadAll(s, a, search, page, perPage)
}
@ -181,8 +188,9 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
// If the project ID is not set, we get all tasks for the user.
// This allows to use this function in Task.ReadAll with a possibility to deprecate the latter at some point.
var projects []*Project
if tf.ProjectID == 0 {
tf.Projects, _, _, err = getRawProjectsForUser(
projects, _, _, err = getRawProjectsForUser(
s,
&projectOptions{
user: &user.User{ID: a.GetID()},
@ -193,7 +201,7 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
return nil, 0, 0, err
}
} else {
// Check the project exists and the user has acess on it
// Check the project exists and the user has access on it
project := &Project{ID: tf.ProjectID}
canRead, _, err := project.CanRead(s, a)
if err != nil {
@ -202,8 +210,8 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
if !canRead {
return nil, 0, 0, ErrUserDoesNotHaveAccessToProject{ProjectID: tf.ProjectID}
}
tf.Projects = []*Project{{ID: tf.ProjectID}}
projects = []*Project{{ID: tf.ProjectID}}
}
return getTasksForProjects(s, tf.Projects, a, taskopts)
return getTasksForProjects(s, projects, a, taskopts)
}

View File

@ -26,7 +26,7 @@ import (
"code.vikunja.io/api/pkg/config"
"github.com/iancoleman/strcase"
"github.com/vectordotdev/go-datemath"
"github.com/jszwedko/go-datemath"
"xorm.io/xorm/schemas"
)
@ -237,24 +237,6 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID")
if realFieldName == "Namespace" {
if comparator == taskFilterComparatorIn {
vals := strings.Split(value, ",")
valueSlice := []interface{}{}
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return nil, nil, err
}
valueSlice = append(valueSlice, v)
}
return nil, valueSlice, nil
}
nativeValue, err = strconv.ParseInt(value, 10, 64)
return
}
if realFieldName == "Assignees" {
vals := strings.Split(value, ",")
valueSlice := append([]string{}, vals...)

View File

@ -17,6 +17,7 @@
package models
import (
"sort"
"testing"
"time"
@ -49,6 +50,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -169,9 +171,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
label4,
},
RelatedTasks: map[RelationKind][]*Task{},
ReminderDates: []time.Time{
time.Unix(1543626824, 0).In(loc),
},
Reminders: []*TaskReminder{
{
ID: 3,
@ -403,11 +402,11 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task21 := &Task{
ID: 21,
Title: "task #21",
Identifier: "test12-1",
Identifier: "#1",
Index: 1,
CreatedByID: 6,
CreatedBy: user6,
ProjectID: 12,
ProjectID: 32, // parent project is shared to user 1 via direct share
RelatedTasks: map[RelationKind][]*Task{},
BucketID: 12,
Created: time.Unix(1543626724, 0).In(loc),
@ -416,26 +415,26 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task22 := &Task{
ID: 22,
Title: "task #22",
Identifier: "test13-1",
Identifier: "#1",
Index: 1,
CreatedByID: 6,
CreatedBy: user6,
ProjectID: 13,
ProjectID: 33,
RelatedTasks: map[RelationKind][]*Task{},
BucketID: 13,
BucketID: 36,
Created: time.Unix(1543626724, 0).In(loc),
Updated: time.Unix(1543626724, 0).In(loc),
}
task23 := &Task{
ID: 23,
Title: "task #23",
Identifier: "test14-1",
Identifier: "#1",
Index: 1,
CreatedByID: 6,
CreatedBy: user6,
ProjectID: 14,
ProjectID: 34,
RelatedTasks: map[RelationKind][]*Task{},
BucketID: 14,
BucketID: 37,
Created: time.Unix(1543626724, 0).In(loc),
Updated: time.Unix(1543626724, 0).In(loc),
}
@ -446,7 +445,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Index: 1,
CreatedByID: 6,
CreatedBy: user6,
ProjectID: 15,
ProjectID: 15, // parent project is shared to user 1 via team
RelatedTasks: map[RelationKind][]*Task{},
BucketID: 15,
Created: time.Unix(1543626724, 0).In(loc),
@ -485,10 +484,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Index: 12,
CreatedByID: 1,
CreatedBy: user1,
ReminderDates: []time.Time{
time.Unix(1543626724, 0).In(loc),
time.Unix(1543626824, 0).In(loc),
},
Reminders: []*TaskReminder{
{
ID: 1,
@ -613,6 +608,54 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Created: time.Unix(1543626724, 0).In(loc),
Updated: time.Unix(1543626724, 0).In(loc),
}
task35 := &Task{
ID: 35,
Title: "task #35",
Identifier: "test21-1",
Index: 1,
CreatedByID: 1,
CreatedBy: user1,
ProjectID: 21,
Assignees: []*user.User{
user2,
},
Labels: []*Label{
label4,
},
RelatedTasks: map[RelationKind][]*Task{
RelationKindRelated: {
{
ID: 1,
Title: "task #1",
Description: "Lorem Ipsum",
Index: 1,
CreatedByID: 1,
ProjectID: 1,
IsFavorite: true,
Created: time.Unix(1543626724, 0).In(loc),
Updated: time.Unix(1543626724, 0).In(loc),
BucketID: 1,
Position: 2,
},
{
ID: 1,
Title: "task #1",
Description: "Lorem Ipsum",
Index: 1,
CreatedByID: 1,
ProjectID: 1,
IsFavorite: true,
Created: time.Unix(1543626724, 0).In(loc),
Updated: time.Unix(1543626724, 0).In(loc),
BucketID: 1,
Position: 2,
},
},
},
BucketID: 19,
Created: time.Unix(1543626724, 0).In(loc),
Updated: time.Unix(1543626724, 0).In(loc),
}
type fields struct {
ProjectID int64
@ -637,7 +680,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
name string
fields fields
args args
want interface{}
want []*Task
wantErr bool
}
@ -684,18 +727,20 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task31,
task32,
task33,
task35,
},
wantErr: false,
},
{
// For more sorting tests see task_collection_sort_test.go
name: "ReadAll Tasks sorted by done asc and id desc",
name: "sorted by done asc and id desc",
fields: fields{
SortBy: []string{"done", "id"},
OrderBy: []string{"asc", "desc"},
},
args: defaultArgs,
want: []*Task{
task35,
task33,
task32,
task31,
@ -812,11 +857,13 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task19,
task20,
task21,
task22,
task23,
task24,
task25,
task26,
task27,
task28,
task29,
@ -824,6 +871,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task31,
task32,
task33,
task35,
},
wantErr: false,
},
@ -894,6 +942,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task31, // has nil dates
task32, // has nil dates
task33, // has nil dates
task35, // has nil dates
},
wantErr: false,
},
@ -1003,6 +1052,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
args: defaultArgs,
want: []*Task{
task30,
task35,
},
wantErr: false,
},
@ -1049,6 +1099,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
args: defaultArgs,
want: []*Task{
task30,
task35,
},
wantErr: false,
},
@ -1063,6 +1114,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
want: []*Task{
task1,
task2,
task35,
},
wantErr: false,
},
@ -1079,33 +1131,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
},
wantErr: false,
},
{
name: "filter namespace",
fields: fields{
FilterBy: []string{"namespace"},
FilterValue: []string{"7"},
FilterComparator: []string{"equals"},
},
args: defaultArgs,
want: []*Task{
task21,
},
wantErr: false,
},
{
name: "filter namespace in",
fields: fields{
FilterBy: []string{"namespace"},
FilterValue: []string{"7,8"},
FilterComparator: []string{"in"},
},
args: defaultArgs,
want: []*Task{
task21,
task22,
},
wantErr: false,
},
// TODO filter parent project?
{
name: "filter by index",
fields: fields{
@ -1175,6 +1201,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task31,
task32,
task33,
task35,
},
},
{
@ -1191,6 +1218,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task6,
task5,
// The other ones don't have a due date
task35,
task33,
task32,
task31,
@ -1240,6 +1268,42 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task5,
},
},
{
name: "saved filter with sort order asc",
fields: fields{
ProjectID: -2,
SortBy: []string{"title", "id"},
OrderBy: []string{"asc", "asc"},
},
args: args{
a: &user.User{ID: 1},
},
want: []*Task{
task5,
task6,
task7,
task8,
task9,
},
},
{
name: "saved filter with sort by due date",
fields: fields{
ProjectID: -2,
SortBy: []string{"due_date", "id"},
OrderBy: []string{"asc", "asc"},
},
args: args{
a: &user.User{ID: 1},
},
want: []*Task{
task6,
task5,
task7,
task8,
task9,
},
},
}
for _, tt := range tests {
@ -1267,11 +1331,35 @@ func TestTaskCollection_ReadAll(t *testing.T) {
return
}
if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
if len(got.([]*Task)) == 0 && len(tt.want.([]*Task)) == 0 {
var is bool
var gotTasks []*Task
gotTasks, is = got.([]*Task)
if !is {
gotTasks = []*Task{}
}
if len(gotTasks) == 0 && len(tt.want) == 0 {
return
}
t.Errorf("Test %s, Task.ReadAll() = %v, \nwant %v, \ndiff: %v", tt.name, got, tt.want, diff)
gotIDs := []int64{}
for _, t := range got.([]*Task) {
gotIDs = append(gotIDs, t.ID)
}
wantIDs := []int64{}
for _, t := range tt.want {
wantIDs = append(wantIDs, t.ID)
}
sort.Slice(wantIDs, func(i, j int) bool {
return wantIDs[i] < wantIDs[j]
})
sort.Slice(gotIDs, func(i, j int) bool {
return gotIDs[i] < gotIDs[j]
})
diffIDs, _ := messagediff.PrettyDiff(gotIDs, wantIDs)
t.Errorf("Test %s, Task.ReadAll() = %v, \nwant %v, \ndiff: %v \n\n diffIDs: %v", tt.name, got, tt.want, diff, diffIDs)
}
})
}

View File

@ -37,9 +37,8 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
var tasks []*Task
err = s.
Where("due_date is not null AND due_date < ? AND projects.is_archived = false AND namespaces.is_archived = false", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
Where("due_date is not null AND due_date < ? AND projects.is_archived = false", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
Join("LEFT", "projects", "projects.id = tasks.project_id").
Join("LEFT", "namespaces", "projects.namespace_id = namespaces.id").
And("done = false").
Find(&tasks)
if err != nil {

View File

@ -157,7 +157,7 @@ func TestTaskRelation_CanCreate(t *testing.T) {
rel := TaskRelation{
TaskID: 1,
OtherTaskID: 13,
OtherTaskID: 32,
RelationKind: RelationKindSubtask,
}
can, err := rel.CanCreate(s, &user.User{ID: 1})

View File

@ -31,8 +31,8 @@ import (
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"dario.cat/mergo"
"github.com/google/uuid"
"github.com/imdario/mergo"
"github.com/jinzhu/copier"
"xorm.io/builder"
"xorm.io/xorm"
@ -61,10 +61,6 @@ type Task struct {
DoneAt time.Time `xorm:"INDEX null 'done_at'" json:"done_at"`
// The time when the task is due.
DueDate time.Time `xorm:"DATETIME INDEX null 'due_date'" json:"due_date"`
// An array of datetimes when the user wants to be reminded of the task.
//
// Deprecated: Use Reminders
ReminderDates []time.Time `xorm:"-" json:"reminder_dates"`
// An array of reminders that are associated with this task.
Reminders []*TaskReminder `xorm:"-" json:"reminders"`
// The project this task belongs to.
@ -151,6 +147,9 @@ func (*Task) TableName() string {
// GetFullIdentifier returns the task identifier if the task has one and the index prefixed with # otherwise.
func (t *Task) GetFullIdentifier() string {
if t.Identifier != "" {
if strings.HasPrefix(t.Identifier, "-") {
return "#" + strings.TrimPrefix(t.Identifier, "-")
}
return t.Identifier
}
@ -333,7 +332,7 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
reminderFilters := []builder.Cond{}
assigneeFilters := []builder.Cond{}
labelFilters := []builder.Cond{}
namespaceFilters := []builder.Cond{}
projectFilters := []builder.Cond{}
var filters = make([]builder.Cond, 0, len(opts.filters))
// To still find tasks with nil values, we exclude 0s when comparing with >/< values.
@ -371,13 +370,13 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
continue
}
if f.field == "namespace" || f.field == "namespace_id" {
f.field = "namespace_id"
if f.field == "parent_project" || f.field == "parent_project_id" {
f.field = "parent_project_id"
filter, err := getFilterCond(f, opts.filterIncludeNulls)
if err != nil {
return nil, 0, 0, err
}
namespaceFilters = append(namespaceFilters, filter)
projectFilters = append(projectFilters, filter)
continue
}
@ -401,30 +400,12 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
}
var projectIDCond builder.Cond
var projectCond builder.Cond
var favoritesCond builder.Cond
if len(projectIDs) > 0 {
projectIDCond = builder.In("project_id", projectIDs)
projectCond = projectIDCond
}
if hasFavoritesProject {
// Make sure users can only see their favorites
userProjects, _, _, err := getRawProjectsForUser(
s,
&projectOptions{
user: &user.User{ID: a.GetID()},
page: -1,
},
)
if err != nil {
return nil, 0, 0, err
}
userProjectIDs := make([]int64, 0, len(userProjects))
for _, l := range userProjects {
userProjectIDs = append(userProjectIDs, l.ID)
}
// All favorite tasks for that user
favCond := builder.
Select("entity_id").
@ -435,7 +416,7 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
builder.Eq{"kind": FavoriteKindTask},
))
projectCond = builder.And(projectCond, builder.And(builder.In("id", favCond), builder.In("project_id", userProjectIDs)))
favoritesCond = builder.In("id", favCond)
}
if len(reminderFilters) > 0 {
@ -456,13 +437,13 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
filters = append(filters, getFilterCondForSeparateTable("label_tasks", opts.filterConcat, labelFilters))
}
if len(namespaceFilters) > 0 {
if len(projectFilters) > 0 {
var filtercond builder.Cond
if opts.filterConcat == filterConcatOr {
filtercond = builder.Or(namespaceFilters...)
filtercond = builder.Or(projectFilters...)
}
if opts.filterConcat == filterConcatAnd {
filtercond = builder.And(namespaceFilters...)
filtercond = builder.And(projectFilters...)
}
cond := builder.In(
@ -486,7 +467,7 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
}
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
cond := builder.And(projectCond, where, filterCond)
cond := builder.And(builder.Or(projectIDCond, favoritesCond), where, filterCond)
query := s.Where(cond)
if limit > 0 {
@ -594,6 +575,11 @@ func getRemindersForTasks(s *xorm.Session, taskIDs []int64) (reminders []*TaskRe
}
func (t *Task) setIdentifier(project *Project) {
if project == nil || (project != nil && project.Identifier == "") {
t.Identifier = "#" + strconv.FormatInt(t.Index, 10)
return
}
t.Identifier = project.Identifier + "-" + strconv.FormatInt(t.Index, 10)
}
@ -776,11 +762,6 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e
// Make created by user objects
task.CreatedBy = users[task.CreatedByID]
// Add the reminder dates (Remove, when ReminderDates is removed)
for _, r := range taskReminders[task.ID] {
task.ReminderDates = append(task.ReminderDates, r.Reminder)
}
// Add the reminders
task.Reminders = taskReminders[task.ID]
@ -1035,12 +1016,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
// Old task has the stored reminders
ot.Reminders = reminders
// Deprecated: remove when ReminderDates is removed
ot.ReminderDates = make([]time.Time, len(reminders))
for i, r := range reminders {
ot.ReminderDates[i] = r.Reminder
}
targetBucket, err := setTaskBucket(s, t, &ot, t.BucketID != 0 && t.BucketID != ot.BucketID)
if err != nil {
return err
@ -1491,24 +1466,6 @@ func updateDone(oldTask *Task, newTask *Task) {
}
}
// Deprecated: will be removed when ReminderDates are removed from Task.
// For now the method just creates TaskReminder objects from the ReminderDates and overwrites Reminder.
func (t *Task) overwriteRemindersWithReminderDates(reminderDates []time.Time) {
// If the client still sends old reminder_dates, then these will overwrite
// the Reminders, if the were sent by the client, too.
// We assume that clients still using the old API with reminder_dates do not understand the new reminders.
// Clients who want to use the new Reminder structure must explicitey unset reminder_dates.
// start with empty Reminders
reminders := make([]*TaskReminder, 0)
// append absolute triggers from ReminderDates
for _, reminderDate := range reminderDates {
reminders = append(reminders, &TaskReminder{TaskID: t.ID, Reminder: reminderDate})
}
t.Reminders = reminders
}
// Set the absolute trigger dates for Reminders with relative period
func updateRelativeReminderDates(task *Task) (err error) {
for _, reminder := range task.Reminders {
@ -1547,11 +1504,6 @@ func updateRelativeReminderDates(task *Task) (err error) {
// The parameter is a slice which holds the new reminders.
func (t *Task) updateReminders(s *xorm.Session, task *Task) (err error) {
// Deprecated: This statement must be removed when ReminderDates will be removed
if task.ReminderDates != nil {
task.overwriteRemindersWithReminderDates(task.ReminderDates)
}
_, err = s.
Where("task_id = ?", t.ID).
Delete(&TaskReminder{})
@ -1571,7 +1523,6 @@ func (t *Task) updateReminders(s *xorm.Session, task *Task) (err error) {
}
t.Reminders = make([]*TaskReminder, 0, len(reminderMap))
t.ReminderDates = make([]time.Time, 0, len(reminderMap))
// Loop through all reminders and add them
for _, r := range reminderMap {
@ -1585,7 +1536,6 @@ func (t *Task) updateReminders(s *xorm.Session, task *Task) (err error) {
return err
}
t.Reminders = append(t.Reminders, taskReminder)
t.ReminderDates = append(t.ReminderDates, taskReminder.Reminder)
}
// sort reminders
@ -1595,7 +1545,6 @@ func (t *Task) updateReminders(s *xorm.Session, task *Task) (err error) {
if len(t.Reminders) == 0 {
t.Reminders = nil
t.ReminderDates = nil
}
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
@ -1621,6 +1570,13 @@ func updateTaskLastUpdated(s *xorm.Session, task *Task) error {
// @Router /tasks/{ID} [delete]
func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
// duplicate the task for the event
fullTask := &Task{ID: t.ID}
err = fullTask.ReadOne(s, a)
if err != nil {
return err
}
if _, err = s.ID(t.ID).Delete(Task{}); err != nil {
return err
}
@ -1675,7 +1631,7 @@ func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
doer, _ := user.GetFromAuth(a)
err = events.Dispatch(&TaskDeletedEvent{
Task: t,
Task: fullTask,
Doer: doer,
})
if err != nil {
@ -1719,5 +1675,9 @@ func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) {
*t = *taskMap[t.ID]
t.Subscription, err = GetSubscription(s, SubscriptionEntityTask, t.ID, a)
if err != nil && IsErrProjectDoesNotExist(err) {
return nil
}
return
}

View File

@ -503,7 +503,6 @@ func TestTask_Update(t *testing.T) {
// when start_date is modified
task := taskBefore
task.StartDate = time.Date(2023, time.March, 8, 8, 5, 0, 0, time.Local)
task.ReminderDates = nil
err = task.Update(s, u)
assert.NoError(t, err)

View File

@ -241,7 +241,7 @@ func (t *Team) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per
// Create is the handler to create a team
// @Summary Creates a new team
// @Description Creates a new team in a given namespace. The user needs write-access to the namespace.
// @Description Creates a new team.
// @tags team
// @Accept json
// @Produce json
@ -307,12 +307,6 @@ func (t *Team) Delete(s *xorm.Session, a web.Auth) (err error) {
return
}
// Delete team <-> namespace relations
_, err = s.Where("team_id = ?", t.ID).Delete(&TeamNamespace{})
if err != nil {
return
}
// Delete team <-> projects relations
_, err = s.Where("team_id = ?", t.ID).Delete(&TeamProject{})
if err != nil {

View File

@ -58,16 +58,6 @@ func TestTeam_CanDoSomething(t *testing.T) {
},
want: map[string]bool{"CanCreate": true, "IsAdmin": true, "CanRead": true, "CanDelete": true, "CanUpdate": true},
},
{
name: "CanDoSomething for a nonexistant namespace",
fields: fields{
ID: 300,
},
args: args{
a: &user.User{ID: 1},
},
want: map[string]bool{"CanCreate": true, "IsAdmin": false, "CanRead": false, "CanDelete": false, "CanUpdate": false},
},
{
name: "CanDoSomething where the user does not have the rights",
fields: fields{

View File

@ -110,7 +110,7 @@ func TestTeam_ReadAll(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
ts := reflect.ValueOf(teams)
assert.Equal(t, 8, ts.Len())
assert.Equal(t, 5, ts.Len())
})
t.Run("search", func(t *testing.T) {
s := db.NewSession()

View File

@ -48,7 +48,6 @@ func SetupTests() {
"labels",
"link_shares",
"projects",
"namespaces",
"task_assignees",
"task_attachments",
"task_comments",
@ -57,12 +56,10 @@ func SetupTests() {
"tasks",
"team_projects",
"team_members",
"team_namespaces",
"teams",
"users",
"user_tokens",
"users_projects",
"users_namespaces",
"buckets",
"saved_filters",
"subscriptions",

View File

@ -87,45 +87,6 @@ func deleteUsers() {
}
}
func getNamespacesToDelete(s *xorm.Session, u *user.User) (namespacesToDelete []*Namespace, err error) {
namespacesToDelete = []*Namespace{}
nm := &Namespace{IsArchived: true}
res, _, _, err := nm.ReadAll(s, u, "", 1, -1)
if err != nil {
return nil, err
}
if res == nil {
return nil, nil
}
namespaces := res.([]*NamespaceWithProjects)
for _, n := range namespaces {
if n.ID < 0 {
continue
}
hadUsers, err := ensureNamespaceAdminUser(s, &n.Namespace)
if err != nil {
return nil, err
}
if hadUsers {
continue
}
hadTeams, err := ensureNamespaceAdminTeam(s, &n.Namespace)
if err != nil {
return nil, err
}
if hadTeams {
continue
}
namespacesToDelete = append(namespacesToDelete, &n.Namespace)
}
return
}
func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Project, err error) {
projectsToDelete = []*Project{}
lm := &Project{IsArchived: true}
@ -166,30 +127,17 @@ func getProjectsToDelete(s *xorm.Session, u *user.User) (projectsToDelete []*Pro
return
}
// DeleteUser completely removes a user and all their associated projects, namespaces and tasks.
// DeleteUser completely removes a user and all their associated projects and tasks.
// This action is irrevocable.
// Public to allow deletion from the CLI.
func DeleteUser(s *xorm.Session, u *user.User) (err error) {
namespacesToDelete, err := getNamespacesToDelete(s, u)
if err != nil {
return err
}
projectsToDelete, err := getProjectsToDelete(s, u)
if err != nil {
return err
}
// Delete everything not shared with anybody else
for _, n := range namespacesToDelete {
err = deleteNamespace(s, n, u, false)
if err != nil {
return err
}
}
for _, l := range projectsToDelete {
err = l.Delete(s, u)
for _, p := range projectsToDelete {
err = p.Delete(s, u)
if err != nil {
return err
}
@ -205,58 +153,6 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) {
})
}
func ensureNamespaceAdminUser(s *xorm.Session, n *Namespace) (hadUsers bool, err error) {
namespaceUsers := []*NamespaceUser{}
err = s.Where("namespace_id = ?", n.ID).Find(&namespaceUsers)
if err != nil {
return
}
if len(namespaceUsers) == 0 {
return false, nil
}
for _, lu := range namespaceUsers {
if lu.Right == RightAdmin {
// Project already has more than one admin, no need to do anything
return true, nil
}
}
firstUser := namespaceUsers[0]
firstUser.Right = RightAdmin
_, err = s.Where("id = ?", firstUser.ID).
Cols("right").
Update(firstUser)
return true, err
}
func ensureNamespaceAdminTeam(s *xorm.Session, n *Namespace) (hadTeams bool, err error) {
namespaceTeams := []*TeamNamespace{}
err = s.Where("namespace_id = ?", n.ID).Find(&namespaceTeams)
if err != nil {
return
}
if len(namespaceTeams) == 0 {
return false, nil
}
for _, lu := range namespaceTeams {
if lu.Right == RightAdmin {
// Project already has more than one admin, no need to do anything
return true, nil
}
}
firstTeam := namespaceTeams[0]
firstTeam.Right = RightAdmin
_, err = s.Where("id = ?", firstTeam.ID).
Cols("right").
Update(firstTeam)
return true, err
}
func ensureProjectAdminUser(s *xorm.Session, l *Project) (hadUsers bool, err error) {
projectUsers := []*ProjectUser{}
err = s.Where("project_id = ?", l.ID).Find(&projectUsers)

View File

@ -46,7 +46,7 @@ func TestDeleteUser(t *testing.T) {
db.AssertExists(t, "projects", map[string]interface{}{"id": 10}, false)
db.AssertExists(t, "projects", map[string]interface{}{"id": 11}, false)
})
t.Run("user with no namespaces", func(t *testing.T) {
t.Run("user with no projects", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
@ -56,6 +56,6 @@ func TestDeleteUser(t *testing.T) {
err := DeleteUser(s, u)
assert.NoError(t, err)
// No assertions for deleted projects and namespaces since that user doesn't have any
// No assertions for deleted projects since that user doesn't have any
})
}

View File

@ -22,14 +22,11 @@ import (
"xorm.io/xorm"
)
// ProjectUIDs hold all kinds of user IDs from accounts who have somehow access to a project
// ProjectUIDs hold all kinds of user IDs from accounts who have access to a project
type ProjectUIDs struct {
ProjectOwnerID int64 `xorm:"projectOwner"`
NamespaceUserID int64 `xorm:"unID"`
ProjectUserID int64 `xorm:"ulID"`
NamespaceOwnerUserID int64 `xorm:"nOwner"`
TeamNamespaceUserID int64 `xorm:"tnUID"`
TeamProjectUserID int64 `xorm:"tlUID"`
ProjectOwnerID int64 `xorm:"projectOwner"`
ProjectUserID int64 `xorm:"ulID"`
TeamProjectUserID int64 `xorm:"tlUID"`
}
// ListUsersFromProject returns a list with all users who have access to a project, regardless of the method which gave them access
@ -37,47 +34,58 @@ func ListUsersFromProject(s *xorm.Session, l *Project, search string) (users []*
userids := []*ProjectUIDs{}
err = s.
Select(`l.owner_id as projectOwner,
un.user_id as unID,
ul.user_id as ulID,
n.owner_id as nOwner,
tm.user_id as tnUID,
tm2.user_id as tlUID`).
Table("projects").
Alias("l").
// User stuff
Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = l.namespace_id").
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id").
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
// Team stuff
Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id").
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
// The actual condition
Where(
builder.Or(
builder.Or(builder.Eq{"ul.right": RightRead}),
builder.Or(builder.Eq{"un.right": RightRead}),
builder.Or(builder.Eq{"tl.right": RightRead}),
builder.Or(builder.Eq{"tn.right": RightRead}),
builder.Or(builder.Eq{"ul.right": RightWrite}),
builder.Or(builder.Eq{"un.right": RightWrite}),
builder.Or(builder.Eq{"tl.right": RightWrite}),
builder.Or(builder.Eq{"tn.right": RightWrite}),
builder.Or(builder.Eq{"ul.right": RightAdmin}),
builder.Or(builder.Eq{"un.right": RightAdmin}),
builder.Or(builder.Eq{"tl.right": RightAdmin}),
builder.Or(builder.Eq{"tn.right": RightAdmin}),
),
builder.Eq{"l.id": l.ID},
).
Find(&userids)
var currentProject *Project
currentProject, err = GetProjectSimpleByID(s, l.ID)
if err != nil {
return
return nil, err
}
for {
currentUserIDs := []*ProjectUIDs{}
err = s.
Select(`l.owner_id as projectOwner,
ul.user_id as ulID,
tm2.user_id as tlUID`).
Table("projects").
Alias("l").
// User stuff
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = l.id").
// Team stuff
Join("LEFT", []string{"team_projects", "tl"}, "l.id = tl.project_id").
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
// The actual condition
Where(
builder.Or(
builder.Or(builder.Eq{"ul.right": RightRead}),
builder.Or(builder.Eq{"tl.right": RightRead}),
builder.Or(builder.Eq{"ul.right": RightWrite}),
builder.Or(builder.Eq{"tl.right": RightWrite}),
builder.Or(builder.Eq{"ul.right": RightAdmin}),
builder.Or(builder.Eq{"tl.right": RightAdmin}),
),
builder.Eq{"l.id": currentProject.ID},
).
Find(&currentUserIDs)
if err != nil {
return
}
userids = append(userids, currentUserIDs...)
if currentProject.ParentProjectID == 0 {
break
}
parent, err := GetProjectSimpleByID(s, currentProject.ParentProjectID)
if err != nil && !IsErrProjectDoesNotExist(err) {
return nil, err
}
if err != nil && IsErrProjectDoesNotExist(err) {
break
}
currentProject = parent
}
// Remove duplicates from the project of ids and make it a slice
@ -85,10 +93,7 @@ func ListUsersFromProject(s *xorm.Session, l *Project, search string) (users []*
uidmap[l.OwnerID] = true
for _, u := range userids {
uidmap[u.ProjectUserID] = true
uidmap[u.NamespaceOwnerUserID] = true
uidmap[u.NamespaceUserID] = true
uidmap[u.TeamProjectUserID] = true
uidmap[u.TeamNamespaceUserID] = true
}
uids := make([]int64, 0, len(uidmap))

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