Merge branch 'main' into renovate/github.com-go-redis-redis-v8-8.x
# Conflicts: # go.mod # go.sum
This commit is contained in:
commit
7fb36dfcdc
13
.drone1.yml
13
.drone1.yml
|
@ -544,20 +544,15 @@ trigger:
|
|||
- main
|
||||
|
||||
steps:
|
||||
- name: submodules
|
||||
image: docker:git
|
||||
commands:
|
||||
- git submodule update --init
|
||||
- git submodule update --recursive --remote
|
||||
|
||||
- name: theme
|
||||
image: kolaente/yarn
|
||||
image: kolaente/toolbox
|
||||
pull: true
|
||||
group: build-static
|
||||
commands:
|
||||
- mkdir docs/themes/vikunja -p
|
||||
- cd docs/themes/vikunja
|
||||
- yarn --production=false
|
||||
- ./node_modules/.bin/gulp prod
|
||||
- wget https://dl.vikunja.io/theme/vikunja-theme.tar.gz
|
||||
- tar -xzf vikunja-theme.tar.gz
|
||||
|
||||
- name: build
|
||||
image: monachus/hugo:v0.54.0
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[*.{yaml,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 4
|
|
@ -4,6 +4,7 @@
|
|||
config.yml
|
||||
config.yaml
|
||||
!docs/config.yml
|
||||
docs/themes/
|
||||
*.db
|
||||
Run
|
||||
dist/
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[submodule "docs/themes/vikunja"]
|
||||
path = docs/themes/vikunja
|
||||
url = ../theme.git
|
181
CHANGELOG.md
181
CHANGELOG.md
|
@ -7,6 +7,187 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
All releases can be found on https://code.vikunja.io/api/releases.
|
||||
|
||||
## [0.17.0] - 2021-05-14
|
||||
|
||||
### Added
|
||||
|
||||
* Add a "done" option to kanban buckets (#821)
|
||||
* Add arm64 builds
|
||||
* Add basic auth for metrics endpoint
|
||||
* Add bucket limit validation
|
||||
* Add crud endpoints for notifications (#801)
|
||||
* Add endpoint to remove a list background
|
||||
* Add events (#777)
|
||||
* Add github funding link
|
||||
* Add link share password authentication (#831)
|
||||
* Add names for link shares (#829)
|
||||
* Add notifications package for easy sending of notifications (#779)
|
||||
* Add reminders for overdue tasks (#832)
|
||||
* Add repeat monthly setting for tasks (#834)
|
||||
* Add security information to readme
|
||||
* Add separate docker manifest file for latest docker images
|
||||
* Add systemd service file to linux packages
|
||||
* Add test for moving a task to another list
|
||||
* Enable searching users by full email or name
|
||||
* Expose tls parameter of Go MySQL driver to config file (#855)
|
||||
* Pagingation for tasks in kanban buckets (#805)
|
||||
|
||||
### Changed
|
||||
|
||||
* Change keyvalue.Get to return if a value exists or not instead of an error
|
||||
* Change main branch to main
|
||||
* Change test file names to unstable
|
||||
* Change the name of the newly created bucket from "New Bucket" to "Backlog"
|
||||
* Change unstable versions in migration tests
|
||||
* Check if we're on main and change the version name accordingly if that's the case
|
||||
* Cleanup listener names
|
||||
* Cleanup old docs themes submodule
|
||||
* Disable deb repo in drone
|
||||
* Don't keep old releases from os packages when releasing for master
|
||||
* Don't try to get users for tasks if no tasks were found when looking for reminders
|
||||
* Explicitly add docker build step for latest
|
||||
* Explicitly check if there are Ids before trying to get items by a list of Ids
|
||||
* Improve duration format of overdue tasks in reminders
|
||||
* Improve loading labels performance (#824)
|
||||
* Improve sending overdue task reminders by only sending one for all overdue tasks
|
||||
* Make sure all tables are properly pluralized
|
||||
* Only send reminders for undone tasks
|
||||
* Re-Enable migration test steps in pipeline
|
||||
* Refactor getting all namespaces
|
||||
* Remove unused tools from tools.go
|
||||
* Run all lint checks at once
|
||||
* Send a notification to the user when they are added to the list
|
||||
* Show empty avatar when the user was not found
|
||||
* Subscribe a user to a task when they are assigned to it
|
||||
* Subscriptions and notifications for namespaces, tasks and lists (#786)
|
||||
* Switch building the docs to download the theme instead of building
|
||||
* Switch telegram notifications to matrix notifications
|
||||
* Temporarily disable migration step
|
||||
* Temporary build fix
|
||||
* Update changelog
|
||||
* Update copyright year
|
||||
* Update README (#858)
|
||||
* Use golang's tzdata package to handle time zones
|
||||
|
||||
### Fixed
|
||||
|
||||
* Explicitly set darwin-10.15 when building binaries
|
||||
* Fix build
|
||||
* Fix checking list rights when accessing a bucket
|
||||
* Fix /dav/principals/*/ throwing a server error when accessed with GET instead of PROPFIND (#769)
|
||||
* Fix deleting task relations
|
||||
* Fix docs
|
||||
* Fix drone file
|
||||
* Fix due dates with times when migrating from todoist
|
||||
* Fix event error handler retrying infinitely
|
||||
* Fix filter for task index
|
||||
* Fix getting lists for shared, favorite and saved lists namespace
|
||||
* Fix getting user info from /user endpoint for link shares
|
||||
* Fix IncrBy and DecrBy in memory keyvalue implementation if there was no value set previously
|
||||
* Fix lint
|
||||
* Fix matrix notify room id
|
||||
* Fix moving repeating tasks to the done bucket
|
||||
* Fix multiarch docker image building
|
||||
* Fix not able to make saved filters favorite
|
||||
* Fix notifications table not being created on initial setup
|
||||
* Fix resetting the bucket limit
|
||||
* Fix retrieving over openid providers if there are none
|
||||
* Fix sending notifications to users if the user object didn't have an email
|
||||
* Fix setting the user in created_by when uploading an attachment
|
||||
* Fix shared lists showing up twice
|
||||
* Fix tests
|
||||
* Fix the shared lists pseudo namespace containing owned lists
|
||||
* Fix unstable version build file names
|
||||
* Fix user uploaded avatars
|
||||
* Pin golang alpine builder image to 3.12 to fix builds on arm
|
||||
* Revert "Update alpine Docker tag to v3.13 (#768)"
|
||||
|
||||
### Dependency Updates
|
||||
|
||||
* Update alpine Docker tag to v3.13 (#768)
|
||||
* Update github.com/gordonklaus/ineffassign commit hash to 2e10b26 (#803)
|
||||
* Update github.com/gordonklaus/ineffassign commit hash to d0e41b2 (#780)
|
||||
* Update golang.org/x/crypto commit hash to 0c34fe9 (#822)
|
||||
* Update golang.org/x/crypto commit hash to 3497b51 (#853)
|
||||
* Update golang.org/x/crypto commit hash to 38f3c27 (#854)
|
||||
* Update golang.org/x/crypto commit hash to 4f45737 (#836)
|
||||
* Update golang.org/x/crypto commit hash to 513c2a4 (#817)
|
||||
* Update golang.org/x/crypto commit hash to 5bf0f12 (#839)
|
||||
* Update golang.org/x/crypto commit hash to 5ea612d (#797)
|
||||
* Update golang.org/x/crypto commit hash to 83a5a9b (#840)
|
||||
* Update golang.org/x/crypto commit hash to b8e89b7 (#793)
|
||||
* Update golang.org/x/crypto commit hash to c07d793 (#861)
|
||||
* Update golang.org/x/crypto commit hash to cd7d49e (#860)
|
||||
* Update golang.org/x/crypto commit hash to e6e6c4f (#816)
|
||||
* Update golang.org/x/crypto commit hash to e9a3299 (#851)
|
||||
* Update golang.org/x/image commit hash to 4410531 (#788)
|
||||
* Update golang.org/x/image commit hash to 55ae14f (#787)
|
||||
* Update golang.org/x/image commit hash to 7319ad4 (#852)
|
||||
* Update golang.org/x/image commit hash to ac19c3e (#798)
|
||||
* Update golang.org/x/oauth2 commit hash to 0101308 (#776)
|
||||
* Update golang.org/x/oauth2 commit hash to 01de73c (#762)
|
||||
* Update golang.org/x/oauth2 commit hash to 16ff188 (#789)
|
||||
* Update golang.org/x/oauth2 commit hash to 22b0ada (#823)
|
||||
* Update golang.org/x/oauth2 commit hash to 2e8d934 (#827)
|
||||
* Update golang.org/x/oauth2 commit hash to 5366d9d (#813)
|
||||
* Update golang.org/x/oauth2 commit hash to 5e61552 (#833)
|
||||
* Update golang.org/x/oauth2 commit hash to 6667018 (#783)
|
||||
* Update golang.org/x/oauth2 commit hash to 81ed05c (#848)
|
||||
* Update golang.org/x/oauth2 commit hash to 8b1d76f (#764)
|
||||
* Update golang.org/x/oauth2 commit hash to 9bb9049 (#796)
|
||||
* Update golang.org/x/oauth2 commit hash to af13f52 (#773)
|
||||
* Update golang.org/x/oauth2 commit hash to ba52d33 (#794)
|
||||
* Update golang.org/x/oauth2 commit hash to cd4f82c (#815)
|
||||
* Update golang.org/x/oauth2 commit hash to d3ed898 (#765)
|
||||
* Update golang.org/x/oauth2 commit hash to f9ce19e (#775)
|
||||
* Update golang.org/x/sync commit hash to 036812b (#799)
|
||||
* Update golang.org/x/term commit hash to 6a3ed07 (#800)
|
||||
* Update golang.org/x/term commit hash to 72f3dc4 (#828)
|
||||
* Update golang.org/x/term commit hash to a79de54 (#850)
|
||||
* Update golang.org/x/term commit hash to b80969c (#843)
|
||||
* Update golang.org/x/term commit hash to c04ba85 (#849)
|
||||
* Update golang.org/x/term commit hash to de623e6 (#818)
|
||||
* Update golang.org/x/term commit hash to f5beecf (#845)
|
||||
* Update module adlio/trello to v1.9.0 (#825)
|
||||
* Update module coreos/go-oidc to v3 (#760)
|
||||
* Update module gabriel-vasile/mimetype to v1.2.0 (#812)
|
||||
* Update module gabriel-vasile/mimetype to v1.3.0 (#857)
|
||||
* Update module getsentry/sentry-go to v0.10.0 (#792)
|
||||
* Update module go-redis/redis/v8 to v8.4.10 (#771)
|
||||
* Update module go-redis/redis/v8 to v8.4.11 (#774)
|
||||
* Update module go-redis/redis/v8 to v8.4.9 (#770)
|
||||
* Update module go-redis/redis/v8 to v8.5.0 (#778)
|
||||
* Update module go-redis/redis/v8 to v8.6.0 (#795)
|
||||
* Update module go-sql-driver/mysql to v1.6.0 (#826)
|
||||
* Update module go-testfixtures/testfixtures/v3 to v3.5.0 (#761)
|
||||
* Update module go-testfixtures/testfixtures/v3 to v3.6.0 (#838)
|
||||
* Update module iancoleman/strcase to v0.1.3 (#766)
|
||||
* Update module imdario/mergo to v0.3.12 (#811)
|
||||
* Update module jgautheron/goconst to v1 (#804)
|
||||
* Update module labstack/echo/v4 to v4.2.0 (#785)
|
||||
* Update module labstack/echo/v4 to v4.2.1 (#810)
|
||||
* Update module labstack/echo/v4 to v4.2.2 (#830)
|
||||
* Update module labstack/echo/v4 to v4.3.0 (#856)
|
||||
* Update module lib/pq to v1.10.0 (#809)
|
||||
* Update module lib/pq to v1.10.1 (#841)
|
||||
* Update module mattn/go-sqlite3 to v1.14.7 (#835)
|
||||
* Update module olekukonko/tablewriter to v0.0.5 (#782)
|
||||
* Update module prometheus/client_golang to v1.10.0 (#819)
|
||||
* Update module spf13/afero to v1.6.0 (#820)
|
||||
* Update module spf13/cobra to v1.1.2 (#781)
|
||||
* Update module spf13/cobra to v1.1.3 (#784)
|
||||
* Update module src.techknowlogick.com/xgo to v1.3.0+1.16.0 (#791)
|
||||
* Update module src.techknowlogick.com/xgo to v1.4.0+1.16.2 (#814)
|
||||
* Update module stretchr/testify to v1.7.0 (#763)
|
||||
|
||||
## [0.16.1] - 2021-04-22
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix checking list rights when accessing a bucket
|
||||
* Remove old deb-structure ci step
|
||||
* Fix docker from
|
||||
|
||||
## [0.16.0] - 2021-01-10
|
||||
|
||||
### Added
|
||||
|
|
22
README.md
22
README.md
|
@ -2,10 +2,10 @@
|
|||
|
||||
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/api/status.svg)](https://drone.kolaente.de/vikunja/api)
|
||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
||||
[![Download](https://img.shields.io/badge/download-v0.16.0-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Download](https://img.shields.io/badge/download-v0.17.0-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/api.svg)](https://hub.docker.com/r/vikunja/api/)
|
||||
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/docs)
|
||||
[![Go Report Card](https://goreportcard.com/badge/git.kolaente.de/vikunja/api)](https://goreportcard.com/report/git.kolaente.de/vikunja/api)
|
||||
[![Go Report Card](https://goreportcard.com/badge/kolaente.dev/vikunja/api)](https://goreportcard.com/report/kolaente.dev/vikunja/api)
|
||||
|
||||
# Vikunja API
|
||||
|
||||
|
@ -13,17 +13,22 @@
|
|||
|
||||
# Table of contents
|
||||
|
||||
* [Security Reports](#security-reports)
|
||||
* [Features](#features)
|
||||
* [Docs](#docs)
|
||||
* [Roadmap](#roadmap)
|
||||
* [Contributing](#contributing)
|
||||
* [License](#license)
|
||||
|
||||
## Security Reports
|
||||
|
||||
If you find any security-related issues you don't want to disclose publicly, please use [the contact information on our website](https://vikunja.io/contact/#security).
|
||||
|
||||
## Features
|
||||
|
||||
* Create TODO lists with tasks
|
||||
* Reminder for tasks
|
||||
* Namespaces: A "group" which bundels multiple lists
|
||||
* Namespaces: A "group" which bundles multiple lists
|
||||
* Share lists and namespaces with teams and users with granular permissions
|
||||
* Plenty of details for tasks
|
||||
|
||||
|
@ -35,23 +40,22 @@ try it on [try.vikunja.io](https://try.vikunja.io)!
|
|||
* [Installing](https://vikunja.io/docs/installing/)
|
||||
* [Build from source](https://vikunja.io/docs/build-from-sources/)
|
||||
* [Development setup](https://vikunja.io/docs/development/)
|
||||
* [Magefile](https://vikunja.io/docs/mage/)
|
||||
* [Magefile](https://vikunja.io/docs/magefile/)
|
||||
* [Testing](https://vikunja.io/docs/testing/)
|
||||
|
||||
All docs can be found on [the vikunja home page](https://vikunja.io/docs/).
|
||||
All docs can be found on [the Vikunja home page](https://vikunja.io/docs/).
|
||||
|
||||
### Roadmap
|
||||
|
||||
See [the roadmap](https://my.vikunja.cloud/share/QFyzYEmEYfSyQfTOmIRSwLUpkFjboaBqQCnaPmWd/auth) (hosted on Vikunja!) for more!
|
||||
|
||||
* [ ] [Mobile apps](https://code.vikunja.io/app) (seperate repo) *In Progress*
|
||||
* [ ] [Webapp](https://code.vikunja.io/frontend) (seperate repo) *In Progress*
|
||||
* [ ] [Mobile apps](https://code.vikunja.io/app) (separate repo) *In Progress*
|
||||
* [ ] [Webapp](https://code.vikunja.io/frontend) (separate repo) *In Progress*
|
||||
|
||||
## Contributing
|
||||
|
||||
Fork -> Push -> Pull-Request. Also see the [dev docs](https://vikunja.io/docs/development/) for more infos.
|
||||
Fork -> Push -> Pull-Request. Also see the [dev docs](https://vikunja.io/docs/development/) for more info.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the AGPLv3 License. See the [LICENSE](LICENSE) file for the full license text.
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ service:
|
|||
enableregistration: true
|
||||
# Whether to enable task attachments or not
|
||||
enabletaskattachments: true
|
||||
# The time zone all timestamps are in
|
||||
# The time zone all timestamps are in. Please note that time zones have to use [the official tz database names](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). UTC or GMT offsets won't work.
|
||||
timezone: GMT
|
||||
# Whether task comments should be enabled or not
|
||||
enabletaskcomments: true
|
||||
|
@ -62,6 +62,8 @@ 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
|
||||
# Enable SSL/TLS for mysql connections. Options: false, true, skip-verify, preferred
|
||||
tls: false
|
||||
|
||||
cache:
|
||||
# If cache is enabled or not
|
||||
|
@ -260,10 +262,12 @@ auth:
|
|||
enabled: true
|
||||
# OpenID configuration will allow users to authenticate through a third-party OpenID Connect compatible provider.<br/>
|
||||
# The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
|
||||
# **Note:** The frontend expects to be redirected after authentication by the third party
|
||||
# **Note:** Some openid providers (like gitlab) only make the email of the user available through openid claims if they have set it to be publicly visible.
|
||||
# If the email is not public in those cases, authenticating will fail.
|
||||
# **Note 2:** The frontend expects to be redirected after authentication by the third party
|
||||
# to <frontend-url>/auth/openid/<auth key>. Please make sure to configure the redirect url with your third party
|
||||
# auth service accordingy if you're using the default vikunja frontend.
|
||||
# Take a look at the [default config file](https://kolaente.dev/vikunja/api/src/branch/master/config.yml.sample) for more information about how to configure openid authentication.
|
||||
# Take a look at the [default config file](https://kolaente.dev/vikunja/api/src/branch/main/config.yml.sample) for more information about how to configure openid authentication.
|
||||
openid:
|
||||
# Enable or disable OpenID Connect authentication
|
||||
enabled: false
|
||||
|
|
|
@ -15,7 +15,7 @@ menu:
|
|||
We use go modules to vendor libraries for Vikunja, so you'll need at least go `1.11` to use these.
|
||||
If you don't intend to add new dependencies, go `1.9` and above should be fine.
|
||||
|
||||
To contribute to Vikunja, fork the project and work on the master branch.
|
||||
To contribute to Vikunja, fork the project and work on the main branch.
|
||||
|
||||
A lot of developing tasks are automated using a Magefile, so make sure to [take a look at it]({{< ref "mage.md">}}).
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ There are multiple categories of subcommands in the magefile:
|
|||
|
||||
## CI
|
||||
|
||||
These tasks are automatically run in our CI every time someone pushes to master or you update a pull request:
|
||||
These tasks are automatically run in our CI every time someone pushes to main or you update a pull request:
|
||||
|
||||
* `mage check:lint`
|
||||
* `mage check:fmt`
|
||||
|
|
|
@ -110,4 +110,4 @@ If it was called, no mails are being sent and you can instead assert they have b
|
|||
|
||||
## Example
|
||||
|
||||
Take a look at the [pkg/user/notifications.go](https://code.vikunja.io/api/src/branch/master/pkg/user/notifications.go) file for a good example.
|
||||
Take a look at the [pkg/user/notifications.go](https://code.vikunja.io/api/src/branch/main/pkg/user/notifications.go) file for a good example.
|
||||
|
|
|
@ -32,7 +32,7 @@ first:
|
|||
Vikunja supports using `toml`, `yaml`, `hcl`, `ini`, `json`, envfile, env variables and Java Properties files.
|
||||
We reccomend yaml or toml, but you're free to use whatever you want.
|
||||
|
||||
Vikunja provides a default [`config.yml`](https://kolaente.dev/vikunja/api/src/branch/master/config.yml.sample) file which you can use as a starting point.
|
||||
Vikunja provides a default [`config.yml`](https://kolaente.dev/vikunja/api/src/branch/main/config.yml.sample) file which you can use as a starting point.
|
||||
|
||||
# Config file locations
|
||||
|
||||
|
@ -46,7 +46,7 @@ Vikunja will search on various places for a config file:
|
|||
# Default configuration with explanations
|
||||
|
||||
The following explains all possible config variables and their defaults.
|
||||
You can find a full example configuration file in [here](https://code.vikunja.io/api/src/branch/master/config.yml.sample).
|
||||
You can find a full example configuration file in [here](https://code.vikunja.io/api/src/branch/main/config.yml.sample).
|
||||
|
||||
If you don't provide a value in your config file, their default will be used.
|
||||
|
||||
|
@ -55,7 +55,7 @@ If you don't provide a value in your config file, their default will be used.
|
|||
Most config variables are nested under some "higher-level" key.
|
||||
For example, the `interface` config variable is a child of the `service` key.
|
||||
|
||||
The docs below aim to reflect that leveling, but please also have a lookt at [the default config](https://code.vikunja.io/api/src/branch/master/config.yml.sample) file
|
||||
The docs below aim to reflect that leveling, but please also have a lookt at [the default config](https://code.vikunja.io/api/src/branch/main/config.yml.sample) file
|
||||
to better grasp how the nesting looks like.
|
||||
|
||||
<!-- Generated config will be injected here -->
|
||||
|
@ -132,7 +132,7 @@ Default: `true`
|
|||
|
||||
### timezone
|
||||
|
||||
The time zone all timestamps are in
|
||||
The time zone all timestamps are in. Please note that time zones have to use [the official tz database names](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). UTC or GMT offsets won't work.
|
||||
|
||||
Default: `GMT`
|
||||
|
||||
|
@ -237,6 +237,12 @@ Secure connection mode. Only used with postgres.
|
|||
|
||||
Default: `disable`
|
||||
|
||||
### tls
|
||||
|
||||
Enable SSL/TLS for mysql connections. Options: false, true, skip-verify, preferred
|
||||
|
||||
Default: `false`
|
||||
|
||||
---
|
||||
|
||||
## cache
|
||||
|
@ -609,10 +615,12 @@ Default: `<empty>`
|
|||
|
||||
OpenID configuration will allow users to authenticate through a third-party OpenID Connect compatible provider.<br/>
|
||||
The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
|
||||
**Note:** The frontend expects to be redirected after authentication by the third party
|
||||
**Note:** Some openid providers (like gitlab) only make the email of the user available through openid claims if they have set it to be publicly visible.
|
||||
If the email is not public in those cases, authenticating will fail.
|
||||
**Note 2:** The frontend expects to be redirected after authentication by the third party
|
||||
to <frontend-url>/auth/openid/<auth key>. Please make sure to configure the redirect url with your third party
|
||||
auth service accordingy if you're using the default vikunja frontend.
|
||||
Take a look at the [default config file](https://kolaente.dev/vikunja/api/src/branch/master/config.yml.sample) for more information about how to configure openid authentication.
|
||||
Take a look at the [default config file](https://kolaente.dev/vikunja/api/src/branch/main/config.yml.sample) for more information about how to configure openid authentication.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ wget <download-url>
|
|||
### Verify the GPG signature
|
||||
|
||||
Starting with version `0.7`, all releases are signed using pgp.
|
||||
Releases from `master` will always be signed.
|
||||
Releases from `main` will always be signed.
|
||||
|
||||
To validate the downloaded zip file use the signiture file `.asc` and the key `FF054DACD908493A`:
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ title: "Errors"
|
|||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
---
|
||||
|
||||
# Errors
|
||||
|
@ -150,3 +150,10 @@ This document describes the different errors Vikunja can return.
|
|||
|-----------|------------------|-------------|
|
||||
| 12001 | 412 | The subscription entity type is invalid. |
|
||||
| 12002 | 412 | The user is already subscribed to the entity itself or a parent entity. |
|
||||
|
||||
## Link Shares
|
||||
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|-------------|
|
||||
| 13001 | 412 | This link share requires a password for authentication, but none was provided. |
|
||||
| 13002 | 403 | The provided link share password was invalid. |
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 1ebcbbb645ad20ea683feef2804314a6c658799b
|
25
go.mod
25
go.mod
|
@ -17,7 +17,6 @@
|
|||
module code.vikunja.io/api
|
||||
|
||||
require (
|
||||
4d63.com/tz v1.2.0
|
||||
code.vikunja.io/web v0.0.0-20210131201003-26386be9a9ae
|
||||
gitea.com/xorm/xorm-redis-cache v0.2.0
|
||||
github.com/ThreeDotsLabs/watermill v1.1.1
|
||||
|
@ -32,25 +31,24 @@ require (
|
|||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
|
||||
github.com/gabriel-vasile/mimetype v1.2.0
|
||||
github.com/getsentry/sentry-go v0.10.0
|
||||
github.com/gabriel-vasile/mimetype v1.3.0
|
||||
github.com/getsentry/sentry-go v0.11.0
|
||||
github.com/go-errors/errors v1.1.1 // indirect
|
||||
github.com/go-redis/redis/v8 v8.7.1
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.5.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.6.1
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/golang/snappy v0.0.2 // indirect
|
||||
github.com/iancoleman/strcase v0.1.3
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.2.2
|
||||
github.com/labstack/echo/v4 v4.3.0
|
||||
github.com/labstack/gommon v0.3.0
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef
|
||||
github.com/lib/pq v1.10.0
|
||||
github.com/lib/pq v1.10.2
|
||||
github.com/magefile/mage v1.11.0
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
github.com/mitchellh/mapstructure v1.3.2 // indirect
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
|
@ -58,7 +56,7 @@ require (
|
|||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/prometheus/client_golang v1.10.0
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/spf13/afero v1.6.0
|
||||
|
@ -72,10 +70,17 @@ require (
|
|||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
|
||||
github.com/yuin/goldmark v1.3.7
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
|
@ -99,4 +104,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.13
|
||||
go 1.15
|
||||
|
|
|
@ -1046,7 +1046,7 @@ const (
|
|||
configInjectComment = `<!-- Generated config will be injected here -->`
|
||||
)
|
||||
|
||||
// Generates the error docs from a commented config.yml.sample file in the repo root.
|
||||
// Generates the config docs from a commented config.yml.sample file in the repo root.
|
||||
func GenerateDocs() error {
|
||||
|
||||
config, err := ioutil.ReadFile("config.yml.sample")
|
||||
|
|
|
@ -25,8 +25,8 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
_ "time/tzdata" // Imports time zone data instead of relying on the os
|
||||
|
||||
"4d63.com/tz"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
|
@ -73,6 +73,7 @@ const (
|
|||
DatabaseMaxIdleConnections Key = `database.maxidleconnections`
|
||||
DatabaseMaxConnectionLifetime Key = `database.maxconnectionlifetime`
|
||||
DatabaseSslMode Key = `database.sslmode`
|
||||
DatabaseTLS Key = `database.tls`
|
||||
|
||||
CacheEnabled Key = `cache.enabled`
|
||||
CacheType Key = `cache.type`
|
||||
|
@ -191,7 +192,7 @@ var timezone *time.Location
|
|||
// it way easier, especially when testing.
|
||||
func GetTimeZone() *time.Location {
|
||||
if timezone == nil {
|
||||
loc, err := tz.LoadLocation(ServiceTimeZone.GetString())
|
||||
loc, err := time.LoadLocation(ServiceTimeZone.GetString())
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing time zone: %s", err)
|
||||
os.Exit(1)
|
||||
|
@ -258,6 +259,7 @@ func InitDefaultConfig() {
|
|||
DatabaseMaxIdleConnections.setDefault(50)
|
||||
DatabaseMaxConnectionLifetime.setDefault(10000)
|
||||
DatabaseSslMode.setDefault("disable")
|
||||
DatabaseTLS.setDefault("false")
|
||||
|
||||
// Cacher
|
||||
CacheEnabled.setDefault(false)
|
||||
|
@ -359,6 +361,10 @@ func InitConfig() {
|
|||
RateLimitStore.Set(KeyvalueType.GetString())
|
||||
}
|
||||
|
||||
if ServiceFrontendurl.GetString() != "" && !strings.HasSuffix(ServiceFrontendurl.GetString(), "/") {
|
||||
ServiceFrontendurl.Set(ServiceFrontendurl.GetString() + "/")
|
||||
}
|
||||
|
||||
if AuthOpenIDRedirectURL.GetString() == "" {
|
||||
AuthOpenIDRedirectURL.Set(ServiceFrontendurl.GetString() + "auth/openid/")
|
||||
}
|
||||
|
|
|
@ -113,11 +113,12 @@ func initMysqlEngine() (engine *xorm.Engine, err error) {
|
|||
// We're using utf8mb here instead of just utf8 because we want to use non-BMP characters.
|
||||
// See https://stackoverflow.com/a/30074553/10924593 for more info.
|
||||
connStr := fmt.Sprintf(
|
||||
"%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=true",
|
||||
"%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=true&tls=%s",
|
||||
config.DatabaseUser.GetString(),
|
||||
config.DatabasePassword.GetString(),
|
||||
config.DatabaseHost.GetString(),
|
||||
config.DatabaseDatabase.GetString())
|
||||
config.DatabaseDatabase.GetString(),
|
||||
config.DatabaseTLS.GetString())
|
||||
engine, err = xorm.NewEngine("mysql", connStr)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
|
@ -47,6 +47,9 @@ func Dump() (data map[string][]byte, err error) {
|
|||
|
||||
// Restore restores a table with all its entries
|
||||
func Restore(table string, contents []map[string]interface{}) (err error) {
|
||||
if _, err := x.IsTableExist(table); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, content := range contents {
|
||||
if _, err := x.Table(table).Insert(content); err != nil {
|
||||
|
|
|
@ -22,3 +22,12 @@
|
|||
shared_by_id: 1
|
||||
created: 2018-12-01 15:13:12
|
||||
updated: 2018-12-02 15:13:12
|
||||
- id: 4
|
||||
hash: testWithPassword
|
||||
list_id: 1
|
||||
right: 0
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
sharing_type: 2
|
||||
shared_by_id: 1
|
||||
created: 2018-12-01 15:13:12
|
||||
updated: 2018-12-02 15:13:12
|
||||
|
|
|
@ -346,3 +346,12 @@
|
|||
index: 2
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
- id: 38
|
||||
title: 'task #37 done with due date'
|
||||
done: true
|
||||
created_by_id: 1
|
||||
list_id: 22
|
||||
index: 2
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
due_date: 2018-10-30 22:25:24
|
||||
|
|
|
@ -93,6 +93,7 @@ func FullInit() {
|
|||
// Start the cron
|
||||
cron.Init()
|
||||
models.RegisterReminderCron()
|
||||
models.RegisterOverdueReminderCron()
|
||||
|
||||
// Start processing events
|
||||
go func() {
|
||||
|
|
|
@ -114,8 +114,8 @@ func bootstrapTestRequest(t *testing.T, method string, payload string, queryPara
|
|||
return
|
||||
}
|
||||
|
||||
func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context) error, payload string) (rec *httptest.ResponseRecorder, err error) {
|
||||
c, rec := bootstrapTestRequest(t, method, payload, nil)
|
||||
func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context) error, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
|
||||
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
|
||||
err = handler(c)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
// 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/models"
|
||||
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLinkSharingAuth(t *testing.T) {
|
||||
t.Run("Without Password", func(t *testing.T) {
|
||||
rec, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, ``, nil, map[string]string{"share": "test"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Contains(t, rec.Body.String(), `"token":"`)
|
||||
assert.Contains(t, rec.Body.String(), `"list_id":1`)
|
||||
})
|
||||
t.Run("Without Password, Password Provided", func(t *testing.T) {
|
||||
rec, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, `{"password":"somethingsomething"}`, nil, map[string]string{"share": "test"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Contains(t, rec.Body.String(), `"token":"`)
|
||||
assert.Contains(t, rec.Body.String(), `"list_id":1`)
|
||||
})
|
||||
t.Run("With Password, No Password Provided", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, ``, nil, map[string]string{"share": "testWithPassword"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeLinkSharePasswordRequired)
|
||||
})
|
||||
t.Run("With Password, Password Provided", func(t *testing.T) {
|
||||
rec, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, `{"password":"1234"}`, nil, map[string]string{"share": "testWithPassword"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Contains(t, rec.Body.String(), `"token":"`)
|
||||
assert.Contains(t, rec.Body.String(), `"list_id":1`)
|
||||
})
|
||||
t.Run("With Wrong Password", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.AuthenticateLinkShare, `{"password":"someWrongPassword"}`, nil, map[string]string{"share": "testWithPassword"})
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeLinkSharePasswordInvalid)
|
||||
})
|
||||
}
|
|
@ -30,12 +30,12 @@ func TestLogin(t *testing.T) {
|
|||
rec, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{
|
||||
"username": "user1",
|
||||
"password": "1234"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), "token")
|
||||
})
|
||||
t.Run("Empty payload", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{}`)
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeNoUsernamePassword)
|
||||
})
|
||||
|
@ -43,7 +43,7 @@ func TestLogin(t *testing.T) {
|
|||
_, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{
|
||||
"username": "userWichDoesNotExist",
|
||||
"password": "1234"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeWrongUsernameOrPassword)
|
||||
})
|
||||
|
@ -51,7 +51,7 @@ func TestLogin(t *testing.T) {
|
|||
_, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{
|
||||
"username": "user1",
|
||||
"password": "wrong"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeWrongUsernameOrPassword)
|
||||
})
|
||||
|
@ -59,7 +59,7 @@ func TestLogin(t *testing.T) {
|
|||
_, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{
|
||||
"username": "user5",
|
||||
"password": "1234"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeEmailNotConfirmed)
|
||||
})
|
||||
|
|
|
@ -31,12 +31,12 @@ func TestRegister(t *testing.T) {
|
|||
"username": "newUser",
|
||||
"password": "1234",
|
||||
"email": "email@example.com"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"username":"newUser"`)
|
||||
})
|
||||
t.Run("Empty payload", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.RegisterUser, `{}`)
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.RegisterUser, `{}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeNoUsernamePassword)
|
||||
})
|
||||
|
@ -45,7 +45,7 @@ func TestRegister(t *testing.T) {
|
|||
"username": "",
|
||||
"password": "1234",
|
||||
"email": "email@example.com"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeNoUsernamePassword)
|
||||
})
|
||||
|
@ -54,7 +54,7 @@ func TestRegister(t *testing.T) {
|
|||
"username": "newUser",
|
||||
"password": "",
|
||||
"email": "email@example.com"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeNoUsernamePassword)
|
||||
})
|
||||
|
@ -63,7 +63,7 @@ func TestRegister(t *testing.T) {
|
|||
"username": "newUser",
|
||||
"password": "1234",
|
||||
"email": ""
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeNoUsernamePassword)
|
||||
})
|
||||
|
@ -72,7 +72,7 @@ func TestRegister(t *testing.T) {
|
|||
"username": "user1",
|
||||
"password": "1234",
|
||||
"email": "email@example.com"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrorCodeUsernameExists)
|
||||
})
|
||||
|
@ -81,7 +81,7 @@ func TestRegister(t *testing.T) {
|
|||
"username": "newUser",
|
||||
"password": "1234",
|
||||
"email": "user1@example.com"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrorCodeUserEmailExists)
|
||||
})
|
||||
|
|
|
@ -113,49 +113,49 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("by priority", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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,"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,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by priority desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"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_from_current_date":false,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","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,"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`)
|
||||
})
|
||||
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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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,"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,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
// Due date without unix suffix
|
||||
t.Run("by duedate asc without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"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_from_current_date":false,"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,"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,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by due_date without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"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_from_current_date":false,"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,"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,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by duedate desc without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
t.Run("by duedate asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"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_from_current_date":false,"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,"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,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("invalid sort parameter", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
||||
|
@ -171,10 +171,10 @@ func TestTaskCollection(t *testing.T) {
|
|||
// Invalid parameter should not sort at all
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
})
|
||||
})
|
||||
t.Run("Filter", func(t *testing.T) {
|
||||
|
@ -341,42 +341,42 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("by priority", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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,"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,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by priority desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"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_from_current_date":false,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","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,"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`)
|
||||
})
|
||||
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_from_current_date":false,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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,"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,"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,"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_from_current_date":false,"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,"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_from_current_date":false,"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,"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,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
t.Run("by duedate asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_from_current_date":false,"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,"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_from_current_date":false,"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,"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,"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,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("invalid parameter", func(t *testing.T) {
|
||||
// Invalid parameter should not sort at all
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_from_current_date":false,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
})
|
||||
})
|
||||
t.Run("Filter", func(t *testing.T) {
|
||||
|
|
|
@ -28,23 +28,23 @@ import (
|
|||
|
||||
func TestUserConfirmEmail(t *testing.T) {
|
||||
t.Run("Normal test", func(t *testing.T) {
|
||||
rec, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{"token": "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael"}`)
|
||||
rec, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{"token": "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael"}`, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `The email was confirmed successfully.`)
|
||||
})
|
||||
t.Run("Empty payload", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{}`)
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, http.StatusPreconditionFailed, err.(*echo.HTTPError).Code)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeInvalidEmailConfirmToken)
|
||||
})
|
||||
t.Run("Empty token", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{"token": ""}`)
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{"token": ""}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeInvalidEmailConfirmToken)
|
||||
})
|
||||
t.Run("Invalid token", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{"token": "invalidToken"}`)
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{"token": "invalidToken"}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeInvalidEmailConfirmToken)
|
||||
})
|
||||
|
|
|
@ -28,22 +28,22 @@ import (
|
|||
|
||||
func TestUserRequestResetPasswordToken(t *testing.T) {
|
||||
t.Run("Normal requesting a password reset token", func(t *testing.T) {
|
||||
rec, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{"email": "user1@example.com"}`)
|
||||
rec, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{"email": "user1@example.com"}`, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Token was sent.`)
|
||||
})
|
||||
t.Run("Empty payload", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{}`)
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeNoUsernamePassword)
|
||||
})
|
||||
t.Run("Invalid email address", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{"email": "user1example.com"}`)
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{"email": "user1example.com"}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, http.StatusBadRequest, err.(*echo.HTTPError).Code)
|
||||
})
|
||||
t.Run("No user with that email address", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{"email": "user1000@example.com"}`)
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{"email": "user1000@example.com"}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeUserDoesNotExist)
|
||||
})
|
||||
|
|
|
@ -31,12 +31,12 @@ func TestUserPasswordReset(t *testing.T) {
|
|||
rec, err := newTestRequest(t, http.MethodPost, apiv1.UserResetPassword, `{
|
||||
"new_password": "1234",
|
||||
"token": "passwordresettesttoken"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `The password was updated successfully.`)
|
||||
})
|
||||
t.Run("Empty payload", func(t *testing.T) {
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserResetPassword, `{}`)
|
||||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserResetPassword, `{}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, http.StatusBadRequest, err.(*echo.HTTPError).Code)
|
||||
})
|
||||
|
@ -44,7 +44,7 @@ func TestUserPasswordReset(t *testing.T) {
|
|||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserResetPassword, `{
|
||||
"new_password": "",
|
||||
"token": "passwordresettesttoken"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeNoUsernamePassword)
|
||||
})
|
||||
|
@ -52,7 +52,7 @@ func TestUserPasswordReset(t *testing.T) {
|
|||
_, err := newTestRequest(t, http.MethodPost, apiv1.UserResetPassword, `{
|
||||
"new_password": "1234",
|
||||
"token": "invalidtoken"
|
||||
}`)
|
||||
}`, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, user.ErrCodeInvalidPasswordResetToken)
|
||||
})
|
||||
|
|
|
@ -91,12 +91,8 @@ func SetUserActive(a web.Auth) (err error) {
|
|||
|
||||
// getActiveUsers returns the active users from redis
|
||||
func getActiveUsers() (users activeUsersMap, err error) {
|
||||
u, _, err := keyvalue.Get(ActiveUsersKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users = u.(activeUsersMap)
|
||||
users = activeUsersMap{}
|
||||
_, err = keyvalue.GetWithValue(ActiveUsersKey, &users)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -17,9 +17,13 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
|
@ -45,8 +49,8 @@ var registry *prometheus.Registry
|
|||
func GetRegistry() *prometheus.Registry {
|
||||
if registry == nil {
|
||||
registry = prometheus.NewRegistry()
|
||||
registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
|
||||
registry.MustRegister(prometheus.NewGoCollector())
|
||||
registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
|
||||
registry.MustRegister(collectors.NewGoCollector())
|
||||
}
|
||||
|
||||
return registry
|
||||
|
@ -132,7 +136,11 @@ func GetCount(key string) (count int64, err error) {
|
|||
return 0, nil
|
||||
}
|
||||
|
||||
count = cnt.(int64)
|
||||
if s, is := cnt.(string); is {
|
||||
count, err = strconv.ParseInt(s, 10, 64)
|
||||
} else {
|
||||
count = cnt.(int64)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type linkShares20210411113105 struct {
|
||||
Password string `xorm:"text null"`
|
||||
SharingType int `xorm:"bigint INDEX not null default 0"`
|
||||
}
|
||||
|
||||
func (linkShares20210411113105) TableName() string {
|
||||
return "link_shares"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210411113105",
|
||||
Description: "Add password field to link shares",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
|
||||
// Make all existing share links type 1 (no password)
|
||||
if _, err := tx.Update(&linkShares20210411113105{SharingType: 1}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Sync2(linkShares20210411113105{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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 users20210411161337 struct {
|
||||
OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"`
|
||||
}
|
||||
|
||||
func (users20210411161337) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210411161337",
|
||||
Description: "Add overdue notifications enabled setting to users",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20210411161337{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
// 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 tasks20210413131057 struct {
|
||||
RepeatFromCurrentDate bool `xorm:"null" json:"repeat_from_current_date"`
|
||||
RepeatMode int `xorm:"not null default 0" json:"repeat_mode"`
|
||||
}
|
||||
|
||||
func (tasks20210413131057) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210413131057",
|
||||
Description: "Add repeat mode column to tasks",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(tasks20210413131057{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.
|
||||
Where("repeat_from_current_date = ?", true).
|
||||
Update(&tasks20210413131057{RepeatMode: 2})
|
||||
return err
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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 users20210527105701 struct {
|
||||
DefaultListID int64 `xorm:"bigint null index" json:"-"`
|
||||
}
|
||||
|
||||
func (users20210527105701) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210527105701",
|
||||
Description: "Add default list for new tasks setting to users",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20210527105701{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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 users20210603174608 struct {
|
||||
WeekStart int `xorm:"null" json:"-"`
|
||||
}
|
||||
|
||||
func (users20210603174608) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210603174608",
|
||||
Description: "Add week start user setting",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20210603174608{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1512,3 +1512,61 @@ func (err ErrSubscriptionAlreadyExists) HTTPError() web.HTTPError {
|
|||
Message: "You're already subscribed.",
|
||||
}
|
||||
}
|
||||
|
||||
// =================
|
||||
// Link Share errors
|
||||
// =================
|
||||
|
||||
// ErrLinkSharePasswordRequired represents an error where a link share authentication requires a password and none was provided
|
||||
type ErrLinkSharePasswordRequired struct {
|
||||
ShareID int64
|
||||
}
|
||||
|
||||
// IsErrLinkSharePasswordRequired checks if an error is ErrLinkSharePasswordRequired.
|
||||
func IsErrLinkSharePasswordRequired(err error) bool {
|
||||
_, ok := err.(*ErrLinkSharePasswordRequired)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrLinkSharePasswordRequired) Error() string {
|
||||
return fmt.Sprintf("Link Share requires a password for authentication [ShareID: %d]", err.ShareID)
|
||||
}
|
||||
|
||||
// ErrCodeLinkSharePasswordRequired holds the unique world-error code of this error
|
||||
const ErrCodeLinkSharePasswordRequired = 13001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrLinkSharePasswordRequired) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeLinkSharePasswordRequired,
|
||||
Message: "This link share requires a password for authentication, but none was provided.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrLinkSharePasswordInvalid represents an error where a subscription entity type is unknown
|
||||
type ErrLinkSharePasswordInvalid struct {
|
||||
ShareID int64
|
||||
}
|
||||
|
||||
// IsErrLinkSharePasswordInvalid checks if an error is ErrLinkSharePasswordInvalid.
|
||||
func IsErrLinkSharePasswordInvalid(err error) bool {
|
||||
_, ok := err.(*ErrLinkSharePasswordInvalid)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrLinkSharePasswordInvalid) Error() string {
|
||||
return fmt.Sprintf("Provided Link Share password did not match the saved one [ShareID: %d]", err.ShareID)
|
||||
}
|
||||
|
||||
// ErrCodeLinkSharePasswordInvalid holds the unique world-error code of this error
|
||||
const ErrCodeLinkSharePasswordInvalid = 13002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrLinkSharePasswordInvalid) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusForbidden,
|
||||
Code: ErrCodeLinkSharePasswordInvalid,
|
||||
Message: "The provided link share password is invalid.",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ type Bucket struct {
|
|||
Tasks []*Task `xorm:"-" json:"tasks"`
|
||||
|
||||
// How many tasks can be at the same time on this board max
|
||||
Limit int64 `xorm:"default 0" json:"limit"`
|
||||
Limit int64 `xorm:"default 0" json:"limit" minimum:"0" valid:"range(0|9223372036854775807)"`
|
||||
// If this bucket is the "done bucket". All tasks moved into this bucket will automatically marked as done. All tasks marked as done from elsewhere will be moved into this bucket.
|
||||
IsDoneBucket bool `xorm:"BOOL" json:"is_done_bucket"`
|
||||
|
||||
|
@ -119,6 +119,19 @@ func getDoneBucketForList(s *xorm.Session, listID int64) (bucket *Bucket, err er
|
|||
// @Router /lists/{id}/buckets [get]
|
||||
func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
|
||||
list, err := GetListSimpleByID(s, b.ListID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
can, _, err := list.CanRead(s, auth)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
if !can {
|
||||
return nil, 0, 0, ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
// Get all buckets for this list
|
||||
buckets := []*Bucket{}
|
||||
err = s.Where("list_id = ?", b.ListID).Find(&buckets)
|
||||
|
|
|
@ -89,12 +89,30 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
assert.Len(t, buckets, 3)
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[0].ID)
|
||||
})
|
||||
t.Run("link share", func(t *testing.T) {
|
||||
t.Run("accessed by link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
testuser := &user.User{ID: 1}
|
||||
linkShare := &LinkSharing{
|
||||
ID: 1,
|
||||
ListID: 1,
|
||||
Right: RightRead,
|
||||
}
|
||||
b := &Bucket{ListID: 1}
|
||||
result, _, _, err := b.ReadAll(s, linkShare, "", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
buckets, _ := result.([]*Bucket)
|
||||
assert.Len(t, buckets, 3)
|
||||
assert.NotNil(t, buckets[0].CreatedBy)
|
||||
assert.Equal(t, int64(1), buckets[0].CreatedByID)
|
||||
})
|
||||
t.Run("created by link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
testuser := &user.User{ID: 12}
|
||||
b := &Bucket{ListID: 23}
|
||||
result, _, _, err := b.ReadAll(s, testuser, "", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -61,7 +61,7 @@ func (Label) TableName() string {
|
|||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param label body models.Label true "The label object"
|
||||
// @Success 200 {object} models.Label "The created label object."
|
||||
// @Success 201 {object} models.Label "The created label object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid label object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /labels [put]
|
||||
|
|
|
@ -75,7 +75,7 @@ func (lt *LabelTask) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param task path int true "Task ID"
|
||||
// @Param label body models.LabelTask true "The label object"
|
||||
// @Success 200 {object} models.LabelTask "The created label relation object."
|
||||
// @Success 201 {object} models.LabelTask "The created label relation object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid label object provided."
|
||||
// @Failure 403 {object} web.HTTPError "Not allowed to add the label."
|
||||
// @Failure 404 {object} web.HTTPError "The label does not exist."
|
||||
|
@ -369,7 +369,7 @@ type LabelTaskBulk struct {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param label body models.LabelTaskBulk true "The array of labels"
|
||||
// @Param taskID path int true "Task ID"
|
||||
// @Success 200 {object} models.LabelTaskBulk "The updated labels object."
|
||||
// @Success 201 {object} models.LabelTaskBulk "The updated labels object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid label object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/labels/bulk [post]
|
||||
|
|
|
@ -69,13 +69,14 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
|||
Updated: testUpdatedTime,
|
||||
CreatedByID: 2,
|
||||
CreatedBy: &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -48,14 +48,15 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
page int
|
||||
}
|
||||
user1 := &user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -98,13 +99,14 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
Updated: testUpdatedTime,
|
||||
CreatedByID: 2,
|
||||
CreatedBy: &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -161,14 +163,15 @@ func TestLabel_ReadOne(t *testing.T) {
|
|||
Rights web.Rights
|
||||
}
|
||||
user1 := &user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -222,13 +225,14 @@ func TestLabel_ReadOne(t *testing.T) {
|
|||
Title: "Label #4 - visible via other task",
|
||||
CreatedByID: 2,
|
||||
CreatedBy: &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
|
|
|
@ -17,8 +17,11 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/web"
|
||||
|
@ -49,9 +52,12 @@ type LinkSharing struct {
|
|||
// The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
Right Right `xorm:"bigint INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
// The kind of this link. 0 = undefined, 1 = without password, 2 = with password (currently not implemented).
|
||||
// The kind of this link. 0 = undefined, 1 = without password, 2 = with password.
|
||||
SharingType SharingType `xorm:"bigint INDEX not null default 0" json:"sharing_type" valid:"length(0|2)" maximum:"2" default:"0"`
|
||||
|
||||
// The password of this link share. You can only set it, not retrieve it after the link share has been created.
|
||||
Password string `xorm:"text null" json:"password"`
|
||||
|
||||
// The user who shared this list
|
||||
SharedBy *user.User `xorm:"-" json:"shared_by"`
|
||||
SharedByID int64 `xorm:"bigint INDEX not null" json:"-"`
|
||||
|
@ -114,7 +120,7 @@ func (share *LinkSharing) toUser() *user.User {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param list path int true "List ID"
|
||||
// @Param label body models.LinkSharing true "The new link share object"
|
||||
// @Success 200 {object} models.LinkSharing "The created link share object."
|
||||
// @Success 201 {object} models.LinkSharing "The created link share object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid link share object provided."
|
||||
// @Failure 403 {object} web.HTTPError "Not allowed to add the list share."
|
||||
// @Failure 404 {object} web.HTTPError "The list does not exist."
|
||||
|
@ -129,7 +135,19 @@ func (share *LinkSharing) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
|
||||
share.SharedByID = a.GetID()
|
||||
share.Hash = utils.MakeRandomString(40)
|
||||
|
||||
if share.Password != "" {
|
||||
share.SharingType = SharingTypeWithPassword
|
||||
share.Password, err = user.HashPassword(share.Password)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
share.SharingType = SharingTypeWithoutPassword
|
||||
}
|
||||
|
||||
_, err = s.Insert(share)
|
||||
share.Password = ""
|
||||
share.SharedBy, _ = user.GetFromAuth(a)
|
||||
return
|
||||
}
|
||||
|
@ -156,6 +174,7 @@ func (share *LinkSharing) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
|||
if !exists {
|
||||
return ErrListShareDoesNotExist{ID: share.ID, Hash: share.Hash}
|
||||
}
|
||||
share.Password = ""
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -212,6 +231,7 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
|
||||
for _, s := range shares {
|
||||
s.SharedBy = users[s.SharedByID]
|
||||
s.Password = ""
|
||||
}
|
||||
|
||||
// Total count
|
||||
|
@ -287,3 +307,20 @@ func GetLinkSharesByIDs(s *xorm.Session, ids []int64) (shares map[int64]*LinkSha
|
|||
err = s.In("id", ids).Find(&shares)
|
||||
return
|
||||
}
|
||||
|
||||
// VerifyLinkSharePassword checks if a password of a link share matches a provided one.
|
||||
func VerifyLinkSharePassword(share *LinkSharing, password string) (err error) {
|
||||
if password == "" {
|
||||
return &ErrLinkSharePasswordRequired{ShareID: share.ID}
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(share.Password), []byte(password))
|
||||
if err != nil {
|
||||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
return &ErrLinkSharePasswordInvalid{ShareID: share.ID}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLinkSharing_Create(t *testing.T) {
|
||||
doer := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
Right: RightRead,
|
||||
}
|
||||
err := share.Create(s, doer)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, share.Hash)
|
||||
assert.NotEmpty(t, share.ID)
|
||||
assert.Equal(t, SharingTypeWithoutPassword, share.SharingType)
|
||||
db.AssertExists(t, "link_shares", map[string]interface{}{
|
||||
"id": share.ID,
|
||||
}, false)
|
||||
})
|
||||
t.Run("invalid right", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
Right: Right(123),
|
||||
}
|
||||
err := share.Create(s, doer)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrInvalidRight(err))
|
||||
})
|
||||
t.Run("password should be hashed", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
Right: RightRead,
|
||||
Password: "somePassword",
|
||||
}
|
||||
err := share.Create(s, doer)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, share.Hash)
|
||||
assert.NotEmpty(t, share.ID)
|
||||
assert.Empty(t, share.Password)
|
||||
db.AssertExists(t, "link_shares", map[string]interface{}{
|
||||
"id": share.ID,
|
||||
"sharing_type": SharingTypeWithPassword,
|
||||
}, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinkSharing_ReadAll(t *testing.T) {
|
||||
doer := &user.User{ID: 1}
|
||||
|
||||
t.Run("all no password", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
}
|
||||
all, _, _, err := share.ReadAll(s, doer, "", 1, -1)
|
||||
shares := all.([]*LinkSharing)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, shares, 2)
|
||||
for _, sharing := range shares {
|
||||
assert.Empty(t, sharing.Password)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinkSharing_ReadOne(t *testing.T) {
|
||||
doer := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ID: 1,
|
||||
}
|
||||
err := share.ReadOne(s, doer)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, share.Hash)
|
||||
assert.Equal(t, SharingTypeWithoutPassword, share.SharingType)
|
||||
})
|
||||
t.Run("with password", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ID: 4,
|
||||
}
|
||||
err := share.ReadOne(s, doer)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, share.Hash)
|
||||
assert.Equal(t, SharingTypeWithPassword, share.SharingType)
|
||||
assert.Empty(t, share.Password)
|
||||
})
|
||||
}
|
|
@ -640,7 +640,7 @@ func updateListByTaskID(s *xorm.Session, taskID int64) (err error) {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param list body models.List true "The list you want to create."
|
||||
// @Success 200 {object} models.List "The created list."
|
||||
// @Success 201 {object} models.List "The created list."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
|
|
|
@ -61,7 +61,7 @@ func (ld *ListDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool,
|
|||
// @Security JWTKeyAuth
|
||||
// @Param listID path int true "The list ID to duplicate"
|
||||
// @Param list body models.ListDuplicate true "The target namespace which should hold the copied list."
|
||||
// @Success 200 {object} models.ListDuplicate "The created list."
|
||||
// @Success 201 {object} models.ListDuplicate "The created list."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid list duplicate object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list or namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
|
@ -107,12 +107,111 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
|
||||
log.Debugf("Duplicated all buckets from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
err = duplicateTasks(s, doer, ld, bucketMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Background files + unsplash info
|
||||
if ld.List.BackgroundFileID != 0 {
|
||||
|
||||
log.Debugf("Duplicating background %d from list %d into %d", ld.List.BackgroundFileID, ld.ListID, ld.List.ID)
|
||||
|
||||
f := &files.File{ID: ld.List.BackgroundFileID}
|
||||
if err := f.LoadFileMetaByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.LoadFileByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.File.Close()
|
||||
|
||||
file, err := files.Create(f.File, f.Name, f.Size, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get unsplash info if applicable
|
||||
up, err := GetUnsplashPhotoByFileID(s, ld.List.BackgroundFileID)
|
||||
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
|
||||
return err
|
||||
}
|
||||
if up != nil {
|
||||
up.ID = 0
|
||||
up.FileID = file.ID
|
||||
if err := up.Save(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := SetListBackground(s, ld.List.ID, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated list background from list %d into %d", ld.ListID, ld.List.ID)
|
||||
}
|
||||
|
||||
// Rights / Shares
|
||||
// To keep it simple(r) we will only copy rights which are directly used with the list, no namespace changes.
|
||||
users := []*ListUser{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, u := range users {
|
||||
u.ID = 0
|
||||
u.ListID = ld.List.ID
|
||||
if _, err := s.Insert(u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated user shares from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
teams := []*TeamList{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&teams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, t := range teams {
|
||||
t.ID = 0
|
||||
t.ListID = ld.List.ID
|
||||
if _, err := s.Insert(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new link shares if any are available
|
||||
linkShares := []*LinkSharing{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&linkShares)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, share := range linkShares {
|
||||
share.ID = 0
|
||||
share.ListID = ld.List.ID
|
||||
share.Hash = utils.MakeRandomString(40)
|
||||
if _, err := s.Insert(share); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all link shares from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap map[int64]int64) (err error) {
|
||||
// Get all tasks + all task details
|
||||
tasks, _, _, err := getTasksForLists(s, []*List{{ID: ld.ListID}}, doer, &taskOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This map contains the old task id as key and the new duplicated task id as value.
|
||||
// It is used to map old task items to new ones.
|
||||
taskMap := make(map[int64]int64)
|
||||
|
@ -255,91 +354,5 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
|
||||
log.Debugf("Duplicated all task relations from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
// Background files + unsplash info
|
||||
if ld.List.BackgroundFileID != 0 {
|
||||
|
||||
log.Debugf("Duplicating background %d from list %d into %d", ld.List.BackgroundFileID, ld.ListID, ld.List.ID)
|
||||
|
||||
f := &files.File{ID: ld.List.BackgroundFileID}
|
||||
if err := f.LoadFileMetaByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.LoadFileByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.File.Close()
|
||||
|
||||
file, err := files.Create(f.File, f.Name, f.Size, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get unsplash info if applicable
|
||||
up, err := GetUnsplashPhotoByFileID(s, ld.List.BackgroundFileID)
|
||||
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
|
||||
return err
|
||||
}
|
||||
if up != nil {
|
||||
up.ID = 0
|
||||
up.FileID = file.ID
|
||||
if err := up.Save(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := SetListBackground(s, ld.List.ID, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated list background from list %d into %d", ld.ListID, ld.List.ID)
|
||||
}
|
||||
|
||||
// Rights / Shares
|
||||
// To keep it simple(r) we will only copy rights which are directly used with the list, no namespace changes.
|
||||
users := []*ListUser{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, u := range users {
|
||||
u.ID = 0
|
||||
u.ListID = ld.List.ID
|
||||
if _, err := s.Insert(u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated user shares from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
teams := []*TeamList{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&teams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, t := range teams {
|
||||
t.ID = 0
|
||||
t.ListID = ld.List.ID
|
||||
if _, err := s.Insert(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new link shares if any are available
|
||||
linkShares := []*LinkSharing{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&linkShares)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, share := range linkShares {
|
||||
share.ID = 0
|
||||
share.ListID = ld.List.ID
|
||||
share.Hash = utils.MakeRandomString(40)
|
||||
if _, err := s.Insert(share); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all link shares from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ type TeamWithRight struct {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Param list body models.TeamList true "The team you want to add to the list."
|
||||
// @Success 200 {object} models.TeamList "The created team<->list relation."
|
||||
// @Success 201 {object} models.TeamList "The created team<->list relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid team list object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The team does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
|
|
|
@ -68,7 +68,7 @@ type UserWithRight struct {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Param list body models.ListUser true "The user you want to add to the list."
|
||||
// @Success 200 {object} models.ListUser "The created user<->list relation."
|
||||
// @Success 201 {object} models.ListUser "The created user<->list relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid user list object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The user does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
|
|
|
@ -177,26 +177,28 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
want: []*UserWithRight{
|
||||
{
|
||||
User: user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
},
|
||||
{
|
||||
User: user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
},
|
||||
|
|
|
@ -291,6 +291,11 @@ func getNamespaceOwnerIDs(namespaces map[int64]*NamespaceWithLists) (namespaceID
|
|||
}
|
||||
|
||||
func getNamespaceSubscriptions(s *xorm.Session, namespaceIDs []int64, userID int64) (map[int64]*Subscription, error) {
|
||||
subscriptionsMap := make(map[int64]*Subscription)
|
||||
if len(namespaceIDs) == 0 {
|
||||
return subscriptionsMap, nil
|
||||
}
|
||||
|
||||
subscriptions := []*Subscription{}
|
||||
err := s.
|
||||
Where("entity_type = ? AND user_id = ?", SubscriptionEntityNamespace, userID).
|
||||
|
@ -299,7 +304,6 @@ func getNamespaceSubscriptions(s *xorm.Session, namespaceIDs []int64, userID int
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subscriptionsMap := make(map[int64]*Subscription)
|
||||
for _, sub := range subscriptions {
|
||||
sub.Entity = sub.EntityType.String()
|
||||
subscriptionsMap[sub.EntityID] = sub
|
||||
|
@ -483,6 +487,10 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
|
||||
namespaceIDs, ownerIDs := getNamespaceOwnerIDs(namespaces)
|
||||
|
||||
if len(namespaceIDs) == 0 {
|
||||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
subscriptionsMap, err := getNamespaceSubscriptions(s, namespaceIDs, doer.ID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
|
@ -570,7 +578,7 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param namespace body models.Namespace true "The namespace you want to create."
|
||||
// @Success 200 {object} models.Namespace "The created namespace."
|
||||
// @Success 201 {object} models.Namespace "The created namespace."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid namespace object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
|
|
|
@ -59,7 +59,7 @@ func (TeamNamespace) TableName() string {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Param namespace body models.TeamNamespace true "The team you want to add to the namespace."
|
||||
// @Success 200 {object} models.TeamNamespace "The created team<->namespace relation."
|
||||
// @Success 201 {object} models.TeamNamespace "The created team<->namespace relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid team namespace object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The team does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The team does not have access to the namespace"
|
||||
|
|
|
@ -346,4 +346,14 @@ func TestNamespace_ReadAll(t *testing.T) {
|
|||
// Assert the first namespace is not the favorites namespace
|
||||
assert.NotEqual(t, SavedFiltersPseudoNamespace.ID, namespaces[0].ID)
|
||||
})
|
||||
t.Run("no results", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user1, "some search string which will never return results", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, nn)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ func (NamespaceUser) TableName() string {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Namespace ID"
|
||||
// @Param namespace body models.NamespaceUser true "The user you want to add to the namespace."
|
||||
// @Success 200 {object} models.NamespaceUser "The created user<->namespace relation."
|
||||
// @Success 201 {object} models.NamespaceUser "The created user<->namespace relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid user namespace object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The user does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
|
|
|
@ -176,26 +176,28 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
|
|||
want: []*UserWithRight{
|
||||
{
|
||||
User: user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
},
|
||||
{
|
||||
User: user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
},
|
||||
|
|
|
@ -20,6 +20,9 @@ import (
|
|||
"bufio"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
|
@ -184,3 +187,64 @@ func (n *TeamMemberAddedNotification) ToDB() interface{} {
|
|||
func (n *TeamMemberAddedNotification) Name() string {
|
||||
return "team.member.added"
|
||||
}
|
||||
|
||||
// UndoneTaskOverdueNotification represents a UndoneTaskOverdueNotification notification
|
||||
type UndoneTaskOverdueNotification struct {
|
||||
User *user.User
|
||||
Task *Task
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for UndoneTaskOverdueNotification
|
||||
func (n *UndoneTaskOverdueNotification) ToMail() *notifications.Mail {
|
||||
until := time.Until(n.Task.DueDate).Round(1*time.Hour) * -1
|
||||
return notifications.NewMail().
|
||||
Subject(`Task "`+n.Task.Title+`" is overdue`).
|
||||
Greeting("Hi "+n.User.GetName()+",").
|
||||
Line(`This is a friendly reminder of the task "`+n.Task.Title+`" which is overdue since `+utils.HumanizeDuration(until)+` and not yet done.`).
|
||||
Action("Open Task", config.ServiceFrontendurl.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)).
|
||||
Line("Have a nice day!")
|
||||
}
|
||||
|
||||
// ToDB returns the UndoneTaskOverdueNotification notification in a format which can be saved in the db
|
||||
func (n *UndoneTaskOverdueNotification) ToDB() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of the notification
|
||||
func (n *UndoneTaskOverdueNotification) Name() string {
|
||||
return "task.undone.overdue"
|
||||
}
|
||||
|
||||
// UndoneTasksOverdueNotification represents a UndoneTasksOverdueNotification notification
|
||||
type UndoneTasksOverdueNotification struct {
|
||||
User *user.User
|
||||
Tasks []*Task
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for UndoneTasksOverdueNotification
|
||||
func (n *UndoneTasksOverdueNotification) ToMail() *notifications.Mail {
|
||||
|
||||
overdueLine := ""
|
||||
for _, task := range n.Tasks {
|
||||
until := time.Until(task.DueDate).Round(1*time.Hour) * -1
|
||||
overdueLine += `* [` + task.Title + `](` + config.ServiceFrontendurl.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `), overdue since ` + utils.HumanizeDuration(until) + "\n"
|
||||
}
|
||||
|
||||
return notifications.NewMail().
|
||||
Subject(`Your overdue tasks`).
|
||||
Greeting("Hi "+n.User.GetName()+",").
|
||||
Line("You have the following overdue tasks:").
|
||||
Line(overdueLine).
|
||||
Action("Open Vikunja", config.ServiceFrontendurl.GetString()).
|
||||
Line("Have a nice day!")
|
||||
}
|
||||
|
||||
// ToDB returns the UndoneTasksOverdueNotification notification in a format which can be saved in the db
|
||||
func (n *UndoneTasksOverdueNotification) ToDB() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of the notification
|
||||
func (n *UndoneTasksOverdueNotification) Name() string {
|
||||
return "task.undone.overdue"
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ func (sf *SavedFilter) toList() *List {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.SavedFilter "The Saved Filter"
|
||||
// @Success 201 {object} models.SavedFilter "The Saved Filter"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters [put]
|
||||
|
|
|
@ -114,7 +114,7 @@ func (et SubscriptionEntityType) validate() error {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param entity path string true "The entity the user subscribes to. Can be either `namespace`, `list` or `task`."
|
||||
// @Param entityID path string true "The numeric id of the entity to subscribe to."
|
||||
// @Success 200 {object} models.Subscription "The subscription"
|
||||
// @Success 201 {object} models.Subscription "The subscription"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
|
||||
// @Failure 412 {object} web.HTTPError "The subscription already exists."
|
||||
// @Failure 412 {object} web.HTTPError "The subscription entity is invalid."
|
||||
|
|
|
@ -187,7 +187,7 @@ func (la *TaskAssginee) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param assignee body models.TaskAssginee true "The assingee object"
|
||||
// @Param taskID path int true "Task ID"
|
||||
// @Success 200 {object} models.TaskAssginee "The created assingee object."
|
||||
// @Success 201 {object} models.TaskAssginee "The created assingee object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid assignee object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/assignees [put]
|
||||
|
@ -308,7 +308,7 @@ type BulkAssignees struct {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param assignee body models.BulkAssignees true "The array of assignees"
|
||||
// @Param taskID path int true "Task ID"
|
||||
// @Success 200 {object} models.TaskAssginee "The created assingees object."
|
||||
// @Success 201 {object} models.TaskAssginee "The created assingees object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid assignee object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/assignees/bulk [post]
|
||||
|
|
|
@ -31,33 +31,36 @@ import (
|
|||
func TestTaskCollection_ReadAll(t *testing.T) {
|
||||
// Dummy users
|
||||
user1 := &user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
user2 := &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
user6 := &user.User{
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
IsActive: true,
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
IsActive: true,
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
linkShareUser2 := &user.User{
|
||||
ID: -2,
|
||||
|
|
|
@ -56,7 +56,7 @@ func (tc *TaskComment) TableName() string {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param relation body models.TaskComment true "The task comment object"
|
||||
// @Param taskID path int true "Task ID"
|
||||
// @Success 200 {object} models.TaskComment "The created task comment object."
|
||||
// @Success 201 {object} models.TaskComment "The created task comment object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid task comment object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/comments [put]
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/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/utils"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (taskIDs []int64, err error) {
|
||||
now = utils.GetTimeWithoutNanoSeconds(now)
|
||||
|
||||
var tasks []*Task
|
||||
err = s.
|
||||
Where("due_date is not null and due_date < ?", now.Format(dbTimeFormat)).
|
||||
And("done = false").
|
||||
Find(&tasks)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
taskIDs = append(taskIDs, task.ID)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type userWithTasks struct {
|
||||
user *user.User
|
||||
tasks []*Task
|
||||
}
|
||||
|
||||
// RegisterOverdueReminderCron registers a function which checks once a day for tasks that are overdue and not done.
|
||||
func RegisterOverdueReminderCron() {
|
||||
if !config.ServiceEnableEmailReminders.GetBool() {
|
||||
return
|
||||
}
|
||||
|
||||
if !config.MailerEnabled.GetBool() {
|
||||
log.Info("Mailer is disabled, not sending overdue per mail")
|
||||
return
|
||||
}
|
||||
|
||||
err := cron.Schedule("0 8 * * *", func() {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
now := time.Now()
|
||||
taskIDs, 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)
|
||||
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))
|
||||
|
||||
for _, ut := range uts {
|
||||
var n notifications.Notification = &UndoneTasksOverdueNotification{
|
||||
User: ut.user,
|
||||
Tasks: ut.tasks,
|
||||
}
|
||||
|
||||
if len(ut.tasks) == 1 {
|
||||
n = &UndoneTaskOverdueNotification{
|
||||
User: ut.user,
|
||||
Task: ut.tasks[0],
|
||||
}
|
||||
}
|
||||
|
||||
err = notifications.Notify(ut.user, n)
|
||||
if err != nil {
|
||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not notify user %d: %s", ut.user.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Fatalf("Could not register undone overdue tasks reminder cron: %s", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetUndoneOverDueTasks(t *testing.T) {
|
||||
t.Run("no undone tasks", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z")
|
||||
assert.NoError(t, err)
|
||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, taskIDs, 0)
|
||||
})
|
||||
t.Run("undone overdue", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T01:13:00Z")
|
||||
assert.NoError(t, err)
|
||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, taskIDs, 1)
|
||||
assert.Equal(t, int64(6), taskIDs[0])
|
||||
})
|
||||
t.Run("done overdue", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z")
|
||||
assert.NoError(t, err)
|
||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, taskIDs, 0)
|
||||
})
|
||||
}
|
|
@ -114,7 +114,7 @@ type RelatedTaskMap map[RelationKind][]*Task
|
|||
// @Security JWTKeyAuth
|
||||
// @Param relation body models.TaskRelation true "The relation object"
|
||||
// @Param taskID path int true "Task ID"
|
||||
// @Success 200 {object} models.TaskRelation "The created task relation object."
|
||||
// @Success 201 {object} models.TaskRelation "The created task relation object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid task relation object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/relations [put]
|
||||
|
|
|
@ -19,6 +19,9 @@ package models
|
|||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"xorm.io/builder"
|
||||
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
@ -48,7 +51,9 @@ type taskUser struct {
|
|||
User *user.User `xorm:"extends"`
|
||||
}
|
||||
|
||||
func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64) (taskUsers []*taskUser, err error) {
|
||||
const dbTimeFormat = `2006-01-02 15:04:05`
|
||||
|
||||
func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (taskUsers []*taskUser, err error) {
|
||||
if len(taskIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -59,7 +64,7 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64) (taskUsers []*taskUs
|
|||
Select("users.id, users.username, users.email, users.name").
|
||||
Join("LEFT", "tasks", "tasks.created_by_id = users.id").
|
||||
In("tasks.id", taskIDs).
|
||||
Where("users.email_reminders_enabled = true").
|
||||
Where(cond).
|
||||
GroupBy("tasks.id, users.id, users.username, users.email, users.name").
|
||||
Find(&creators)
|
||||
if err != nil {
|
||||
|
@ -84,15 +89,18 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64) (taskUsers []*taskUs
|
|||
})
|
||||
}
|
||||
|
||||
assignees, err := getRawTaskAssigneesForTasks(s, taskIDs)
|
||||
var assignees []*TaskAssigneeWithUser
|
||||
err = s.Table("task_assignees").
|
||||
Select("task_id, users.*").
|
||||
In("task_id", taskIDs).
|
||||
Join("INNER", "users", "task_assignees.user_id = users.id").
|
||||
Where(cond).
|
||||
Find(&assignees)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, assignee := range assignees {
|
||||
if !assignee.EmailRemindersEnabled { // Can't filter that through a query directly since we're using another function
|
||||
continue
|
||||
}
|
||||
taskUsers = append(taskUsers, &taskUser{
|
||||
Task: taskMap[assignee.TaskID],
|
||||
User: &assignee.User,
|
||||
|
@ -103,13 +111,8 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64) (taskUsers []*taskUs
|
|||
}
|
||||
|
||||
func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskIDs []int64, err error) {
|
||||
now = utils.GetTimeWithoutNanoSeconds(now)
|
||||
|
||||
tz := config.GetTimeZone()
|
||||
const dbFormat = `2006-01-02 15:04:05`
|
||||
|
||||
// By default, time.Now() includes nanoseconds which we don't save. That results in getting the wrong dates,
|
||||
// so we make sure the time we use to get the reminders don't contain nanoseconds.
|
||||
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, now.Location()).In(tz)
|
||||
nextMinute := now.Add(1 * time.Minute)
|
||||
|
||||
log.Debugf("[Task Reminder Cron] Looking for reminders between %s and %s to send...", now, nextMinute)
|
||||
|
@ -117,7 +120,7 @@ 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(dbFormat), nextMinute.Format(dbFormat)).
|
||||
Where("reminder >= ? and reminder < ?", now.Format(dbTimeFormat), nextMinute.Format(dbTimeFormat)).
|
||||
And("tasks.done = false").
|
||||
Find(&reminders)
|
||||
if err != nil {
|
||||
|
@ -154,9 +157,9 @@ func RegisterReminderCron() {
|
|||
|
||||
log.Debugf("[Task Reminder Cron] Timezone is %s", tz)
|
||||
|
||||
s := db.NewSession()
|
||||
|
||||
err := cron.Schedule("* * * * *", func() {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
now := time.Now()
|
||||
taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now)
|
||||
|
@ -169,7 +172,7 @@ func RegisterReminderCron() {
|
|||
return
|
||||
}
|
||||
|
||||
users, err := getTaskUsersForTasks(s, taskIDs)
|
||||
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
|
||||
|
|
|
@ -36,6 +36,14 @@ import (
|
|||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
type TaskRepeatMode int
|
||||
|
||||
const (
|
||||
TaskRepeatModeDefault TaskRepeatMode = iota
|
||||
TaskRepeatModeMonth
|
||||
TaskRepeatModeFromCurrentDate
|
||||
)
|
||||
|
||||
// Task represents an task in a todolist
|
||||
type Task struct {
|
||||
// The unique, numeric id of this task.
|
||||
|
@ -57,8 +65,8 @@ type Task struct {
|
|||
ListID int64 `xorm:"bigint INDEX not null" json:"list_id" param:"list"`
|
||||
// An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount.
|
||||
RepeatAfter int64 `xorm:"bigint INDEX null" json:"repeat_after"`
|
||||
// If specified, a repeating task will repeat from the current date rather than the last set date.
|
||||
RepeatFromCurrentDate bool `xorm:"null" json:"repeat_from_current_date"`
|
||||
// Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.
|
||||
RepeatMode TaskRepeatMode `xorm:"not null default 0" json:"repeat_mode"`
|
||||
// The task priority. Can be anything you want, it is possible to sort by this later.
|
||||
Priority int64 `xorm:"bigint null" json:"priority"`
|
||||
// When this task starts.
|
||||
|
@ -793,7 +801,7 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke
|
|||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Param task body models.Task true "The task object"
|
||||
// @Success 200 {object} models.Task "The created task object."
|
||||
// @Success 201 {object} models.Task "The created task object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid task object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
|
@ -942,18 +950,18 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
"list_id",
|
||||
"bucket_id",
|
||||
"position",
|
||||
"repeat_from_current_date",
|
||||
"is_favorite",
|
||||
"repeat_mode",
|
||||
}
|
||||
|
||||
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
|
||||
updateDone(&ot, t)
|
||||
|
||||
err = setTaskBucket(s, t, &ot, t.BucketID != ot.BucketID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
|
||||
updateDone(&ot, t)
|
||||
|
||||
// If the task is being moved between lists, make sure to move the bucket + index as well
|
||||
if t.ListID != 0 && ot.ListID != t.ListID {
|
||||
latestTask := &Task{}
|
||||
|
@ -1035,8 +1043,8 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
ot.Position = 0
|
||||
}
|
||||
// Repeat from current date
|
||||
if !t.RepeatFromCurrentDate {
|
||||
ot.RepeatFromCurrentDate = false
|
||||
if t.RepeatMode == TaskRepeatModeDefault {
|
||||
ot.RepeatMode = TaskRepeatModeDefault
|
||||
}
|
||||
// Is Favorite
|
||||
if !t.IsFavorite {
|
||||
|
@ -1071,93 +1079,156 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
return updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
}
|
||||
|
||||
func addOneMonthToDate(d time.Time) time.Time {
|
||||
return time.Date(d.Year(), d.Month()+1, d.Day(), d.Hour(), d.Minute(), d.Second(), d.Nanosecond(), config.GetTimeZone())
|
||||
}
|
||||
|
||||
func setTaskDatesDefault(oldTask, newTask *Task) {
|
||||
if oldTask.RepeatAfter == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Current time in an extra variable to base all calculations on the same time
|
||||
now := time.Now()
|
||||
|
||||
repeatDuration := time.Duration(oldTask.RepeatAfter) * time.Second
|
||||
|
||||
// assuming we'll merge the new task over the old task
|
||||
if !oldTask.DueDate.IsZero() {
|
||||
// Always add one instance of the repeating interval to catch cases where a due date is already in the future
|
||||
// but not the repeating interval
|
||||
newTask.DueDate = oldTask.DueDate.Add(repeatDuration)
|
||||
// Add the repeating interval until the new due date is in the future
|
||||
for !newTask.DueDate.After(now) {
|
||||
newTask.DueDate = newTask.DueDate.Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
|
||||
newTask.Reminders = oldTask.Reminders
|
||||
// When repeating from the current date, all reminders should keep their difference to each other.
|
||||
// To make this easier, we sort them first because we can then rely on the fact the first is the smallest
|
||||
if len(oldTask.Reminders) > 0 {
|
||||
for in, r := range oldTask.Reminders {
|
||||
newTask.Reminders[in] = r.Add(repeatDuration)
|
||||
for !newTask.Reminders[in].After(now) {
|
||||
newTask.Reminders[in] = newTask.Reminders[in].Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a task has a start and end date, the end date should keep the difference to the start date when setting them as new
|
||||
if !oldTask.StartDate.IsZero() {
|
||||
newTask.StartDate = oldTask.StartDate.Add(repeatDuration)
|
||||
for !newTask.StartDate.After(now) {
|
||||
newTask.StartDate = newTask.StartDate.Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
|
||||
if !oldTask.EndDate.IsZero() {
|
||||
newTask.EndDate = oldTask.EndDate.Add(repeatDuration)
|
||||
for !newTask.EndDate.After(now) {
|
||||
newTask.EndDate = newTask.EndDate.Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
|
||||
newTask.Done = false
|
||||
}
|
||||
|
||||
func setTaskDatesMonthRepeat(oldTask, newTask *Task) {
|
||||
if !oldTask.DueDate.IsZero() {
|
||||
newTask.DueDate = addOneMonthToDate(oldTask.DueDate)
|
||||
}
|
||||
|
||||
newTask.Reminders = oldTask.Reminders
|
||||
if len(oldTask.Reminders) > 0 {
|
||||
for in, r := range oldTask.Reminders {
|
||||
newTask.Reminders[in] = addOneMonthToDate(r)
|
||||
}
|
||||
}
|
||||
|
||||
if !oldTask.StartDate.IsZero() && !oldTask.EndDate.IsZero() {
|
||||
diff := oldTask.EndDate.Sub(oldTask.StartDate)
|
||||
newTask.StartDate = addOneMonthToDate(oldTask.StartDate)
|
||||
newTask.EndDate = newTask.StartDate.Add(diff)
|
||||
} else {
|
||||
if !oldTask.StartDate.IsZero() {
|
||||
newTask.StartDate = addOneMonthToDate(oldTask.StartDate)
|
||||
}
|
||||
|
||||
if !oldTask.EndDate.IsZero() {
|
||||
newTask.EndDate = addOneMonthToDate(oldTask.EndDate)
|
||||
}
|
||||
}
|
||||
|
||||
newTask.Done = false
|
||||
}
|
||||
|
||||
func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
|
||||
if oldTask.RepeatAfter == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Current time in an extra variable to base all calculations on the same time
|
||||
now := time.Now()
|
||||
|
||||
repeatDuration := time.Duration(oldTask.RepeatAfter) * time.Second
|
||||
|
||||
// assuming we'll merge the new task over the old task
|
||||
if !oldTask.DueDate.IsZero() {
|
||||
newTask.DueDate = now.Add(repeatDuration)
|
||||
}
|
||||
|
||||
newTask.Reminders = oldTask.Reminders
|
||||
// When repeating from the current date, all reminders should keep their difference to each other.
|
||||
// To make this easier, we sort them first because we can then rely on the fact the first is the smallest
|
||||
if len(oldTask.Reminders) > 0 {
|
||||
sort.Slice(oldTask.Reminders, func(i, j int) bool {
|
||||
return oldTask.Reminders[i].Unix() < oldTask.Reminders[j].Unix()
|
||||
})
|
||||
first := oldTask.Reminders[0]
|
||||
for in, r := range oldTask.Reminders {
|
||||
diff := r.Sub(first)
|
||||
newTask.Reminders[in] = now.Add(repeatDuration + diff)
|
||||
}
|
||||
}
|
||||
|
||||
// If a task has a start and end date, the end date should keep the difference to the start date when setting them as new
|
||||
if !oldTask.StartDate.IsZero() && !oldTask.EndDate.IsZero() {
|
||||
diff := oldTask.EndDate.Sub(oldTask.StartDate)
|
||||
newTask.StartDate = now.Add(repeatDuration)
|
||||
newTask.EndDate = now.Add(repeatDuration + diff)
|
||||
} else {
|
||||
if !oldTask.StartDate.IsZero() {
|
||||
newTask.StartDate = now.Add(repeatDuration)
|
||||
}
|
||||
|
||||
if !oldTask.EndDate.IsZero() {
|
||||
newTask.EndDate = now.Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
|
||||
newTask.Done = false
|
||||
}
|
||||
|
||||
// This helper function updates the reminders, doneAt, start and end dates of the *old* task
|
||||
// and saves the new values in the newTask object.
|
||||
// We make a few assumtions here:
|
||||
// 1. Everything in oldTask is the truth - we figure out if we update anything at all if oldTask.RepeatAfter has a value > 0
|
||||
// 2. Because of 1., this functions should not be used to update values other than Done in the same go
|
||||
func updateDone(oldTask *Task, newTask *Task) {
|
||||
if !oldTask.Done && newTask.Done && oldTask.RepeatAfter > 0 {
|
||||
|
||||
repeatDuration := time.Duration(oldTask.RepeatAfter) * time.Second
|
||||
|
||||
// Current time in an extra variable to base all calculations on the same time
|
||||
now := time.Now()
|
||||
|
||||
// assuming we'll merge the new task over the old task
|
||||
if !oldTask.DueDate.IsZero() {
|
||||
if oldTask.RepeatFromCurrentDate {
|
||||
newTask.DueDate = now.Add(repeatDuration)
|
||||
} else {
|
||||
// Always add one instance of the repeating interval to catch cases where a due date is already in the future
|
||||
// but not the repeating interval
|
||||
newTask.DueDate = oldTask.DueDate.Add(repeatDuration)
|
||||
// Add the repeating interval until the new due date is in the future
|
||||
for !newTask.DueDate.After(now) {
|
||||
newTask.DueDate = newTask.DueDate.Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newTask.Reminders = oldTask.Reminders
|
||||
// When repeating from the current date, all reminders should keep their difference to each other.
|
||||
// To make this easier, we sort them first because we can then rely on the fact the first is the smallest
|
||||
if len(oldTask.Reminders) > 0 {
|
||||
if oldTask.RepeatFromCurrentDate {
|
||||
sort.Slice(oldTask.Reminders, func(i, j int) bool {
|
||||
return oldTask.Reminders[i].Unix() < oldTask.Reminders[j].Unix()
|
||||
})
|
||||
first := oldTask.Reminders[0]
|
||||
for in, r := range oldTask.Reminders {
|
||||
diff := r.Sub(first)
|
||||
newTask.Reminders[in] = now.Add(repeatDuration + diff)
|
||||
}
|
||||
} else {
|
||||
for in, r := range oldTask.Reminders {
|
||||
newTask.Reminders[in] = r.Add(repeatDuration)
|
||||
for !newTask.Reminders[in].After(now) {
|
||||
newTask.Reminders[in] = newTask.Reminders[in].Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a task has a start and end date, the end date should keep the difference to the start date when setting them as new
|
||||
if oldTask.RepeatFromCurrentDate && !oldTask.StartDate.IsZero() && !oldTask.EndDate.IsZero() {
|
||||
diff := oldTask.EndDate.Sub(oldTask.StartDate)
|
||||
newTask.StartDate = now.Add(repeatDuration)
|
||||
newTask.EndDate = now.Add(repeatDuration + diff)
|
||||
} else {
|
||||
if !oldTask.StartDate.IsZero() {
|
||||
if oldTask.RepeatFromCurrentDate {
|
||||
newTask.StartDate = now.Add(repeatDuration)
|
||||
} else {
|
||||
newTask.StartDate = oldTask.StartDate.Add(repeatDuration)
|
||||
for !newTask.StartDate.After(now) {
|
||||
newTask.StartDate = newTask.StartDate.Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !oldTask.EndDate.IsZero() {
|
||||
if oldTask.RepeatFromCurrentDate {
|
||||
newTask.EndDate = now.Add(repeatDuration)
|
||||
} else {
|
||||
newTask.EndDate = oldTask.EndDate.Add(repeatDuration)
|
||||
for !newTask.EndDate.After(now) {
|
||||
newTask.EndDate = newTask.EndDate.Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newTask.Done = false
|
||||
}
|
||||
|
||||
// Update the "done at" timestamp
|
||||
if !oldTask.Done && newTask.Done {
|
||||
switch oldTask.RepeatMode {
|
||||
case TaskRepeatModeMonth:
|
||||
setTaskDatesMonthRepeat(oldTask, newTask)
|
||||
case TaskRepeatModeFromCurrentDate:
|
||||
setTaskDatesFromCurrentDateRepeat(oldTask, newTask)
|
||||
case TaskRepeatModeDefault:
|
||||
setTaskDatesDefault(oldTask, newTask)
|
||||
}
|
||||
|
||||
newTask.DoneAt = time.Now()
|
||||
}
|
||||
|
||||
// When unmarking a task as done, reset the timestamp
|
||||
if oldTask.Done && !newTask.Done {
|
||||
newTask.DoneAt = time.Time{}
|
||||
|
|
|
@ -302,6 +302,28 @@ func TestTask_Update(t *testing.T) {
|
|||
"bucket_id": 4,
|
||||
}, false)
|
||||
})
|
||||
t.Run("repeating tasks should not be moved to the done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 28,
|
||||
Done: true,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, task.Done)
|
||||
assert.Equal(t, int64(1), task.BucketID)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 28,
|
||||
"done": false,
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTask_Delete(t *testing.T) {
|
||||
|
@ -345,6 +367,23 @@ func TestUpdateDone(t *testing.T) {
|
|||
updateDone(oldTask, newTask)
|
||||
assert.Equal(t, time.Time{}, newTask.DoneAt)
|
||||
})
|
||||
t.Run("no interval set, default repeat mode", func(t *testing.T) {
|
||||
dueDate := time.Unix(1550000000, 0)
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatAfter: 0,
|
||||
RepeatMode: TaskRepeatModeDefault,
|
||||
DueDate: dueDate,
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
DueDate: dueDate,
|
||||
}
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
assert.Equal(t, dueDate.Unix(), newTask.DueDate.Unix())
|
||||
assert.True(t, newTask.Done)
|
||||
})
|
||||
t.Run("repeating interval", func(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
|
@ -363,6 +402,7 @@ func TestUpdateDone(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, expected, newTask.DueDate)
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("don't update if due date is zero", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
|
@ -376,6 +416,7 @@ func TestUpdateDone(t *testing.T) {
|
|||
}
|
||||
updateDone(oldTask, newTask)
|
||||
assert.Equal(t, time.Unix(1543626724, 0), newTask.DueDate)
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("update reminders", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
|
@ -403,6 +444,7 @@ func TestUpdateDone(t *testing.T) {
|
|||
assert.Len(t, newTask.Reminders, 2)
|
||||
assert.Equal(t, expected1, newTask.Reminders[0])
|
||||
assert.Equal(t, expected2, newTask.Reminders[1])
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("update start date", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
|
@ -421,6 +463,7 @@ func TestUpdateDone(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, expected, newTask.StartDate)
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("update end date", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
|
@ -439,6 +482,7 @@ func TestUpdateDone(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, expected, newTask.EndDate)
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("ensure due date is repeated even if the original one is in the future", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
|
@ -452,14 +496,15 @@ func TestUpdateDone(t *testing.T) {
|
|||
updateDone(oldTask, newTask)
|
||||
expected := oldTask.DueDate.Add(time.Duration(oldTask.RepeatAfter) * time.Second)
|
||||
assert.Equal(t, expected, newTask.DueDate)
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("repeat from current date", func(t *testing.T) {
|
||||
t.Run("due date", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatFromCurrentDate: true,
|
||||
DueDate: time.Unix(1550000000, 0),
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatMode: TaskRepeatModeFromCurrentDate,
|
||||
DueDate: time.Unix(1550000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
|
@ -468,12 +513,13 @@ func TestUpdateDone(t *testing.T) {
|
|||
|
||||
// Only comparing unix timestamps because time.Time use nanoseconds which can't ever possibly have the same value
|
||||
assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.DueDate.Unix())
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("reminders", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatFromCurrentDate: true,
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatMode: TaskRepeatModeFromCurrentDate,
|
||||
Reminders: []time.Time{
|
||||
time.Unix(1550000000, 0),
|
||||
time.Unix(1555000000, 0),
|
||||
|
@ -490,13 +536,14 @@ func TestUpdateDone(t *testing.T) {
|
|||
// Only comparing unix timestamps because time.Time use nanoseconds which can't ever possibly have the same value
|
||||
assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[0].Unix())
|
||||
assert.Equal(t, time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[1].Unix())
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("start date", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatFromCurrentDate: true,
|
||||
StartDate: time.Unix(1550000000, 0),
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatMode: TaskRepeatModeFromCurrentDate,
|
||||
StartDate: time.Unix(1550000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
|
@ -505,13 +552,14 @@ func TestUpdateDone(t *testing.T) {
|
|||
|
||||
// Only comparing unix timestamps because time.Time use nanoseconds which can't ever possibly have the same value
|
||||
assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.StartDate.Unix())
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("end date", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatFromCurrentDate: true,
|
||||
EndDate: time.Unix(1560000000, 0),
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatMode: TaskRepeatModeFromCurrentDate,
|
||||
EndDate: time.Unix(1560000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
|
@ -520,14 +568,15 @@ func TestUpdateDone(t *testing.T) {
|
|||
|
||||
// Only comparing unix timestamps because time.Time use nanoseconds which can't ever possibly have the same value
|
||||
assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.EndDate.Unix())
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("start and end date", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatFromCurrentDate: true,
|
||||
StartDate: time.Unix(1550000000, 0),
|
||||
EndDate: time.Unix(1560000000, 0),
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatMode: TaskRepeatModeFromCurrentDate,
|
||||
StartDate: time.Unix(1550000000, 0),
|
||||
EndDate: time.Unix(1560000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
|
@ -539,6 +588,107 @@ func TestUpdateDone(t *testing.T) {
|
|||
// Only comparing unix timestamps because time.Time use nanoseconds which can't ever possibly have the same value
|
||||
assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.StartDate.Unix())
|
||||
assert.Equal(t, time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.EndDate.Unix())
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
})
|
||||
t.Run("repeat each month", func(t *testing.T) {
|
||||
t.Run("due date", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatMode: TaskRepeatModeMonth,
|
||||
DueDate: time.Unix(1550000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
}
|
||||
oldDueDate := oldTask.DueDate
|
||||
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
assert.True(t, newTask.DueDate.After(oldDueDate))
|
||||
assert.NotEqual(t, oldDueDate.Month(), newTask.DueDate.Month())
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("reminders", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatMode: TaskRepeatModeMonth,
|
||||
Reminders: []time.Time{
|
||||
time.Unix(1550000000, 0),
|
||||
time.Unix(1555000000, 0),
|
||||
},
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
}
|
||||
oldReminders := make([]time.Time, len(oldTask.Reminders))
|
||||
copy(oldReminders, oldTask.Reminders)
|
||||
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
assert.Len(t, newTask.Reminders, len(oldReminders))
|
||||
for i, r := range newTask.Reminders {
|
||||
assert.True(t, r.After(oldReminders[i]))
|
||||
assert.NotEqual(t, oldReminders[i].Month(), r.Month())
|
||||
}
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("start date", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatMode: TaskRepeatModeMonth,
|
||||
StartDate: time.Unix(1550000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
}
|
||||
oldStartDate := oldTask.StartDate
|
||||
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
assert.True(t, newTask.StartDate.After(oldStartDate))
|
||||
assert.NotEqual(t, oldStartDate.Month(), newTask.StartDate.Month())
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("end date", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatMode: TaskRepeatModeMonth,
|
||||
EndDate: time.Unix(1560000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
}
|
||||
oldEndDate := oldTask.EndDate
|
||||
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
assert.True(t, newTask.EndDate.After(oldEndDate))
|
||||
assert.NotEqual(t, oldEndDate.Month(), newTask.EndDate.Month())
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("start and end date", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatMode: TaskRepeatModeMonth,
|
||||
StartDate: time.Unix(1550000000, 0),
|
||||
EndDate: time.Unix(1560000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
}
|
||||
oldStartDate := oldTask.StartDate
|
||||
oldEndDate := oldTask.EndDate
|
||||
oldDiff := oldTask.EndDate.Sub(oldTask.StartDate)
|
||||
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
assert.True(t, newTask.StartDate.After(oldStartDate))
|
||||
assert.NotEqual(t, oldStartDate.Month(), newTask.StartDate.Month())
|
||||
assert.True(t, newTask.EndDate.After(oldEndDate))
|
||||
assert.NotEqual(t, oldEndDate.Month(), newTask.EndDate.Month())
|
||||
assert.Equal(t, oldDiff, newTask.EndDate.Sub(newTask.StartDate))
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -32,7 +32,7 @@ import (
|
|||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "Team ID"
|
||||
// @Param team body models.TeamMember true "The user to be added to a team."
|
||||
// @Success 200 {object} models.TeamMember "The newly created member object"
|
||||
// @Success 201 {object} models.TeamMember "The newly created member object"
|
||||
// @Failure 400 {object} web.HTTPError "Invalid member object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the team"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
|
|
|
@ -247,7 +247,7 @@ func (t *Team) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per
|
|||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param team body models.Team true "The team you want to create."
|
||||
// @Success 200 {object} models.Team "The created team."
|
||||
// @Success 201 {object} models.Team "The created team."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid team object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /teams [put]
|
||||
|
|
|
@ -26,139 +26,152 @@ import (
|
|||
|
||||
func TestListUsersFromList(t *testing.T) {
|
||||
testuser1 := &user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser2 := &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser3 := &user.User{
|
||||
ID: 3,
|
||||
Username: "user3",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
PasswordResetToken: "passwordresettesttoken",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 3,
|
||||
Username: "user3",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
PasswordResetToken: "passwordresettesttoken",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser4 := &user.User{
|
||||
ID: 4,
|
||||
Username: "user4",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: false,
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 4,
|
||||
Username: "user4",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: false,
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser5 := &user.User{
|
||||
ID: 5,
|
||||
Username: "user5",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: false,
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 5,
|
||||
Username: "user5",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: false,
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser6 := &user.User{
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser7 := &user.User{
|
||||
ID: 7,
|
||||
Username: "user7",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
DiscoverableByEmail: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 7,
|
||||
Username: "user7",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
DiscoverableByEmail: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser8 := &user.User{
|
||||
ID: 8,
|
||||
Username: "user8",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 8,
|
||||
Username: "user8",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser9 := &user.User{
|
||||
ID: 9,
|
||||
Username: "user9",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 9,
|
||||
Username: "user9",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser10 := &user.User{
|
||||
ID: 10,
|
||||
Username: "user10",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 10,
|
||||
Username: "user10",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser11 := &user.User{
|
||||
ID: 11,
|
||||
Username: "user11",
|
||||
Name: "Some one else",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 11,
|
||||
Username: "user11",
|
||||
Name: "Some one else",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser12 := &user.User{
|
||||
ID: 12,
|
||||
Username: "user12",
|
||||
Name: "Name with spaces",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
DiscoverableByName: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 12,
|
||||
Username: "user12",
|
||||
Name: "Name with spaces",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
DiscoverableByName: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser13 := &user.User{
|
||||
ID: 13,
|
||||
Username: "user13",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 13,
|
||||
Username: "user13",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
||||
type args struct {
|
||||
|
|
|
@ -23,6 +23,8 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/web/handler"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"xorm.io/xorm"
|
||||
|
||||
|
@ -44,25 +46,32 @@ type Callback struct {
|
|||
|
||||
// Provider is the structure of an OpenID Connect provider
|
||||
type Provider struct {
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
AuthURL string `json:"auth_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"-"`
|
||||
OpenIDProvider *oidc.Provider `json:"-"`
|
||||
Oauth2Config *oauth2.Config `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
OriginalAuthURL string `json:"-"`
|
||||
AuthURL string `json:"auth_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"-"`
|
||||
openIDProvider *oidc.Provider
|
||||
Oauth2Config *oauth2.Config `json:"-"`
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
|
||||
func (p *Provider) setOicdProvider() (err error) {
|
||||
p.openIDProvider, err = oidc.NewProvider(context.Background(), p.OriginalAuthURL)
|
||||
return err
|
||||
}
|
||||
|
||||
// HandleCallback handles the auth request callback after redirecting from the provider with an auth code
|
||||
// @Summary Authenticate a user with OpenID Connect
|
||||
// @Description After a redirect from the OpenID Connect provider to the frontend has been made with the authentication `code`, this endpoint can be used to obtain a jwt token for that user and thus log them in.
|
||||
|
@ -86,7 +95,7 @@ func HandleCallback(c echo.Context) error {
|
|||
provider, err := GetProvider(providerKey)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
if provider == nil {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{Message: "Provider does not exist"})
|
||||
|
@ -100,7 +109,8 @@ func HandleCallback(c echo.Context) error {
|
|||
|
||||
details := make(map[string]interface{})
|
||||
if err := json.Unmarshal(rerr.Body, &details); err != nil {
|
||||
return err
|
||||
log.Errorf("Error unmarshaling token for provider %s: %v", provider.Name, err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{
|
||||
|
@ -109,7 +119,7 @@ func HandleCallback(c echo.Context) error {
|
|||
})
|
||||
}
|
||||
|
||||
return err
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// Extract the ID Token from OAuth2 token.
|
||||
|
@ -118,19 +128,57 @@ func HandleCallback(c echo.Context) error {
|
|||
return c.JSON(http.StatusBadRequest, models.Message{Message: "Missing token"})
|
||||
}
|
||||
|
||||
verifier := provider.OpenIDProvider.Verifier(&oidc.Config{ClientID: provider.ClientID})
|
||||
verifier := provider.openIDProvider.Verifier(&oidc.Config{ClientID: provider.ClientID})
|
||||
|
||||
// Parse and verify ID Token payload.
|
||||
idToken, err := verifier.Verify(context.Background(), rawIDToken)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Errorf("Error verifying token for provider %s: %v", provider.Name, err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// Extract custom claims
|
||||
cl := &claims{}
|
||||
err = idToken.Claims(cl)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Errorf("Error getting token claims for provider %s: %v", provider.Name, err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
if cl.Email == "" || cl.Name == "" || cl.PreferredUsername == "" {
|
||||
info, err := provider.openIDProvider.UserInfo(context.Background(), provider.Oauth2Config.TokenSource(context.Background(), oauth2Token))
|
||||
if err != nil {
|
||||
log.Errorf("Error getting userinfo for provider %s: %v", provider.Name, err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
cl2 := &claims{}
|
||||
err = info.Claims(cl2)
|
||||
if err != nil {
|
||||
log.Errorf("Error parsing userinfo claims for provider %s: %v", provider.Name, err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
if cl.Email == "" {
|
||||
cl.Email = cl2.Email
|
||||
}
|
||||
|
||||
if cl.Name == "" {
|
||||
cl.Name = cl2.Name
|
||||
}
|
||||
|
||||
if cl.PreferredUsername == "" {
|
||||
cl.PreferredUsername = cl2.PreferredUsername
|
||||
}
|
||||
|
||||
if cl.PreferredUsername == "" && cl2.Nickname != "" {
|
||||
cl.PreferredUsername = cl2.Nickname
|
||||
}
|
||||
|
||||
if cl.Email == "" {
|
||||
log.Errorf("Claim does not contain an email address for provider %s", provider.Name)
|
||||
return handler.HandleHTTPError(&user.ErrNoOpenIDEmailProvided{}, c)
|
||||
}
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
|
@ -140,12 +188,13 @@ func HandleCallback(c echo.Context) error {
|
|||
u, err := getOrCreateUser(s, cl, idToken.Issuer, idToken.Subject)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return err
|
||||
log.Errorf("Error creating new user for provider %s: %v", provider.Name, err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// Create token
|
||||
|
|
|
@ -17,11 +17,12 @@
|
|||
package openid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
"github.com/coreos/go-oidc"
|
||||
|
@ -34,7 +35,8 @@ func GetAllProviders() (providers []*Provider, err error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
ps, exists, err := keyvalue.Get("openid_providers")
|
||||
providers = []*Provider{}
|
||||
exists, err := keyvalue.GetWithValue("openid_providers", &providers)
|
||||
if !exists {
|
||||
rawProviders := config.AuthOpenIDProviders.Get()
|
||||
if rawProviders == nil {
|
||||
|
@ -44,11 +46,26 @@ func GetAllProviders() (providers []*Provider, err error) {
|
|||
rawProvider := rawProviders.([]interface{})
|
||||
|
||||
for _, p := range rawProvider {
|
||||
pi := p.(map[interface{}]interface{})
|
||||
var pi map[string]interface{}
|
||||
var is bool
|
||||
pi, is = p.(map[string]interface{})
|
||||
// JSON config is a map[string]interface{}, other providers are not. Under the hood they are all strings so
|
||||
// it is save to cast.
|
||||
if !is {
|
||||
pis := p.(map[interface{}]interface{})
|
||||
pi = make(map[string]interface{}, len(pis))
|
||||
for i, s := range pis {
|
||||
pi[i.(string)] = s
|
||||
}
|
||||
}
|
||||
|
||||
provider, err := getProviderFromMap(pi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if provider != nil {
|
||||
log.Errorf("Error while getting openid provider %s: %s", provider.Name, err)
|
||||
}
|
||||
log.Errorf("Error while getting openid provider: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
providers = append(providers, provider)
|
||||
|
@ -62,31 +79,30 @@ func GetAllProviders() (providers []*Provider, err error) {
|
|||
err = keyvalue.Put("openid_providers", providers)
|
||||
}
|
||||
|
||||
if ps != nil {
|
||||
return ps.([]*Provider), nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetProvider retrieves a provider from keyvalue
|
||||
func GetProvider(key string) (provider *Provider, err error) {
|
||||
var p interface{}
|
||||
p, exists, err := keyvalue.Get("openid_provider_" + key)
|
||||
if exists {
|
||||
provider = &Provider{}
|
||||
exists, err := keyvalue.GetWithValue("openid_provider_"+key, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
_, err = GetAllProviders() // This will put all providers in cache
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, _, err = keyvalue.Get("openid_provider_" + key)
|
||||
_, err = keyvalue.GetWithValue("openid_provider_"+key, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
return p.(*Provider), nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
err = provider.setOicdProvider()
|
||||
return
|
||||
}
|
||||
|
||||
func getKeyFromName(name string) string {
|
||||
|
@ -94,7 +110,7 @@ func getKeyFromName(name string) string {
|
|||
return reg.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
|
||||
func getProviderFromMap(pi map[interface{}]interface{}) (*Provider, error) {
|
||||
func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err error) {
|
||||
name, is := pi["name"].(string)
|
||||
if !is {
|
||||
return nil, nil
|
||||
|
@ -102,11 +118,12 @@ func getProviderFromMap(pi map[interface{}]interface{}) (*Provider, error) {
|
|||
|
||||
k := getKeyFromName(name)
|
||||
|
||||
provider := &Provider{
|
||||
Name: pi["name"].(string),
|
||||
Key: k,
|
||||
AuthURL: pi["authurl"].(string),
|
||||
ClientSecret: pi["clientsecret"].(string),
|
||||
provider = &Provider{
|
||||
Name: pi["name"].(string),
|
||||
Key: k,
|
||||
AuthURL: pi["authurl"].(string),
|
||||
OriginalAuthURL: pi["authurl"].(string),
|
||||
ClientSecret: pi["clientsecret"].(string),
|
||||
}
|
||||
|
||||
cl, is := pi["clientid"].(int)
|
||||
|
@ -116,10 +133,9 @@ func getProviderFromMap(pi map[interface{}]interface{}) (*Provider, error) {
|
|||
provider.ClientID = pi["clientid"].(string)
|
||||
}
|
||||
|
||||
var err error
|
||||
provider.OpenIDProvider, err = oidc.NewProvider(context.Background(), provider.AuthURL)
|
||||
err = provider.setOicdProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
|
||||
provider.Oauth2Config = &oauth2.Config{
|
||||
|
@ -128,7 +144,7 @@ func getProviderFromMap(pi map[interface{}]interface{}) (*Provider, error) {
|
|||
RedirectURL: config.AuthOpenIDRedirectURL.GetString() + k,
|
||||
|
||||
// Discovery returns the OAuth2 endpoints.
|
||||
Endpoint: provider.OpenIDProvider.Endpoint(),
|
||||
Endpoint: provider.openIDProvider.Endpoint(),
|
||||
|
||||
// "openid" is a required scope for OpenID Connect flows.
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
|
@ -136,5 +152,5 @@ func getProviderFromMap(pi map[interface{}]interface{}) (*Provider, error) {
|
|||
|
||||
provider.AuthURL = provider.Oauth2Config.Endpoint.AuthURL
|
||||
|
||||
return provider, nil
|
||||
return
|
||||
}
|
||||
|
|
|
@ -127,7 +127,8 @@ func getCacheKey(prefix string, keys ...int64) string {
|
|||
func getAvatarForUser(u *user.User) (fullSizeAvatar *image.RGBA64, err error) {
|
||||
cacheKey := getCacheKey("full", u.ID)
|
||||
|
||||
a, exists, err := keyvalue.Get(cacheKey)
|
||||
fullSizeAvatar = &image.RGBA64{}
|
||||
exists, err := keyvalue.GetWithValue(cacheKey, fullSizeAvatar)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -145,8 +146,6 @@ func getAvatarForUser(u *user.User) (fullSizeAvatar *image.RGBA64, err error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
fullSizeAvatar = a.(*image.RGBA64)
|
||||
}
|
||||
|
||||
return fullSizeAvatar, nil
|
||||
|
@ -156,7 +155,7 @@ func getAvatarForUser(u *user.User) (fullSizeAvatar *image.RGBA64, err error) {
|
|||
func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType string, err error) {
|
||||
cacheKey := getCacheKey("resized", u.ID, size)
|
||||
|
||||
a, exists, err := keyvalue.Get(cacheKey)
|
||||
exists, err := keyvalue.GetWithValue(cacheKey, &avatar)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
@ -180,7 +179,6 @@ func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType
|
|||
return nil, "", err
|
||||
}
|
||||
} else {
|
||||
avatar = a.([]byte)
|
||||
log.Debugf("Serving initials avatar for user %d and size %d from cache", u.ID, size)
|
||||
}
|
||||
|
||||
|
|
|
@ -39,22 +39,17 @@ func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType
|
|||
|
||||
cacheKey := "avatar_upload_" + strconv.Itoa(int(u.ID))
|
||||
|
||||
ai, exists, err := keyvalue.Get(cacheKey)
|
||||
var cached map[int64][]byte
|
||||
exists, err := keyvalue.GetWithValue(cacheKey, &cached)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
var cached map[int64][]byte
|
||||
|
||||
if ai != nil {
|
||||
cached = ai.(map[int64][]byte)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
// Nothing ever cached for this user so we need to create the size map to avoid panics
|
||||
cached = make(map[int64][]byte)
|
||||
} else {
|
||||
a := ai.(map[int64][]byte)
|
||||
a := cached
|
||||
if a != nil && a[size] != nil {
|
||||
log.Debugf("Serving uploaded avatar for user %d and size %d from cache.", u.ID, size)
|
||||
return a[size], "", nil
|
||||
|
|
|
@ -122,7 +122,8 @@ func getImageID(fullURL string) string {
|
|||
// Gets an unsplash photo either from cache or directly from the unsplash api
|
||||
func getUnsplashPhotoInfoByID(photoID string) (photo *Photo, err error) {
|
||||
|
||||
p, exists, err := keyvalue.Get(cachePrefix + photoID)
|
||||
photo = &Photo{}
|
||||
exists, err := keyvalue.GetWithValue(cachePrefix+photoID, photo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -134,8 +135,6 @@ func getUnsplashPhotoInfoByID(photoID string) (photo *Photo, err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
photo = p.(*Photo)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
type Storage interface {
|
||||
Put(key string, value interface{}) (err error)
|
||||
Get(key string) (value interface{}, exists bool, err error)
|
||||
GetWithValue(key string, value interface{}) (exists bool, err error)
|
||||
Del(key string) (err error)
|
||||
IncrBy(key string, update int64) (err error)
|
||||
DecrBy(key string, update int64) (err error)
|
||||
|
@ -55,6 +56,10 @@ func Get(key string) (value interface{}, exists bool, err error) {
|
|||
return store.Get(key)
|
||||
}
|
||||
|
||||
func GetWithValue(key string, value interface{}) (exists bool, err error) {
|
||||
return store.GetWithValue(key, value)
|
||||
}
|
||||
|
||||
// Del removes a save value from a storage backend
|
||||
func Del(key string) (err error) {
|
||||
return store.Del(key)
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
e "code.vikunja.io/api/pkg/modules/keyvalue/error"
|
||||
|
@ -39,6 +40,14 @@ func NewStorage() *Storage {
|
|||
func (s *Storage) Put(key string, value interface{}) (err error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
val := reflect.ValueOf(value)
|
||||
// Make sure to store the underlying value when value is a pointer to a value
|
||||
if val.Kind() == reflect.Ptr {
|
||||
s.store[key] = val.Elem().Interface()
|
||||
return nil
|
||||
}
|
||||
|
||||
s.store[key] = value
|
||||
return nil
|
||||
}
|
||||
|
@ -52,6 +61,25 @@ func (s *Storage) Get(key string) (value interface{}, exists bool, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (s *Storage) GetWithValue(key string, ptr interface{}) (exists bool, err error) {
|
||||
stored, exists, err := s.Get(key)
|
||||
if !exists {
|
||||
return exists, err
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(ptr)
|
||||
if val.Kind() != reflect.Ptr {
|
||||
panic("value must be a pointer")
|
||||
}
|
||||
if val.IsNil() {
|
||||
panic("pointer must not be a nil-pointer")
|
||||
}
|
||||
|
||||
val.Elem().Set(reflect.ValueOf(stored))
|
||||
|
||||
return exists, err
|
||||
}
|
||||
|
||||
// Del removes a saved value from a memory storage
|
||||
func (s *Storage) Del(key string) (err error) {
|
||||
s.mutex.Lock()
|
||||
|
|
|
@ -17,8 +17,10 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
|
||||
"code.vikunja.io/api/pkg/red"
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
@ -40,9 +42,28 @@ func NewStorage() *Storage {
|
|||
|
||||
// Put puts a value into redis
|
||||
func (s *Storage) Put(key string, value interface{}) (err error) {
|
||||
v, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
var v interface{}
|
||||
|
||||
switch value.(type) {
|
||||
case int:
|
||||
v = value
|
||||
case int8:
|
||||
v = value
|
||||
case int16:
|
||||
v = value
|
||||
case int32:
|
||||
v = value
|
||||
case int64:
|
||||
v = value
|
||||
default:
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
err = enc.Encode(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.client.Set(context.Background(), key, buf.Bytes(), 0).Err()
|
||||
}
|
||||
|
||||
return s.client.Set(context.Background(), key, v, 0).Err()
|
||||
|
@ -50,13 +71,32 @@ func (s *Storage) Put(key string, value interface{}) (err error) {
|
|||
|
||||
// Get retrieves a saved value from redis
|
||||
func (s *Storage) Get(key string) (value interface{}, exists bool, err error) {
|
||||
value, err = s.client.Get(context.Background(), key).Result()
|
||||
if err != nil && errors.Is(err, redis.Nil) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return value, true, err
|
||||
}
|
||||
|
||||
func (s *Storage) GetWithValue(key string, value interface{}) (exists bool, err error) {
|
||||
b, err := s.client.Get(context.Background(), key).Bytes()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, value)
|
||||
return
|
||||
var buf bytes.Buffer
|
||||
_, err = buf.Write(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
dec := gob.NewDecoder(&buf)
|
||||
err = dec.Decode(value)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// Del removed a value from redis
|
||||
|
|
|
@ -24,6 +24,8 @@ import (
|
|||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
)
|
||||
|
||||
const mailTemplatePlain = `
|
||||
|
@ -54,10 +56,8 @@ const mailTemplateHTML = `
|
|||
{{ .Greeting }}
|
||||
</p>
|
||||
|
||||
{{ range $line := .IntroLines}}
|
||||
<p>
|
||||
{{ $line }}
|
||||
</p>
|
||||
{{ range $line := .IntroLinesHTML}}
|
||||
{{ $line }}
|
||||
{{ end }}
|
||||
|
||||
{{ if .ActionURL }}
|
||||
|
@ -67,10 +67,8 @@ const mailTemplateHTML = `
|
|||
</a>
|
||||
{{end}}
|
||||
|
||||
{{ range $line := .OutroLines}}
|
||||
<p>
|
||||
{{ $line }}
|
||||
</p>
|
||||
{{ range $line := .OutroLinesHTML}}
|
||||
{{ $line }}
|
||||
{{ end }}
|
||||
|
||||
{{ if .ActionURL }}
|
||||
|
@ -114,6 +112,32 @@ func RenderMail(m *Mail) (mailOpts *mail.Opts, err error) {
|
|||
data["Boundary"] = boundary
|
||||
data["FrontendURL"] = config.ServiceFrontendurl.GetString()
|
||||
|
||||
var introLinesHTML []templatehtml.HTML
|
||||
for _, line := range m.introLines {
|
||||
md := []byte(templatehtml.HTMLEscapeString(line))
|
||||
var buf bytes.Buffer
|
||||
err = goldmark.Convert(md, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//#nosec - the html is escaped few lines before
|
||||
introLinesHTML = append(introLinesHTML, templatehtml.HTML(buf.String()))
|
||||
}
|
||||
data["IntroLinesHTML"] = introLinesHTML
|
||||
|
||||
var outroLinesHTML []templatehtml.HTML
|
||||
for _, line := range m.outroLines {
|
||||
md := []byte(templatehtml.HTMLEscapeString(line))
|
||||
var buf bytes.Buffer
|
||||
err = goldmark.Convert(md, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//#nosec - the html is escaped few lines before
|
||||
outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(buf.String()))
|
||||
}
|
||||
data["OutroLinesHTML"] = outroLinesHTML
|
||||
|
||||
err = plain.Execute(&plainContent, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -90,6 +90,7 @@ func TestRenderMail(t *testing.T) {
|
|||
Subject("Testmail").
|
||||
Greeting("Hi there,").
|
||||
Line("This is a line").
|
||||
Line("This **line** contains [a link](https://vikunja.io)").
|
||||
Line("And another one").
|
||||
Action("The action", "https://example.com").
|
||||
Line("This should be an outro line").
|
||||
|
@ -105,6 +106,8 @@ Hi there,
|
|||
|
||||
This is a line
|
||||
|
||||
This **line** contains [a link](https://vikunja.io)
|
||||
|
||||
And another one
|
||||
|
||||
The action:
|
||||
|
@ -132,13 +135,14 @@ And one more, because why not?
|
|||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
This is a line
|
||||
</p>
|
||||
<p>This is a line</p>
|
||||
|
||||
|
||||
<p>This <strong>line</strong> contains <a href="https://vikunja.io">a link</a></p>
|
||||
|
||||
|
||||
<p>And another one</p>
|
||||
|
||||
<p>
|
||||
And another one
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
@ -149,13 +153,11 @@ And one more, because why not?
|
|||
|
||||
|
||||
|
||||
<p>
|
||||
This should be an outro line
|
||||
</p>
|
||||
<p>This should be an outro line</p>
|
||||
|
||||
|
||||
<p>And one more, because why not?</p>
|
||||
|
||||
<p>
|
||||
And one more, because why not?
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -34,32 +34,44 @@ type LinkShareToken struct {
|
|||
ListID int64 `json:"list_id"`
|
||||
}
|
||||
|
||||
// LinkShareAuth represents everything required to authenticate a link share
|
||||
type LinkShareAuth struct {
|
||||
Hash string `param:"share" json:"-"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// AuthenticateLinkShare gives a jwt auth token for valid share hashes
|
||||
// @Summary Get an auth token for a share
|
||||
// @Description Get a jwt auth token for a shared list from a share hash.
|
||||
// @tags sharing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param password body v1.LinkShareAuth true "The password for link shares which require one."
|
||||
// @Param share path string true "The share hash"
|
||||
// @Success 200 {object} auth.Token "The valid jwt auth token."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid link share object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /shares/{share}/auth [post]
|
||||
func AuthenticateLinkShare(c echo.Context) error {
|
||||
hash := c.Param("share")
|
||||
sh := &LinkShareAuth{}
|
||||
err := c.Bind(sh)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share, err := models.GetLinkShareByHash(s, hash)
|
||||
share, err := models.GetLinkShareByHash(s, sh.Hash)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
if err := s.Commit(); err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
if share.SharingType == models.SharingTypeWithPassword {
|
||||
err := models.VerifyLinkSharePassword(share, sh.Password)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
}
|
||||
|
||||
t, err := auth.NewLinkShareJWTAuthtoken(share)
|
||||
|
@ -67,6 +79,8 @@ func AuthenticateLinkShare(c echo.Context) error {
|
|||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
share.Password = ""
|
||||
|
||||
return c.JSON(http.StatusOK, LinkShareToken{
|
||||
Token: auth.Token{Token: t},
|
||||
LinkSharing: share,
|
||||
|
|
|
@ -58,7 +58,13 @@ func HandleTesting(c echo.Context) error {
|
|||
})
|
||||
}
|
||||
|
||||
err = db.RestoreAndTruncate(table, content)
|
||||
truncate := c.QueryParam("truncate")
|
||||
if truncate == "true" || truncate == "" {
|
||||
err = db.RestoreAndTruncate(table, content)
|
||||
} else {
|
||||
err = db.Restore(table, content)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||
"error": true,
|
||||
|
|
|
@ -43,6 +43,13 @@ type UserSettings struct {
|
|||
DiscoverableByName bool `json:"discoverable_by_name"`
|
||||
// If true, the user can be found when searching for their exact email.
|
||||
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"`
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// GetUserAvatarProvider returns the currently set user avatar
|
||||
|
@ -167,6 +174,9 @@ func UpdateGeneralUserSettings(c echo.Context) error {
|
|||
user.EmailRemindersEnabled = us.EmailRemindersEnabled
|
||||
user.DiscoverableByEmail = us.DiscoverableByEmail
|
||||
user.DiscoverableByName = us.DiscoverableByName
|
||||
user.OverdueTasksRemindersEnabled = us.OverdueTasksRemindersEnabled
|
||||
user.DefaultListID = us.DefaultListID
|
||||
user.WeekStart = us.WeekStart
|
||||
|
||||
_, err = user2.UpdateUser(s, user)
|
||||
if err != nil {
|
||||
|
|
|
@ -63,10 +63,13 @@ func UserShow(c echo.Context) error {
|
|||
us := &userWithSettings{
|
||||
User: *u,
|
||||
Settings: &UserSettings{
|
||||
Name: u.Name,
|
||||
EmailRemindersEnabled: u.EmailRemindersEnabled,
|
||||
DiscoverableByName: u.DiscoverableByName,
|
||||
DiscoverableByEmail: u.DiscoverableByEmail,
|
||||
Name: u.Name,
|
||||
EmailRemindersEnabled: u.EmailRemindersEnabled,
|
||||
DiscoverableByName: u.DiscoverableByName,
|
||||
DiscoverableByEmail: u.DiscoverableByEmail,
|
||||
OverdueTasksRemindersEnabled: u.OverdueTasksRemindersEnabled,
|
||||
DefaultListID: u.DefaultListID,
|
||||
WeekStart: u.WeekStart,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -243,7 +243,7 @@ var doc = `{
|
|||
],
|
||||
"summary": "Creates a new saved filter",
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The Saved Filter",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.SavedFilter"
|
||||
|
@ -524,7 +524,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created label object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Label"
|
||||
|
@ -874,7 +874,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created task object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Task"
|
||||
|
@ -1572,7 +1572,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created team\u003c-\u003elist relation.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TeamList"
|
||||
|
@ -1710,7 +1710,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created user\u003c-\u003elist relation.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ListUser"
|
||||
|
@ -1905,7 +1905,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created list.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ListDuplicate"
|
||||
|
@ -2393,7 +2393,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created link share object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LinkSharing"
|
||||
|
@ -3189,7 +3189,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created namespace.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Namespace"
|
||||
|
@ -3478,7 +3478,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created team\u003c-\u003enamespace relation.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TeamNamespace"
|
||||
|
@ -3616,7 +3616,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created user\u003c-\u003enamespace relation.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.NamespaceUser"
|
||||
|
@ -3686,7 +3686,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created list.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.List"
|
||||
|
@ -4141,6 +4141,15 @@ var doc = `{
|
|||
],
|
||||
"summary": "Get an auth token for a share",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The password for link shares which require one.",
|
||||
"name": "password",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.LinkShareAuth"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The share hash",
|
||||
|
@ -4206,7 +4215,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The subscription",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
|
@ -4960,7 +4969,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created assingee object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskAssginee"
|
||||
|
@ -5018,7 +5027,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created assingees object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskAssginee"
|
||||
|
@ -5176,7 +5185,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created task comment object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskComment"
|
||||
|
@ -5416,7 +5425,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The updated labels object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LabelTaskBulk"
|
||||
|
@ -5474,7 +5483,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created task relation object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskRelation"
|
||||
|
@ -5671,7 +5680,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created label relation object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LabelTask"
|
||||
|
@ -5851,7 +5860,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created team.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Team"
|
||||
|
@ -6058,7 +6067,7 @@ var doc = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The newly created member object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TeamMember"
|
||||
|
@ -7195,7 +7204,8 @@ var doc = `{
|
|||
},
|
||||
"limit": {
|
||||
"description": "How many tasks can be at the same time on this board max",
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"list_id": {
|
||||
"description": "The list this bucket belongs to.",
|
||||
|
@ -7353,9 +7363,9 @@ var doc = `{
|
|||
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
|
||||
"type": "integer"
|
||||
},
|
||||
"repeat_from_current_date": {
|
||||
"description": "If specified, a repeating task will repeat from the current date rather than the last set date.",
|
||||
"type": "boolean"
|
||||
"repeat_mode": {
|
||||
"description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.",
|
||||
"type": "integer"
|
||||
},
|
||||
"start_date": {
|
||||
"description": "When this task starts.",
|
||||
|
@ -7493,6 +7503,10 @@ var doc = `{
|
|||
"description": "The name of this link share. All actions someone takes while being authenticated with that link will appear with that name.",
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"description": "The password of this link share. You can only set it, not retrieve it after the link share has been created.",
|
||||
"type": "string"
|
||||
},
|
||||
"right": {
|
||||
"description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.",
|
||||
"type": "integer",
|
||||
|
@ -7504,7 +7518,7 @@ var doc = `{
|
|||
"$ref": "#/definitions/user.User"
|
||||
},
|
||||
"sharing_type": {
|
||||
"description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password (currently not implemented).",
|
||||
"description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password.",
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"maximum": 2
|
||||
|
@ -7926,9 +7940,9 @@ var doc = `{
|
|||
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
|
||||
"type": "integer"
|
||||
},
|
||||
"repeat_from_current_date": {
|
||||
"description": "If specified, a repeating task will repeat from the current date rather than the last set date.",
|
||||
"type": "boolean"
|
||||
"repeat_mode": {
|
||||
"description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.",
|
||||
"type": "integer"
|
||||
},
|
||||
"start_date": {
|
||||
"description": "When this task starts.",
|
||||
|
@ -8511,6 +8525,14 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.LinkShareAuth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserAvatarProvider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8534,6 +8556,10 @@ var doc = `{
|
|||
"v1.UserSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default_list_id": {
|
||||
"description": "If a task is created without a specified list this value should be used. Applies\nto tasks made directly in API and from clients.",
|
||||
"type": "integer"
|
||||
},
|
||||
"discoverable_by_email": {
|
||||
"description": "If true, the user can be found when searching for their exact email.",
|
||||
"type": "boolean"
|
||||
|
@ -8549,6 +8575,14 @@ var doc = `{
|
|||
"name": {
|
||||
"description": "The new name of the current user.",
|
||||
"type": "string"
|
||||
},
|
||||
"overdue_tasks_reminders_enabled": {
|
||||
"description": "If enabled, the user will get an email for their overdue tasks each morning.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"week_start": {
|
||||
"description": "The day when the week starts for this user. 0 = sunday, 1 = monday, etc.",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -226,7 +226,7 @@
|
|||
],
|
||||
"summary": "Creates a new saved filter",
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The Saved Filter",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.SavedFilter"
|
||||
|
@ -507,7 +507,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created label object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Label"
|
||||
|
@ -857,7 +857,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created task object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Task"
|
||||
|
@ -1555,7 +1555,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created team\u003c-\u003elist relation.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TeamList"
|
||||
|
@ -1693,7 +1693,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created user\u003c-\u003elist relation.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ListUser"
|
||||
|
@ -1888,7 +1888,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created list.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ListDuplicate"
|
||||
|
@ -2376,7 +2376,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created link share object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LinkSharing"
|
||||
|
@ -3172,7 +3172,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created namespace.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Namespace"
|
||||
|
@ -3461,7 +3461,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created team\u003c-\u003enamespace relation.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TeamNamespace"
|
||||
|
@ -3599,7 +3599,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created user\u003c-\u003enamespace relation.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.NamespaceUser"
|
||||
|
@ -3669,7 +3669,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created list.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.List"
|
||||
|
@ -4124,6 +4124,15 @@
|
|||
],
|
||||
"summary": "Get an auth token for a share",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The password for link shares which require one.",
|
||||
"name": "password",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.LinkShareAuth"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The share hash",
|
||||
|
@ -4189,7 +4198,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The subscription",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
|
@ -4943,7 +4952,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created assingee object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskAssginee"
|
||||
|
@ -5001,7 +5010,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created assingees object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskAssginee"
|
||||
|
@ -5159,7 +5168,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created task comment object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskComment"
|
||||
|
@ -5399,7 +5408,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The updated labels object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LabelTaskBulk"
|
||||
|
@ -5457,7 +5466,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created task relation object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TaskRelation"
|
||||
|
@ -5654,7 +5663,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created label relation object.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LabelTask"
|
||||
|
@ -5834,7 +5843,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The created team.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Team"
|
||||
|
@ -6041,7 +6050,7 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"description": "The newly created member object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.TeamMember"
|
||||
|
@ -7178,7 +7187,8 @@
|
|||
},
|
||||
"limit": {
|
||||
"description": "How many tasks can be at the same time on this board max",
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"list_id": {
|
||||
"description": "The list this bucket belongs to.",
|
||||
|
@ -7336,9 +7346,9 @@
|
|||
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
|
||||
"type": "integer"
|
||||
},
|
||||
"repeat_from_current_date": {
|
||||
"description": "If specified, a repeating task will repeat from the current date rather than the last set date.",
|
||||
"type": "boolean"
|
||||
"repeat_mode": {
|
||||
"description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.",
|
||||
"type": "integer"
|
||||
},
|
||||
"start_date": {
|
||||
"description": "When this task starts.",
|
||||
|
@ -7476,6 +7486,10 @@
|
|||
"description": "The name of this link share. All actions someone takes while being authenticated with that link will appear with that name.",
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"description": "The password of this link share. You can only set it, not retrieve it after the link share has been created.",
|
||||
"type": "string"
|
||||
},
|
||||
"right": {
|
||||
"description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.",
|
||||
"type": "integer",
|
||||
|
@ -7487,7 +7501,7 @@
|
|||
"$ref": "#/definitions/user.User"
|
||||
},
|
||||
"sharing_type": {
|
||||
"description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password (currently not implemented).",
|
||||
"description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password.",
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"maximum": 2
|
||||
|
@ -7909,9 +7923,9 @@
|
|||
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
|
||||
"type": "integer"
|
||||
},
|
||||
"repeat_from_current_date": {
|
||||
"description": "If specified, a repeating task will repeat from the current date rather than the last set date.",
|
||||
"type": "boolean"
|
||||
"repeat_mode": {
|
||||
"description": "Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.",
|
||||
"type": "integer"
|
||||
},
|
||||
"start_date": {
|
||||
"description": "When this task starts.",
|
||||
|
@ -8494,6 +8508,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.LinkShareAuth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserAvatarProvider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8517,6 +8539,10 @@
|
|||
"v1.UserSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default_list_id": {
|
||||
"description": "If a task is created without a specified list this value should be used. Applies\nto tasks made directly in API and from clients.",
|
||||
"type": "integer"
|
||||
},
|
||||
"discoverable_by_email": {
|
||||
"description": "If true, the user can be found when searching for their exact email.",
|
||||
"type": "boolean"
|
||||
|
@ -8532,6 +8558,14 @@
|
|||
"name": {
|
||||
"description": "The new name of the current user.",
|
||||
"type": "string"
|
||||
},
|
||||
"overdue_tasks_reminders_enabled": {
|
||||
"description": "If enabled, the user will get an email for their overdue tasks each morning.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"week_start": {
|
||||
"description": "The day when the week starts for this user. 0 = sunday, 1 = monday, etc.",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -399,3 +399,29 @@ func (err ErrInvalidAvatarProvider) HTTPError() web.HTTPError {
|
|||
Message: "Invalid avatar provider setting. See docs for valid types.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrNoOpenIDEmailProvided represents a "NoEmailProvided" kind of error.
|
||||
type ErrNoOpenIDEmailProvided struct {
|
||||
}
|
||||
|
||||
// IsErrNoEmailProvided checks if an error is a ErrNoOpenIDEmailProvided.
|
||||
func IsErrNoEmailProvided(err error) bool {
|
||||
_, ok := err.(*ErrNoOpenIDEmailProvided)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrNoOpenIDEmailProvided) Error() string {
|
||||
return "No email provided"
|
||||
}
|
||||
|
||||
// ErrCodeNoOpenIDEmailProvided holds the unique world-error code of this error
|
||||
const ErrCodeNoOpenIDEmailProvided = 1019
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrNoOpenIDEmailProvided) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeNoOpenIDEmailProvided,
|
||||
Message: "No email address available. Please make sure the openid provider publicly provides an email address for your account.",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,11 +67,12 @@ type User struct {
|
|||
Issuer string `xorm:"text null" json:"-"`
|
||||
Subject string `xorm:"text null" json:"-"`
|
||||
|
||||
// If enabled, sends email reminders of tasks to the user.
|
||||
EmailRemindersEnabled bool `xorm:"bool default true" json:"-"`
|
||||
|
||||
DiscoverableByName bool `xorm:"bool default false index" json:"-"`
|
||||
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
|
||||
EmailRemindersEnabled bool `xorm:"bool default true" json:"-"`
|
||||
DiscoverableByName bool `xorm:"bool default false index" json:"-"`
|
||||
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
|
||||
OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"`
|
||||
DefaultListID int64 `xorm:"bigint null index" json:"-"`
|
||||
WeekStart int `xorm:"null" json:"-"`
|
||||
|
||||
// A timestamp when this task was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
|
@ -371,6 +372,9 @@ func UpdateUser(s *xorm.Session, user *User) (updatedUser *User, err error) {
|
|||
"email_reminders_enabled",
|
||||
"discoverable_by_name",
|
||||
"discoverable_by_email",
|
||||
"overdue_tasks_reminders_enabled",
|
||||
"default_list_id",
|
||||
"week_start",
|
||||
).
|
||||
Update(user)
|
||||
if err != nil {
|
||||
|
@ -400,7 +404,7 @@ func UpdateUserPassword(s *xorm.Session, user *User, newPassword string) (err er
|
|||
}
|
||||
|
||||
// Hash the new password and set it
|
||||
hashed, err := hashPassword(newPassword)
|
||||
hashed, err := HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {
|
|||
|
||||
if user.Issuer == issuerLocal {
|
||||
// Hash the password
|
||||
user.Password, err = hashPassword(user.Password)
|
||||
user.Password, err = HashPassword(user.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {
|
|||
}
|
||||
|
||||
// HashPassword hashes a password
|
||||
func hashPassword(password string) (string, error) {
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 11)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ func ResetPassword(s *xorm.Session, reset *PasswordReset) (err error) {
|
|||
}
|
||||
|
||||
// Hash the password
|
||||
user.Password, err = hashPassword(reset.NewPassword)
|
||||
user.Password, err = HashPassword(reset.NewPassword)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
// 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 utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HumanizeDuration formats a time.Duration in a human-friendly format.
|
||||
// Based on https://gist.github.com/harshavardhana/327e0577c4fed9211f65
|
||||
func HumanizeDuration(duration time.Duration) string {
|
||||
years := int64(duration.Hours() / 24 / 365)
|
||||
days := int64(duration.Hours()/24) - years*365
|
||||
weeks := days / 7
|
||||
days -= weeks * 7
|
||||
|
||||
hours := int64(math.Mod(duration.Hours(), 24))
|
||||
minutes := int64(math.Mod(duration.Minutes(), 60))
|
||||
|
||||
chunks := []struct {
|
||||
singularName string
|
||||
amount int64
|
||||
}{
|
||||
{"year", years},
|
||||
{"week", weeks},
|
||||
{"day", days},
|
||||
{"hour", hours},
|
||||
{"minute", minutes},
|
||||
}
|
||||
|
||||
parts := []string{}
|
||||
|
||||
for _, chunk := range chunks {
|
||||
switch chunk.amount {
|
||||
case 0:
|
||||
continue
|
||||
case 1:
|
||||
parts = append(parts, fmt.Sprintf("one %s", chunk.singularName))
|
||||
default:
|
||||
parts = append(parts, fmt.Sprintf("%d %ss", chunk.amount, chunk.singularName))
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) > 1 {
|
||||
return strings.Join(parts[:len(parts)-1], ", ") + " and " + parts[len(parts)-1]
|
||||
}
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// 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 utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHumanizeDuration(t *testing.T) {
|
||||
t.Run("one part", func(t *testing.T) {
|
||||
d := 1 * time.Hour
|
||||
dur := HumanizeDuration(d)
|
||||
|
||||
assert.Equal(t, "one hour", dur)
|
||||
})
|
||||
t.Run("amount > 1", func(t *testing.T) {
|
||||
d := 2 * time.Hour
|
||||
dur := HumanizeDuration(d)
|
||||
|
||||
assert.Equal(t, "2 hours", dur)
|
||||
})
|
||||
t.Run("2 parts", func(t *testing.T) {
|
||||
d := 2*time.Hour + 48*time.Hour
|
||||
dur := HumanizeDuration(d)
|
||||
|
||||
assert.Equal(t, "2 days and 2 hours", dur)
|
||||
})
|
||||
t.Run("multiple parts", func(t *testing.T) {
|
||||
d := 2*time.Hour + 24*15*time.Hour
|
||||
dur := HumanizeDuration(d)
|
||||
|
||||
assert.Equal(t, "2 weeks, one day and 2 hours", dur)
|
||||
})
|
||||
t.Run("years", func(t *testing.T) {
|
||||
day := 24 * time.Hour
|
||||
d := 2*time.Hour + 365*day + 14*day
|
||||
dur := HumanizeDuration(d)
|
||||
|
||||
assert.Equal(t, "one year, 2 weeks and 2 hours", dur)
|
||||
})
|
||||
t.Run("ignore seconds", func(t *testing.T) {
|
||||
d := 2*time.Hour + 48*time.Hour + 23*time.Second
|
||||
dur := HumanizeDuration(d)
|
||||
|
||||
assert.Equal(t, "2 days and 2 hours", dur)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// 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 utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
)
|
||||
|
||||
// GetTimeWithoutNanoSeconds returns a time.Time without the nanoseconds.
|
||||
func GetTimeWithoutNanoSeconds(t time.Time) time.Time {
|
||||
tz := config.GetTimeZone()
|
||||
|
||||
// By default, time.Now() includes nanoseconds which we don't save. That results in getting the wrong dates,
|
||||
// so we make sure the time we use to get the reminders don't contain nanoseconds.
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location()).In(tz)
|
||||
}
|
Loading…
Reference in New Issue