forked from vikunja/vikunja
Compare commits
40 Commits
090c67138a
...
b5ee39b887
Author | SHA1 | Date |
---|---|---|
kolaente | b5ee39b887 | |
kolaente | 0d8451ab6e | |
renovate | 81f09f7dc0 | |
kolaente | 5a40100ac5 | |
renovate | 0694314e52 | |
renovate | a547a9eb25 | |
renovate | 0fcd03f561 | |
renovate | ffedd02b08 | |
renovate | c84684a425 | |
kolaente | aed560339b | |
kolaente | 0612f4d0e0 | |
kolaente | ce621ee5d6 | |
kolaente | 9c4bb5a244 | |
kolaente | c076f73a87 | |
Dominik Pschenitschni | 36265fcedf | |
konrad | 53419180be | |
renovate | c5bd09702a | |
kolaente | fcb205a842 | |
kolaente | 4323803fd6 | |
kolaente | 903b8ff438 | |
kolaente | b1fd13bbcb | |
kolaente | 878d19beb8 | |
kolaente | 96ed1e33e3 | |
renovate | 374a0f9ce3 | |
kolaente | 580bd5aeaa | |
kolaente | c359d6a97d | |
kolaente | 038702a2a0 | |
kolaente | 6426d40825 | |
kolaente | bbe102dd57 | |
kolaente | 65484bc432 | |
kolaente | 54f6cc7a64 | |
kolaente | f1b2338227 | |
kolaente | 45defebcf4 | |
kolaente | 86ee8273bc | |
Luca Bernstein | 3adfeb3b34 | |
Felix Breidenstein | 9bb8a26706 | |
Luca Bernstein | 54b7f7127c | |
Luca Bernstein | 25609db567 | |
kolaente | 2e3603507c | |
kolaente | 2efc1b5a87 |
71
.drone.yml
71
.drone.yml
|
@ -132,14 +132,15 @@ steps:
|
|||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: lint
|
||||
image: golang:1.17-alpine
|
||||
image: golang:1.19-alpine
|
||||
pull: true
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
depends_on: [ build ]
|
||||
commands:
|
||||
- export "GOROOT=$(go env GOROOT)"
|
||||
- apk --no-cache add build-base git
|
||||
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.47.3
|
||||
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.49.0
|
||||
- ./mage-static check:all
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
@ -502,7 +503,7 @@ steps:
|
|||
depends_on: [ sign-release ]
|
||||
|
||||
# Build os packages and push it to our bucket
|
||||
- name: build-os-packages
|
||||
- name: build-os-packages-unstable
|
||||
image: goreleaser/nfpm
|
||||
pull: true
|
||||
commands:
|
||||
|
@ -511,6 +512,25 @@ steps:
|
|||
- mv dist/os-packages/vikunja*.x86_64.rpm dist/os-packages/vikunja-unstable-x86_64.rpm
|
||||
- mv dist/os-packages/vikunja*_amd64.deb dist/os-packages/vikunja-unstable-amd64.deb
|
||||
- mv dist/os-packages/vikunja*_x86_64.apk dist/os-packages/vikunja-unstable-x86_64.apk
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
depends_on: [ static-build-linux ]
|
||||
|
||||
- name: build-os-packages-version
|
||||
image: goreleaser/nfpm
|
||||
pull: true
|
||||
commands:
|
||||
- apk add git go
|
||||
- ./mage-static release:packages
|
||||
- mv dist/os-packages/vikunja*.x86_64.rpm dist/os-packages/vikunja-${DRONE_TAG##v}-x86_64.rpm
|
||||
- mv dist/os-packages/vikunja*_amd64.deb dist/os-packages/vikunja-${DRONE_TAG##v}-amd64.deb
|
||||
- mv dist/os-packages/vikunja*_x86_64.apk dist/os-packages/vikunja-${DRONE_TAG##v}-x86_64.apk
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
depends_on: [ static-build-linux ]
|
||||
|
||||
# Push the os releases to our pseudo-s3-bucket
|
||||
|
@ -534,7 +554,7 @@ steps:
|
|||
- main
|
||||
event:
|
||||
- push
|
||||
depends_on: [ build-os-packages ]
|
||||
depends_on: [ build-os-packages-unstable ]
|
||||
|
||||
- name: release-os-version
|
||||
image: plugins/s3
|
||||
|
@ -554,44 +574,7 @@ steps:
|
|||
when:
|
||||
event:
|
||||
- tag
|
||||
depends_on: [ build-os-packages ]
|
||||
|
||||
### Broken, disabled until we figure out how to fix it
|
||||
# - name: deb-structure
|
||||
# image: kolaente/reprepro
|
||||
# pull: true
|
||||
# environment:
|
||||
# GPG_PRIVATE_KEY:
|
||||
# from_secret: gpg_privatekey
|
||||
# commands:
|
||||
# - export GPG_TTY=$(tty)
|
||||
# - gpg -qk
|
||||
# - echo "use-agent" >> ~/.gnupg/gpg.conf
|
||||
# - gpgconf --kill gpg-agent
|
||||
# - echo $GPG_PRIVATE_KEY > ~/frederik.gpg
|
||||
# - gpg --import ~/frederik.gpg
|
||||
# - mkdir debian/conf -p
|
||||
# - cp build/reprepro-dist-conf debian/conf/distributions
|
||||
# - ./mage-static release:reprepro
|
||||
# depends_on: [ build-os-packages ]
|
||||
|
||||
# Push the releases to our pseudo-s3-bucket
|
||||
- name: release-deb
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
access_key:
|
||||
from_secret: aws_access_key_id
|
||||
secret_key:
|
||||
from_secret: aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
path_style: true
|
||||
strip_prefix: debian
|
||||
source: debian/*/*/*/*/*
|
||||
target: /deb/
|
||||
# depends_on: [ deb-structure ]
|
||||
depends_on: [ build-os-packages-version ]
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
|
@ -622,7 +605,7 @@ steps:
|
|||
- tar -xzf vikunja-theme.tar.gz
|
||||
|
||||
- name: build
|
||||
image: klakegg/hugo:0.93.3
|
||||
image: klakegg/hugo:0.101.0
|
||||
pull: true
|
||||
commands:
|
||||
- cd docs
|
||||
|
@ -878,6 +861,6 @@ steps:
|
|||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: e72b631f902689777e3263ae9527e5aa47738b9021538f7cb5034f95ac265f07
|
||||
hmac: 8b0e235be5cc1aa66fa02f2c5e86508c183ea50e42dd6ff134568a7a7b7d312c
|
||||
|
||||
...
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
<!--
|
||||
|
||||
Please fill out this issue template to report a bug.
|
||||
If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
|
||||
|
||||
-->
|
||||
|
||||
**Version information:**
|
||||
|
||||
Frontend Version:
|
||||
API Version:
|
||||
Browser and OS Version:
|
||||
|
||||
**Steps to reproduce:**
|
||||
|
||||
<!--
|
||||
Add clear steps to reproduce the bug. Provide screenshots where applicable.
|
||||
-->
|
||||
|
||||
1.
|
||||
2.
|
||||
...
|
||||
|
||||
**Expected behavior:**
|
||||
|
||||
<!--
|
||||
Describe what happened.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
**Actual behavior:**
|
||||
|
||||
<!--
|
||||
Describe what happened instead.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
**Checklist:**
|
||||
|
||||
* [ ] I have provided all required information
|
||||
* [ ] I am using the latest release or the latest unstable build
|
||||
* [ ] I was able to reproduce the bug on [try](https://try.vikunja.io)
|
|
@ -0,0 +1,58 @@
|
|||
name: Bug Report
|
||||
description: Found something you weren't expecting? Report it here!
|
||||
labels: kind/bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
NOTE: If your issue is a security concern, please send an email to security@vikunja.io instead of opening a public issue.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill out this issue template to report a bug.
|
||||
|
||||
1. If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
|
||||
2. Please ask questions or configuration/deploy problems on our [Matrix Room](https://matrix.to/#/#vikunja:matrix.org) or forum (https://community.vikunja.io).
|
||||
3. Make sure you are using the latest release and
|
||||
take a moment to check that your issue hasn't been reported before.
|
||||
4. Please give all relevant information below for bug reports, because
|
||||
incomplete details will be handled as an invalid report and closed.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below).
|
||||
- type: input
|
||||
id: frontend-version
|
||||
attributes:
|
||||
label: Vikunja Frontend Version
|
||||
description: Vikunja frontend version (or commit reference) of your instance
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: api-version
|
||||
attributes:
|
||||
label: Vikunja API Version
|
||||
description: Vikunja API version (or commit reference) of your instance
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: browser-version
|
||||
attributes:
|
||||
label: Browser and version
|
||||
description: If your issue is related to a frontend problem, please provide the browser and version you used to reproduce it.
|
||||
- type: dropdown
|
||||
id: can-reproduce
|
||||
attributes:
|
||||
label: Can you reproduce the bug on the Vikunja demo site?
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If this issue involves the Web Interface, please provide one or more screenshots
|
|
@ -1,11 +0,0 @@
|
|||
# Description
|
||||
|
||||
|
||||
|
||||
# Checklist
|
||||
|
||||
* [ ] I added or improved tests
|
||||
* [ ] I added or improved docs for my feature
|
||||
* [ ] Swagger (including `mage do-the-swag`)
|
||||
* [ ] Error codes
|
||||
* [ ] New config options (including adding them to `config.yml.saml` and running `mage generate-docs`)
|
|
@ -0,0 +1,58 @@
|
|||
name: Bug Report
|
||||
description: Found something you weren't expecting? Report it here!
|
||||
labels: kind/bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
NOTE: If your issue is a security concern, please send an email to security@vikunja.io instead of opening a public issue.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill out this issue template to report a bug.
|
||||
|
||||
1. If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
|
||||
2. Please ask questions or configuration/deploy problems on our [Matrix Room](https://matrix.to/#/#vikunja:matrix.org) or forum (https://community.vikunja.io).
|
||||
3. Make sure you are using the latest release and
|
||||
take a moment to check that your issue hasn't been reported before.
|
||||
4. Please give all relevant information below for bug reports, because
|
||||
incomplete details will be handled as an invalid report and closed.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below).
|
||||
- type: input
|
||||
id: frontend-version
|
||||
attributes:
|
||||
label: Vikunja Frontend Version
|
||||
description: Vikunja frontend version (or commit reference) of your instance
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: api-version
|
||||
attributes:
|
||||
label: Vikunja API Version
|
||||
description: Vikunja API version (or commit reference) of your instance
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: browser-version
|
||||
attributes:
|
||||
label: Browser and version
|
||||
description: If your issue is related to a frontend problem, please provide the browser and version you used to reproduce it.
|
||||
- type: dropdown
|
||||
id: can-reproduce
|
||||
attributes:
|
||||
label: Can you reproduce the bug on the Vikunja demo site?
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If this issue involves the Web Interface, please provide one or more screenshots
|
|
@ -0,0 +1,17 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Frontend issues
|
||||
url: https://code.vikunja.io/frontend/issues
|
||||
about: This is the API repo. Please open frontend-related bug reports and discussions in the frontend repo. Not sure if you issue is frontend or api? Ask in Matrix or the forum first.
|
||||
- name: Forum
|
||||
url: https://community.vikunja.io/
|
||||
about: Feature Requests, Questions, configuration or deployment problems should be discussed in the forum.
|
||||
- name: Security-related issues
|
||||
url: https://vikunja.io/contact/#security
|
||||
about: For security concerns, please send a mail to security@vikunja.io instead of opening a public issue.
|
||||
- name: Chat on Matrix
|
||||
url: https://matrix.to/#/#vikunja:matrix.org
|
||||
about: Please ask any quick questions here.
|
||||
- name: Translations
|
||||
url: https://crowdin.com/project/vikunja
|
||||
about: Any problems or requests for new languages about translations should be handled in crowdin.
|
|
@ -4,6 +4,8 @@
|
|||
config.yml
|
||||
config.yaml
|
||||
!docs/config.yml
|
||||
!.github/ISSUE_TEMPLATE/config.yml
|
||||
!.gitea/ISSUE_TEMPLATE/config.yml
|
||||
docs/themes/
|
||||
*.db
|
||||
Run
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
##############
|
||||
# Build stage
|
||||
FROM golang:1.18-alpine AS build-env
|
||||
FROM golang:1.19-alpine AS build-env
|
||||
|
||||
RUN apk --no-cache add build-base git && \
|
||||
go install github.com/magefile/mage@latest && \
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
##############
|
||||
# Build stage
|
||||
FROM golang:1.18-buster AS build-env
|
||||
FROM golang:1.19-buster AS build-env
|
||||
|
||||
RUN go install github.com/magefile/mage@latest && \
|
||||
mv /go/bin/mage /usr/local/go/bin
|
||||
|
|
|
@ -324,3 +324,28 @@ metrics:
|
|||
username:
|
||||
# If set to a non-empty value the /metrics endpoint will require this as a password via basic auth in combination with the username below.
|
||||
password:
|
||||
|
||||
# Provide default settings for new users. When a new user is created, these settings will automatically be set for the user. If you change them in the config file afterwards they will not be changed back for existing users.
|
||||
defaultsettings:
|
||||
# The avatar source for the user. Can be `gravatar`, `initials`, `upload` or `marble`. If you set this to `upload` you'll also need to specify `defaultsettings.avatar_file_id`.
|
||||
avatar_provider: initials
|
||||
# The id of the file used as avatar.
|
||||
avatar_file_id: 0
|
||||
# If set to true users will get task reminders via email.
|
||||
email_reminders_enabled: false
|
||||
# If set to true will allow other users to find this user when searching for parts of their name.
|
||||
discoverable_by_name: false
|
||||
# If set to true will allow other users to find this user when searching for their exact email.
|
||||
discoverable_by_email: false
|
||||
# If set to true will send an email every day with all overdue tasks at a configured time.
|
||||
overdue_tasks_reminders_enabled: true
|
||||
# When to send the overdue task reminder email.
|
||||
overdue_tasks_reminders_time: 9:00
|
||||
# The id of the default list. Make sure users actually have access to this list when setting this value.
|
||||
default_list_id: 0
|
||||
# Start of the week for the user. `0` is sunday, `1` is monday and so on.
|
||||
week_start: 0
|
||||
# The language of the user interface. Must be an ISO 639-1 language code. Will default to the browser language the user uses when signing up.
|
||||
language: <unset>
|
||||
# The time zone of each individual user. This will affect when users get reminders and overdue task emails.
|
||||
timezone: <time zone set at service.timezone>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
date: "2022-09-21:00:00+02:00"
|
||||
title: "Development"
|
||||
toc: true
|
||||
draft: false
|
||||
|
@ -36,13 +36,13 @@ Make sure to check the other doc articles for specific development tasks like [t
|
|||
## Frontend requirements
|
||||
|
||||
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
|
||||
More instructions can be found in the repo's README.
|
||||
|
||||
You need to have yarn v1 and nodejs in version 16 installed.
|
||||
You need to have [pnpm](https://pnpm.io/) and nodejs in version 16 or 18 installed.
|
||||
|
||||
## Git flow
|
||||
|
||||
The `main` branch is the latest and bleeding edge branch with all changes. Unstable releases are automatically
|
||||
created from this branch.
|
||||
The `main` branch is the latest and bleeding edge branch with all changes. Unstable releases are automatically created from this branch.
|
||||
|
||||
A release gets tagged from the main branch with the version name as tag name.
|
||||
|
||||
|
@ -50,7 +50,6 @@ Backports and point-releases should go to a `release/version` branch, based on t
|
|||
|
||||
## Conventional commits
|
||||
|
||||
We're using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) because they greatly simplify
|
||||
generating release notes.
|
||||
We're using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) because they greatly simplify generating release notes.
|
||||
|
||||
It is not required to use them when creating a PR, but appreciated.
|
||||
|
|
|
@ -98,12 +98,12 @@ Check out the docs [in the frontend repo](https://kolaente.dev/vikunja/frontend/
|
|||
To run the frontend unit tests, run
|
||||
|
||||
{{< highlight bash >}}
|
||||
yarn test:unit
|
||||
pnpm run test:unit
|
||||
{{< /highlight >}}
|
||||
|
||||
The frontend also has a watcher available that re-runs all unit tests every time you change something.
|
||||
To use it, simply run
|
||||
|
||||
{{< highlight bash >}}
|
||||
yarn test:unit-watch
|
||||
pnpm run test:unit-watch
|
||||
{{< /highlight >}}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
date: "2022-09-21:00:00+02:00"
|
||||
title: "Build from sources"
|
||||
draft: false
|
||||
type: "doc"
|
||||
|
@ -16,13 +16,13 @@ To completely build Vikunja from source, you need to build the api and frontend.
|
|||
|
||||
## API
|
||||
|
||||
The Vikunja API has no other dependencies than go itself.
|
||||
The Vikunja API has no other dependencies than go itself.
|
||||
That means compiling it boils down to these steps:
|
||||
|
||||
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.17`.
|
||||
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
|
||||
3. Clone the repo with `git clone https://code.vikunja.io/api` and switch into the directory.
|
||||
3. Run `mage build:build` in the source of this repo. This will build a binary in the root of the repo which will be able to run on your system.
|
||||
4. Run `mage build:build` in the source of this repo. This will build a binary in the root of the repo which will be able to run on your system.
|
||||
|
||||
*Note:* Static ressources such as email templates are built into the binary.
|
||||
For these to work, you may need to run `mage build:generate` before building the vikunja binary.
|
||||
|
@ -38,9 +38,7 @@ More options are available, please refer to the [magefile docs]({{< ref "../deve
|
|||
|
||||
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
|
||||
|
||||
You need to have yarn v1 and nodejs in version 16 installed.
|
||||
|
||||
1. Make sure [yarn v1](https://yarnpkg.com/getting-started/install) is properly installed on your system.
|
||||
3. Clone the repo with `git clone https://code.vikunja.io/frontend` and switch into the directory.
|
||||
3. Install all dependencies with `yarn install`
|
||||
4. Build the frontend with `yarn build`. This will result in a js bundle in the `dist/` folder which you can deploy.
|
||||
1. Make sure you have [pnpm](https://pnpm.io/installation) properly installed on your system.
|
||||
2. Clone the repo with `git clone https://code.vikunja.io/frontend` and switch into the directory.
|
||||
3. Install all dependencies with `pnpm install`
|
||||
4. Build the frontend with `pnpm run build`. This will result in a static js bundle in the `dist/` folder which you can deploy.
|
||||
|
|
|
@ -1174,3 +1174,132 @@ Full path: `metrics.password`
|
|||
Environment path: `VIKUNJA_METRICS_PASSWORD`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## defaultsettings
|
||||
|
||||
Provide default settings for new users. When a new user is created, these settings will automatically be set for the user. If you change them in the config file afterwards they will not be changed back for existing users.
|
||||
|
||||
|
||||
|
||||
### avatar_provider
|
||||
|
||||
The avatar source for the user. Can be `gravatar`, `initials`, `upload` or `marble`. If you set this to `upload` you'll also need to specify `defaultsettings.avatar_file_id`.
|
||||
|
||||
Default: `initials`
|
||||
|
||||
Full path: `defaultsettings.avatar_provider`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_AVATAR_PROVIDER`
|
||||
|
||||
|
||||
### avatar_file_id
|
||||
|
||||
The id of the file used as avatar.
|
||||
|
||||
Default: `0`
|
||||
|
||||
Full path: `defaultsettings.avatar_file_id`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_AVATAR_FILE_ID`
|
||||
|
||||
|
||||
### email_reminders_enabled
|
||||
|
||||
If set to true users will get task reminders via email.
|
||||
|
||||
Default: `false`
|
||||
|
||||
Full path: `defaultsettings.email_reminders_enabled`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_EMAIL_REMINDERS_ENABLED`
|
||||
|
||||
|
||||
### discoverable_by_name
|
||||
|
||||
If set to true will allow other users to find this user when searching for parts of their name.
|
||||
|
||||
Default: `false`
|
||||
|
||||
Full path: `defaultsettings.discoverable_by_name`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_DISCOVERABLE_BY_NAME`
|
||||
|
||||
|
||||
### discoverable_by_email
|
||||
|
||||
If set to true will allow other users to find this user when searching for their exact email.
|
||||
|
||||
Default: `false`
|
||||
|
||||
Full path: `defaultsettings.discoverable_by_email`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_DISCOVERABLE_BY_EMAIL`
|
||||
|
||||
|
||||
### overdue_tasks_reminders_enabled
|
||||
|
||||
If set to true will send an email every day with all overdue tasks at a configured time.
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `defaultsettings.overdue_tasks_reminders_enabled`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_OVERDUE_TASKS_REMINDERS_ENABLED`
|
||||
|
||||
|
||||
### overdue_tasks_reminders_time
|
||||
|
||||
When to send the overdue task reminder email.
|
||||
|
||||
Default: `9:00`
|
||||
|
||||
Full path: `defaultsettings.overdue_tasks_reminders_time`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_OVERDUE_TASKS_REMINDERS_TIME`
|
||||
|
||||
|
||||
### default_list_id
|
||||
|
||||
The id of the default list. Make sure users actually have access to this list when setting this value.
|
||||
|
||||
Default: `0`
|
||||
|
||||
Full path: `defaultsettings.default_list_id`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_DEFAULT_LIST_ID`
|
||||
|
||||
|
||||
### week_start
|
||||
|
||||
Start of the week for the user. `0` is sunday, `1` is monday and so on.
|
||||
|
||||
Default: `0`
|
||||
|
||||
Full path: `defaultsettings.week_start`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_WEEK_START`
|
||||
|
||||
|
||||
### language
|
||||
|
||||
The language of the user interface. Must be an ISO 639-1 language code. Will default to the browser language the user uses when signing up.
|
||||
|
||||
Default: `<unset>`
|
||||
|
||||
Full path: `defaultsettings.language`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_LANGUAGE`
|
||||
|
||||
|
||||
### timezone
|
||||
|
||||
The time zone of each individual user. This will affect when users get reminders and overdue task emails.
|
||||
|
||||
Default: `<time zone set at service.timezone>`
|
||||
|
||||
Full path: `defaultsettings.timezone`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_TIMEZONE`
|
||||
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ Authelia config:
|
|||
description: Vikunja
|
||||
secret: <vikunja secret>
|
||||
redirect_uris:
|
||||
- https://vikunja.mydomain.com/auth/openid/ <----- Matching slash at the end
|
||||
- https://vikunja.mydomain.com/auth/openid/authelia
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
title: "Running Vikunja in a subdirectory"
|
||||
date: 2022-09-23T12:15:04+02:00
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# Running Vikunja in a subdirectory
|
||||
|
||||
Running Vikunja in a subdirectory is not supported out of the box.
|
||||
However, you can still run it in a subdirectory but need to build the frontend yourself.
|
||||
|
||||
## Frontend
|
||||
|
||||
First, make sure you're able to build the frontend from source.
|
||||
Check [the guide about building from source]({{< ref "build-from-source.md">}}#frontend) about that.
|
||||
|
||||
Then, run
|
||||
|
||||
```
|
||||
pnpm vite build --base=/SUBPATH
|
||||
pnpm workbox copyLibraries dist/
|
||||
```
|
||||
|
||||
Where `SUBPATH` is the subdirectory you want to run Vikunja on.
|
||||
|
||||
Once you have the build files you can deploy them as usual.
|
||||
Note that when deploying in docker you'll need to put the files in a web container yourself, you
|
||||
can't use the `Dockerfile` in the repo without modifications.
|
||||
|
||||
## API
|
||||
|
||||
If you're not using a reverse proxy you're good to go.
|
||||
Simply configure the api url in the frontend as you normally would.
|
||||
|
||||
If you're using a reverse proxy you'll need to adjust the paths so that the api is available at `/SUBPATH/api/v1`.
|
||||
You can check if everything is working correctly by opening `/SUBPATH/api/v1/info` in a browser.
|
|
@ -54,13 +54,14 @@ This document describes the different errors Vikunja can return.
|
|||
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 3001 | 404 | The list does not exist. |
|
||||
| 3004 | 403 | The user needs to have read permissions on that list to perform that action. |
|
||||
| 3005 | 400 | The list title cannot be empty. |
|
||||
| 3006 | 404 | The list share does not exist. |
|
||||
| 3007 | 400 | A list with this identifier already exists. |
|
||||
| 3008 | 412 | The list is archived and can therefore only be accessed read only. This is also true for all tasks associated with this list. |
|
||||
| 3009 | 412 | The list cannot belong to a dynamically generated namespace like "Favorites". |
|
||||
| 3001 | 404 | The list does not exist. |
|
||||
| 3004 | 403 | The user needs to have read permissions on that list to perform that action. |
|
||||
| 3005 | 400 | The list title cannot be empty. |
|
||||
| 3006 | 404 | The list share does not exist. |
|
||||
| 3007 | 400 | A list with this identifier already exists. |
|
||||
| 3008 | 412 | The list is archived and can therefore only be accessed read only. This is also true for all tasks associated with this list. |
|
||||
| 3009 | 412 | The list cannot belong to a dynamically generated namespace like "Favorites". |
|
||||
| 3010 | 412 | The list must belong to a namespace. |
|
||||
|
||||
## Task
|
||||
|
||||
|
|
54
go.mod
54
go.mod
|
@ -25,7 +25,7 @@ require (
|
|||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
|
||||
github.com/bbrks/go-blurhash v1.1.1
|
||||
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
|
||||
github.com/coreos/go-oidc/v3 v3.2.0
|
||||
github.com/coreos/go-oidc/v3 v3.4.0
|
||||
github.com/cweill/gotests v1.6.0
|
||||
github.com/d4l3k/messagediff v1.2.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
|
@ -41,10 +41,10 @@ require (
|
|||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/imdario/mergo v0.3.13
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/labstack/echo/v4 v4.8.0
|
||||
github.com/labstack/echo/v4 v4.9.0
|
||||
github.com/labstack/gommon v0.3.1
|
||||
github.com/lib/pq v1.10.6
|
||||
github.com/magefile/mage v1.13.0
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/magefile/mage v1.14.0
|
||||
github.com/mattn/go-sqlite3 v1.14.15
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
|
@ -54,27 +54,26 @@ require (
|
|||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/spf13/afero v1.9.2
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/spf13/viper v1.13.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/swaggo/swag v1.8.4
|
||||
github.com/swaggo/swag v1.8.6
|
||||
github.com/tkuchiki/go-timezone v0.2.2
|
||||
github.com/ulule/limiter/v3 v3.10.0
|
||||
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93
|
||||
github.com/wneessen/go-mail v0.2.6
|
||||
github.com/yuin/goldmark v1.4.13
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
||||
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539
|
||||
golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
|
||||
github.com/wneessen/go-mail v0.2.9
|
||||
github.com/yuin/goldmark v1.5.2
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
|
||||
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69
|
||||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec
|
||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
src.techknowlogick.com/xgo v1.4.1-0.20210311222705-d25c33fcd864
|
||||
src.techknowlogick.com/xormigrate v1.4.0
|
||||
xorm.io/builder v0.3.9
|
||||
xorm.io/core v0.7.3
|
||||
xorm.io/xorm v1.1.2
|
||||
src.techknowlogick.com/xgo v1.5.1-0.20220906164532-735bfdfb90d9
|
||||
src.techknowlogick.com/xormigrate v1.5.0
|
||||
xorm.io/builder v0.3.12
|
||||
xorm.io/xorm v1.3.2
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -98,6 +97,7 @@ require (
|
|||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
|
@ -111,8 +111,8 @@ require (
|
|||
github.com/lithammer/shortuuid/v3 v3.0.4 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
|
@ -120,7 +120,7 @@ require (
|
|||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
|
@ -130,21 +130,21 @@ require (
|
|||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.3.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
|
||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
||||
golang.org/x/tools v0.1.10 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
|
|
15
magefile.go
15
magefile.go
|
@ -27,7 +27,6 @@ import (
|
|||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
@ -665,7 +664,7 @@ func (Release) Zip() error {
|
|||
|
||||
fmt.Printf("Zipping %s...\n", info.Name())
|
||||
|
||||
c := exec.Command("zip", "-r", RootPath+"/"+DIST+"/zip/"+info.Name(), ".", "-i", "*")
|
||||
c := exec.Command("zip", "-r", RootPath+"/"+DIST+"/zip/"+info.Name()+".zip", ".", "-i", "*")
|
||||
c.Dir = path
|
||||
out, err := c.Output()
|
||||
fmt.Print(string(out))
|
||||
|
@ -701,14 +700,14 @@ func (Release) Packages() error {
|
|||
|
||||
// Because nfpm does not support templating, we replace the values in the config file and restore it after running
|
||||
nfpmConfigPath := RootPath + "/nfpm.yaml"
|
||||
nfpmconfig, err := ioutil.ReadFile(nfpmConfigPath)
|
||||
nfpmconfig, err := os.ReadFile(nfpmConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fixedConfig := strings.ReplaceAll(string(nfpmconfig), "<version>", VersionNumber)
|
||||
fixedConfig = strings.ReplaceAll(fixedConfig, "<binlocation>", BinLocation)
|
||||
if err := ioutil.WriteFile(nfpmConfigPath, []byte(fixedConfig), 0); err != nil {
|
||||
if err := os.WriteFile(nfpmConfigPath, []byte(fixedConfig), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -721,7 +720,7 @@ func (Release) Packages() error {
|
|||
runAndStreamOutput(binpath, "pkg", "--packager", "rpm", "--target", releasePath)
|
||||
runAndStreamOutput(binpath, "pkg", "--packager", "apk", "--target", releasePath)
|
||||
|
||||
return ioutil.WriteFile(nfpmConfigPath, nfpmconfig, 0)
|
||||
return os.WriteFile(nfpmConfigPath, nfpmconfig, 0)
|
||||
}
|
||||
|
||||
type Dev mg.Namespace
|
||||
|
@ -885,7 +884,7 @@ func (s *` + name + `) Handle(msg *message.Message) (err error) {
|
|||
if _, err := f.Seek(idx, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
remainder, err := ioutil.ReadAll(f)
|
||||
remainder, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1074,7 +1073,7 @@ const (
|
|||
// 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")
|
||||
config, err := os.ReadFile("config.yml.sample")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1124,7 +1123,7 @@ func GenerateDocs() error {
|
|||
|
||||
// We write the full file to prevent old content leftovers at the end
|
||||
// I know, there are probably better ways to do this.
|
||||
if err := ioutil.WriteFile(configDocPath, []byte(fullConfig), 0); err != nil {
|
||||
if err := os.WriteFile(configDocPath, []byte(fullConfig), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -161,6 +161,18 @@ const (
|
|||
MetricsEnabled Key = `metrics.enabled`
|
||||
MetricsUsername Key = `metrics.username`
|
||||
MetricsPassword Key = `metrics.password`
|
||||
|
||||
DefaultSettingsAvatarProvider Key = `defaultsettings.avatar_provider`
|
||||
DefaultSettingsAvatarFileID Key = `defaultsettings.avatar_file_id`
|
||||
DefaultSettingsEmailRemindersEnabled Key = `defaultsettings.email_reminders_enabled`
|
||||
DefaultSettingsDiscoverableByName Key = `defaultsettings.discoverable_by_name`
|
||||
DefaultSettingsDiscoverableByEmail Key = `defaultsettings.discoverable_by_email`
|
||||
DefaultSettingsOverdueTaskRemindersEnabled Key = `defaultsettings.overdue_tasks_reminders_enabled`
|
||||
DefaultSettingsDefaultListID Key = `defaultsettings.default_list_id`
|
||||
DefaultSettingsWeekStart Key = `defaultsettings.week_start`
|
||||
DefaultSettingsLanguage Key = `defaultsettings.language`
|
||||
DefaultSettingsTimezone Key = `defaultsettings.timezone`
|
||||
DefaultSettingsOverdueTaskRemindersTime Key = `defaultsettings.overdue_tasks_reminders_time`
|
||||
)
|
||||
|
||||
// GetString returns a string config value
|
||||
|
@ -371,6 +383,10 @@ func InitDefaultConfig() {
|
|||
KeyvalueType.setDefault("memory")
|
||||
// Metrics
|
||||
MetricsEnabled.setDefault(false)
|
||||
// Settings
|
||||
DefaultSettingsAvatarProvider.setDefault("initials")
|
||||
DefaultSettingsOverdueTaskRemindersEnabled.setDefault(true)
|
||||
DefaultSettingsOverdueTaskRemindersTime.setDefault("9:00")
|
||||
}
|
||||
|
||||
// InitConfig initializes the config, sets defaults etc.
|
||||
|
@ -438,6 +454,10 @@ func InitConfig() {
|
|||
MigrationMicrosoftTodoRedirectURL.Set(ServiceFrontendurl.GetString() + "migrate/microsoft-todo")
|
||||
}
|
||||
|
||||
if DefaultSettingsTimezone.GetString() == "" {
|
||||
DefaultSettingsTimezone.Set(ServiceTimeZone.GetString())
|
||||
}
|
||||
|
||||
if ServiceEnableMetrics.GetBool() {
|
||||
log.Println("WARNING: service.enablemetrics is deprecated and will be removed in a future release. Please use metrics.enable.")
|
||||
MetricsEnabled.Set(true)
|
||||
|
|
11
pkg/db/db.go
11
pkg/db/db.go
|
@ -27,9 +27,9 @@ import (
|
|||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
xrc "gitea.com/xorm/xorm-redis-cache"
|
||||
"xorm.io/core"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/caches"
|
||||
"xorm.io/xorm/names"
|
||||
"xorm.io/xorm/schemas"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // Because.
|
||||
|
@ -80,7 +80,7 @@ func CreateDBEngine() (engine *xorm.Engine, err error) {
|
|||
log.Fatalf("Error parsing time zone: %s", err)
|
||||
}
|
||||
engine.SetTZDatabase(loc)
|
||||
engine.SetMapper(core.GonicMapper{})
|
||||
engine.SetMapper(names.GonicMapper{})
|
||||
logger := log.NewXormLogger("")
|
||||
engine.SetLogger(logger)
|
||||
|
||||
|
@ -149,11 +149,12 @@ func parsePostgreSQLHostPort(info string) (string, string) {
|
|||
|
||||
func initPostgresEngine() (engine *xorm.Engine, err error) {
|
||||
host, port := parsePostgreSQLHostPort(config.DatabaseHost.GetString())
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s",
|
||||
host,
|
||||
port,
|
||||
// postgresql://username:password@host:port/dbname[?paramspec]
|
||||
connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s&sslcert=%s&sslkey=%s&sslrootcert=%s",
|
||||
config.DatabaseUser.GetString(),
|
||||
config.DatabasePassword.GetString(),
|
||||
host,
|
||||
port,
|
||||
config.DatabaseDatabase.GetString(),
|
||||
config.DatabaseSslMode.GetString(),
|
||||
config.DatabaseSslCert.GetString(),
|
||||
|
|
|
@ -23,9 +23,10 @@ import (
|
|||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/core"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/names"
|
||||
)
|
||||
|
||||
// CreateTestEngine creates an instance of the db engine which lives in memory
|
||||
|
@ -48,7 +49,7 @@ func CreateTestEngine() (engine *xorm.Engine, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
engine.SetMapper(core.GonicMapper{})
|
||||
engine.SetMapper(names.GonicMapper{})
|
||||
logger := log.NewXormLogger("DEBUG")
|
||||
logger.ShowSQL(os.Getenv("UNIT_TESTS_VERBOSE") == "1")
|
||||
engine.SetLogger(logger)
|
||||
|
|
|
@ -227,7 +227,7 @@ func TestArchived(t *testing.T) {
|
|||
assertHandlerErrorCode(t, err, models.ErrCodeListIsArchived)
|
||||
})
|
||||
t.Run("unarchivable", func(t *testing.T) {
|
||||
rec, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "22"}, `{"title":"LoremIpsum","is_archived":false}`)
|
||||
rec, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "22"}, `{"title":"LoremIpsum","is_archived":false,"namespace_id":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
|
||||
})
|
||||
|
|
|
@ -232,12 +232,12 @@ func TestLinkSharing(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerListWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandlerListWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerListAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandlerListAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"TestLoremIpsum","namespace_id":2}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
|
|
|
@ -171,7 +171,7 @@ func TestList(t *testing.T) {
|
|||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
// Check the list was loaded successfully afterwards, see testReadOneWithUser
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
// The description should not be updated but returned correctly
|
||||
|
@ -183,7 +183,7 @@ func TestList(t *testing.T) {
|
|||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
})
|
||||
t.Run("Normal with updating the description", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet","namespace_id":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`)
|
||||
|
@ -211,12 +211,12 @@ func TestList(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
|
@ -227,12 +227,12 @@ func TestList(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
|
@ -243,12 +243,12 @@ func TestList(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "13"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "13"}, `{"title":"TestLoremIpsum","namespace_id":8}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum","namespace_id":9}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
|
@ -259,12 +259,12 @@ func TestList(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "16"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "16"}, `{"title":"TestLoremIpsum","namespace_id":11}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum","namespace_id":12}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
|
|
|
@ -55,6 +55,7 @@ func getClient() (*mail.Client, error) {
|
|||
InsecureSkipVerify: config.MailerSkipTLSVerify.GetBool(),
|
||||
ServerName: config.MailerHost.GetString(),
|
||||
}),
|
||||
mail.WithTimeout((config.MailerQueueTimeout.GetDuration() + 3) * time.Second), // 3s more for us to close before mail server timeout
|
||||
}
|
||||
|
||||
if config.MailerUsername.GetString() != "" && config.MailerPassword.GetString() != "" {
|
||||
|
|
|
@ -283,6 +283,34 @@ func (err *ErrListCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// ErrListMustBelongToANamespace represents an error where a list must belong to a namespace
|
||||
type ErrListMustBelongToANamespace struct {
|
||||
ListID int64
|
||||
NamespaceID int64
|
||||
}
|
||||
|
||||
// IsErrListMustBelongToANamespace checks if an error is a list must belong to a namespace error.
|
||||
func IsErrListMustBelongToANamespace(err error) bool {
|
||||
_, ok := err.(*ErrListMustBelongToANamespace)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrListMustBelongToANamespace) Error() string {
|
||||
return fmt.Sprintf("List must belong to a namespace [ListID: %d, NamespaceID: %d]", err.ListID, err.NamespaceID)
|
||||
}
|
||||
|
||||
// ErrCodeListMustBelongToANamespace holds the unique world-error code of this error
|
||||
const ErrCodeListMustBelongToANamespace = 3010
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrListMustBelongToANamespace) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeListMustBelongToANamespace,
|
||||
Message: "This list must belong to a namespace.",
|
||||
}
|
||||
}
|
||||
|
||||
// ================
|
||||
// List task errors
|
||||
// ================
|
||||
|
|
|
@ -476,12 +476,22 @@ func addListDetails(s *xorm.Session, lists []*List, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
subscriptions, err := GetSubscriptions(s, SubscriptionEntityList, listIDs, a)
|
||||
if err != nil {
|
||||
log.Errorf("An error occurred while getting list subscriptions for a namespace item: %s", err.Error())
|
||||
subscriptions = make(map[int64]*Subscription)
|
||||
}
|
||||
|
||||
for _, list := range lists {
|
||||
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
|
||||
if list.IsFavorite {
|
||||
continue
|
||||
}
|
||||
list.IsFavorite = favs[list.ID]
|
||||
|
||||
if subscription, exists := subscriptions[list.ID]; exists {
|
||||
list.Subscription = subscription
|
||||
}
|
||||
}
|
||||
|
||||
if len(fileIDs) == 0 {
|
||||
|
@ -630,6 +640,13 @@ func UpdateList(s *xorm.Session, list *List, auth web.Auth, updateListBackground
|
|||
return
|
||||
}
|
||||
|
||||
if list.NamespaceID == 0 {
|
||||
return &ErrListMustBelongToANamespace{
|
||||
ListID: list.ID,
|
||||
NamespaceID: list.NamespaceID,
|
||||
}
|
||||
}
|
||||
|
||||
// We need to specify the cols we want to update here to be able to un-archive lists
|
||||
colsToUpdate := []string{
|
||||
"title",
|
||||
|
|
|
@ -66,6 +66,7 @@ func (ld *ListDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool,
|
|||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list or namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/duplicate [put]
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
||||
|
||||
|
|
|
@ -140,8 +140,9 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 99999999,
|
||||
Title: "test",
|
||||
ID: 99999999,
|
||||
Title: "test",
|
||||
NamespaceID: 1,
|
||||
}
|
||||
err := list.Update(s, usr)
|
||||
assert.Error(t, err)
|
||||
|
|
|
@ -335,7 +335,7 @@ func getListsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool)
|
|||
func getSharedListsInNamespace(s *xorm.Session, archived bool, doer *user.User) (sharedListsNamespace *NamespaceWithLists, err error) {
|
||||
// Create our pseudo namespace to hold the shared lists
|
||||
sharedListsPseudonamespace := SharedListsPseudoNamespace
|
||||
sharedListsPseudonamespace.Owner = doer
|
||||
sharedListsPseudonamespace.OwnerID = doer.ID
|
||||
sharedListsNamespace = &NamespaceWithLists{
|
||||
sharedListsPseudonamespace,
|
||||
[]*List{},
|
||||
|
@ -385,12 +385,13 @@ func getSharedListsInNamespace(s *xorm.Session, archived bool, doer *user.User)
|
|||
func getFavoriteLists(s *xorm.Session, lists []*List, namespaceIDs []int64, doer *user.User) (favoriteNamespace *NamespaceWithLists, err error) {
|
||||
// Create our pseudo namespace with favorite lists
|
||||
pseudoFavoriteNamespace := FavoritesPseudoNamespace
|
||||
pseudoFavoriteNamespace.Owner = doer
|
||||
pseudoFavoriteNamespace.OwnerID = doer.ID
|
||||
favoriteNamespace = &NamespaceWithLists{
|
||||
Namespace: pseudoFavoriteNamespace,
|
||||
Lists: []*List{{}},
|
||||
}
|
||||
*favoriteNamespace.Lists[0] = FavoritesPseudoList // Copying the list to be able to modify it later
|
||||
favoriteNamespace.Lists[0].Owner = doer
|
||||
|
||||
for _, list := range lists {
|
||||
if !list.IsFavorite {
|
||||
|
@ -448,7 +449,7 @@ func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *N
|
|||
}
|
||||
|
||||
savedFiltersPseudoNamespace := SavedFiltersPseudoNamespace
|
||||
savedFiltersPseudoNamespace.Owner = doer
|
||||
savedFiltersPseudoNamespace.OwnerID = doer.ID
|
||||
savedFiltersNamespace = &NamespaceWithLists{
|
||||
Namespace: savedFiltersPseudoNamespace,
|
||||
Lists: make([]*List, 0, len(savedFilters)),
|
||||
|
@ -479,6 +480,7 @@ func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *N
|
|||
// @Success 200 {array} models.NamespaceWithLists "The Namespaces."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces [get]
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
|
@ -517,6 +519,7 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
ownerMap[doer.ID] = doer
|
||||
|
||||
if n.NamespacesOnly {
|
||||
all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
|
||||
|
|
|
@ -18,6 +18,7 @@ package models
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -230,14 +231,23 @@ func (n *UndoneTaskOverdueNotification) Name() string {
|
|||
// UndoneTasksOverdueNotification represents a UndoneTasksOverdueNotification notification
|
||||
type UndoneTasksOverdueNotification struct {
|
||||
User *user.User
|
||||
Tasks []*Task
|
||||
Tasks map[int64]*Task
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for UndoneTasksOverdueNotification
|
||||
func (n *UndoneTasksOverdueNotification) ToMail() *notifications.Mail {
|
||||
|
||||
overdueLine := ""
|
||||
sortedTasks := make([]*Task, 0, len(n.Tasks))
|
||||
for _, task := range n.Tasks {
|
||||
sortedTasks = append(sortedTasks, task)
|
||||
}
|
||||
|
||||
sort.Slice(sortedTasks, func(i, j int) bool {
|
||||
return sortedTasks[i].DueDate.Before(sortedTasks[j].DueDate)
|
||||
})
|
||||
|
||||
overdueLine := ""
|
||||
for _, task := range sortedTasks {
|
||||
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"
|
||||
}
|
||||
|
|
|
@ -228,28 +228,53 @@ func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int6
|
|||
// that task, if there is none it will look for a subscription on the list the task belongs to and if that also
|
||||
// doesn't exist it will check for a subscription for the namespace the list is belonging to.
|
||||
func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) {
|
||||
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, a)
|
||||
if err != nil || len(subs) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
if sub, exists := subs[entityID]; exists {
|
||||
return sub, nil // Take exact match first, if available
|
||||
}
|
||||
for _, sub := range subs {
|
||||
return sub, nil // For parents, take next available
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetSubscriptions returns a map of subscriptions to a set of given entity IDs
|
||||
func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (listsToSubscriptions map[int64]*Subscription, err error) {
|
||||
u, is := a.(*user.User)
|
||||
if !is {
|
||||
return
|
||||
}
|
||||
|
||||
if err := entityType.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscription = &Subscription{}
|
||||
cond := getSubscriberCondForEntity(entityType, entityID)
|
||||
exists, err := s.
|
||||
var entitiesFilter builder.Cond
|
||||
for _, eID := range entityIDs {
|
||||
if entitiesFilter == nil {
|
||||
entitiesFilter = getSubscriberCondForEntity(entityType, eID)
|
||||
continue
|
||||
}
|
||||
entitiesFilter = entitiesFilter.Or(getSubscriberCondForEntity(entityType, eID))
|
||||
}
|
||||
|
||||
var subscriptions []*Subscription
|
||||
err = s.
|
||||
Where("user_id = ?", u.ID).
|
||||
And(cond).
|
||||
Get(subscription)
|
||||
if !exists {
|
||||
And(entitiesFilter).
|
||||
Find(&subscriptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscription.Entity = subscription.EntityType.String()
|
||||
|
||||
return subscription, err
|
||||
listsToSubscriptions = make(map[int64]*Subscription)
|
||||
for _, sub := range subscriptions {
|
||||
sub.Entity = sub.EntityType.String()
|
||||
listsToSubscriptions[sub.EntityID] = sub
|
||||
}
|
||||
return listsToSubscriptions, nil
|
||||
}
|
||||
|
||||
func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType, entityID int64) (subscriptions []*Subscription, err error) {
|
||||
|
|
|
@ -92,10 +92,10 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
|
|||
if !exists {
|
||||
uts[t.User.ID] = &userWithTasks{
|
||||
user: t.User,
|
||||
tasks: []*Task{},
|
||||
tasks: make(map[int64]*Task),
|
||||
}
|
||||
}
|
||||
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
|
||||
uts[t.User.ID].tasks[t.Task.ID] = t.Task
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,7 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
|
|||
|
||||
type userWithTasks struct {
|
||||
user *user.User
|
||||
tasks []*Task
|
||||
tasks map[int64]*Task
|
||||
}
|
||||
|
||||
// RegisterOverdueReminderCron registers a function which checks once a day for tasks that are overdue and not done.
|
||||
|
|
|
@ -979,6 +979,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its list)"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{id} [post]
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
|
@ -1331,8 +1332,8 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
|
|||
// 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
|
||||
// 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 {
|
||||
switch oldTask.RepeatMode {
|
||||
|
|
|
@ -18,7 +18,7 @@ package gravatar
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -74,7 +74,7 @@ func (g *Provider) GetAvatar(user *user.User, size int64) ([]byte, string, error
|
|||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
avatarContent, err := ioutil.ReadAll(resp.Body)
|
||||
avatarContent, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
|
|
@ -20,13 +20,14 @@ import (
|
|||
"bytes"
|
||||
"image"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
|
@ -82,7 +83,7 @@ func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType
|
|||
return nil, "", err
|
||||
}
|
||||
|
||||
avatar, err = ioutil.ReadAll(buf)
|
||||
avatar, err = io.ReadAll(buf)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
|
|
@ -18,15 +18,14 @@ package migration
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
|
||||
"code.vikunja.io/api/pkg/modules/background/handler"
|
||||
"io"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/background/handler"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
)
|
||||
|
||||
|
@ -212,7 +211,7 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas
|
|||
// Check if we have a file to create
|
||||
if len(a.File.FileContent) > 0 {
|
||||
a.TaskID = t.ID
|
||||
fr := ioutil.NopCloser(bytes.NewReader(a.File.FileContent))
|
||||
fr := io.NopCloser(bytes.NewReader(a.File.FileContent))
|
||||
err = a.NewAttachment(s, fr, a.File.Name, a.File.Size, user)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
|
@ -105,6 +105,10 @@ type item struct {
|
|||
DateCompleted time.Time `json:"date_completed"`
|
||||
}
|
||||
|
||||
type itemWrapper struct {
|
||||
Item *item `json:"item"`
|
||||
}
|
||||
|
||||
type doneItem struct {
|
||||
CompletedDate time.Time `json:"completed_date"`
|
||||
Content string `json:"content"`
|
||||
|
@ -115,7 +119,8 @@ type doneItem struct {
|
|||
}
|
||||
|
||||
type doneItemSync struct {
|
||||
Items []*doneItem `json:"items"`
|
||||
Items []*doneItem `json:"items"`
|
||||
Projects map[string]*project `json:"projects"`
|
||||
}
|
||||
|
||||
type fileAttachment struct {
|
||||
|
@ -358,6 +363,11 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik
|
|||
|
||||
tasks[i.ID] = task
|
||||
|
||||
if _, exists := lists[i.ProjectID]; !exists {
|
||||
log.Debugf("[Todoist Migration] Tried to put item %d in project %d but the project does not exist", i.ID, i.ProjectID)
|
||||
continue
|
||||
}
|
||||
|
||||
lists[i.ProjectID].Tasks = append(lists[i.ProjectID].Tasks, task)
|
||||
}
|
||||
|
||||
|
@ -540,33 +550,62 @@ func (m *Migration) Migrate(u *user.User) (err error) {
|
|||
}
|
||||
|
||||
log.Debugf("[Todoist Migration] Getting done items for user %d", u.ID)
|
||||
// Get all done tasks
|
||||
resp, err = migration.DoPost("https://api.todoist.com/sync/v8/completed/get_all", form)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
completedSyncResponse := &doneItemSync{}
|
||||
err = json.NewDecoder(resp.Body).Decode(completedSyncResponse)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Get all done tasks and projects
|
||||
offset := 0
|
||||
doneItems := make(map[int64]*doneItem)
|
||||
|
||||
sort.Slice(completedSyncResponse.Items, func(i, j int) bool {
|
||||
return completedSyncResponse.Items[i].CompletedDate.After(completedSyncResponse.Items[j].CompletedDate)
|
||||
})
|
||||
|
||||
doneItems := make(map[int64]*doneItem, len(completedSyncResponse.Items))
|
||||
for _, i := range completedSyncResponse.Items {
|
||||
if _, has := doneItems[i.TaskID]; has {
|
||||
// Only set the newest completion date
|
||||
continue
|
||||
for {
|
||||
resp, err = migration.DoPost("https://api.todoist.com/sync/v8/completed/get_all?limit=200&offset="+strconv.Itoa(offset), form)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
doneItems[i.TaskID] = i
|
||||
defer resp.Body.Close()
|
||||
|
||||
completedSyncResponse := &doneItemSync{}
|
||||
err = json.NewDecoder(resp.Body).Decode(completedSyncResponse)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sort.Slice(completedSyncResponse.Items, func(i, j int) bool {
|
||||
return completedSyncResponse.Items[i].CompletedDate.After(completedSyncResponse.Items[j].CompletedDate)
|
||||
})
|
||||
|
||||
for _, i := range completedSyncResponse.Items {
|
||||
if _, has := doneItems[i.TaskID]; has {
|
||||
// Only set the newest completion date
|
||||
continue
|
||||
}
|
||||
doneItems[i.TaskID] = i
|
||||
|
||||
// need to get done item data
|
||||
resp, err = migration.DoPost("https://api.todoist.com/sync/v8/items/get", url.Values{
|
||||
"token": []string{token},
|
||||
"item_id": []string{strconv.FormatInt(i.TaskID, 10)},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
doneI := &itemWrapper{}
|
||||
err = json.NewDecoder(resp.Body).Decode(doneI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Debugf("[Todoist Migration] Retrieved full task data for done task %d", i.TaskID)
|
||||
syncResponse.Items = append(syncResponse.Items, doneI.Item)
|
||||
}
|
||||
|
||||
if len(completedSyncResponse.Items) < 200 {
|
||||
break
|
||||
}
|
||||
offset++
|
||||
log.Debugf("[Todoist Migration] User %d has more than 200 done tasks or projects, looping to get more; iteration %d", u.ID, offset)
|
||||
}
|
||||
|
||||
log.Debugf("[Todoist Migration] Got %d done items for user %d", len(completedSyncResponse.Items), u.ID)
|
||||
log.Debugf("[Todoist Migration] Got %d done items for user %d", len(doneItems), u.ID)
|
||||
log.Debugf("[Todoist Migration] Getting archived projects for user %d", u.ID)
|
||||
|
||||
// Get all archived projects
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
package todoist
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -47,7 +47,7 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
dueTimeWithTime = dueTimeWithTime.In(config.GetTimeZone())
|
||||
nilTime, err := time.Parse(time.RFC3339Nano, "0001-01-01T00:00:00Z")
|
||||
assert.NoError(t, err)
|
||||
exampleFile, err := ioutil.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/wunderlist/testimage.jpg")
|
||||
exampleFile, err := os.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/wunderlist/testimage.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
makeTestItem := func(id, projectId int64, hasDueDate, hasLabels, done bool) *item {
|
||||
|
|
|
@ -18,7 +18,7 @@ package trello
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -36,7 +36,7 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||
|
||||
time1, err := time.Parse(time.RFC3339Nano, "2014-09-26T08:25:05Z")
|
||||
assert.NoError(t, err)
|
||||
exampleFile, err := ioutil.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/wunderlist/testimage.jpg")
|
||||
exampleFile, err := os.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/wunderlist/testimage.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
trelloData := []*trello.Board{
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
package wunderlist
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -46,7 +46,7 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
time4 = time4.In(config.GetTimeZone())
|
||||
|
||||
exampleFile, err := ioutil.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/wunderlist/testimage.jpg")
|
||||
exampleFile, err := os.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/wunderlist/testimage.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
createTestTask := func(id, listID int, done bool) *task {
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
@ -35,37 +36,47 @@ func BasicAuth(username, password string, c echo.Context) (bool, error) {
|
|||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
u, err := user.CheckUserCredentials(s, credentials)
|
||||
if err != nil && !user.IsErrWrongUsernameOrPassword(err) && !user.IsErrAccountIsNotLocal(err) {
|
||||
log.Errorf("Error during basic auth for caldav: %v", err)
|
||||
var err error
|
||||
u, err := checkUserCaldavTokens(s, credentials)
|
||||
if user.IsErrUserDoesNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if u == nil {
|
||||
u, err = user.CheckUserCredentials(s, credentials)
|
||||
if err != nil {
|
||||
log.Errorf("Error during basic auth for caldav: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if u != nil && err == nil {
|
||||
c.Set("userBasicAuth", u)
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
tokens, err := user.GetCaldavTokens(u)
|
||||
func checkUserCaldavTokens(s *xorm.Session, login *user.Login) (*user.User, error) {
|
||||
usr, err := user.GetUserByUsername(s, login.Username)
|
||||
if err != nil || usr == nil {
|
||||
log.Warningf("Error while retrieving users from database: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
tokens, err := user.GetCaldavTokens(usr)
|
||||
if err != nil {
|
||||
log.Errorf("Error while getting tokens for caldav auth: %v", err)
|
||||
return false, nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Looping over all tokens until we find one that matches
|
||||
for _, token := range tokens {
|
||||
err = bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(password))
|
||||
err = bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(login.Password))
|
||||
if err != nil {
|
||||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
continue
|
||||
}
|
||||
log.Errorf("Error while verifying tokens for caldav auth: %v", err)
|
||||
return false, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
c.Set("userBasicAuth", u)
|
||||
return true, nil
|
||||
return usr, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ package caldav
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -61,9 +61,9 @@ func ListHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Try to parse a task from the request payload
|
||||
body, _ := ioutil.ReadAll(c.Request().Body)
|
||||
body, _ := io.ReadAll(c.Request().Body)
|
||||
// Restore the io.ReadCloser to its original state
|
||||
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
c.Request().Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
// Parse it
|
||||
vtodo := string(body)
|
||||
if vtodo != "" && strings.HasPrefix(vtodo, `BEGIN:VCALENDAR`) {
|
||||
|
@ -127,9 +127,9 @@ func PrincipalHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Try to parse a task from the request payload
|
||||
body, _ := ioutil.ReadAll(c.Request().Body)
|
||||
body, _ := io.ReadAll(c.Request().Body)
|
||||
// Restore the io.ReadCloser to its original state
|
||||
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
c.Request().Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
log.Debugf("[CALDAV] Request Body: %v\n", string(body))
|
||||
log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
|
||||
|
@ -157,9 +157,9 @@ func EntryHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Try to parse a task from the request payload
|
||||
body, _ := ioutil.ReadAll(c.Request().Body)
|
||||
body, _ := io.ReadAll(c.Request().Body)
|
||||
// Restore the io.ReadCloser to its original state
|
||||
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
c.Request().Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
log.Debugf("[CALDAV] Request Body: %v\n", string(body))
|
||||
log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
|
||||
|
|
|
@ -54,7 +54,17 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {
|
|||
}
|
||||
|
||||
user.Status = StatusActive
|
||||
user.AvatarProvider = "initials"
|
||||
user.AvatarProvider = config.DefaultSettingsAvatarProvider.GetString()
|
||||
user.AvatarFileID = config.DefaultSettingsAvatarFileID.GetInt64()
|
||||
user.EmailRemindersEnabled = config.DefaultSettingsEmailRemindersEnabled.GetBool()
|
||||
user.DiscoverableByName = config.DefaultSettingsDiscoverableByName.GetBool()
|
||||
user.DiscoverableByEmail = config.DefaultSettingsDiscoverableByEmail.GetBool()
|
||||
user.OverdueTasksRemindersEnabled = config.DefaultSettingsOverdueTaskRemindersEnabled.GetBool()
|
||||
user.OverdueTasksRemindersTime = config.DefaultSettingsOverdueTaskRemindersTime.GetString()
|
||||
user.DefaultListID = config.DefaultSettingsDefaultListID.GetInt64()
|
||||
user.WeekStart = config.DefaultSettingsWeekStart.GetInt()
|
||||
user.Language = config.DefaultSettingsLanguage.GetString()
|
||||
user.Timezone = config.DefaultSettingsTimezone.GetString()
|
||||
|
||||
// Insert it
|
||||
_, err = s.Insert(user)
|
||||
|
|
|
@ -369,6 +369,16 @@ func TestListUsers(t *testing.T) {
|
|||
assert.True(t, len(all) > 0)
|
||||
assert.Equal(t, all[0].Username, "user1")
|
||||
})
|
||||
t.Run("case insensitive", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
all, err := ListUsers(s, "uSEr1", nil)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(all) > 0)
|
||||
assert.Equal(t, all[0].Username, "user1")
|
||||
})
|
||||
t.Run("all users", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
|
|
@ -19,8 +19,11 @@ package user
|
|||
import (
|
||||
"strings"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
type ListUserOpts struct {
|
||||
|
@ -45,14 +48,22 @@ func ListUsers(s *xorm.Session, search string, opts *ListUserOpts) (users []*Use
|
|||
|
||||
if search != "" {
|
||||
for _, queryPart := range strings.Split(search, ",") {
|
||||
var usernameCond builder.Cond = builder.Eq{"username": queryPart}
|
||||
if db.Type() == schemas.POSTGRES {
|
||||
usernameCond = builder.Expr("username ILIKE ?", queryPart)
|
||||
}
|
||||
if db.Type() == schemas.SQLITE {
|
||||
usernameCond = builder.Expr("username = ? COLLATE NOCASE", queryPart)
|
||||
}
|
||||
|
||||
conds = append(conds,
|
||||
builder.Eq{"username": queryPart},
|
||||
usernameCond,
|
||||
builder.And(
|
||||
builder.Eq{"email": queryPart},
|
||||
builder.Eq{"discoverable_by_email": true},
|
||||
),
|
||||
builder.And(
|
||||
builder.Like{"name", "%" + queryPart + "%"},
|
||||
db.ILIKE("name", queryPart),
|
||||
builder.Eq{"discoverable_by_name": true},
|
||||
),
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue