Compare commits

...

274 Commits

Author SHA1 Message Date
5c793bf47d
Add exec to run script to run as PID 1 2022-07-12 12:35:53 +02:00
4bb77b5539
fix(mail): don't set a username by default 2022-07-12 11:49:23 +02:00
5743a4afe5
fix: properly set tls config for mailer 2022-07-11 16:10:28 +02:00
62325de9cd
feat: use actual uuids for tasks 2022-07-11 14:54:33 +02:00
8759937e3c
feat(docs): add versions explanation 2022-07-08 00:14:01 +02:00
5cc4927b9e
fix: add missing error check 2022-07-07 23:23:15 +02:00
2b074c60a7
fix(caldav): properly parse durations when returning VTODOs
Resolves https://github.com/go-vikunja/frontend/issues/55
2022-07-07 23:20:37 +02:00
f5a4c136fb
fix: cycles in tasks array when memory caching was enabled
Resolves #1119
2022-07-07 18:34:49 +02:00
230478aae9
fix: remove credential escaping for postgres connections to allow for passwords with special characters
Resolves https://github.com/go-vikunja/api/issues/22
2022-07-07 18:04:16 +02:00
7e99618319
chore: upgrade trello api wrapper and remove fork 2022-07-07 16:21:33 +02:00
73c4c399e5
feat: use embed fs directly to embed the logo in mails 2022-07-07 15:54:38 +02:00
25ffa1bc2e
fix: prevent logging openid provider errors twice 2022-07-07 15:47:37 +02:00
4429ba2da1
fix(caldav): make sure the caldav tokens of non-local accounts are properly checked 2022-07-04 18:08:46 +02:00
db1ccff0de fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.14 (#1194)
Reviewed-on: vikunja/api#1194
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-07-04 14:45:42 +00:00
2c9ab3d86f fix(deps): update module github.com/go-testfixtures/testfixtures/v3 to v3.8.0 (#1168)
Reviewed-on: vikunja/api#1168
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-07-04 14:45:09 +00:00
951d74b272
fix: go mod tidy 2022-06-30 16:41:47 +02:00
a38efef734
fix(docs): clarify frontend requirements to use Vikunja 2022-06-30 16:40:38 +02:00
a060cbe820
chore(docs): clarify openid setup with environment variables 2022-06-30 16:27:06 +02:00
ad17ff5c32
fix(docs): image urls in synology setup explanation 2022-06-30 16:25:24 +02:00
d0e09d69d0 fix: tests 2022-06-30 14:21:17 +00:00
7a30294407 fix: go mod tidy 2022-06-30 14:21:17 +00:00
bc7f6a8586 fix: set the correct go version in go.mod 2022-06-30 14:21:17 +00:00
f30a9d1038 chore(docs): add new mailer option to docs 2022-06-30 14:21:17 +00:00
c62e26b6fe fix: revert renaming Attachments to Embeds everywhere 2022-06-30 14:21:17 +00:00
f4f8450d16 feat: embed the vikunja logo as inline attachment 2022-06-30 14:21:17 +00:00
30e0e98f77 feat: migrate away from gomail 2022-06-30 14:21:17 +00:00
12557163b2 fix(deps): update module github.com/stretchr/testify to v1.8.0 (#1191)
Reviewed-on: vikunja/api#1191
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-06-29 21:36:48 +00:00
8b82aab7aa fix(deps): update module github.com/golang-jwt/jwt/v4 to v4.4.2 (#1193)
Reviewed-on: vikunja/api#1193
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-06-29 18:30:48 +00:00
70018613da fix(deps): update module github.com/spf13/cobra to v1.5.0 (#1192)
Reviewed-on: vikunja/api#1192
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-06-29 18:30:17 +00:00
01271c4c01
feat: allow only the authors of task comments to edit them 2022-06-16 17:38:27 +02:00
d837f8a624
fix: add missing migration 2022-06-16 16:56:35 +02:00
8869adfc27
feat: add setting to change overdue tasks reminder email time 2022-06-16 16:20:26 +02:00
030bbfa47e fix(deps): update module github.com/swaggo/swag to v1.8.3 (#1185)
Reviewed-on: vikunja/api#1185
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-06-16 12:47:17 +00:00
7eb3b96a44
feat: send overdue tasks email notification at 9:00 in the user's time zone 2022-06-12 21:24:28 +02:00
2f25b48869
feat: restrict max avatar size
resolves #1171
2022-06-12 18:29:12 +02:00
k2s
172a6214d7 fix: VIKUNJA_SERVICE_JWT_SECRET should be VIKUNJA_SERVICE_JWTSECRET (#1184)
Reviewed-on: vikunja/api#1184
Reviewed-by: konrad <k@knt.li>
Co-authored-by: k2s <k2s@noreply.kolaente.de>
Co-committed-by: k2s <k2s@noreply.kolaente.de>
2022-06-12 12:50:43 +00:00
92a87cfe4f fix(deps): update module github.com/stretchr/testify to v1.7.2 (#1182)
Reviewed-on: vikunja/api#1182
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-06-07 18:10:50 +00:00
163a4624ee fix(deps): update module github.com/imdario/mergo to v0.3.13 (#1178)
Reviewed-on: vikunja/api#1178
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-06-01 20:51:01 +00:00
37a07aa677 fix(deps): update module gopkg.in/yaml.v3 to v3.0.1 (#1179)
Reviewed-on: vikunja/api#1179
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-06-01 17:07:40 +00:00
e52c45d5aa fix: sort tasks logically and consistent across dbms (#1177)
This PR changes the behavoir of sorting tasks. Before, tasks were sorted with null values first. Now, null values are always sorted last which is usually what you want.

Partial fix for https://github.com/go-vikunja/frontend/issues/54

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1177
2022-05-30 20:53:58 +00:00
grahammiln
acaa85083f feat: ability to serve static files (#1174)
Added the configuration key, `service.staticpath`, to serve files from the configuration path on root (/).

Serving static files allows the api service to also serve the frontend content. This is a simple option for deploying Vikunja without needing any other servers or proxies.

Running a complete instance becomes:

    VIKUNJA_SERVICE_STATICPATH=/path/to/frontend ./vikunja

Where `/path/to/frontend` is a copy of Vikunja's frontend static files.

## Implementation

Providing a path, via the configuration or environment, adds a static file middleware to serve the path's contents from root (/).

By default, the configuration path is empty and Vikunja's existing behaviour is unchanged.

Co-authored-by: Graham Miln <graham.miln@dssw.co.uk>
Reviewed-on: vikunja/api#1174
Reviewed-by: konrad <k@knt.li>
Co-authored-by: grahammiln <grahammiln@noreply.kolaente.de>
Co-committed-by: grahammiln <grahammiln@noreply.kolaente.de>
2022-05-23 20:49:28 +00:00
f5ebada913
fix: set derived default values only after reading config from file or env 2022-05-23 22:12:15 +02:00
7b10176a10 fix(deps): update module github.com/lib/pq to v1.10.6 (#1169)
Reviewed-on: vikunja/api#1169
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-05-16 20:58:16 +00:00
3ab0ac9f27 fix(deps): update module github.com/swaggo/swag to v1.8.2 (#1167)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1167
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-05-15 19:57:00 +00:00
dc5faaf2cf fix(deps): update module github.com/coreos/go-oidc/v3 to v3.2.0 (#1164)
Reviewed-on: vikunja/api#1164
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-05-14 15:09:22 +00:00
85be5a7bcd fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.13 (#1165)
Reviewed-on: vikunja/api#1165
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-05-14 15:08:28 +00:00
9ab00fd2e6 fix(deps): update module github.com/prometheus/client_golang to v1.12.2 (#1166)
Reviewed-on: vikunja/api#1166
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-05-14 15:07:58 +00:00
9845fcc170
fix: add more methods to figure out the current binary location 2022-05-08 21:54:26 +02:00
b4f57dc3e1 fix(deps): update module github.com/yuin/goldmark to v1.4.12 (#1162)
Reviewed-on: vikunja/api#1162
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-04-24 15:38:36 +00:00
tuxthepenguin
4960a498ff Add client-cert parameters of the Go pq driver to the Vikunja config (#1161)
Co-authored-by: tuxthepenguin <tux@saturnv.uphus-internal.de>
Reviewed-on: vikunja/api#1161
Reviewed-by: konrad <k@knt.li>
Co-authored-by: tuxthepenguin <tuxthepenguin@noreply.kolaente.de>
Co-committed-by: tuxthepenguin <tuxthepenguin@noreply.kolaente.de>
2022-04-23 15:46:00 +00:00
Subhaditya Nath
96e519ea96
fix: broken link (#27) 2022-04-18 21:00:46 +02:00
81a18661ad fix(deps): update module github.com/spf13/viper to v1.11.0 (#1159)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1159
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-04-18 17:33:19 +00:00
fabd3471a8 fix(deps): update module github.com/lib/pq to v1.10.5 (#1157)
Reviewed-on: vikunja/api#1157
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-04-08 19:56:37 +00:00
8cdbc78b1c fix(deps): update module github.com/spf13/cobra to v1.4.0 (#1145)
Reviewed-on: vikunja/api#1145
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-04-01 19:17:32 +00:00
91c89931f8 fix(deps): update module github.com/yuin/goldmark to v1.4.11 (#1143)
Reviewed-on: vikunja/api#1143
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-04-01 15:55:57 +00:00
e4b50e84a4 feat: add caldav tokens (#1065)
# Description

This PR adds API routes to create and manage caldav tokens. These tokens can be used instead of a user password - required for users who are using external auth providers and don't have a password.

Fixes #842

Frontend: vikunja/frontend#1186

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1065
2022-03-30 18:25:56 +00:00
726a517bec chore: go mod tidy 2022-03-30 16:36:07 +00:00
de97fcbd12 fix: lint 2022-03-30 16:36:07 +00:00
d3bdafb717 fix: decoding images for blurHash generation 2022-03-30 16:36:07 +00:00
e19ad11846 fix: go mod tidy 2022-03-30 16:36:07 +00:00
6b51fae093 fix: return BlurHash in unsplash search results 2022-03-30 16:36:07 +00:00
ba2bdff391 chore: generate swagger docs 2022-03-30 16:36:07 +00:00
7fa0865188 fix: lint 2022-03-30 16:36:07 +00:00
6df865876d feat: return BlurHash for unsplash search results 2022-03-30 16:36:07 +00:00
2ec7d7a8a8 feat: save BlurHash from unsplash when selecting a photo from unsplash 2022-03-30 16:36:07 +00:00
f83b09af59 feat: generate a BlurHash when uploading a new image 2022-03-30 16:36:07 +00:00
362706b38d feat: add migration to create BlurHash strings for all list backgrounds 2022-03-30 16:36:07 +00:00
1fa1cd365e fix(deps): update module github.com/swaggo/swag to v1.8.1 (#1156)
Reviewed-on: vikunja/api#1156
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-30 15:51:03 +00:00
0a1d8c9404 feat: add date math for filters (#1086)
This adds support for relative dates in filters, similar to the ones from [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls) or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math).

In short, it allows you to filter for due dates by passing in dates like "now - 7d" to get a date from 7 days ago.

This is a very powerful addition for saved filters as they will allow you to create filters for all kinds of stuff where you previously only could use fixed dates. Now you can for example create a saved filter for "all tasks this week".

Frontend PR: vikunja/frontend#1342

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1086
2022-03-27 20:35:04 +00:00
d08dcc4e44 A bunch of dependency updates at once (#1155)
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1155
2022-03-27 19:21:36 +00:00
ac6818a476
fix: checking for error types 2022-03-27 17:52:33 +02:00
5cf263a86f
feat: upgrade golangci-lint to 1.45.2 2022-03-27 16:55:37 +02:00
09c0d14444 fix(deps): update module github.com/stretchr/testify to v1.7.1 (#1148)
Reviewed-on: vikunja/api#1148
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-15 22:21:40 +00:00
da0bcc6322 fix(deps): update module github.com/labstack/echo/v4 to v4.7.1 (#1146)
Reviewed-on: vikunja/api#1146
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-13 19:17:28 +00:00
f5b0c603a3 chore(deps): update klakegg/hugo docker tag to v0.93.3 (#1142)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1142
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-13 15:59:41 +00:00
aa30baf1bc fix(deps): update golang.org/x/sys commit hash to b874c99 (#1141)
Reviewed-on: vikunja/api#1141
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-10 06:54:02 +00:00
581dcd41f1 fix(deps): update golang.org/x/oauth2 commit hash to 6242fa9 (#1140)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [golang.org/x/oauth2](https://github.com/golang/oauth2) | require | digest | `ee48083` -> `6242fa9` |

---

### Configuration

📅 **Schedule**: At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box.

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

Reviewed-on: vikunja/api#1140
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-09 16:26:50 +00:00
d013020268 fix(deps): update golang.org/x/crypto commit hash to efcb850 (#1139)
Reviewed-on: vikunja/api#1139
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-07 22:27:03 +00:00
bc8a02d794 fix(deps): update golang.org/x/sys commit hash to 22a9840 (#1138)
Reviewed-on: vikunja/api#1138
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-07 21:56:23 +00:00
b60c69c5a8
chore: cleanup namespace creation 2022-03-06 17:09:05 +01:00
9d816205da chore(deps): update klakegg/hugo docker tag to v0.93.2 (#1137)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1137
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-06 13:14:51 +00:00
8aa646e4d9
chore(docs): redirect properly from /docs/docs 2022-03-06 13:42:07 +01:00
b167aac624 fix(deps): update module github.com/yuin/goldmark to v1.4.8 (#1136)
Reviewed-on: vikunja/api#1136
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-05 11:09:10 +00:00
75f74b429e
fix: validate email address when creating a user via cli 2022-03-04 19:58:40 +01:00
21541bc118 chore(deps): update klakegg/hugo docker tag to v0.93.0 (#1135)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1135
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-04 14:06:12 +00:00
72ee84d43d fix(deps): update module github.com/labstack/echo/v4 to v4.7.0 (#1134)
Reviewed-on: vikunja/api#1134
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-03 08:23:42 +00:00
f2f6cc68d9 fix(deps): update golang.org/x/image commit hash to 723b81c (#1133)
Reviewed-on: vikunja/api#1133
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-03-02 10:37:17 +00:00
c870627644 fix(deps): update golang.org/x/sys commit hash to 4e6760a (#1131)
Reviewed-on: vikunja/api#1131
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-28 06:51:29 +00:00
edc5084278 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.12 (#1132)
Reviewed-on: vikunja/api#1132
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-28 06:51:08 +00:00
82158d7718
fix(docs): don't use cannonify url 2022-02-25 14:25:03 +01:00
16a98a5c70 fix(deps): update golang.org/x/oauth2 commit hash to ee48083 (#1128)
Reviewed-on: vikunja/api#1128
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-24 15:17:36 +00:00
f77f2387b6 fix(deps): update golang.org/x/sys commit hash to 95c6836 (#1130)
Reviewed-on: vikunja/api#1130
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-24 14:03:30 +00:00
a423e111e9 fix(deps): update golang.org/x/sys commit hash to dbe011f (#1129)
Reviewed-on: vikunja/api#1129
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-24 07:36:49 +00:00
f96c2fe23f chore(deps): update klakegg/hugo docker tag to v0.92.2 (#1127)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1127
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-23 08:00:21 +00:00
81ab23385f fix(deps): update golang.org/x/sys commit hash to f242548 (#1126)
Reviewed-on: vikunja/api#1126
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-23 07:12:52 +00:00
325ba622e0 fix(deps): update golang.org/x/sys commit hash to 0005352 (#1125)
Reviewed-on: vikunja/api#1125
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-22 20:52:32 +00:00
cda78ea702 fix(deps): update module github.com/swaggo/swag to v1.8.0 (#1124)
Reviewed-on: vikunja/api#1124
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-22 13:56:36 +00:00
8bf2254f4b
fix: restoring dumps with no config file saved in them 2022-02-19 17:46:10 +01:00
22e3f242a3
fix: disabling logging completely now works 2022-02-19 17:42:32 +01:00
8cb92b3924
fix(dump): don't try to save a config file if none was provided and dump vikunja env variables 2022-02-19 11:32:30 +01:00
44aaf0a4ec
fix: clarify which config file is used on startup 2022-02-19 11:32:30 +01:00
2bc82bc1a0 fix(deps): update module github.com/yuin/goldmark to v1.4.7 (#1123)
Reviewed-on: vikunja/api#1123
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-18 20:25:20 +00:00
43f1daf40c
fix: microsoft todo migration not importing all tasks
Previously, we did not check if a list has more tasks than the ones returned. By default, the Microsoft Graph API only returns 10 tasks. If the user had more they would not get imported.
Now we check if there are more pages with tasks and pull them all in until we have everything.
2022-02-18 20:00:42 +01:00
545999cf5e fix(deps): update module github.com/yuin/goldmark to v1.4.6 (#1122)
Reviewed-on: vikunja/api#1122
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-17 22:35:36 +00:00
c8aab8a8ad fix(deps): update golang.org/x/crypto commit hash to 8634188 (#1121)
Reviewed-on: vikunja/api#1121
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-17 12:04:47 +00:00
7978c91d09 fix(deps): update golang.org/x/crypto commit hash to f4118a5 (#1118)
Reviewed-on: vikunja/api#1118
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-10 19:16:06 +00:00
519675ab23 fix(deps): update golang.org/x/crypto commit hash to db63837 (#1115)
Reviewed-on: vikunja/api#1115
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-10 10:38:08 +00:00
4e257b82a3 fix(deps): update golang.org/x/sys commit hash to 3681064 (#1116)
Reviewed-on: vikunja/api#1116
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-10 10:37:49 +00:00
386412caca fix(deps): update module github.com/golang-jwt/jwt/v4 to v4.3.0 (#1117)
Reviewed-on: vikunja/api#1117
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-10 10:37:26 +00:00
8f10a852c2 fix(deps): update golang.org/x/crypto commit hash to dad3315 (#1114)
Reviewed-on: vikunja/api#1114
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-09 16:32:41 +00:00
d056d2df51 fix(deps): update golang.org/x/crypto commit hash to bba287d (#1113)
Reviewed-on: vikunja/api#1113
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-09 06:47:42 +00:00
4432ebf4a5 fix(deps): update module github.com/yuin/goldmark to v1.4.5 (#1112)
://community.spotify.com/t5/Content-Questions/How-to-disable-all-podcasts/td-p/5180261 PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/yuin/goldmark](https://github.com/yuin/goldmark) | require | patch | `v1.4.4` -> `v1.4.5` |

---

### Release Notes

<details>
<summary>yuin/goldmark</summary>

### [`v1.4.5`](https://github.com/yuin/goldmark/compare/v1.4.4...v1.4.5)

[Compare Source](https://github.com/yuin/goldmark/compare/v1.4.4...v1.4.5)

</details>

---

### Configuration

📅 **Schedule**: At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box.

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

Reviewed-on: vikunja/api#1112
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-08 10:39:26 +00:00
c5611f79ea fix(deps): update golang.org/x/crypto commit hash to 20e1d8d (#1111)
Reviewed-on: vikunja/api#1111
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-08 06:39:35 +00:00
d2adf00790 fix(deps): update golang.org/x/sys commit hash to 5739886 (#1110)
Reviewed-on: vikunja/api#1110
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-08 06:38:51 +00:00
1322cb16d7 feat: add long-lived api tokens (#1085)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1085
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
2022-02-06 13:18:08 +00:00
2598550e49 fix(deps): update module github.com/spf13/afero to v1.8.1 (#1108)
Reviewed-on: vikunja/api#1108
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-05 12:10:39 +00:00
9196826390 fix(deps): update golang.org/x/sys commit hash to 1c1b9b1 (#1107)
Reviewed-on: vikunja/api#1107
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-04 14:36:59 +00:00
6cbad9546a fix(deps): update module github.com/swaggo/swag to v1.7.9 (#1106)
Reviewed-on: vikunja/api#1106
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-03 22:54:32 +00:00
df35531e70 fix(deps): update golang.org/x/crypto commit hash to 30dcbda (#1105)
Reviewed-on: vikunja/api#1105
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-02-01 06:54:10 +00:00
daa7d26b3d chore(deps): update klakegg/hugo docker tag to v0.92.1 (#1104)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1104
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-30 13:29:54 +00:00
272fd6dabf chore(deps): update klakegg/hugo docker tag to v0.92.0 (#1103)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1103
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-30 11:06:12 +00:00
1d8d0f140e
chore: sign drone config 2022-01-29 23:31:58 +01:00
7af4761cc3
fix(docs): use up-to-date hugo image for building 2022-01-29 23:31:14 +01:00
7a27e1317f fix(deps): update module github.com/prometheus/client_golang to v1.12.1 (#1102)
Reviewed-on: vikunja/api#1102
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-29 16:11:35 +00:00
59a1e8d0a9 fix(deps): update golang.org/x/sys commit hash to 99c3d69 (#1101)
Reviewed-on: vikunja/api#1101
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-28 22:45:06 +00:00
4e7ceb22dd fix(deps): update golang.org/x/crypto commit hash to 198e437 (#1100)
Reviewed-on: vikunja/api#1100
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-28 22:44:53 +00:00
b615f06da3 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.11 (#1099)
Reviewed-on: vikunja/api#1099
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-28 20:04:24 +00:00
7c6fc41543 fix(deps): update golang.org/x/crypto commit hash to aa10faf (#1098)
Reviewed-on: vikunja/api#1098
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-27 09:27:55 +00:00
230c784f55 fix(deps): update golang.org/x/crypto commit hash to e04a857 (#1097)
Reviewed-on: vikunja/api#1097
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-26 19:26:28 +00:00
049ae39c62
docs: add guide for Synology NAS 2022-01-25 22:40:19 +01:00
f7a06e4644 Enable a list to be moved across namespaces (#1096)
Co-authored-by: Simon Hilchenbach <simon@hilchenba.ch>
Reviewed-on: vikunja/api#1096
Reviewed-by: konrad <k@knt.li>
Co-authored-by: shilch <simon@hilchenba.ch>
Co-committed-by: shilch <simon@hilchenba.ch>
2022-01-23 12:59:43 +00:00
da318e3db1
docs: update testing 2022-01-23 11:49:03 +01:00
bb88beb417 fix(deps): update module github.com/prometheus/client_golang to v1.12.0 (#1094)
Reviewed-on: vikunja/api#1094
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-19 09:29:02 +00:00
61d49c3a56 feat: add time zone setting for reminders (#1092)
Instead of naeveily checking for all reminders due in the next minute, we now check all reminders in all time zones in the next minutes. This essentially means checking for reminders due in the next 14 or past 12 hours. We then check for each user who would receive a reminder from that result if it is actually due in their time zone.

This should prevent issues where users would get the reminder in the time zone of their server, not in their own.

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1092
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
2022-01-16 11:05:56 +00:00
e116fbad79 fix(deps): update golang.org/x/sys commit hash to da31bd3 (#1093)
Reviewed-on: vikunja/api#1093
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-15 20:28:17 +00:00
f255fb2d5c fix(deps): update golang.org/x/crypto commit hash to 5e0467b (#1091)
Reviewed-on: vikunja/api#1091
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-12 19:40:20 +00:00
4aa045d710 fix(deps): update golang.org/x/sys commit hash to 5a964db (#1090)
Reviewed-on: vikunja/api#1090
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-11 10:34:09 +00:00
b316a412ed fix(deps): update golang.org/x/sys commit hash to a018aaa (#1088)
Reviewed-on: vikunja/api#1088
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-10 21:25:18 +00:00
a9296d807f fix(deps): update module github.com/labstack/echo/v4 to v4.6.3 (#1089)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/labstack/echo/v4](https://github.com/labstack/echo) | require | patch | `v4.6.2` -> `v4.6.3` |

---

### Release Notes

<details>
<summary>labstack/echo</summary>

### [`v4.6.3`](https://github.com/labstack/echo/blob/master/CHANGELOG.md#v463---2022-01-10)

[Compare Source](https://github.com/labstack/echo/compare/v4.6.2...v4.6.3)

**Fixes**

-   Fixed Echo version number in greeting message which was not incremented to `4.6.2` [#&#8203;2066](https://github.com/labstack/echo/issues/2066)

</details>

---

### Configuration

📅 **Schedule**: At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box.

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

Reviewed-on: vikunja/api#1089
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-10 21:24:23 +00:00
614dcaeb9d fix(deps): update module github.com/labstack/echo/v4 to v4.6.2 (#1084)
Reviewed-on: vikunja/api#1084
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-08 10:54:46 +00:00
832b7184f3 fix(deps): update module github.com/spf13/afero to v1.8.0 (#1083)
Reviewed-on: vikunja/api#1083
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-05 11:44:50 +00:00
8fb597cae2 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.10 (#1082)
Reviewed-on: vikunja/api#1082
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-29 16:05:22 +00:00
64cc299fb1 fix(deps): update module github.com/spf13/afero to v1.7.1 (#1081)
Reviewed-on: vikunja/api#1081
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-29 13:33:59 +00:00
fbf271978d fix(deps): update module github.com/spf13/afero to v1.7.0 (#1078)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1078
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-25 20:56:04 +00:00
514c11d342 fix(deps): update module github.com/swaggo/swag to v1.7.8 (#1080)
Reviewed-on: vikunja/api#1080
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-25 20:21:36 +00:00
7b2fe5db50 fix(deps): update module github.com/getsentry/sentry-go to v0.12.0 (#1079)
Reviewed-on: vikunja/api#1079
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-23 08:51:14 +00:00
Mike
a4c85fed55
feat(docs): add details of using NGINX Proxy Manager to the Reverse Proxy docs (#13) 2021-12-19 19:54:18 +01:00
9d8f9d9de9 fix(deps): update module github.com/magefile/mage to v1.12.1 (#1077)
Reviewed-on: vikunja/api#1077
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-19 17:30:08 +00:00
c2e8a8ad98 fix(deps): update module github.com/magefile/mage to v1.12.0 (#1076)
Reviewed-on: vikunja/api#1076
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-17 06:52:28 +00:00
5e4d632e79 fix(deps): update golang.org/x/sys commit hash to 1d35b9e (#1075)
Reviewed-on: vikunja/api#1075
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-16 12:30:20 +00:00
402c34fb12 fix(deps): update golang.org/x/sys commit hash to 4abf325 (#1074)
Reviewed-on: vikunja/api#1074
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-15 22:30:24 +00:00
94fff0ac88 fix(deps): update golang.org/x/crypto commit hash to e495a2d (#1073)
Reviewed-on: vikunja/api#1073
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-15 16:14:35 +00:00
62c3dec6e3 fix(deps): update module github.com/spf13/viper to v1.10.1 (#1072)
Reviewed-on: vikunja/api#1072
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-15 14:47:10 +00:00
c5d831ec7c fix(deps): update golang.org/x/sys commit hash to 4825e8c (#1071)
Reviewed-on: vikunja/api#1071
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-15 07:46:53 +00:00
351d72f02d fix(deps): update module github.com/spf13/cobra to v1.3.0 (#1070)
Reviewed-on: vikunja/api#1070
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-14 20:39:53 +00:00
4752888c06 fix(deps): update golang.org/x/sys commit hash to 3b038e5 (#1068)
Reviewed-on: vikunja/api#1068
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-14 19:33:50 +00:00
d31c0dfeac fix(deps): update golang.org/x/sys commit hash to 03aa0b5 (#1067)
Reviewed-on: vikunja/api#1067
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-14 08:31:37 +00:00
a31086a7a9
fix: lint 2021-12-12 16:03:58 +01:00
a98119f2d6
feat: save user language in the settings 2021-12-12 15:39:47 +01:00
8bb3f8d37c
fix: importing archived lists or namespaces 2021-12-12 13:18:01 +01:00
190a9f2a4c
fix: friendly name not getting synced on first login from openid
Resolves #874
2021-12-12 12:35:13 +01:00
5c88dfe88e
fix: user deletion schedule 2021-12-12 12:29:05 +01:00
70e005e7ce
fix: user not actually deleted 2021-12-12 12:28:18 +01:00
f581885e65
fix: user deletion reminder emails counting up
Resolves #1014
2021-12-12 12:24:42 +01:00
72d3c54efd
fix: user deletion never happens
Resolves #1024
2021-12-12 12:04:17 +01:00
2f0a36eb47 fix(deps): update module github.com/spf13/viper to v1.10.0 (#1064)
Reviewed-on: vikunja/api#1064
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-12 10:01:46 +00:00
22e1a4f151 fix(deps): update golang.org/x/sys commit hash to af8b642 (#1063)
Reviewed-on: vikunja/api#1063
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-11 16:35:04 +00:00
a9ff6b7561 fix(deps): update golang.org/x/sys commit hash to 798191b (#1061)
Reviewed-on: vikunja/api#1061
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-10 18:24:23 +00:00
24e1460e04 fix(deps): update golang.org/x/crypto commit hash to 4570a08 (#1062)
Reviewed-on: vikunja/api#1062
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-10 18:24:09 +00:00
73ee696fc3 feat: add marble avatar (#1060)
This adds the marble avatar from [boring avatars](https://github.com/boringdesigners/boring-avatars) as an option for user avatars. Each user gets a different one (based on their id).

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1060
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
2021-12-07 21:11:23 +00:00
13561f2114
docs: improve wording for systemd
(cherry picked from commit 094138b0c55a1d2013615275362749b77bb4dc9a)
2021-12-07 18:35:39 +01:00
f9724181b1 fix(deps): update golang.org/x/sys commit hash to 97ca703 (#1059)
Reviewed-on: vikunja/api#1059
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-05 19:23:14 +00:00
87515d133d fix(deps): update golang.org/x/sys commit hash to 94396e4 (#1058)
Reviewed-on: vikunja/api#1058
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-04 13:54:04 +00:00
acc4a8a294 fix(deps): update module github.com/golang-jwt/jwt/v4 to v4.2.0 (#1057)
Reviewed-on: vikunja/api#1057
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-04 13:53:05 +00:00
9194ac6776 fix(deps): update golang.org/x/crypto commit hash to 5770296 (#1056)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [golang.org/x/crypto](https://github.com/golang/crypto) | require | digest | `ae814b3` -> `5770296` |

---

### Configuration

📅 **Schedule**: At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box.

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

Reviewed-on: vikunja/api#1056
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-02 20:11:49 +00:00
6ece909286
chore(docs): update docs about compiling from source 2021-11-28 12:56:46 +01:00
445cc4f79d
fix(docs): update minimum required go version 2021-11-28 12:49:15 +01:00
d75a13891a fix(deps): update module github.com/swaggo/swag to v1.7.6 (#1055)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1055
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-28 11:43:33 +00:00
922eac6029 fix(deps): update golang.org/x/sys commit hash to fe61309 (#1054)
Reviewed-on: vikunja/api#1054
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-25 06:07:45 +00:00
fd0d462bf4
fix: importing tasks from todoist without a due time set
Resolves #897
2021-11-23 22:29:57 +01:00
86480ad969 fix(deps): update golang.org/x/sys commit hash to ef496fb (#1052)
Reviewed-on: vikunja/api#1052
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-23 18:11:56 +00:00
f8a0a7e953
fix: deleting users with no namespaces
resolves #984
2021-11-22 22:34:20 +01:00
b0622732a1 fix(deps): update golang.org/x/sys commit hash to dee7805 (#1051)
Reviewed-on: vikunja/api#1051
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-17 19:13:59 +00:00
072b82412b fix(deps): update golang.org/x/crypto commit hash to ae814b3 (#1050)
Reviewed-on: vikunja/api#1050
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-17 19:13:46 +00:00
ea539cc284 fix(deps): update module github.com/ulule/limiter/v3 to v3.9.0 (#1049)
Reviewed-on: vikunja/api#1049
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-17 17:57:27 +00:00
Joseph Kavanagh
36bf3d216a
feat: gravatar - Lowercase emails before MD5 hash (#10) 2021-11-16 22:10:22 +01:00
efd0970971 fix(deps): update golang.org/x/crypto commit hash to b4de73f (#1047)
Reviewed-on: vikunja/api#1047
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-16 20:54:16 +00:00
677ca03489 fix(deps): update golang.org/x/sys commit hash to 0a5406a (#1048)
Reviewed-on: vikunja/api#1048
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-16 20:54:01 +00:00
1fa74cba64
docs: add another tutorial link 2021-11-16 21:17:53 +01:00
0b7762590f
fix: lint 2021-11-16 20:37:37 +01:00
c3e0e6405a
fix: importing trello attachments
Since jan 2021, trello requires authentication to access attachments. This commit passes the required auth headers to make downloading card attachments work.

resolves https://github.com/go-vikunja/api/issues/6
2021-11-14 21:47:51 +01:00
57e5d10eee
fix: sorting for saved filters
resolves #786
2021-11-14 21:03:55 +01:00
88a2cede19
feat: use wallpaper topic for default unsplash background list 2021-11-14 20:47:15 +01:00
093d0c65ca
feat: enable rate limit for unauthenticated routes 2021-11-14 20:42:33 +01:00
da2d5e41c7 fix(deps): update module github.com/yuin/goldmark to v1.4.4 (#1046)
Reviewed-on: vikunja/api#1046
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-14 11:14:35 +00:00
9bf32aae99
docs: improve development docs 2021-11-13 18:20:23 +01:00
2aea1691cf
docs: add postgres to docker-compose examples 2021-11-13 18:08:57 +01:00
4829c89940
docs: update backup instructions 2021-11-13 18:00:49 +01:00
cf05de19b3
fix: updating a list might remove its background
resolves #1039
2021-11-13 17:52:14 +01:00
2ae1da473e fix(deps): update golang.org/x/sys commit hash to 0c823b9 (#1045)
Reviewed-on: vikunja/api#1045
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-13 13:31:17 +00:00
62361d8ad9 fix(deps): update golang.org/x/sys commit hash to f221eed (#1044)
Reviewed-on: vikunja/api#1044
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-12 06:56:18 +00:00
98316a04b2 fix(deps): update golang.org/x/sys commit hash to 99a5385 (#1043)
Reviewed-on: vikunja/api#1043
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-10 17:16:01 +00:00
ae192c1291 fix(deps): update golang.org/x/sys commit hash to 51b60fd (#1042)
Reviewed-on: vikunja/api#1042
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-09 21:02:29 +00:00
e3210a11df fix(deps): update module github.com/lib/pq to v1.10.4 (#1040)
Reviewed-on: vikunja/api#1040
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-09 07:14:03 +00:00
5763aca428 fix(deps): update golang.org/x/crypto commit hash to ceb1ce7 (#1041)
Reviewed-on: vikunja/api#1041
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-09 07:13:37 +00:00
609497d08c fix(deps): update module github.com/yuin/goldmark to v1.4.3 (#1038)
Reviewed-on: vikunja/api#1038
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-08 11:36:38 +00:00
b92c370d6d fix(deps): update golang.org/x/sys commit hash to e0b2ad0 (#1037)
Reviewed-on: vikunja/api#1037
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-07 12:17:43 +00:00
0b707369d0 fix(deps): update golang.org/x/sys commit hash to ebca88c (#1035)
Reviewed-on: vikunja/api#1035
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-06 20:29:05 +00:00
e64f06fabc fix(deps): update golang.org/x/sys commit hash to c75c477 (#1034)
Reviewed-on: vikunja/api#1034
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-05 21:31:57 +00:00
bcfaa78c1a fix(deps): update module github.com/labstack/gommon to v0.3.1 (#1033)
Reviewed-on: vikunja/api#1033
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-05 11:03:42 +00:00
29eb0765f3 fix(deps): update golang.org/x/oauth2 commit hash to d3ed0bb (#1032)
Reviewed-on: vikunja/api#1032
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-04 21:28:37 +00:00
473692cf7a fix(deps): update golang.org/x/sys commit hash to 7861aae (#1031)
Reviewed-on: vikunja/api#1031
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-04 05:50:10 +00:00
b9819de157 fix(deps): update golang.org/x/sys commit hash to ae416a5 (#1030)
Reviewed-on: vikunja/api#1030
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-03 19:13:48 +00:00
b3e520427b fix(deps): update golang.org/x/sys commit hash to 4dd7244 (#1029)
Reviewed-on: vikunja/api#1029
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-02 20:31:19 +00:00
cb74337738 fix(deps): update golang.org/x/sys commit hash to a2f17f7 (#1028)
Reviewed-on: vikunja/api#1028
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-02 12:52:59 +00:00
a03d119a06 fix(deps): update golang.org/x/sys commit hash to 39c9dd3 (#1027)
Reviewed-on: vikunja/api#1027
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-11-01 21:33:48 +00:00
2683ef23d5
feat: expose if a user is a local user through the /user endpoint 2021-10-31 13:57:19 +01:00
516c812043
feat: expose if a user is a local user through its jwt token 2021-10-31 12:37:31 +01:00
9eca971c93
feat: don't require a password for data export from users authenticated with third-party auth 2021-10-31 12:37:08 +01:00
cc612d505f
docs: make sure all links to vikunja pages are https 2021-10-31 11:23:42 +01:00
53703dd0c4 fix(deps): update golang.org/x/sys commit hash to 611d5d6 (#1026)
Reviewed-on: vikunja/api#1026
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-31 09:16:18 +00:00
068ad56d2b fix(deps): update golang.org/x/sys commit hash to b3129d9 (#1025)
Reviewed-on: vikunja/api#1025
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-30 18:06:39 +00:00
a20cbc99fd fix(deps): update golang.org/x/sys commit hash to 6e78728 (#1024)
Reviewed-on: vikunja/api#1024
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-29 17:33:07 +00:00
eb47161c64 fix(deps): update golang.org/x/image commit hash to 6944b10 (#1023)
Reviewed-on: vikunja/api#1023
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-28 21:19:49 +00:00
b194715690 fix(deps): update golang.org/x/oauth2 commit hash to ba495a6 (#1022)
Reviewed-on: vikunja/api#1022
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-28 20:26:11 +00:00
8f55af07c9
feat: add more debug logging when deleting users
related to #1021
2021-10-27 22:08:31 +02:00
9869fd694a fix(deps): update golang.org/x/sys commit hash to 69cdffd (#1020)
Reviewed-on: vikunja/api#1020
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-25 21:15:15 +00:00
ff7270838e fix(deps): update golang.org/x/sys commit hash to 711f33c (#1019)
Reviewed-on: vikunja/api#1019
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-25 16:19:50 +00:00
4bdf147c89 fix(deps): update module github.com/swaggo/swag to v1.7.4 (#1018)
Reviewed-on: vikunja/api#1018
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-25 16:19:21 +00:00
1443f8aa2d fix(deps): update golang.org/x/sys commit hash to d6a326f (#1017)
Reviewed-on: vikunja/api#1017
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-23 11:57:43 +00:00
af306e5ad3 fix(deps): update golang.org/x/sys commit hash to 8e51046 (#1016)
Reviewed-on: vikunja/api#1016
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-23 06:02:24 +00:00
9fbda1d503 fix(deps): update module github.com/yuin/goldmark to v1.4.2 (#1012)
Reviewed-on: vikunja/api#1012
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-21 17:11:46 +00:00
445703e155 fix(deps): update golang.org/x/sys commit hash to 9d61738 (#1011)
Reviewed-on: vikunja/api#1011
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-20 18:55:56 +00:00
4c05912633 fix(deps): update golang.org/x/sys commit hash to 0ec99a6 (#1010)
Reviewed-on: vikunja/api#1010
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-20 07:17:11 +00:00
8dfbb2c344 fix(deps): update golang.org/x/sys commit hash to 9d821ac (#1009)
Reviewed-on: vikunja/api#1009
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-19 19:56:31 +00:00
84fe135f69 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.9 (#1008)
Reviewed-on: vikunja/api#1008
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-19 15:52:30 +00:00
0a6e6dcac1 fix(deps): update module github.com/go-redis/redis/v8 to v8.11.4 (#1003)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1003
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-17 12:56:28 +00:00
jayden-c
dcb52c00f1 feat: improve account deletion email grammar (#1006)
Co-authored-by: Jayden Chan <jaydencn7@gmail.com>
Reviewed-on: vikunja/api#1006
Reviewed-by: konrad <k@knt.li>
Co-authored-by: jayden-c <jaydencn7@gmail.com>
Co-committed-by: jayden-c <jaydencn7@gmail.com>
2021-10-16 21:53:33 +00:00
50b65a517d
fix: correctly load and pass the user when deleting it
Fixes #984
2021-10-16 17:00:48 +02:00
d7e47a28d4
fix: don't try to load the namespace of a list if it is a shared list 2021-10-16 16:50:16 +02:00
c2b6119434
fix: make sure the full task is available in notifications 2021-10-16 16:38:59 +02:00
f46c1c5d13 chore(deps): update postgres docker tag to v14 (#1005)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#1005
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-16 14:31:04 +00:00
66589ca37d fix(deps): update module github.com/gabriel-vasile/mimetype to v1.4.0 (#1004)
Reviewed-on: vikunja/api#1004
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-15 21:50:50 +00:00
45a8c63015 fix(deps): update golang.org/x/sys commit hash to 69063c4 (#1001)
Reviewed-on: vikunja/api#1001
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-15 21:50:32 +00:00
fce8b89b45 fix(deps): update golang.org/x/oauth2 commit hash to 6b3c2da (#1000)
Reviewed-on: vikunja/api#1000
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-10-15 20:19:29 +00:00
82a3330412 Added the ability to configure the JWT expiry date using a new server.jwtttl config parameter. (#999)
Co-authored-by: Stephen Hill <stephen@gatekiller.co.uk>
Reviewed-on: vikunja/api#999
Co-authored-by: stephen-hill <stephen@gatekiller.co.uk>
Co-committed-by: stephen-hill <stephen@gatekiller.co.uk>
2021-10-09 11:02:28 +00:00
fb9fa27488 healthcheck endpoint (#998)
Co-authored-by: Igor Rzegocki <igor@rzegocki.pl>
Reviewed-on: vikunja/api#998
Reviewed-by: konrad <k@knt.li>
Co-authored-by: ajgon <vikunja-gitea@ajgon.ovh>
Co-committed-by: ajgon <vikunja-gitea@ajgon.ovh>
2021-10-03 18:37:02 +00:00
cd6cd840a2
chore(ci): sign drone config 2021-10-02 16:36:07 +02:00
c2f33868c8
chore(ci): use latest version of s3 plugin 2021-10-02 16:31:06 +02:00
73a99ebd92
chore(mage): don't set api packages when they are not used 2021-10-02 15:40:56 +02:00
68998e90a4
docs: fix api url in docker examples without a proxy 2021-09-29 20:38:09 +02:00
c67456d420 fix(deps): update golang.org/x/term commit hash to 03fcf44 (#996)
Reviewed-on: vikunja/api#996
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-28 07:04:56 +00:00
2ab27c72f2 fix(deps): update golang.org/x/sys commit hash to 39ccf1d (#995)
Reviewed-on: vikunja/api#995
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-27 10:57:11 +00:00
df5c5e3be2 fix(deps): update golang.org/x/sys commit hash to 1cf2251 (#994)
Reviewed-on: vikunja/api#994
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-27 08:05:48 +00:00
1f067930a2 fix(deps): update module github.com/labstack/echo/v4 to v4.6.1 (#993)
Reviewed-on: vikunja/api#993
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-26 16:25:10 +00:00
0ca1560bf1 fix(deps): update module github.com/swaggo/swag to v1.7.3 (#990)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#990
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-26 12:44:37 +00:00
4de8ec56a6
fix: generate swagger docs 2021-09-26 14:18:44 +02:00
ae8db176db
feat: expose if task comments are enabled or not in /info 2021-09-26 13:37:57 +02:00
56efbdc297 fix(deps): update golang.org/x/sys commit hash to 92d5a99 (#992)
Reviewed-on: vikunja/api#992
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-25 06:34:41 +00:00
4764a8e4f0 fix(deps): update module github.com/golang-jwt/jwt/v4 to v4.1.0 (#991)
Reviewed-on: vikunja/api#991
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-25 06:34:22 +00:00
dbd6f36da6
docs: add another youtube tutorial 2021-09-24 22:11:03 +02:00
4255bc3a94
docs: add docker-compose example with no proxy 2021-09-24 20:45:37 +02:00
bb086eb9f8
feat: add better error logs for mage commands 2021-09-24 20:03:38 +02:00
c4da0b424b fix(deps): update golang.org/x/sys commit hash to b8560ed (#989)
Reviewed-on: vikunja/api#989
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-23 18:36:23 +00:00
037457e865 fix(deps): update module github.com/labstack/echo/v4 to v4.6.0 (#988)
Reviewed-on: vikunja/api#988
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 20:08:42 +00:00
e51f125931 fix(deps): update golang.org/x/term commit hash to 140adaa (#983)
Reviewed-on: vikunja/api#983
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 19:40:27 +00:00
57d447b165 fix(deps): update golang.org/x/crypto commit hash to 089bfa5 (#979)
Reviewed-on: vikunja/api#979
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 19:40:15 +00:00
4136255c67 fix(deps): update module github.com/spf13/viper to v1.9.0 (#987)
Reviewed-on: vikunja/api#987
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:18:39 +00:00
0f2013f915 fix(deps): update module github.com/coreos/go-oidc/v3 to v3.1.0 (#985)
Reviewed-on: vikunja/api#985
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:18:19 +00:00
0ca31aed28 fix(deps): update module github.com/yuin/goldmark to v1.4.1 (#976)
Reviewed-on: vikunja/api#976
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:18:02 +00:00
351fe1f624 fix(deps): update golang.org/x/sys commit hash to 437939a (#975)
Reviewed-on: vikunja/api#975
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:17:50 +00:00
abfdae0012 fix(deps): update golang.org/x/sys commit hash to 528a39c (#974)
Reviewed-on: vikunja/api#974
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-09 22:22:38 +00:00
5f5936c972 fix(deps): update golang.org/x/sys commit hash to aa78b53 (#973)
Reviewed-on: vikunja/api#973
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-09 16:16:28 +00:00
111ac92619 fix(deps): update golang.org/x/sys commit hash to a851e7d (#972)
Reviewed-on: vikunja/api#972
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-08 20:17:39 +00:00
124 changed files with 4197 additions and 1139 deletions

View File

@ -62,7 +62,7 @@ services:
- name: tmp-mysql-migration
path: /var/lib/mysql
- name: test-postgres-unit
image: postgres:13
image: postgres:14
environment:
POSTGRES_PASSWORD: vikunjatest
POSTGRES_DB: vikunjatest
@ -72,7 +72,7 @@ services:
commands:
- docker-entrypoint.sh -c fsync=off -c full_page_writes=off # turns of wal
- name: test-postgres-integration
image: postgres:13
image: postgres:14
environment:
POSTGRES_PASSWORD: vikunjatest
POSTGRES_DB: vikunjatest
@ -82,7 +82,7 @@ services:
commands:
- docker-entrypoint.sh -c fsync=off -c full_page_writes=off # turns of wal
- name: test-postgres-migration
image: postgres:13
image: postgres:14
environment:
POSTGRES_PASSWORD: vikunjatest
POSTGRES_DB: vikunjatest
@ -138,7 +138,7 @@ steps:
GOPROXY: 'https://goproxy.kolaente.de'
depends_on: [ build ]
commands:
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.31.0
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.45.2
- ./mage-static check:all
when:
event: [ push, tag, pull_request ]
@ -459,7 +459,7 @@ steps:
# Push the releases to our pseudo-s3-bucket
- name: release-latest
image: plugins/s3:1
image: plugins/s3
pull: true
settings:
bucket: vikunja-releases
@ -481,7 +481,7 @@ steps:
depends_on: [ sign-release ]
- name: release-version
image: plugins/s3:1
image: plugins/s3
pull: true
settings:
bucket: vikunja-releases
@ -514,7 +514,7 @@ steps:
# Push the os releases to our pseudo-s3-bucket
- name: release-os-latest
image: plugins/s3:1
image: plugins/s3
pull: true
settings:
bucket: vikunja-releases
@ -536,7 +536,7 @@ steps:
depends_on: [ build-os-packages ]
- name: release-os-version
image: plugins/s3:1
image: plugins/s3
pull: true
settings:
bucket: vikunja-releases
@ -576,7 +576,7 @@ steps:
# Push the releases to our pseudo-s3-bucket
- name: release-deb
image: plugins/s3:1
image: plugins/s3
pull: true
settings:
bucket: vikunja-releases
@ -621,7 +621,7 @@ steps:
- tar -xzf vikunja-theme.tar.gz
- name: build
image: monachus/hugo:v0.75.1
image: klakegg/hugo:0.93.3
pull: true
commands:
- cd docs
@ -874,6 +874,6 @@ steps:
- failure
---
kind: signature
hmac: 110b782e9b704b4b3b3d618678383718c92262cf3c214f4fe6705d40cd3da367
hmac: 1c4c211e66e4b6eddd2a1c1bad31e5c960d4f67d6033f4d5c4de7896dfae6c30
...

View File

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

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"go.testEnvVars": {
"VIKUNJA_SERVICE_ROOTPATH": "${workspaceRoot}"
}
}

View File

@ -3,6 +3,13 @@ service:
# Default is a random token which will be generated at each startup of vikunja.
# (This means all already issued tokens will be invalid once you restart vikunja)
JWTSecret: "<jwt-secret>"
# The duration of the issed JWT tokens in seconds.
# The default is 259200 seconds (3 Days).
jwtttl: 259200
# The duration of the "remember me" time in seconds. When the login request is made with
# the long param set, the token returned will be valid for this period.
# The default is 2592000 seconds (30 Days).
jwtttllong: 2592000
# The interface on which to run the webserver
interface: ":3456"
# Path to Unix socket. If set, it will be created and used instead of tcp
@ -15,6 +22,8 @@ service:
# Vikunja will also look in this path for a config file, so you could provide only this variable to point to a folder
# with a config file which will then be used.
rootpath: <rootpath>
# Path on the file system to serve static files from. Set to the path of the frontend files to host frontend alongside the api.
staticpath: ""
# The max number of items which can be returned per page
maxitemsperpage: 50
# Enable the caldav endpoint, see the docs for more details
@ -47,17 +56,20 @@ service:
# it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
# for user deletion.
enableuserdeletion: true
# The maximum size clients will be able to request for user avatars.
# If clients request a size bigger than this, it will be changed on the fly.
maxavatarsize: 1024
database:
# Database type to use. Supported types are mysql, postgres and sqlite.
type: "sqlite"
# Database user which is used to connect to the database.
user: "vikunja"
# Databse password
# Database password
password: ""
# Databse host
# Database host
host: "localhost"
# Databse to use
# Database to use
database: "vikunja"
# When using sqlite, this is the path where to store the data
path: "./vikunja.db"
@ -70,6 +82,12 @@ database:
# Secure connection mode. Only used with postgres.
# (see https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection_String_Parameters)
sslmode: disable
# The path to the client cert. Only used with postgres.
sslcert: ""
# The path to the client key. Only used with postgres.
sslkey: ""
# The path to the ca cert. Only used with postgres.
sslrootcert: ""
# Enable SSL/TLS for mysql connections. Options: false, true, skip-verify, preferred
tls: false
@ -111,6 +129,8 @@ mailer:
host: ""
# SMTP Host port
port: 587
# SMTP Auth Type. Can be either `plain`, `login` or `cram-md5`.
authtype: "plain"
# SMTP username
username: "user"
# SMTP password

View File

@ -2,7 +2,7 @@ baseurl: https://vikunja.io/docs/
title: Vikunja
theme: vikunja
enableRobotsTXT: true
canonifyURLs: true
canonifyURLs: false
pygmentsUseClasses: true

View File

@ -10,8 +10,8 @@ menu:
# Database
Vikunja uses [xorm](http://xorm.io/) as an abstraction layer to handle the database connection.
Please refer to [their](http://xorm.io/docs/) documentation on how to exactly use it.
Vikunja uses [xorm](https://xorm.io/) as an abstraction layer to handle the database connection.
Please refer to [their](https://xorm.io/docs/) documentation on how to exactly use it.
{{< table_of_contents >}}
@ -24,7 +24,7 @@ In other packages, use the `db.NewSession()` method to get a new database sessio
To add a new table to the database, create the struct and [add a migration for it]({{< ref "db-migrations.md" >}}).
To learn more about how to configure your struct to create "good" tables, refer to [the xorm documentaion](http://xorm.io/docs/).
To learn more about how to configure your struct to create "good" tables, refer to [the xorm documentaion](https://xorm.io/docs/).
In most cases you will also need to implement the `TableName() string` method on the new struct to make sure the table
name matches the rest of the tables - plural.

View File

@ -12,11 +12,45 @@ menu:
# Development
We use go modules to manage third-party libraries for Vikunja, so you'll need at least go `1.11` to use these.
{{< table_of_contents >}}
## General
To contribute to Vikunja, fork the project and work on the main branch.
Once you feel like your changes are ready, open a PR in the respective repo.
A maintainer will take a look and give you feedback. Once everyone is happy, the PR gets merged and released.
If you plan to do a bigger change, it is better to open an issue for discussion first.
## API
The code for the api is located at [code.vikunja.io/api](https://code.vikunja.io/api).
We use go modules to manage third-party libraries for Vikunja, so you'll need at least go `1.17` to use these.
A lot of developing tasks are automated using a Magefile, so make sure to [take a look at it]({{< ref "mage.md">}}).
Make sure to check the other doc articles for specific development tasks like [testing]({{< ref "test.md">}}),
Make sure to check the other doc articles for specific development tasks like [testing]({{< ref "test.md">}}),
[database migrations]({{< ref "db-migrations.md" >}}) and the [project structure]({{< ref "structure.md" >}}).
## Frontend requirements
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
You need to have yarn v1 and nodejs in version 16 installed.
## Git flow
The `main` branch is the latest and bleeding edge branch with all changes. Unstable releases are automatically
created from this branch.
A release gets tagged from the main branch with the version name as tag name.
Backports and point-releases should go to a `release/version` branch, based on the tag they are building on top of.
## Conventional commits
We're using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) because they greatly simplify
generating release notes.
It is not required to use them when creating a PR, but appreciated.

View File

@ -11,9 +11,9 @@ menu:
# Mage
Vikunja uses [Mage](https://magefile.org/) to script common development tasks and even releasing.
Mage is a pure go solution which allows for greater flexibility and things like better paralelization.
Mage is a pure go solution which allows for greater flexibility and things like better parallelization.
This document explains what taks are available and what they do.
This document explains what tasks are available and what they do.
{{< table_of_contents >}}

View File

@ -10,32 +10,31 @@ menu:
# Testing
You can run unit tests with [mage]({{< ref "mage.md">}}) with
{{< table_of_contents >}}
## API Tests
The following parts are about the kinds of tests in the API package and how to run them.
### Prerequesites
To run any kind of test, you need to specify Vikunja's [root path](https://vikunja.io/docs/config-options/#rootpath).
This is required to make sure all test fixtures are correctly loaded.
The easies way to do that is to set the environment variable `VIKUNJA_SERVICE_ROOTPATH` to the path where you cloned the working directory.
### Unit tests
To run unit tests with [mage]({{< ref "mage.md">}}), execute
{{< highlight bash >}}
mage test:unit
{{< /highlight >}}
{{< table_of_contents >}}
In Vikunja, everything that is not an integration test counts as unit test - even if it accesses the db.
This definition is a bit blurry, but we haven't found a better one yet.
## Running tests with config
You can run tests with all available config variables if you want, enabeling you to run tests for a lot of scenarios.
To use the normal config set the enviroment variable `VIKUNJA_TESTS_USE_CONFIG=1`.
## Show sql queries
When `UNIT_TESTS_VERBOSE=1` is set, all sql queries will be shown when tests are run.
## Fixtures
All tests are run against a set of db fixtures.
These fixtures are defined in `pkg/models/fixtures` in YAML-Files which represent the database structure.
When you add a new test case which requires new database entries to test against, update these files.
## Integration tests
### Integration tests
All integration tests live in `pkg/integrations`.
You can run them by executing `mage test:integration`.
@ -45,7 +44,25 @@ see at the beginning of this document.
To run integration tests, use `mage test:integration`.
## Initializing db fixtures when writing tests
### Running tests with config
You can run tests with all available config variables if you want, enabeling you to run tests for a lot of scenarios.
We use this in CI to run all tests with different databases.
To use the normal config set the enviroment variable `VIKUNJA_TESTS_USE_CONFIG=1`.
### Showing sql queries
When the environment variable `UNIT_TESTS_VERBOSE=1` is set, all sql queries will be shown during the test run.
### Fixtures
All tests are run against a set of db fixtures.
These fixtures are defined in `pkg/models/fixtures` in YAML-Files which represent the database structure.
When you add a new test case which requires new database entries to test against, update these files.
#### Initializing db fixtures when writing tests
All db fixtures for all tests live in the `pkg/db/fixtures/` folder as yaml files.
Each file has the same name as the table the fixtures are for.
@ -54,19 +71,39 @@ You should put new fixtures in this folder.
When initializing db fixtures, you are responsible for defining which tables your package needs in your test init function.
Usually, this is done as follows (this code snippet is taken from the `user` package):
```go
{{< highlight go >}}
err = db.InitTestFixtures("users")
if err != nil {
log.Fatal(err)
}
```
{{< /highlight >}}
In your actual tests, you then load the fixtures into the in-memory db like so:
```go
{{< highlight go >}}
db.LoadAndAssertFixtures(t)
```
{{< /highlight >}}
This will load all fixtures you defined in your test init method.
You should always use this method to load fixtures, the only exception is when your package tests require extra test
fixtures other than db fixtures (like files).
## Frontend tests
The frontend has end to end tests with Cypress that use a Vikunja instance and drive a browser against it.
Check out the docs [in the frontend repo](https://kolaente.dev/vikunja/frontend/src/branch/main/cypress/README.md) about how they work and how to get them running.
### Unit Tests
To run the frontend unit tests, run
{{< highlight bash >}}
yarn test:unit
{{< /highlight >}}
The frontend also has a watcher available that re-runs all unit tests every time you change something.
To use it, simply run
{{< highlight bash >}}
yarn test:unit-watch
{{< /highlight >}}

View File

@ -73,4 +73,4 @@ Beispiel: „Benutzer:in“
## Weiterführende Links
* http://docs.translatehouse.org/projects/localization-guide/en/latest/guide/translation_guidelines_german.html
* https://docs.translatehouse.org/projects/localization-guide/en/latest/guide/translation_guidelines_german.html

View File

@ -10,12 +10,17 @@ menu:
# What to backup
Vikunja does not store any data outside of the database.
So, all you need to backup are the contents of that database and maybe the config file.
There are two parts you need to back up: The database and attachment files.
{{< table_of_contents >}}
## MySQL
## Files
To back up attachments and other files, it is enough to copy them [from the attachments folder]({{< ref "config.md" >}}#basepath) to some other place.
## Database
### MySQL
To create a backup from mysql use the `mysqldump` command:
@ -31,7 +36,7 @@ To restore it, simply pipe it back into the `mysql` command:
mysql -u <user> -p -h <db-host> <database> < vkunja-backup.sql
{{< /highlight >}}
## PostgreSQL
### PostgreSQL
To create a backup from PostgreSQL use the `pg_dump` command:
@ -49,6 +54,6 @@ psql -U <user> -h <db-host> <database> < vikunja-backup.sql
For more information, please visit the [relevant PostgreSQL documentation](https://www.postgresql.org/docs/12/backup-dump.html).
## SQLite
### SQLite
To backup sqllite databases, it is enough to copy the database elsewhere.
To back up sqllite databases, it is enough to copy the [database file]({{< ref "config.md" >}}#path) to somwhere else.

View File

@ -10,20 +10,37 @@ menu:
# Build Vikunja from source
Vikunja being a go application, has no other dependencies than go itself.
All libraries are bundeled inside the repo in the `vendor/` folder, so all it boils down to are these steps:
To completely build Vikunja from source, you need to build the api and frontend.
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.9`.
2. Make sure [Mage](https://magefile) is properly installed on your system.
3. Clone the repo with `git clone https://code.vikunja.io/api`
{{< table_of_contents >}}
## API
The Vikunja API has no other dependencies than go itself.
That means compiling it boils down to these steps:
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.17`.
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
3. Clone the repo with `git clone https://code.vikunja.io/api` and switch into the directory.
3. Run `mage build:build` in the source of this repo. This will build a binary in the root of the repo which will be able to run on your system.
*Note:* Static ressources such as email templates are built into the binary.
For these to work, you may need to run `mage build:generate` before building the vikunja binary.
When builing entirely with `mage`, you dont need to do this, `mage build:generate` will be run automatically when running `mage build:build`.
# Build for different architectures
### Build for different architectures
To build for other platforms and architectures than the one you're currently on, simply run `mage release:release` or `mage release:{linux|windows|darwin}`.
More options are available, please refer to the [magefile docs]({{< ref "../development/mage.md">}}) for more details.
More options are available, please refer to the [magefile docs]({{< ref "../development/mage.md">}}) for more details.
## Frontend
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
You need to have yarn v1 and nodejs in version 16 installed.
1. Make sure [yarn v1](https://yarnpkg.com/getting-started/install) is properly installed on your system.
3. Clone the repo with `git clone https://code.vikunja.io/frontend` and switch into the directory.
3. Install all dependencies with `yarn install`
4. Build the frontend with `yarn build`. This will result in a js bundle in the `dist/` folder which you can deploy.

View File

@ -10,8 +10,9 @@ menu:
# Configuration options
You can either use a `config.yml` file in the root directory of vikunja or set all config option with
You can either use a `config.yml` file in the root directory of vikunja or set almost all config option with
environment variables. If you have both, the value set in the config file is used.
Right now it is not possible to configure openid authentication via environment variables.
Variables are nested in the `config.yml`, these nested variables become `VIKUNJA_FIRST_CHILD` when configuring via
environment variables. So setting
@ -76,7 +77,32 @@ Default: `<jwt-secret>`
Full path: `service.JWTSecret`
Environment path: `VIKUNJA_SERVICE_JWT_SECRET`
Environment path: `VIKUNJA_SERVICE_JWTSECRET`
### jwtttl
The duration of the issed JWT tokens in seconds.
The default is 259200 seconds (3 Days).
Default: `259200`
Full path: `service.jwtttl`
Environment path: `VIKUNJA_SERVICE_JWTTTL`
### jwtttllong
The duration of the "remember me" time in seconds. When the login request is made with
the long param set, the token returned will be valid for this period.
The default is 2592000 seconds (30 Days).
Default: `2592000`
Full path: `service.jwtttllong`
Environment path: `VIKUNJA_SERVICE_JWTTTLLONG`
### interface
@ -136,6 +162,17 @@ Full path: `service.rootpath`
Environment path: `VIKUNJA_SERVICE_ROOTPATH`
### staticpath
Path on the file system to serve static files from. Set to the path of the frontend files to host frontend alongside the api.
Default: `<empty>`
Full path: `service.staticpath`
Environment path: `VIKUNJA_SERVICE_STATICPATH`
### maxitemsperpage
The max number of items which can be returned per page
@ -285,6 +322,18 @@ Full path: `service.enableuserdeletion`
Environment path: `VIKUNJA_SERVICE_ENABLEUSERDELETION`
### maxavatarsize
The maximum size clients will be able to request for user avatars.
If clients request a size bigger than this, it will be changed on the fly.
Default: `1024`
Full path: `service.maxavatarsize`
Environment path: `VIKUNJA_SERVICE_MAXAVATARSIZE`
---
## database
@ -315,7 +364,7 @@ Environment path: `VIKUNJA_DATABASE_USER`
### password
Databse password
Database password
Default: `<empty>`
@ -326,7 +375,7 @@ Environment path: `VIKUNJA_DATABASE_PASSWORD`
### host
Databse host
Database host
Default: `localhost`
@ -337,7 +386,7 @@ Environment path: `VIKUNJA_DATABASE_HOST`
### database
Databse to use
Database to use
Default: `vikunja`
@ -402,6 +451,39 @@ Full path: `database.sslmode`
Environment path: `VIKUNJA_DATABASE_SSLMODE`
### sslcert
The path to the client cert. Only used with postgres.
Default: `<empty>`
Full path: `database.sslcert`
Environment path: `VIKUNJA_DATABASE_SSLCERT`
### sslkey
The path to the client key. Only used with postgres.
Default: `<empty>`
Full path: `database.sslkey`
Environment path: `VIKUNJA_DATABASE_SSLKEY`
### sslrootcert
The path to the ca cert. Only used with postgres.
Default: `<empty>`
Full path: `database.sslrootcert`
Environment path: `VIKUNJA_DATABASE_SSLROOTCERT`
### tls
Enable SSL/TLS for mysql connections. Options: false, true, skip-verify, preferred
@ -584,6 +666,17 @@ Full path: `mailer.port`
Environment path: `VIKUNJA_MAILER_PORT`
### authtype
SMTP Auth Type. Can be either `plain`, `login` or `cram-md5`.
Default: `plain`
Full path: `mailer.authtype`
Environment path: `VIKUNJA_MAILER_AUTHTYPE`
### username
SMTP username

View File

@ -25,6 +25,9 @@ All examples on this page already reflect this and do not require additional wor
## Redis
While Vikunja has support to use redis as a caching backend, you'll probably not need it unless you're using Vikunja
with more than a handful of users.
To use redis, you'll need to add this to the config examples below:
{{< highlight yaml >}}
@ -44,6 +47,78 @@ services:
image: redis
{{< /highlight >}}
## PostgreSQL
Vikunja supports postgres, mysql and sqlite as a database backend. The examples on this page use mysql with a mariadb container.
To use postgres as a database backend, change the `db` section of the examples to this:
{{< highlight yaml >}}
db:
image: postgres:13
environment:
POSTGRES_PASSWORD: secret
POSTGRES_USER: vikunja
volumes:
- ./db:/var/lib/postgresql/data
restart: unless-stopped
{{< /highlight >}}
You'll also need to change the `VIKUNJA_DATABASE_TYPE` to `postgres` on the api container declaration.
<div class="notification is-warning">
<b>NOTE:</b> The mariadb container can sometimes take a while to initialize, especially on the first run.
During this time, the api container will fail to start at all. It will automatically restart every few seconds.
</div>
## Example without any proxy
This example lets you host Vikunja without any reverse proxy in front of it. This is the absolute minimum configuration
you need to get something up and running. If you want to host Vikunja on one single port instead of two different ones
or need tls termination, check out one of the other examples.
Not that you need to change the `VIKUNJA_API_URL` environment variable to the ip (the docker host you're running this on)
is reachable at. Because the browser you'll use to access the Vikunja frontend uses that url to make the requests, it
has to be able to reach that ip + port from the outside. Putting everything in a private network won't work.
{{< highlight yaml >}}
version: '3'
services:
db:
image: mariadb:10
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_ROOT_PASSWORD: supersecret
MYSQL_USER: vikunja
MYSQL_PASSWORD: secret
MYSQL_DATABASE: vikunja
volumes:
- ./db:/var/lib/mysql
restart: unless-stopped
api:
image: vikunja/api
environment:
VIKUNJA_DATABASE_HOST: db
VIKUNJA_DATABASE_PASSWORD: secret
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
ports:
- 3456:3456
volumes:
- ./files:/app/vikunja/files
depends_on:
- db
restart: unless-stopped
frontend:
image: vikunja/frontend
ports:
- 80:80
environment:
VIKUNJA_API_URL: http://<your-ip-here>:3456/api/v1
restart: unless-stopped
{{< /highlight >}}
## Example with traefik 2
This example assumes [traefik](https://traefik.io) version 2 installed and configured to [use docker as a configuration provider](https://docs.traefik.io/providers/docker/).
@ -295,3 +370,81 @@ services:
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
{{< /highlight >}}
## Setup on a Synology NAS
There is a proxy preinstalled in DSM, so if you want to access vikunja from outside,
you can prepare 2 proxy rules:
* a redirection rule for vikunja's api (see example screenshot using port 3456)
* a similar redirection rule for vikunja's frontend (using port 4321)
![Synology Proxy Settings](/docs/synology-proxy-1.png)
You should also add 2 empty folders for mariadb and vikunja inside Synology's
docker main folders:
* Docker
* vikunja
* mariadb
Synology has it's own GUI for managing Docker containers... But it's easier via docker compose.
To do that, you can
* either activate SSH and paste the adapted compose file in a terminal (using Putty or similar)
* without activating SSH as a "custom script" (go to Control Panel / Task Scheduler / Create / Scheduled Task / User-defined script)
* without activating SSH, by using Portainer (you have to install first, check out [this tutorial](https://www.portainer.io/blog/how-to-install-portainer-on-a-synology-nas) for exmple):
1. Go to **Dashboard / Stacks** click the button **"Add Stack"**
2. Give it the name Vikunja and paste the adapted docker compose file
3. Deploy the Stack with the "Delpoy Stack" button:
![Portainer Stack deploy](/docs/synology-proxy-2.png)
The docker-compose file we're going to use is very similar to the [example without any proxy](#example-without-any-proxy) above:
{{< highlight yaml >}}
version: '3'
services:
db:
image: mariadb:10
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_ROOT_PASSWORD: supersecret
MYSQL_USER: vikunja
MYSQL_PASSWORD: secret
MYSQL_DATABASE: vikunja
volumes:
- ./db:/var/lib/mysql
restart: unless-stopped
api:
image: vikunja/api
environment:
VIKUNJA_DATABASE_HOST: db
VIKUNJA_DATABASE_PASSWORD: secret
VIKUNJA_DATABASE_TYPE: mysql
VIKUNJA_DATABASE_USER: vikunja
VIKUNJA_DATABASE_DATABASE: vikunja
ports:
- 3456:3456
volumes:
- ./files:/app/vikunja/files
depends_on:
- db
restart: unless-stopped
frontend:
image: vikunja/frontend
ports:
- 4321:80
environment:
VIKUNJA_API_URL: http://vikunja-api-domain.tld/api/v1
restart: unless-stopped
{{< /highlight >}}
You may want to change the volumes to match the rest of your setup.
Once deployed, you might want to change the [`PUID` and `GUID` settings]({{< ref "install-backend.md">}}#setting-user-and-group-id-of-the-user-running-vikunja) or [set the time zone]({{< ref "config.md">}}#timezone).
After registering all your users, you might also want to [disable the user registration]({{<ref "config.md">}}#enableregistration).

View File

@ -52,7 +52,7 @@ ln -s /opt/vikunja/vikunja /usr/bin/vikunja
### Systemd service
Take the following `service` file and adapt it to your needs:
Save the following service file to `/etc/systemd/system/vikunja.service` and adapt it to your needs:
{{< highlight service >}}
[Unit]
@ -83,8 +83,6 @@ WantedBy=multi-user.target
If you've installed Vikunja to a directory other than `/opt/vikunja`, you need to adapt `WorkingDirectory` accordingly.
Save the file to `/etc/systemd/system/vikunja.service`
After you made all nessecary modifications, it's time to start the service:
{{< highlight bash >}}

View File

@ -11,16 +11,20 @@ menu:
# Installing
Vikunja consists of two parts: [Backend](https://code.vikunja.io/api) and [frontend](https://code.vikunja.io/frontend).
While the backend is required, the frontend is not.
You don't neccesarily need to have a web-frontend, using Vikunja via the [mobile app](https://code.vikunja.io/app) is totally fine.
Vikunja consists of two parts: [API](https://code.vikunja.io/api) and [frontend](https://code.vikunja.io/frontend).
However, using the web frontend is highly reccommended.
You will always need to install at least the API.
To actually use Vikunja you'll also need to somehow install a frontend to use it.
You can either:
Vikunja can be installed in various forms.
* [Install the web frontend]({{< ref "install-frontend.md">}})
* Use the desktop app, which is essentially a web frontend packaged for easy installation on desktop devices
* Use the mobile app only, but as of right now it only supports the very basic features of Vikunja
Vikunja can be installed in various ways.
This document provides an overview and instructions for the different methods.
* [Backend]({{< ref "install-backend.md">}})
* [API]({{< ref "install-backend.md">}})
* [Installing from binary]({{< ref "install-backend.md#install-from-binary">}})
* [Verify the GPG signature]({{< ref "install-backend.md#verify-the-gpg-signature">}})
* [Set it up]({{< ref "install-backend.md#set-it-up">}})
@ -50,4 +54,5 @@ A third-party Helm Chart is available from the k8s-at-home project [here](https:
* [Setup Vikunja using Docker Compose - Homelab Wiki](https://thehomelab.wiki/books/docker/page/setup-vikunja-using-docker-compose)
* [A Closer look at Vikunja - Email Notifications - Enable or Disable Registrations - Allow Attachments](https://www.youtube.com/watch?v=47wj9pRT6Gw) (Youtube)
* [Install Vikunja in Docker for self-hosted Task Tracking](https://smarthomepursuits.com/install-vikunja-in-docker-for-self-hosted-task-tracking/)
* [Self-Hosted To-Do List with Vikunja in Docker](https://www.youtube.com/watch?v=DqyqDWpEvKI) (Youtube)
* [Vikunja self-hosted (step by step)](https://nguyenminhhung.com/vikunja-self-hosted-step-by-step/)

View File

@ -80,6 +80,22 @@ server {
<b>NOTE:</b> If you change the max upload size in Vikunja's settings, you'll need to also change the <code>client_max_body_size</code> in the nginx proxy config.
</div>
## NGINX Proxy Manager (NPM)
1. Create a standard Proxy Host for the Vikunja Frontend within NPM and point it to the URL you plan to use. The next several steps will enable the Proxy Host to successfully navigate to the API (on port 3456).
2. Verify that the page will pull up in your browser. (Do not bother trying to log in. It won't work. Trust me.)
3. Now, we'll work with the NPM container, so you need to identify the container name for your NPM installation. e.g. NGINX-PM
4. From the command line, enter `sudo docker exec -it [NGINX-PM container name] /bin/bash` and navigate to the proxy hosts folder where the `.conf` files are stashed. Probably `/data/nginx/proxy_host`. (This folder is a persistent folder created in the NPM container and mounted by NPM.)
5. Locate the `.conf` file where the server_name inside the file matches your Vikunja Proxy Host. Once found, add the following code, unchanged, just above the existing location block in that file. (They are listed by number, not name.)
```
location ~* ^/(api|dav|\.well-known)/ {
proxy_pass http://api:3456;
client_max_body_size 20M;
}
```
6. After saving the edited file, return to NPM's UI browser window and refresh the page to verify your Proxy Host for Vikunja is still online.
7. Now, switch over to your Vikunja browswer window and hit refresh. If you configured your URL correctly in original Vikunja container, you should be all set and the browser will correctly show Vikunja. If not, you'll need to adjust the address in the top of the login subscreen to match your proxy address.
## Apache
Put the following config in `cat /etc/apache2/sites-available/vikunja.conf`:
@ -108,4 +124,4 @@ Put the following config in `cat /etc/apache2/sites-available/vikunja.conf`:
**Note:** The apache modules `proxy`, `proxy_http` and `rewrite` must be enabled for this.
For more details see the [frontend apache configuration]({{< ref "install-frontend.md#apache">}}).
For more details see the [frontend apache configuration]({{< ref "install-frontend.md#apache">}}).

View File

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

View File

@ -11,7 +11,7 @@ menu:
# API Documentation
You can find the api docs under `http://vikunja.tld/api/v1/docs` of your instance.
A public instance is available on [try.vikunja.io](http://try.vikunja.io/api/v1/docs).
A public instance is available on [try.vikunja.io](https://try.vikunja.io/api/v1/docs).
These docs are autgenerated from annotations in the code with swagger.

View File

@ -18,4 +18,8 @@ server {
location /docs/contact {
return 301 $scheme://vikunja.io/en/contact;
}
location /docs/docs {
return 301 $scheme://vikunja.io/docs;
}
}

BIN
docs/static/synology-proxy-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
docs/static/synology-proxy-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

141
go.mod
View File

@ -20,58 +20,56 @@ require (
code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3
gitea.com/xorm/xorm-redis-cache v0.2.0
github.com/ThreeDotsLabs/watermill v1.1.1
github.com/adlio/trello v1.9.0
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/adlio/trello v1.10.0
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/beevik/etree v1.1.0 // indirect
github.com/bbrks/go-blurhash v1.1.1
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
github.com/coreos/go-oidc/v3 v3.0.0
github.com/coreos/go-oidc/v3 v3.2.0
github.com/cweill/gotests v1.6.0
github.com/d4l3k/messagediff v1.2.1
github.com/disintegration/imaging v1.6.2
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
github.com/gabriel-vasile/mimetype v1.3.1
github.com/getsentry/sentry-go v0.11.0
github.com/go-errors/errors v1.1.1 // indirect
github.com/go-redis/redis/v8 v8.11.3
github.com/gabriel-vasile/mimetype v1.4.0
github.com/getsentry/sentry-go v0.13.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.6.0
github.com/go-testfixtures/testfixtures/v3 v3.6.1
github.com/golang-jwt/jwt/v4 v4.0.0
github.com/go-testfixtures/testfixtures/v3 v3.8.0
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0
github.com/iancoleman/strcase v0.2.0
github.com/imdario/mergo v0.3.12
github.com/labstack/echo/v4 v4.5.0
github.com/labstack/gommon v0.3.0
github.com/imdario/mergo v0.3.13
github.com/jinzhu/copier v0.3.5
github.com/labstack/echo/v4 v4.7.2
github.com/labstack/gommon v0.3.1
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef
github.com/lib/pq v1.10.3
github.com/magefile/mage v1.11.0
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.8
github.com/lib/pq v1.10.6
github.com/magefile/mage v1.13.0
github.com/mattn/go-sqlite3 v1.14.14
github.com/olekukonko/tablewriter v0.0.5
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pquerna/otp v1.3.0
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/client_golang v1.12.2
github.com/robfig/cron/v3 v3.0.1
github.com/samedi/caldav-go v3.0.0+incompatible
github.com/spf13/afero v1.6.0
github.com/spf13/cobra v1.2.1
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.7.0
github.com/swaggo/swag v1.7.1
github.com/ulule/limiter/v3 v3.8.0
github.com/yuin/goldmark v1.4.0
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
github.com/spf13/afero v1.8.2
github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.11.0
github.com/stretchr/testify v1.8.0
github.com/swaggo/swag v1.8.3
github.com/tkuchiki/go-timezone v0.2.2
github.com/ulule/limiter/v3 v3.10.0
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93
github.com/wneessen/go-mail v0.2.5
github.com/yuin/goldmark v1.4.12
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/image v0.0.0-20220302094943-723b81ca9867
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210908143011-c212e7322662
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gopkg.in/yaml.v3 v3.0.1
src.techknowlogick.com/xgo v1.4.1-0.20210311222705-d25c33fcd864
src.techknowlogick.com/xormigrate v1.4.0
xorm.io/builder v0.3.9
@ -79,8 +77,77 @@ require (
xorm.io/xorm v1.1.2
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/garyburd/redigo v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-chi/chi v4.0.2+incompatible // indirect
github.com/go-errors/errors v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lithammer/shortuuid/v3 v3.0.4 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace (
github.com/adlio/trello => github.com/kolaente/trello v1.7.1-0.20201216234312-5c4ef79b531e
github.com/coreos/bbolt => go.etcd.io/bbolt v1.3.4
github.com/coreos/go-systemd => github.com/coreos/go-systemd/v22 v22.0.0
github.com/hpcloud/tail => github.com/jeffbean/tail v1.0.1 // See https://github.com/hpcloud/tail/pull/159
@ -88,4 +155,4 @@ replace (
gopkg.in/fsnotify.v1 => github.com/kolaente/fsnotify v1.4.10-0.20200411160148-1bc3c8ff4048 // See https://github.com/fsnotify/fsnotify/issues/328 and https://github.com/golang/go/issues/26904
)
go 1.15
go 1.17

602
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
// 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/>.
//go:build mage
// +build mage
package main
@ -74,9 +75,19 @@ var (
}
)
func runCmdWithOutput(name string, arg ...string) (output []byte, err error) {
cmd := exec.Command(name, arg...)
output, err = cmd.Output()
if err != nil {
ee := err.(*exec.ExitError)
return nil, fmt.Errorf("error running command: %s, %s", string(ee.Stderr), err)
}
return output, nil
}
func setVersion() {
versionCmd := exec.Command("git", "describe", "--tags", "--always", "--abbrev=10")
version, err := versionCmd.Output()
version, err := runCmdWithOutput("git", "describe", "--tags", "--always", "--abbrev=10")
if err != nil {
fmt.Printf("Error getting version: %s\n", err)
os.Exit(1)
@ -117,8 +128,7 @@ func setExecutable() {
}
func setApiPackages() {
cmd := exec.Command("go", "list", "all")
pkgs, err := cmd.Output()
pkgs, err := runCmdWithOutput("go", "list", "all")
if err != nil {
fmt.Printf("Error getting packages: %s\n", err)
os.Exit(1)
@ -145,8 +155,7 @@ func setRootPath() {
func setGoFiles() {
// GOFILES := $(shell find . -name "*.go" -type f ! -path "*/bindata.go")
cmd := exec.Command("find", ".", "-name", "*.go", "-type", "f", "!", "-path", "*/bindata.go")
files, err := cmd.Output()
files, err := runCmdWithOutput("find", ".", "-name", "*.go", "-type", "f", "!", "-path", "*/bindata.go")
if err != nil {
fmt.Printf("Error getting go files: %s\n", err)
os.Exit(1)
@ -170,7 +179,6 @@ func initVars() {
setVersion()
setBinLocation()
setPkgVersion()
setApiPackages()
setGoFiles()
Ldflags = `-X "` + PACKAGE + `/pkg/version.Version=` + VersionNumber + `" -X "main.Tags=` + Tags + `"`
}
@ -340,8 +348,9 @@ type Test mg.Namespace
// Runs all tests except integration tests
func (Test) Unit() {
mg.Deps(initVars)
setApiPackages()
// We run everything sequentially and not in parallel to prevent issues with real test databases
args := append([]string{"test", Goflags[0], "-p", "1", "-timeout", "20m"}, ApiPackages...)
args := append([]string{"test", Goflags[0], "-p", "1", "-coverprofile", "cover.out", "-timeout", "20m"}, ApiPackages...)
runAndStreamOutput("go", args...)
}
@ -1042,7 +1051,7 @@ func printConfig(config []*configOption, level int, parent string) (rendered str
fullPath := parent + "." + option.key
rendered += "Full path: `" + fullPath + "`\n\n"
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(fullPath) + "`\n\n"
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(strings.ToUpper(fullPath)) + "`\n\n"
}
}

View File

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

View File

@ -24,6 +24,8 @@ import (
"strings"
"time"
"github.com/asaskevich/govalidator"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/initialize"
"code.vikunja.io/api/pkg/log"
@ -103,7 +105,7 @@ func getUserFromArg(s *xorm.Session, arg string) *user.User {
log.Fatalf("Invalid user id: %s", err)
}
u, err := user.GetUserByID(s, id)
u, err := user.GetUserWithEmail(s, &user.User{ID: id})
if err != nil {
log.Fatalf("Could not get user: %s", err)
}
@ -175,6 +177,11 @@ var userCreateCmd = &cobra.Command{
Email: userFlagEmail,
Password: getPasswordFromFlagOrInput(),
}
if !govalidator.IsEmail(userFlagEmail) {
log.Fatalf("Provided email is invalid.")
}
newUser, err := user.CreateUser(s, u)
if err != nil {
_ = s.Rollback()

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,33 @@
// 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 integrations
import (
"net/http"
"testing"
"code.vikunja.io/api/pkg/routes"
"github.com/stretchr/testify/assert"
)
func TestHealthcheck(t *testing.T) {
t.Run("healthcheck", func(t *testing.T) {
rec, err := newTestRequest(t, http.MethodGet, routes.HealthcheckHandler, ``, nil, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "OK")
})
}

View File

@ -17,6 +17,7 @@
package integrations
import (
"errors"
"net/http"
"net/http/httptest"
"net/url"
@ -119,7 +120,7 @@ func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context)
func addUserTokenToContext(t *testing.T, user *user.User, c echo.Context) {
// Get the token as a string
token, err := auth.NewUserJWTAuthtoken(user)
token, err := auth.NewUserJWTAuthtoken(user, false)
assert.NoError(t, err)
// We send the string token through the parsing function to get a valid jwt.Token
tken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
@ -174,8 +175,8 @@ func assertHandlerErrorCode(t *testing.T, err error, expectedErrorCode int) {
t.Error("Error is nil")
t.FailNow()
}
httperr, ok := err.(*echo.HTTPError)
if !ok {
var httperr *echo.HTTPError
if !errors.As(err, &httperr) {
t.Error("Error is not *echo.HTTPError")
t.FailNow()
}

View File

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

View File

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

View File

@ -63,23 +63,26 @@ func InitLogger() {
}
}
// We define our two backends
if config.LogStandard.GetString() != "off" {
// The backend is the part which actually handles logging the log entries somewhere.
cf := config.LogStandard.GetString()
var backend logging.Backend
backend = &NoopBackend{}
if cf != "off" && cf != "false" {
stdWriter := GetLogWriter("standard")
level, err := logging.LogLevel(strings.ToUpper(config.LogLevel.GetString()))
if err != nil {
Fatalf("Error setting database log level: %s", err.Error())
}
logBackend := logging.NewLogBackend(stdWriter, "", 0)
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(Fmt+"\n"))
backendLeveled := logging.AddModuleLevel(backend)
backendLeveled.SetLevel(level, logModule)
logInstance.SetBackend(backendLeveled)
backend = logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(Fmt+"\n"))
}
level, err := logging.LogLevel(strings.ToUpper(config.LogLevel.GetString()))
if err != nil {
Fatalf("Error setting database log level: %s", err.Error())
}
backendLeveled := logging.AddModuleLevel(backend)
backendLeveled.SetLevel(level, logModule)
logInstance.SetBackend(backendLeveled)
}
// GetLogWriter returns the writer to where the normal log goes, depending on the config

28
pkg/log/noop.go Normal file
View File

@ -0,0 +1,28 @@
// 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 log
import (
"github.com/op/go-logging"
)
// NoopBackend doesn't log anything. Used in cases where we want to disable logging completely.
type NoopBackend struct{}
func (n *NoopBackend) Log(level logging.Level, i int, record *logging.Record) error {
return nil
}

View File

@ -45,8 +45,13 @@ func NewWatermillLogger() *WatermillLogger {
logger: logging.MustGetLogger(watermillLogModule),
}
logBackend := logging.NewLogBackend(GetLogWriter("events"), "", 0)
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(watermillFmt+"\n"))
cf := config.LogEvents.GetString()
var backend logging.Backend
backend = &NoopBackend{}
if cf != "off" && cf != "false" {
logBackend := logging.NewLogBackend(GetLogWriter("events"), "", 0)
backend = logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(watermillFmt+"\n"))
}
backendLeveled := logging.AddModuleLevel(backend)
backendLeveled.SetLevel(level, watermillLogModule)

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,95 @@
// 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 (
"image"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"github.com/bbrks/go-blurhash"
"golang.org/x/image/draw"
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type lists20211212210054 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"list"`
BackgroundFileID int64 `xorm:"null" json:"-"`
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
}
func (lists20211212210054) TableName() string {
return "lists"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20211212210054",
Description: "Add blurHash to list backgrounds.",
Migrate: func(tx *xorm.Engine) error {
err := tx.Sync2(lists20211212210054{})
if err != nil {
return err
}
lists := []*lists20211212210054{}
err = tx.Where("background_file_id is not null AND background_file_id != ?", 0).Find(&lists)
if err != nil {
return err
}
log.Infof("Creating BlurHash for %d list backgrounds, this might take a while...", len(lists))
for _, l := range lists {
bgFile := &files.File{
ID: l.BackgroundFileID,
}
if err := bgFile.LoadFileByID(); err != nil {
return err
}
src, _, err := image.Decode(bgFile.File)
if err != nil {
return err
}
dst := image.NewRGBA(image.Rect(0, 0, 32, 32))
draw.NearestNeighbor.Scale(dst, dst.Rect, src, src.Bounds(), draw.Over, nil)
hash, err := blurhash.Encode(4, 3, dst)
if err != nil {
return err
}
l.BackgroundBlurHash = hash
_, err = tx.Where("id = ?", l.ID).
Cols("background_blur_hash").
Update(l)
if err != nil {
return err
}
log.Debugf("Created BlurHash for list %d", l.ID)
}
return nil
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -0,0 +1,50 @@
// 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 (
"code.vikunja.io/api/pkg/config"
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type users20220112211537 struct {
Timezone string `xorm:"varchar(255) null" json:"-"`
}
func (users20220112211537) TableName() string {
return "users"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20220112211537",
Description: "Add time zone setting for users",
Migrate: func(tx *xorm.Engine) error {
err := tx.Sync2(users20220112211537{})
if err != nil {
return err
}
_, err = tx.Update(&users20220112211537{Timezone: config.GetTimeZone().String()})
return err
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

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

View File

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

View File

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

View File

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

View File

@ -59,6 +59,8 @@ type List struct {
BackgroundFileID int64 `xorm:"null" json:"-"`
// Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background
BackgroundInformation interface{} `xorm:"-" json:"background_information"`
// Contains a very small version of the list background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
// True if a list is a favorite. Favorite lists show up in a separate namespace. This value depends on the user making the call to the api.
IsFavorite bool `xorm:"-" json:"is_favorite"`
@ -540,12 +542,10 @@ func (l *List) CheckIsArchived(s *xorm.Session) (err error) {
return nil
}
// CreateOrUpdateList updates a list or creates it if it doesn't exist
func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error) {
func checkListBeforeUpdateOrDelete(s *xorm.Session, list *List) error {
// Check if the namespace exists
if list.NamespaceID != 0 && list.NamespaceID != FavoritesPseudoNamespace.ID {
_, err = GetNamespaceByID(s, list.NamespaceID)
if list.NamespaceID > 0 {
_, err := GetNamespaceByID(s, list.NamespaceID)
if err != nil {
return err
}
@ -565,65 +565,116 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error)
}
}
if list.ID == 0 {
_, err = s.Insert(list)
if err != nil {
return
}
return nil
}
list.Position = calculateDefaultPosition(list.ID, list.Position)
_, err = s.Where("id = ?", list.ID).Update(list)
if err != nil {
return
}
if list.IsFavorite {
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
return err
}
}
} else {
// We need to specify the cols we want to update here to be able to un-archive lists
colsToUpdate := []string{
"title",
"is_archived",
"identifier",
"hex_color",
"background_file_id",
"position",
}
if list.Description != "" {
colsToUpdate = append(colsToUpdate, "description")
}
func CreateList(s *xorm.Session, list *List, auth web.Auth) (err error) {
err = list.CheckIsArchived(s)
if err != nil {
return err
}
wasFavorite, err := isFavorite(s, list.ID, auth, FavoriteKindList)
if err != nil {
return err
}
if list.IsFavorite && !wasFavorite {
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
return err
}
}
doer, err := user.GetFromAuth(auth)
if err != nil {
return err
}
if !list.IsFavorite && wasFavorite {
if err := removeFromFavorite(s, list.ID, auth, FavoriteKindList); err != nil {
return err
}
}
list.OwnerID = doer.ID
list.Owner = doer
list.ID = 0 // Otherwise only the first time a new list would be created
_, err = s.
ID(list.ID).
Cols(colsToUpdate...).
Update(list)
if err != nil {
err = checkListBeforeUpdateOrDelete(s, list)
if err != nil {
return
}
_, err = s.Insert(list)
if err != nil {
return
}
list.Position = calculateDefaultPosition(list.ID, list.Position)
_, err = s.Where("id = ?", list.ID).Update(list)
if err != nil {
return
}
if list.IsFavorite {
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
return err
}
}
// Create a new first bucket for this list
b := &Bucket{
ListID: list.ID,
Title: "Backlog",
}
err = b.Create(s, auth)
if err != nil {
return
}
return events.Dispatch(&ListCreatedEvent{
List: list,
Doer: doer,
})
}
func UpdateList(s *xorm.Session, list *List, auth web.Auth, updateListBackground bool) (err error) {
err = checkListBeforeUpdateOrDelete(s, list)
if err != nil {
return
}
// We need to specify the cols we want to update here to be able to un-archive lists
colsToUpdate := []string{
"title",
"is_archived",
"identifier",
"hex_color",
"namespace_id",
"position",
}
if list.Description != "" {
colsToUpdate = append(colsToUpdate, "description")
}
if updateListBackground {
colsToUpdate = append(colsToUpdate, "background_file_id", "background_blur_hash")
}
wasFavorite, err := isFavorite(s, list.ID, auth, FavoriteKindList)
if err != nil {
return err
}
if list.IsFavorite && !wasFavorite {
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
return err
}
}
if !list.IsFavorite && wasFavorite {
if err := removeFromFavorite(s, list.ID, auth, FavoriteKindList); err != nil {
return err
}
}
_, err = s.
ID(list.ID).
Cols(colsToUpdate...).
Update(list)
if err != nil {
return err
}
err = events.Dispatch(&ListUpdatedEvent{
List: list,
Doer: auth,
})
if err != nil {
return err
}
l, err := GetListSimpleByID(s, list.ID)
if err != nil {
return err
@ -668,15 +719,7 @@ func (l *List) Update(s *xorm.Session, a web.Auth) (err error) {
return nil
}
err = CreateOrUpdateList(s, l, a)
if err != nil {
return err
}
return events.Dispatch(&ListUpdatedEvent{
List: l,
Doer: a,
})
return UpdateList(s, l, a, false)
}
func updateListLastUpdated(s *xorm.Session, list *List) error {
@ -709,39 +752,12 @@ func updateListByTaskID(s *xorm.Session, taskID int64) (err error) {
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/lists [put]
func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
err = l.CheckIsArchived(s)
if err != nil {
return err
}
doer, err := user.GetFromAuth(a)
if err != nil {
return err
}
l.OwnerID = doer.ID
l.Owner = doer
l.ID = 0 // Otherwise only the first time a new list would be created
err = CreateOrUpdateList(s, l, a)
err = CreateList(s, l, a)
if err != nil {
return
}
// Create a new first bucket for this list
b := &Bucket{
ListID: l.ID,
Title: "Backlog",
}
err = b.Create(s, a)
if err != nil {
return
}
return events.Dispatch(&ListCreatedEvent{
List: l,
Doer: doer,
})
return l.ReadOne(s, a)
}
// Delete implements the delete method of CRUDable
@ -785,14 +801,15 @@ func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
}
// SetListBackground sets a background file as list background in the db
func SetListBackground(s *xorm.Session, listID int64, background *files.File) (err error) {
func SetListBackground(s *xorm.Session, listID int64, background *files.File, blurHash string) (err error) {
l := &List{
ID: listID,
BackgroundFileID: background.ID,
ID: listID,
BackgroundFileID: background.ID,
BackgroundBlurHash: blurHash,
}
_, err = s.
Where("id = ?", l.ID).
Cols("background_file_id").
Cols("background_file_id", "background_blur_hash").
Update(l)
return
}

View File

@ -75,7 +75,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
ld.List.Identifier = "" // Reset the identifier to trigger regenerating a new one
// Set the owner to the current user
ld.List.OwnerID = doer.GetID()
if err := CreateOrUpdateList(s, ld.List, doer); err != nil {
if err := CreateList(s, ld.List, doer); err != nil {
// If there is no available unique list identifier, just reset it.
if IsErrListIdentifierIsNotUnique(err) {
ld.List.Identifier = ""
@ -144,7 +144,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
}
}
if err := SetListBackground(s, ld.List.ID, file); err != nil {
if err := SetListBackground(s, ld.List.ID, file, ld.List.BackgroundBlurHash); err != nil {
return err
}
@ -216,7 +216,7 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
// It is used to map old task items to new ones.
taskMap := make(map[int64]int64)
// Create + update all tasks (includes reminders)
oldTaskIDs := make([]int64, len(tasks))
oldTaskIDs := make([]int64, 0, len(tasks))
for _, t := range tasks {
oldID := t.ID
t.ID = 0

View File

@ -116,6 +116,25 @@ func (l *List) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error
return false, nil
}
// Get the list
ol, err := GetListSimpleByID(s, l.ID)
if err != nil {
return false, err
}
// Check if we're moving the list into a different namespace.
// 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 err != nil {
return false, err
}
if !can {
return false, ErrGenericForbidden{}
}
}
fid := getSavedFilterIDFromListID(l.ID)
if fid > 0 {
sf, err := getSavedFilterSimpleByID(s, fid)

View File

@ -163,6 +163,65 @@ func TestList_CreateOrUpdate(t *testing.T) {
assert.True(t, IsErrListIdentifierIsNotUnique(err))
_ = s.Close()
})
t.Run("change namespace", func(t *testing.T) {
t.Run("own", func(t *testing.T) {
usr := &user.User{
ID: 6,
Username: "user6",
Email: "user6@example.com",
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
list := List{
ID: 6,
Title: "Test6",
Description: "Lorem Ipsum",
NamespaceID: 7, // from 6
}
can, err := list.CanUpdate(s, usr)
assert.NoError(t, err)
assert.True(t, can)
err = list.Update(s, usr)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
db.AssertExists(t, "lists", map[string]interface{}{
"id": list.ID,
"title": list.Title,
"description": list.Description,
"namespace_id": list.NamespaceID,
}, 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()
// list := List{
// ID: 1,
// Title: "Test1",
// Description: "Lorem Ipsum",
// NamespaceID: 16, // from 1
// }
// can, err := list.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()
list := List{
ID: 1,
Title: "Test1",
Description: "Lorem Ipsum",
NamespaceID: 2, // from 1
}
can, _ := list.CanUpdate(s, usr)
assert.False(t, can) // namespace is not writeable by us
_ = s.Close()
})
})
})
}

View File

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

View File

@ -247,6 +247,11 @@ func (s *SendTaskAssignedNotification) Handle(msg *message.Message) (err error)
log.Debugf("Sending task assigned notifications to %d subscribers for task %d", len(subscribers), event.Task.ID)
task, err := GetTaskByIDSimple(sess, event.Task.ID)
if err != nil {
return err
}
for _, subscriber := range subscribers {
if subscriber.UserID == event.Doer.ID {
continue
@ -254,7 +259,7 @@ func (s *SendTaskAssignedNotification) Handle(msg *message.Message) (err error)
n := &TaskAssignedNotification{
Doer: event.Doer,
Task: event.Task,
Task: &task,
Assignee: event.Assignee,
}
err = notifications.Notify(subscriber.User, n)

View File

@ -603,20 +603,17 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
// @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 name
// Check if we have at least a title
if n.Title == "" {
return ErrNamespaceNameCannotBeEmpty{NamespaceID: 0, UserID: a.GetID()}
}
n.ID = 0 // This would otherwise prevent the creation of new lists after one was created
// Check if the User exists
n.Owner, err = user.GetUserByID(s, a.GetID())
if err != nil {
return
}
n.OwnerID = n.Owner.ID
// Insert
if _, err = s.Insert(n); err != nil {
return err
}

View File

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

View File

@ -112,7 +112,7 @@ type TaskAssignedNotification struct {
func (n *TaskAssignedNotification) ToMail() *notifications.Mail {
return notifications.NewMail().
Subject(n.Task.Title+"("+n.Task.GetFullIdentifier()+")"+" has been assigned to "+n.Assignee.GetName()).
Line(n.Doer.GetName()+" has assigned this task to "+n.Assignee.GetName()).
Line(n.Doer.GetName()+" has assigned this task to "+n.Assignee.GetName()+".").
Action("View Task", n.Task.GetFrontendURL())
}

View File

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

View File

@ -25,6 +25,7 @@ import (
"code.vikunja.io/api/pkg/config"
"github.com/iancoleman/strcase"
"github.com/vectordotdev/go-datemath"
"xorm.io/xorm/schemas"
)
@ -159,8 +160,14 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
value, err = strconv.ParseBool(rawValue)
case reflect.Struct:
if field.Type == schemas.TimeType {
value, err = time.Parse(time.RFC3339, rawValue)
value = value.(time.Time).In(config.GetTimeZone())
var t datemath.Expression
t, err = datemath.Parse(rawValue)
if err == nil {
value = t.Time(datemath.WithLocation(config.GetTimeZone()))
} else {
value, err = time.Parse(time.RFC3339, rawValue)
value = value.(time.Time).In(config.GetTimeZone())
}
}
case reflect.Slice:
// If this is a slice of pointers we're dealing with some property which is a relation

View File

@ -37,6 +37,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -47,6 +48,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -57,6 +59,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -1046,6 +1049,9 @@ func TestTaskCollection_ReadAll(t *testing.T) {
a: &user.User{ID: 1},
},
want: []*Task{
// The only tasks with a position set
task1,
task2,
// the other ones don't have a position set
task3,
task4,
@ -1076,9 +1082,69 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task31,
task32,
task33,
// The only tasks with a position set
task1,
},
},
{
name: "order by due date",
fields: fields{
SortBy: []string{"due_date", "id"},
OrderBy: []string{"asc", "desc"},
},
args: args{
a: &user.User{ID: 1},
},
want: []*Task{
// The only tasks with a due date
task6,
task5,
// The other ones don't have a due date
task33,
task32,
task31,
task30,
task29,
task28,
task27,
task26,
task25,
task24,
task23,
task22,
task21,
task20,
task19,
task18,
task17,
task16,
task15,
task12,
task11,
task10,
task9,
task8,
task7,
task4,
task3,
task2,
task1,
},
},
{
name: "saved filter with sort order",
fields: fields{
ListID: -2,
SortBy: []string{"title", "id"},
OrderBy: []string{"desc", "asc"},
},
args: args{
a: &user.User{ID: 1},
},
want: []*Task{
task9,
task8,
task7,
task6,
task5,
},
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,11 +61,11 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
// Get all creators of tasks
creators := make(map[int64]*user.User, len(taskIDs))
err = s.
Select("users.id, users.username, users.email, users.name").
Select("users.*").
Join("LEFT", "tasks", "tasks.created_by_id = users.id").
In("tasks.id", taskIDs).
Where(cond).
GroupBy("tasks.id, users.id, users.username, users.email, users.name").
GroupBy("tasks.id, users.id, users.username, users.email, users.name, users.timezone").
Find(&creators)
if err != nil {
return
@ -77,14 +77,14 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
return
}
for _, taskID := range taskIDs {
u, exists := creators[taskMap[taskID].CreatedByID]
for _, task := range taskMap {
u, exists := creators[task.CreatedByID]
if !exists {
continue
}
taskUsers = append(taskUsers, &taskUser{
Task: taskMap[taskID],
Task: taskMap[task.ID],
User: u,
})
}
@ -110,8 +110,9 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
return
}
func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskIDs []int64, err error) {
func getTasksWithRemindersDueAndTheirUsers(s *xorm.Session, now time.Time) (reminderNotifications []*ReminderDueNotification, err error) {
now = utils.GetTimeWithoutNanoSeconds(now)
reminderNotifications = []*ReminderDueNotification{}
nextMinute := now.Add(1 * time.Minute)
@ -120,7 +121,8 @@ func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskI
reminders := []*TaskReminder{}
err = s.
Join("INNER", "tasks", "tasks.id = task_reminders.task_id").
Where("reminder >= ? and reminder < ?", now.Format(dbTimeFormat), nextMinute.Format(dbTimeFormat)).
// All reminders from -12h to +14h to include all time zones
Where("reminder >= ? and reminder < ?", now.Add(time.Hour*-12).Format(dbTimeFormat), nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
And("tasks.done = false").
Find(&reminders)
if err != nil {
@ -133,11 +135,56 @@ func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskI
return
}
// We're sending a reminder to everyone who is assigned to the task or has created it.
var taskIDs []int64
for _, r := range reminders {
taskIDs = append(taskIDs, r.TaskID)
}
if len(taskIDs) == 0 {
return
}
usersWithReminders, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.email_reminders_enabled": true})
if err != nil {
return
}
usersPerTask := make(map[int64][]*taskUser, len(usersWithReminders))
for _, ur := range usersWithReminders {
usersPerTask[ur.Task.ID] = append(usersPerTask[ur.Task.ID], ur)
}
// Time zone cache per time zone string to avoid parsing the same time zone over and over again
tzs := make(map[string]*time.Location)
// Figure out which reminders are actually due in the time zone of the users
for _, r := range reminders {
for _, u := range usersPerTask[r.TaskID] {
if u.User.Timezone == "" {
u.User.Timezone = config.GetTimeZone().String()
}
// I think this will break once there's more reminders than what we can handle in one minute
tz, exists := tzs[u.User.Timezone]
if !exists {
tz, err = time.LoadLocation(u.User.Timezone)
if err != nil {
return
}
tzs[u.User.Timezone] = tz
}
actualReminder := r.Reminder.In(tz)
if (actualReminder.After(now) && actualReminder.Before(now.Add(time.Minute))) || actualReminder.Equal(now) {
reminderNotifications = append(reminderNotifications, &ReminderDueNotification{
User: u.User,
Task: u.Task,
})
}
}
}
return
}
@ -162,37 +209,26 @@ func RegisterReminderCron() {
defer s.Close()
now := time.Now()
taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now)
reminders, err := getTasksWithRemindersDueAndTheirUsers(s, now)
if err != nil {
log.Errorf("[Task Reminder Cron] Could not get tasks with reminders in the next minute: %s", err)
return
}
if len(taskIDs) == 0 {
if len(reminders) == 0 {
return
}
users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.email_reminders_enabled": true})
if err != nil {
log.Errorf("[Task Reminder Cron] Could not get task users to send them reminders: %s", err)
return
}
log.Debugf("[Task Reminder Cron] Sending %d reminders", len(reminders))
log.Debugf("[Task Reminder Cron] Sending reminders to %d users", len(users))
for _, u := range users {
n := &ReminderDueNotification{
User: u.User,
Task: u.Task,
}
err = notifications.Notify(u.User, n)
for _, n := range reminders {
err = notifications.Notify(n.User, n)
if err != nil {
log.Errorf("[Task Reminder Cron] Could not notify user %d: %s", u.User.ID, err)
log.Errorf("[Task Reminder Cron] Could not notify user %d: %s", n.User.ID, err)
return
}
log.Debugf("[Task Reminder Cron] Sent reminder email for task %d to user %d", u.Task.ID, u.User.ID)
log.Debugf("[Task Reminder Cron] Sent reminder email for task %d to user %d", n.Task.ID, n.User.ID)
}
})
if err != nil {

View File

@ -32,10 +32,10 @@ func TestReminderGetTasksInTheNextMinute(t *testing.T) {
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T01:13:00Z")
assert.NoError(t, err)
taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now)
notifications, err := getTasksWithRemindersDueAndTheirUsers(s, now)
assert.NoError(t, err)
assert.Len(t, taskIDs, 1)
assert.Equal(t, int64(27), taskIDs[0])
assert.Len(t, notifications, 1)
assert.Equal(t, int64(27), notifications[0].Task.ID)
})
t.Run("Found No Tasks", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
@ -44,7 +44,7 @@ func TestReminderGetTasksInTheNextMinute(t *testing.T) {
now, err := time.Parse(time.RFC3339Nano, "2018-12-02T01:13:00Z")
assert.NoError(t, err)
taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now)
taskIDs, err := getTasksWithRemindersDueAndTheirUsers(s, now)
assert.NoError(t, err)
assert.Len(t, taskIDs, 0)
})

View File

@ -29,9 +29,11 @@ import (
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web"
"github.com/google/uuid"
"github.com/imdario/mergo"
"github.com/jinzhu/copier"
"xorm.io/builder"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
@ -296,17 +298,20 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
if err := param.validate(); err != nil {
return nil, 0, 0, err
}
// Mysql sorts columns with null values before ones without null value.
// Because it does not have support for NULLS FIRST or NULLS LAST we work around this by
// first sorting for null (or not null) values and then the order we actually want to.
if db.Type() == schemas.MYSQL {
orderby += param.sortBy + " IS NULL, "
}
orderby += param.sortBy + " " + param.orderBy.String()
// Postgres sorts by default entries with null values after ones with values.
// Postgres and sqlite allow us to control how columns with null values are sorted.
// To make that consistent with the sort order we have and other dbms, we're adding a separate clause here.
if db.Type() == schemas.POSTGRES {
if param.orderBy == orderAscending {
orderby += " NULLS FIRST"
}
if param.orderBy == orderDescending {
orderby += " NULLS LAST"
}
if db.Type() == schemas.POSTGRES || db.Type() == schemas.SQLITE {
orderby += " NULLS LAST"
}
if (i + 1) < len(opts.sortby) {
@ -402,7 +407,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
return nil, 0, 0, err
}
userListIDs := make([]int64, len(userLists))
userListIDs := make([]int64, 0, len(userLists))
for _, l := range userLists {
userListIDs = append(userListIDs, l.ID)
}
@ -673,7 +678,17 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]
continue
}
fullRelatedTasks[rt.OtherTaskID].IsFavorite = taskFavorites[rt.OtherTaskID]
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], fullRelatedTasks[rt.OtherTaskID])
// We're duplicating the other task to avoid cycles as these can't be represented properly in json
// and would thus fail with an error.
otherTask := &Task{}
err = copier.Copy(otherTask, fullRelatedTasks[rt.OtherTaskID])
if err != nil {
log.Errorf("Could not duplicate task object: %v", err)
continue
}
otherTask.RelatedTasks = nil
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], otherTask)
}
return
@ -883,7 +898,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
// Generate a uuid if we don't already have one
if t.UID == "" {
t.UID = utils.MakeRandomString(40)
t.UID = uuid.NewString()
}
// Get the default bucket and move the task there

View File

@ -54,9 +54,13 @@ func deleteUsers() {
return
}
log.Debugf("Found %d users scheduled for deletion", len(users))
now := time.Now()
for _, u := range users {
if u.DeletionScheduledAt.Before(time.Now()) {
log.Debugf("User %d is not yet scheduled for deletion.", u.ID)
if !u.DeletionScheduledAt.Before(now) {
log.Debugf("User %d is not yet scheduled for deletion. Scheduled at %s, now is %s", u.ID, u.DeletionScheduledAt, now)
continue
}
@ -73,6 +77,8 @@ func deleteUsers() {
return
}
log.Debugf("Deleted user %d", u.ID)
err = s.Commit()
if err != nil {
log.Errorf("Could not commit transaction: %s", err)
@ -81,17 +87,18 @@ func deleteUsers() {
}
}
// DeleteUser completely removes a user and all their associated lists, namespaces and tasks.
// This action is irrevocable.
// Public to allow deletion from the CLI.
func DeleteUser(s *xorm.Session, u *user.User) (err error) {
namespacesToDelete := []*Namespace{}
// Get all namespaces and lists this u has access to
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 err
return nil, err
}
if res == nil {
return nil, nil
}
namespaces := res.([]*NamespaceWithLists)
for _, n := range namespaces {
if n.ID < 0 {
@ -100,14 +107,14 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) {
hadUsers, err := ensureNamespaceAdminUser(s, &n.Namespace)
if err != nil {
return err
return nil, err
}
if hadUsers {
continue
}
hadTeams, err := ensureNamespaceAdminTeam(s, &n.Namespace)
if err != nil {
return err
return nil, err
}
if hadTeams {
continue
@ -116,13 +123,21 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) {
namespacesToDelete = append(namespacesToDelete, &n.Namespace)
}
// Get all lists to delete
listsToDelete := []*List{}
return
}
func getListsToDelete(s *xorm.Session, u *user.User) (listsToDelete []*List, err error) {
listsToDelete = []*List{}
lm := &List{IsArchived: true}
res, _, _, err = lm.ReadAll(s, u, "", 0, -1)
res, _, _, err := lm.ReadAll(s, u, "", 0, -1)
if err != nil {
return err
return nil, err
}
if res == nil {
return nil, nil
}
lists := res.([]*List)
for _, l := range lists {
if l.ID < 0 {
@ -131,15 +146,16 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) {
hadUsers, err := ensureListAdminUser(s, l)
if err != nil {
return err
return nil, err
}
if hadUsers {
continue
}
hadTeams, err := ensureListAdminTeam(s, l)
if err != nil {
return err
return nil, err
}
if hadTeams {
continue
}
@ -147,6 +163,23 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) {
listsToDelete = append(listsToDelete, l)
}
return
}
// DeleteUser completely removes a user and all their associated lists, namespaces 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
}
listsToDelete, err := getListsToDelete(s, u)
if err != nil {
return err
}
// Delete everything not shared with anybody else
for _, n := range namespacesToDelete {
err = deleteNamespace(s, n, u, false)
@ -162,12 +195,14 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) {
}
}
_, err = s.Where("id = ?", u.ID).Delete(u)
_, err = s.Where("id = ?", u.ID).Delete(&user.User{})
if err != nil {
return err
}
return notifications.Notify(u, &user.AccountDeletedNotification{})
return notifications.Notify(u, &user.AccountDeletedNotification{
User: u,
})
}
func ensureNamespaceAdminUser(s *xorm.Session, n *Namespace) (hadUsers bool, err error) {

View File

@ -27,21 +27,35 @@ import (
)
func TestDeleteUser(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
notifications.Fake()
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
notifications.Fake()
u := &user.User{ID: 6}
err := DeleteUser(s, u)
u := &user.User{ID: 6}
err := DeleteUser(s, u)
assert.NoError(t, err)
db.AssertMissing(t, "users", map[string]interface{}{"id": u.ID})
db.AssertMissing(t, "lists", map[string]interface{}{"id": 24}) // only user6 had access to this list
db.AssertExists(t, "lists", map[string]interface{}{"id": 6}, false)
db.AssertExists(t, "lists", map[string]interface{}{"id": 7}, false)
db.AssertExists(t, "lists", map[string]interface{}{"id": 8}, false)
db.AssertExists(t, "lists", map[string]interface{}{"id": 9}, false)
db.AssertExists(t, "lists", map[string]interface{}{"id": 10}, false)
db.AssertExists(t, "lists", map[string]interface{}{"id": 11}, false)
assert.NoError(t, err)
db.AssertMissing(t, "users", map[string]interface{}{"id": u.ID})
db.AssertMissing(t, "lists", map[string]interface{}{"id": 24}) // only user6 had access to this list
db.AssertExists(t, "lists", map[string]interface{}{"id": 6}, false)
db.AssertExists(t, "lists", map[string]interface{}{"id": 7}, false)
db.AssertExists(t, "lists", map[string]interface{}{"id": 8}, false)
db.AssertExists(t, "lists", map[string]interface{}{"id": 9}, false)
db.AssertExists(t, "lists", map[string]interface{}{"id": 10}, false)
db.AssertExists(t, "lists", map[string]interface{}{"id": 11}, false)
})
t.Run("user with no namespaces", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
notifications.Fake()
u := &user.User{ID: 4}
err := DeleteUser(s, u)
assert.NoError(t, err)
// No assertions for deleted lists and namespaces since that user doesn't have any
})
}

View File

@ -91,7 +91,7 @@ func ListUsersFromList(s *xorm.Session, l *List, search string) (users []*user.U
uidmap[u.TeamNamespaceUserID] = true
}
uids := make([]int64, len(uidmap))
uids := make([]int64, 0, len(uidmap))
for id := range uidmap {
uids = append(uids, id)
}

View File

@ -32,6 +32,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -42,6 +43,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -52,6 +54,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -63,6 +66,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -74,6 +78,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -84,6 +89,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -95,6 +101,7 @@ func TestListUsersFromList(t *testing.T) {
EmailRemindersEnabled: true,
DiscoverableByEmail: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -105,6 +112,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -115,6 +123,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -125,6 +134,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -136,6 +146,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -148,6 +159,7 @@ func TestListUsersFromList(t *testing.T) {
EmailRemindersEnabled: true,
DiscoverableByName: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -158,6 +170,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}

View File

@ -24,6 +24,7 @@ import (
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
)
@ -41,8 +42,8 @@ type Token struct {
}
// NewUserAuthTokenResponse creates a new user auth token response from a user object.
func NewUserAuthTokenResponse(u *user.User, c echo.Context) error {
t, err := NewUserJWTAuthtoken(u)
func NewUserAuthTokenResponse(u *user.User, c echo.Context, long bool) error {
t, err := NewUserJWTAuthtoken(u, long)
if err != nil {
return err
}
@ -51,18 +52,26 @@ func NewUserAuthTokenResponse(u *user.User, c echo.Context) error {
}
// NewUserJWTAuthtoken generates and signes a new jwt token for a user. This is a global function to be able to call it from integration tests.
func NewUserJWTAuthtoken(user *user.User) (token string, err error) {
func NewUserJWTAuthtoken(u *user.User, long bool) (token string, err error) {
t := jwt.New(jwt.SigningMethodHS256)
var ttl = time.Duration(config.ServiceJWTTTL.GetInt64())
if long {
ttl = time.Duration(config.ServiceJWTTTLLong.GetInt64())
}
var exp = time.Now().Add(time.Second * ttl).Unix()
// Set claims
claims := t.Claims.(jwt.MapClaims)
claims["type"] = AuthTypeUser
claims["id"] = user.ID
claims["username"] = user.Username
claims["email"] = user.Email
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
claims["name"] = user.Name
claims["emailRemindersEnabled"] = user.EmailRemindersEnabled
claims["id"] = u.ID
claims["username"] = u.Username
claims["email"] = u.Email
claims["exp"] = exp
claims["name"] = u.Name
claims["emailRemindersEnabled"] = u.EmailRemindersEnabled
claims["isLocalUser"] = u.Issuer == user.IssuerLocal
claims["long"] = long
// Generate encoded token and send it as response.
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))
@ -72,6 +81,9 @@ func NewUserJWTAuthtoken(user *user.User) (token string, err error) {
func NewLinkShareJWTAuthtoken(share *models.LinkSharing) (token string, err error) {
t := jwt.New(jwt.SigningMethodHS256)
var ttl = time.Duration(config.ServiceJWTTTL.GetInt64())
var exp = time.Now().Add(time.Second * ttl).Unix()
// Set claims
claims := t.Claims.(jwt.MapClaims)
claims["type"] = AuthTypeLinkShare
@ -80,7 +92,8 @@ func NewLinkShareJWTAuthtoken(share *models.LinkSharing) (token string, err erro
claims["list_id"] = share.ListID
claims["right"] = share.Right
claims["sharedByID"] = share.SharedByID
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
claims["exp"] = exp
claims["isLocalUser"] = true // Link shares are always local
// Generate encoded token and send it as response.
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))

View File

@ -19,6 +19,7 @@ package openid
import (
"context"
"encoding/json"
"errors"
"math/rand"
"net/http"
"time"
@ -104,12 +105,13 @@ func HandleCallback(c echo.Context) error {
// Parse the access & ID token
oauth2Token, err := provider.Oauth2Config.Exchange(context.Background(), cb.Code)
if err != nil {
if rerr, is := err.(*oauth2.RetrieveError); is {
var rerr *oauth2.RetrieveError
if errors.As(err, &rerr) {
log.Error(err)
details := make(map[string]interface{})
if err := json.Unmarshal(rerr.Body, &details); err != nil {
log.Errorf("Error unmarshaling token for provider %s: %v", provider.Name, err)
log.Errorf("Error unmarshalling token for provider %s: %v", provider.Name, err)
return handler.HandleHTTPError(err, c)
}
@ -198,7 +200,7 @@ func HandleCallback(c echo.Context) error {
}
// Create token
return auth.NewUserAuthTokenResponse(u, c)
return auth.NewUserAuthTokenResponse(u, c, false)
}
func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *user.User, err error) {
@ -216,6 +218,7 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us
uu := &user.User{
Username: cl.PreferredUsername,
Email: cl.Email,
Name: cl.Name,
Status: user.StatusActive,
Issuer: issuer,
Subject: subject,

View File

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

View File

@ -21,6 +21,7 @@ import (
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/config"
@ -64,7 +65,7 @@ func (g *Provider) GetAvatar(user *user.User, size int64) ([]byte, string, error
}
if !exists || needsRefetch {
log.Debugf("Gravatar for user %d with size %d not cached, requesting from gravatar...", user.ID, size)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://www.gravatar.com/avatar/"+utils.Md5String(user.Email)+"?s="+sizeString+"&d=mp", nil)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://www.gravatar.com/avatar/"+utils.Md5String(strings.ToLower(user.Email))+"?s="+sizeString+"&d=mp", nil)
if err != nil {
return nil, "", err
}

View File

@ -0,0 +1,122 @@
// 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 marble
import (
"math"
"strconv"
"code.vikunja.io/api/pkg/user"
)
// Provider generates a random avatar based on https://github.com/boringdesigners/boring-avatars
type Provider struct {
}
const avatarSize = 80
var colors = []string{
"#A3A948",
"#EDB92E",
"#F85931",
"#CE1836",
"#009989",
}
type props struct {
Color string
TranslateX int
TranslateY int
Rotate int
Scale float64
}
func getUnit(number int, rang, index int) int {
value := number % rang
digit := math.Floor(math.Mod(float64(number)/math.Pow(10, float64(index)), 10))
if index > 0 && (math.Mod(digit, 2) == 0) {
return -value
}
return value
}
func getPropsForUser(u *user.User) []*props {
ps := []*props{}
for i := 0; i < 3; i++ {
f := float64(getUnit(int(u.ID)*(i+1), avatarSize/10, 0))
ps = append(ps, &props{
Color: colors[(int(u.ID)+i)%(len(colors)-1)],
TranslateX: getUnit(int(u.ID)*(i+1), avatarSize/10, 1),
TranslateY: getUnit(int(u.ID)*(i+1), avatarSize/10, 2),
Scale: 1.2 + f/10,
Rotate: getUnit(int(u.ID)*(i+1), 360, 1),
})
}
return ps
}
func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType string, err error) {
s := strconv.FormatInt(size, 10)
avatarSizeStr := strconv.Itoa(avatarSize)
avatarSizeHalf := strconv.Itoa(avatarSize / 2)
ps := getPropsForUser(u)
return []byte(`<svg
viewBox="0 0 ` + avatarSizeStr + ` ` + avatarSizeStr + `"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width="` + s + `"
height="` + s + `"
>
<mask id="mask__marble" maskUnits="userSpaceOnUse" x="0" y="0" width="` + avatarSizeStr + `" height="` + avatarSizeStr + `">
<rect width="` + avatarSizeStr + `" height="` + avatarSizeStr + `" rx="` + strconv.Itoa(avatarSize*2) + `" fill="white" />
</mask>
<g mask="url(#mask__marble)">
<rect width="` + avatarSizeStr + `" height="` + avatarSizeStr + `" rx="2" fill="` + ps[0].Color + `" />
<path
filter="url(#prefix__filter0_f)"
d="M32.414 59.35L50.376 70.5H72.5v-71H33.728L26.5 13.381l19.057 27.08L32.414 59.35z"
fill="` + ps[1].Color + `"
transform="translate(` + strconv.Itoa(ps[1].TranslateX) + ` ` + strconv.Itoa(ps[1].TranslateY) + `) rotate(` + strconv.Itoa(ps[1].Rotate) + ` ` + avatarSizeHalf + ` ` + avatarSizeHalf + `) scale(` + strconv.FormatFloat(ps[2].Scale, 'f', 2, 64) + `)"
/>
<path
filter="url(#prefix__filter0_f)"
style="mix-blend-mode: overlay;"
d="M22.216 24L0 46.75l14.108 38.129L78 86l-3.081-59.276-22.378 4.005 12.972 20.186-23.35 27.395L22.215 24z"
fill="` + ps[2].Color + `"
transform="translate(` + strconv.Itoa(ps[2].TranslateX) + ` ` + strconv.Itoa(ps[2].TranslateY) + `) rotate(` + strconv.Itoa(ps[2].Rotate) + ` ` + avatarSizeHalf + ` ` + avatarSizeHalf + `) scale(` + strconv.FormatFloat(ps[2].Scale, 'f', 2, 64) + `)"
/>
</g>
<defs>
<filter
id="prefix__filter0_f"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="7" result="effect1_foregroundBlur" />
</filter>
</defs>
</svg>`), "image/svg+xml", nil
}

View File

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

View File

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

View File

@ -61,6 +61,7 @@ type Photo struct {
Height int `json:"height"`
Color string `json:"color"`
Description string `json:"description"`
BlurHash string `json:"blur_hash"`
User struct {
Username string `json:"username"`
Name string `json:"name"`
@ -163,14 +164,14 @@ func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []
}
if existsForPage {
log.Debugf("Serving initial unsplash collection for page %d from cache, last updated at %v", page, emptySearchResult.lastCached)
log.Debugf("Serving initial wallpaper topic from unsplash for page %d from cache, last updated at %v", page, emptySearchResult.lastCached)
return emptySearchResult.images[page], nil
}
log.Debugf("Retrieving initial unsplash collection for page %d from unsplash api", page)
log.Debugf("Retrieving initial wallpaper topic from unsplash for page %d from unsplash api", page)
collectionResult := []*Photo{}
err = doGet("collections/317099/photos?page="+strconv.FormatInt(page, 10)+"&per_page=25&order_by=latest", &collectionResult)
err = doGet("topics/wallpapers/photos?page="+strconv.FormatInt(page, 10)+"&per_page=25&order_by=latest", &collectionResult)
if err != nil {
return
}
@ -178,8 +179,9 @@ func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []
result = []*background.Image{}
for _, p := range collectionResult {
result = append(result, &background.Image{
ID: p.ID,
URL: getImageID(p.Urls.Raw),
ID: p.ID,
URL: getImageID(p.Urls.Raw),
BlurHash: p.BlurHash,
Info: &models.UnsplashPhoto{
UnsplashID: p.ID,
Author: p.User.Username,
@ -213,8 +215,9 @@ func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []
result = []*background.Image{}
for _, p := range searchResult.Results {
result = append(result, &background.Image{
ID: p.ID,
URL: getImageID(p.Urls.Raw),
ID: p.ID,
URL: getImageID(p.Urls.Raw),
BlurHash: p.BlurHash,
Info: &models.UnsplashPhoto{
UnsplashID: p.ID,
Author: p.User.Username,
@ -315,7 +318,7 @@ func (p *Provider) Set(s *xorm.Session, image *background.Image, list *models.Li
list.BackgroundInformation = unsplashPhoto
// Set it as the list background
return models.SetListBackground(s, list.ID, file)
return models.SetListBackground(s, list.ID, file, photo.BlurHash)
}
// Pingback pings the unsplash api if an unsplash photo has been accessed.

View File

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

View File

@ -21,6 +21,7 @@ import (
"fmt"
"io"
"os"
"strings"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files"
@ -34,7 +35,7 @@ import (
func Dump(filename string) error {
dumpFile, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error opening dump file: %s", err)
return fmt.Errorf("error opening dump file: %w", err)
}
defer dumpFile.Close()
@ -43,17 +44,36 @@ func Dump(filename string) error {
// Config
log.Info("Start dumping config file...")
err = writeFileToZip(viper.ConfigFileUsed(), dumpWriter)
if err != nil {
return fmt.Errorf("error saving config file: %s", err)
if viper.ConfigFileUsed() != "" {
err = writeFileToZip(viper.ConfigFileUsed(), dumpWriter)
if err != nil {
return fmt.Errorf("error saving config file: %w", err)
}
} else {
log.Warning("No config file found, not including one in the dump. This usually happens when environment variables are used for configuration.")
}
log.Info("Dumped config file")
env := os.Environ()
dotEnv := ""
for _, e := range env {
if strings.Contains(e, "VIKUNJA_") {
dotEnv += e + "\n"
}
}
if dotEnv != "" {
err = utils.WriteBytesToZip(".env", []byte(dotEnv), dumpWriter)
if err != nil {
return fmt.Errorf("error saving env file: %w", err)
}
log.Info("Dumped .env file")
}
// Version
log.Info("Start dumping version file...")
err = utils.WriteBytesToZip("VERSION", []byte(version.Version), dumpWriter)
if err != nil {
return fmt.Errorf("error saving version: %s", err)
return fmt.Errorf("error saving version: %w", err)
}
log.Info("Dumped version")
@ -61,12 +81,12 @@ func Dump(filename string) error {
log.Info("Start dumping database...")
data, err := db.Dump()
if err != nil {
return fmt.Errorf("error saving database data: %s", err)
return fmt.Errorf("error saving database data: %w", err)
}
for t, d := range data {
err = utils.WriteBytesToZip("database/"+t+".json", d, dumpWriter)
if err != nil {
return fmt.Errorf("error writing database table %s: %s", t, err)
return fmt.Errorf("error writing database table %s: %w", t, err)
}
}
log.Info("Dumped database")
@ -75,7 +95,7 @@ func Dump(filename string) error {
log.Info("Start dumping files...")
allFiles, err := files.Dump()
if err != nil {
return fmt.Errorf("error saving file: %s", err)
return fmt.Errorf("error saving file: %w", err)
}
err = utils.WriteFilesToZip(allFiles, dumpWriter)

View File

@ -44,7 +44,7 @@ func Restore(filename string) error {
r, err := zip.OpenReader(filename)
if err != nil {
return fmt.Errorf("could not open zip file: %s", err)
return fmt.Errorf("could not open zip file: %w", err)
}
log.Warning("Restoring a dump will wipe your current installation!")
@ -52,7 +52,7 @@ func Restore(filename string) error {
cr := bufio.NewReader(os.Stdin)
text, err := cr.ReadString('\n')
if err != nil {
return fmt.Errorf("could not read confirmation message: %s", err)
return fmt.Errorf("could not read confirmation message: %w", err)
}
if text != "Yes, I understand\n" {
return fmt.Errorf("invalid confirmation message")
@ -60,6 +60,7 @@ func Restore(filename string) error {
// Find the configFile, database and files files
var configFile *zip.File
var dotEnvFile *zip.File
dbfiles := make(map[string]*zip.File)
filesFiles := make(map[string]*zip.File)
for _, file := range r.File {
@ -72,44 +73,21 @@ func Restore(filename string) error {
dbfiles[fname[:len(fname)-5]] = file
continue
}
if file.Name == ".env" {
dotEnvFile = file
continue
}
if strings.HasPrefix(file.Name, "files/") {
filesFiles[strings.ReplaceAll(file.Name, "files/", "")] = file
}
}
if configFile == nil {
return fmt.Errorf("dump does not contain a config file")
}
///////
// Restore the config file
if configFile.UncompressedSize64 > maxConfigSize {
return fmt.Errorf("config file too large, is %d, max size is %d", configFile.UncompressedSize64, maxConfigSize)
}
outFile, err := os.OpenFile(configFile.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, configFile.Mode())
if err != nil {
return fmt.Errorf("could not open config file for writing: %s", err)
}
cfgr, err := configFile.Open()
err = restoreConfig(configFile, dotEnvFile)
if err != nil {
return err
}
// #nosec - We eliminated the potential decompression bomb by erroring out above if the file is larger than a threshold.
_, err = io.Copy(outFile, cfgr)
if err != nil {
return fmt.Errorf("could not create config file: %s", err)
}
_ = cfgr.Close()
_ = outFile.Close()
log.Infof("The config file has been restored to '%s'.", configFile.Name)
log.Infof("You can now make changes to it, hit enter when you're done.")
if _, err := bufio.NewReader(os.Stdin).ReadString('\n'); err != nil {
return fmt.Errorf("could not read from stdin: %s", err)
}
log.Info("Restoring...")
// Init the configFile again since the restored configuration is most likely different from the one before
@ -121,7 +99,7 @@ func Restore(filename string) error {
// Restore the db
// Start by wiping everything
if err := db.WipeEverything(); err != nil {
return fmt.Errorf("could not wipe database: %s", err)
return fmt.Errorf("could not wipe database: %w", err)
}
log.Info("Wiped database.")
@ -130,18 +108,18 @@ func Restore(filename string) error {
migrations := dbfiles["migration"]
rc, err := migrations.Open()
if err != nil {
return fmt.Errorf("could not open migrations: %s", err)
return fmt.Errorf("could not open migrations: %w", err)
}
defer rc.Close()
var buf bytes.Buffer
if _, err := buf.ReadFrom(rc); err != nil {
return fmt.Errorf("could not read migrations: %s", err)
return fmt.Errorf("could not read migrations: %w", err)
}
ms := []*xormigrate.Migration{}
if err := json.Unmarshal(buf.Bytes(), &ms); err != nil {
return fmt.Errorf("could not read migrations: %s", err)
return fmt.Errorf("could not read migrations: %w", err)
}
sort.Slice(ms, func(i, j int) bool {
return ms[i].ID > ms[j].ID
@ -149,17 +127,17 @@ func Restore(filename string) error {
lastMigration := ms[len(ms)-1]
if err := migration.MigrateTo(lastMigration.ID, nil); err != nil {
return fmt.Errorf("could not create db structure: %s", err)
return fmt.Errorf("could not create db structure: %w", err)
}
// Restore all db data
for table, d := range dbfiles {
content, err := unmarshalFileToJSON(d)
if err != nil {
return fmt.Errorf("could not read table %s: %s", table, err)
return fmt.Errorf("could not read table %s: %w", table, err)
}
if err := db.Restore(table, content); err != nil {
return fmt.Errorf("could not restore table data for table %s: %s", table, err)
return fmt.Errorf("could not restore table data for table %s: %w", table, err)
}
log.Infof("Restored table %s", table)
}
@ -173,18 +151,18 @@ func Restore(filename string) error {
for i, file := range filesFiles {
id, err := strconv.ParseInt(i, 10, 64)
if err != nil {
return fmt.Errorf("could not parse file id %s: %s", i, err)
return fmt.Errorf("could not parse file id %s: %w", i, err)
}
f := &files.File{ID: id}
fc, err := file.Open()
if err != nil {
return fmt.Errorf("could not open file %s: %s", i, err)
return fmt.Errorf("could not open file %s: %w", i, err)
}
if err := f.Save(fc); err != nil {
return fmt.Errorf("could not save file: %s", err)
return fmt.Errorf("could not save file: %w", err)
}
_ = fc.Close()
@ -218,3 +196,62 @@ func unmarshalFileToJSON(file *zip.File) (contents []map[string]interface{}, err
}
return
}
func restoreConfig(configFile, dotEnvFile *zip.File) error {
if configFile != nil {
if configFile.UncompressedSize64 > maxConfigSize {
return fmt.Errorf("config file too large, is %d, max size is %d", configFile.UncompressedSize64, maxConfigSize)
}
outFile, err := os.OpenFile(configFile.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, configFile.Mode())
if err != nil {
return fmt.Errorf("could not open config file for writing: %w", err)
}
cfgr, err := configFile.Open()
if err != nil {
return err
}
// #nosec - We eliminated the potential decompression bomb by erroring out above if the file is larger than a threshold.
_, err = io.Copy(outFile, cfgr)
if err != nil {
return fmt.Errorf("could not create config file: %w", err)
}
_ = cfgr.Close()
_ = outFile.Close()
log.Infof("The config file has been restored to '%s'.", configFile.Name)
log.Infof("You can now make changes to it, hit enter when you're done.")
if _, err := bufio.NewReader(os.Stdin).ReadString('\n'); err != nil {
return fmt.Errorf("could not read from stdin: %w", err)
}
return nil
}
log.Warning("No config file found, not restoring one.")
log.Warning("You'll likely have had Vikunja configured through environment variables.")
if dotEnvFile != nil {
dotenv, err := dotEnvFile.Open()
if err != nil {
return err
}
buf := bytes.Buffer{}
_, err = buf.ReadFrom(dotenv)
if err != nil {
return err
}
log.Warningf("Please make sure the following settings are properly configured in your instance:\n%s", buf.String())
log.Warning("Make sure your current config matches the following env variables, confirm by pressing enter when done.")
log.Warning("If your config does not match, you'll have to make the changes and restart the restoring process afterwards.")
if _, err := bufio.NewReader(os.Stdin).ReadString('\n'); err != nil {
return fmt.Errorf("could not read from stdin: %w", err)
}
}
return nil
}

View File

@ -20,10 +20,11 @@ import (
"bytes"
"io/ioutil"
"code.vikunja.io/api/pkg/modules/background/handler"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
@ -51,13 +52,29 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas
labels := make(map[string]*models.Label)
archivedLists := []int64{}
archivedNamespaces := []int64{}
// Create all namespaces
for _, n := range str {
n.ID = 0
// Saving the archived status to archive the namespace again after creating it
var wasArchived bool
if n.IsArchived {
n.IsArchived = false
wasArchived = true
}
err = n.Create(s, user)
if err != nil {
return
}
if wasArchived {
archivedNamespaces = append(archivedNamespaces, n.ID)
}
log.Debugf("[creating structure] Created namespace %d", n.ID)
log.Debugf("[creating structure] Creating %d lists", len(n.Lists))
@ -70,29 +87,39 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas
originalBackgroundInformation := l.BackgroundInformation
needsDefaultBucket := false
// Saving the archived status to archive the list again after creating it
var wasArchived bool
if l.IsArchived {
wasArchived = true
l.IsArchived = false
}
l.NamespaceID = n.ID
l.ID = 0
err = l.Create(s, user)
if err != nil {
return
}
if wasArchived {
archivedLists = append(archivedLists, l.ID)
}
log.Debugf("[creating structure] Created list %d", l.ID)
backgroundFile, is := originalBackgroundInformation.(*bytes.Buffer)
bf, is := originalBackgroundInformation.(*bytes.Buffer)
if is {
backgroundFile := bytes.NewReader(bf.Bytes())
log.Debugf("[creating structure] Creating a background file for list %d", l.ID)
file, err := files.Create(backgroundFile, "", uint64(backgroundFile.Len()), user)
err = handler.SaveBackgroundFile(s, user, &l.List, backgroundFile, "", uint64(backgroundFile.Len()))
if err != nil {
return err
}
err = models.SetListBackground(s, l.ID, file)
if err != nil {
return err
}
log.Debugf("[creating structure] Created a background file as new file %d for list %d", file.ID, l.ID)
log.Debugf("[creating structure] Created a background file for list %d", l.ID)
}
// Create all buckets
@ -216,7 +243,7 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas
TaskID: t.ID,
}
err = lt.Create(s, user)
if err != nil {
if err != nil && !models.IsErrLabelIsAlreadyOnTask(err) {
return err
}
log.Debugf("[creating structure] Associated task %d with label %d", t.ID, lb.ID)
@ -251,6 +278,26 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas
}
}
if len(archivedLists) > 0 {
_, err = s.
Cols("is_archived").
In("id", archivedLists).
Update(&models.List{IsArchived: true})
if err != nil {
return err
}
}
if len(archivedNamespaces) > 0 {
_, err = s.
Cols("is_archived").
In("id", archivedNamespaces).
Update(&models.Namespace{IsArchived: true})
if err != nil {
return err
}
}
log.Debugf("[creating structure] Done inserting new task structure")
return nil

View File

@ -26,10 +26,22 @@ import (
// DownloadFile downloads a file and returns its contents
func DownloadFile(url string) (buf *bytes.Buffer, err error) {
return DownloadFileWithHeaders(url, nil)
}
// DownloadFileWithHeaders downloads a file and allows you to pass in headers
func DownloadFileWithHeaders(url string, headers http.Header) (buf *bytes.Buffer, err error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil {
return nil, err
}
for key, h := range headers {
for _, hh := range h {
req.Header.Add(key, hh)
}
}
hc := http.Client{}
resp, err := hc.Do(req)
if err != nil {
@ -38,6 +50,7 @@ func DownloadFile(url string) (buf *bytes.Buffer, err error) {
defer resp.Body.Close()
buf = &bytes.Buffer{}
_, err = buf.ReadFrom(resp.Body)
return
}

View File

@ -23,6 +23,7 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"code.vikunja.io/api/pkg/config"
@ -33,6 +34,7 @@ import (
)
const apiScopes = `tasks.read tasks.read.shared`
const apiPrefix = `https://graph.microsoft.com/v1.0/me/todo/`
type Migration struct {
Code string `json:"code"`
@ -92,6 +94,7 @@ type recurrence struct {
type tasksResponse struct {
OdataContext string `json:"@odata.context"`
Nextlink string `json:"@odata.nextLink"`
Value []*task `json:"value"`
}
@ -178,7 +181,7 @@ func getMicrosoftGraphAuthToken(code string) (accessToken string, err error) {
}
func makeAuthenticatedGetRequest(token, urlPart string, v interface{}) error {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://graph.microsoft.com/v1.0/me/todo/"+urlPart, nil)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiPrefix+urlPart, nil)
if err != nil {
return err
}
@ -224,17 +227,30 @@ func getMicrosoftTodoData(token string) (microsoftTodoData []*list, err error) {
log.Debugf("[Microsoft Todo Migration] Got %d lists", len(lists.Value))
for _, list := range lists.Value {
tasksResponse := &tasksResponse{}
err = makeAuthenticatedGetRequest(token, "lists/"+list.ID+"/tasks", tasksResponse)
if err != nil {
log.Errorf("[Microsoft Todo Migration] Could not get tasks for list %s: %s", list.ID, err)
return
link := "lists/" + list.ID + "/tasks"
list.Tasks = []*task{}
// Microsoft's Graph API has pagination, so we're going through all pages to get all tasks
for {
tr := &tasksResponse{}
err = makeAuthenticatedGetRequest(token, link, tr)
if err != nil {
log.Errorf("[Microsoft Todo Migration] Could not get tasks for list %s: %s", list.ID, err)
return
}
log.Debugf("[Microsoft Todo Migration] Got %d tasks for list %s", len(tr.Value), list.ID)
list.Tasks = append(list.Tasks, tr.Value...)
if tr.Nextlink == "" {
break
}
link = strings.ReplaceAll(tr.Nextlink, apiPrefix, "")
}
log.Debugf("[Microsoft Todo Migration] Got %d tasks for list %s", len(tasksResponse.Value), list.ID)
list.Tasks = tasksResponse.Value
microsoftTodoData = append(microsoftTodoData, list)
}

View File

@ -241,6 +241,15 @@ func (m *Migration) AuthURL() string {
}
func parseDate(dateString string) (date time.Time, err error) {
if len(dateString) == 10 {
// We're probably dealing with a date in the form of 2021-11-23 without a time
date, err = time.Parse("2006-01-02", dateString)
if err == nil {
// round the day to eod
return date.Add(time.Hour*23 + time.Minute*59), nil
}
}
date, err = time.Parse("2006-01-02T15:04:05Z", dateString)
if err != nil {
date, err = time.Parse("2006-01-02T15:04:05", dateString)

View File

@ -39,7 +39,7 @@ func TestConvertTodoistToVikunja(t *testing.T) {
time3, err := time.Parse(time.RFC3339Nano, "2014-10-21T08:25:05Z")
assert.NoError(t, err)
time3 = time3.In(config.GetTimeZone())
dueTime, err := time.Parse(time.RFC3339Nano, "2020-05-31T00:00:00Z")
dueTime, err := time.Parse(time.RFC3339Nano, "2020-05-31T23:59:00Z")
assert.NoError(t, err)
dueTime = dueTime.In(config.GetTimeZone())
dueTimeWithTime, err := time.Parse(time.RFC3339Nano, "2021-01-31T19:00:00Z")
@ -401,7 +401,7 @@ func TestConvertTodoistToVikunja(t *testing.T) {
Done: false,
Created: time1,
Reminders: []time.Time{
time.Date(2020, time.June, 15, 0, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
time.Date(2020, time.June, 15, 23, 59, 0, 0, time.UTC).In(config.GetTimeZone()),
time.Date(2020, time.June, 16, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
},

View File

@ -144,7 +144,7 @@ func getTrelloData(token string) (trelloData []*trello.Board, err error) {
// Converts all previously obtained data from trello into the vikunja format.
// `trelloData` should contain all boards with their lists and cards respectively.
func convertTrelloDataToVikunja(trelloData []*trello.Board) (fullVikunjaHierachie []*models.NamespaceWithListsAndTasks, err error) {
func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullVikunjaHierachie []*models.NamespaceWithListsAndTasks, err error) {
log.Debugf("[Trello Migration] ")
@ -254,7 +254,9 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board) (fullVikunjaHierachi
log.Debugf("[Trello Migration] Downloading card attachment %s", attachment.ID)
buf, err := migration.DownloadFile(attachment.URL)
buf, err := migration.DownloadFileWithHeaders(attachment.URL, map[string][]string{
"Authorization": {`OAuth oauth_consumer_key="` + config.MigrationTrelloKey.GetString() + `", oauth_token="` + token + `"`},
})
if err != nil {
return nil, err
}
@ -309,7 +311,7 @@ func (m *Migration) Migrate(u *user.User) (err error) {
log.Debugf("[Trello Migration] Got all trello data for user %d", u.ID)
log.Debugf("[Trello Migration] Start converting trello data for user %d", u.ID)
fullVikunjaHierachie, err := convertTrelloDataToVikunja(trelloData)
fullVikunjaHierachie, err := convertTrelloDataToVikunja(trelloData, m.Token)
if err != nil {
return
}

View File

@ -359,7 +359,7 @@ func TestConvertTrelloToVikunja(t *testing.T) {
},
}
hierachie, err := convertTrelloDataToVikunja(trelloData)
hierachie, err := convertTrelloDataToVikunja(trelloData, "")
assert.NoError(t, err)
assert.NotNil(t, hierachie)
if diff, equal := messagediff.PrettyDiff(hierachie, expectedHierachie); !equal {

View File

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

BIN
pkg/notifications/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

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

View File

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

View File

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

View File

@ -51,6 +51,7 @@ type vikunjaInfos struct {
AuthInfo authInfo `json:"auth"`
EmailRemindersEnabled bool `json:"email_reminders_enabled"`
UserDeletionEnabled bool `json:"user_deletion_enabled"`
TaskCommentsEnabled bool `json:"task_comments_enabled"`
}
type authInfo struct {
@ -93,6 +94,7 @@ func Info(c echo.Context) error {
CaldavEnabled: config.ServiceEnableCaldav.GetBool(),
EmailRemindersEnabled: config.ServiceEnableEmailReminders.GetBool(),
UserDeletionEnabled: config.ServiceEnableUserDeletion.GetBool(),
TaskCommentsEnabled: config.ServiceEnableTaskComments.GetBool(),
AvailableMigrators: []string{
(&vikunja_file.FileMigrator{}).Name(),
},

View File

@ -102,7 +102,7 @@ func Login(c echo.Context) error {
}
// Create token
return auth.NewUserAuthTokenResponse(user, c)
return auth.NewUserAuthTokenResponse(user, c, u.LongToken)
}
// RenewToken gives a new token to every user with a valid token
@ -156,6 +156,12 @@ func RenewToken(c echo.Context) (err error) {
return handler.HandleHTTPError(err, c)
}
var long bool
lng, has := claims["long"]
if has {
long = lng.(bool)
}
// Create token
return auth.NewUserAuthTokenResponse(user, c)
return auth.NewUserAuthTokenResponse(user, c, long)
}

View File

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

View File

@ -30,16 +30,6 @@ import (
)
func checkExportRequest(c echo.Context) (s *xorm.Session, u *user.User, err error) {
var pass UserPasswordConfirmation
if err := c.Bind(&pass); err != nil {
return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
}
err = c.Validate(pass)
if err != nil {
return nil, nil, echo.NewHTTPError(http.StatusBadRequest, err)
}
s = db.NewSession()
defer s.Close()
@ -54,6 +44,21 @@ func checkExportRequest(c echo.Context) (s *xorm.Session, u *user.User, err erro
return nil, nil, handler.HandleHTTPError(err, c)
}
// Users authenticated with a third-party are unable to provide their password.
if u.Issuer != user.IssuerLocal {
return
}
var pass UserPasswordConfirmation
if err := c.Bind(&pass); err != nil {
return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
}
err = c.Validate(pass)
if err != nil {
return nil, nil, echo.NewHTTPError(http.StatusBadRequest, err)
}
err = user.CheckUserPassword(u, pass.Password)
if err != nil {
_ = s.Rollback()

View File

@ -17,19 +17,22 @@
package v1
import (
"errors"
"fmt"
"net/http"
"code.vikunja.io/api/pkg/db"
"github.com/labstack/echo/v4"
"github.com/tkuchiki/go-timezone"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/models"
user2 "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web/handler"
"github.com/labstack/echo/v4"
)
// UserAvatarProvider holds the user avatar provider type
type UserAvatarProvider struct {
// The avatar provider. Valid types are `gravatar` (uses the user email), `upload`, `initials`, `default`.
// The avatar provider. Valid types are `gravatar` (uses the user email), `upload`, `initials`, `marble` (generates a random avatar for each user), `default`.
AvatarProvider string `json:"avatar_provider"`
}
@ -45,11 +48,17 @@ type UserSettings struct {
DiscoverableByEmail bool `json:"discoverable_by_email"`
// If enabled, the user will get an email for their overdue tasks each morning.
OverdueTasksRemindersEnabled bool `json:"overdue_tasks_reminders_enabled"`
// The time when the daily summary of overdue tasks will be sent via email.
OverdueTasksRemindersTime string `json:"overdue_tasks_reminders_time" valid:"time,required"`
// If a task is created without a specified list this value should be used. Applies
// to tasks made directly in API and from clients.
DefaultListID int64 `json:"default_list_id"`
// The day when the week starts for this user. 0 = sunday, 1 = monday, etc.
WeekStart int `json:"week_start"`
WeekStart int `json:"week_start" valid:"range(0|7)"`
// The user's language
Language string `json:"language"`
// The user's time zone. Used to send task reminders in the time zone of the user.
Timezone string `json:"timezone"`
}
// GetUserAvatarProvider returns the currently set user avatar
@ -153,7 +162,16 @@ func UpdateGeneralUserSettings(c echo.Context) error {
us := &UserSettings{}
err := c.Bind(us)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad user name provided.")
var he *echo.HTTPError
if errors.As(err, &he) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided. Error was: %s", he.Message))
}
return echo.NewHTTPError(http.StatusBadRequest, "Invalid model provided.")
}
err = c.Validate(us)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
u, err := user2.GetCurrentUser(c)
@ -177,6 +195,9 @@ func UpdateGeneralUserSettings(c echo.Context) error {
user.OverdueTasksRemindersEnabled = us.OverdueTasksRemindersEnabled
user.DefaultListID = us.DefaultListID
user.WeekStart = us.WeekStart
user.Language = us.Language
user.Timezone = us.Timezone
user.OverdueTasksRemindersTime = us.OverdueTasksRemindersTime
_, err = user2.UpdateUser(s, user)
if err != nil {
@ -191,3 +212,31 @@ func UpdateGeneralUserSettings(c echo.Context) error {
return c.JSON(http.StatusOK, &models.Message{Message: "The settings were updated successfully."})
}
// GetAvailableTimezones
// @Summary Get all available time zones on this vikunja instance
// @Description Because available time zones depend on the system Vikunja is running on, this endpoint returns a list of all valid time zones this particular Vikunja instance can handle. The list of time zones is not sorted, you should sort it on the client.
// @tags user
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {array} string "All available time zones."
// @Failure 500 {object} models.Message "Internal server error."
// @Router /user/timezones [get]
func GetAvailableTimezones(c echo.Context) error {
allTimezones := timezone.New().Timezones()
timezoneMap := make(map[string]bool) // to filter all duplicates
for _, s := range allTimezones {
for _, t := range s {
timezoneMap[t] = true
}
}
ts := []string{}
for s := range timezoneMap {
ts = append(ts, s)
}
return c.JSON(http.StatusOK, ts)
}

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