forked from vikunja/vikunja
Compare commits
1 Commits
main
...
renovate/g
Author | SHA1 | Date | |
---|---|---|---|
11806b7654 |
|
@ -1,7 +1,6 @@
|
|||
files/
|
||||
dist/
|
||||
logs/
|
||||
docs/
|
||||
|
||||
Dockerfile
|
||||
docker-manifest.tmpl
|
||||
|
|
142
.drone.yml
142
.drone.yml
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-and-test
|
||||
name: build-and-test-api
|
||||
|
||||
workspace:
|
||||
base: /go
|
||||
|
@ -122,7 +122,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: api-build
|
||||
- name: build
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -133,7 +133,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: api-lint
|
||||
- name: lint
|
||||
image: golangci/golangci-lint:v1.55.2
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -156,9 +156,7 @@ steps:
|
|||
- name: test-migration-sqlite
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
depends_on:
|
||||
- test-migration-prepare
|
||||
- api-build
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
environment:
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
VIKUNJA_DATABASE_PATH: /db/vikunja-migration-test.db
|
||||
|
@ -177,9 +175,7 @@ steps:
|
|||
- name: test-migration-mysql
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
depends_on:
|
||||
- test-migration-prepare
|
||||
- api-build
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
environment:
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_HOST: test-mysql-migration
|
||||
|
@ -198,9 +194,7 @@ steps:
|
|||
- name: test-migration-psql
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
depends_on:
|
||||
- test-migration-prepare
|
||||
- api-build
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
environment:
|
||||
VIKUNJA_DATABASE_TYPE: postgres
|
||||
VIKUNJA_DATABASE_HOST: test-postgres-migration
|
||||
|
@ -217,7 +211,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: api-test-unit
|
||||
- name: test
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -228,7 +222,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: api-test-unit-sqlite
|
||||
- name: test-sqlite
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -245,7 +239,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: api-test-unit-mysql
|
||||
- name: test-mysql
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -262,7 +256,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: api-test-unit-postgres
|
||||
- name: test-postgres
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -343,22 +337,30 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: test-api-run
|
||||
image: vikunja/golang-build:latest
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-and-test-frontend
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
include:
|
||||
- main
|
||||
event:
|
||||
include:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
services:
|
||||
- name: api
|
||||
image: vikunja/api:unstable
|
||||
pull: always
|
||||
environment:
|
||||
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
|
||||
VIKUNJA_LOG_LEVEL: DEBUG
|
||||
VIKUNJA_CORS_ENABLE: 1
|
||||
VIKUNJA_DATABASE_PATH: memory
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
commands:
|
||||
- ./vikunja
|
||||
detach: true
|
||||
depends_on:
|
||||
- api-build
|
||||
|
||||
- name: frontend-dependencies
|
||||
steps:
|
||||
- name: dependencies
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -372,7 +374,7 @@ steps:
|
|||
# depends_on:
|
||||
# - restore-cache
|
||||
|
||||
- name: frontend-lint
|
||||
- name: lint
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -382,9 +384,9 @@ steps:
|
|||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm run lint
|
||||
depends_on:
|
||||
- frontend-dependencies
|
||||
- dependencies
|
||||
|
||||
- name: frontend-build-prod
|
||||
- name: build-prod
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -392,11 +394,11 @@ steps:
|
|||
commands:
|
||||
- cd frontend
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm run build:test
|
||||
- pnpm run build
|
||||
depends_on:
|
||||
- frontend-dependencies
|
||||
- dependencies
|
||||
|
||||
- name: frontend-test-unit
|
||||
- name: test-unit
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
commands:
|
||||
|
@ -404,9 +406,9 @@ steps:
|
|||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm run test:unit
|
||||
depends_on:
|
||||
- frontend-dependencies
|
||||
- dependencies
|
||||
|
||||
- name: frontend-typecheck
|
||||
- name: typecheck
|
||||
failure: ignore
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
|
@ -417,13 +419,13 @@ steps:
|
|||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm run typecheck
|
||||
depends_on:
|
||||
- frontend-dependencies
|
||||
- dependencies
|
||||
|
||||
- name: frontend-test
|
||||
- name: test-frontend
|
||||
image: cypress/browsers:node18.12.0-chrome107
|
||||
pull: always
|
||||
environment:
|
||||
CYPRESS_API_URL: http://test-api-run:3456/api/v1
|
||||
CYPRESS_API_URL: http://api:3456/api/v1
|
||||
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress
|
||||
|
@ -432,15 +434,14 @@ steps:
|
|||
from_secret: cypress_project_key
|
||||
commands:
|
||||
- cd frontend
|
||||
- sed -i 's/localhost/test-api-run/g' dist-test/index.html
|
||||
- sed -i 's/localhost/api/g' dist/index.html
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm cypress install
|
||||
- pnpm run test:e2e-record-test
|
||||
- pnpm run test:e2e-record
|
||||
depends_on:
|
||||
- frontend-build-prod
|
||||
- test-api-run
|
||||
- build-prod
|
||||
|
||||
- name: frontend-deploy-preview
|
||||
- name: deploy-preview
|
||||
image: williamjackson/netlify-cli
|
||||
pull: always
|
||||
user: root # The rest runs as root and thus the permissions wouldn't work
|
||||
|
@ -453,7 +454,7 @@ steps:
|
|||
from_secret: gitea_token
|
||||
commands:
|
||||
- cd frontend
|
||||
- cp -r dist-test dist-preview
|
||||
- cp -r dist dist-preview
|
||||
# Override the default api url used for preview
|
||||
- sed -i 's|http://localhost:3456|https://try.vikunja.io|g' dist-preview/index.html
|
||||
- apk add --no-cache perl-utils
|
||||
|
@ -462,7 +463,7 @@ steps:
|
|||
- shasum -a 384 -c ./scripts/deploy-preview-netlify.mjs.sha384
|
||||
- node ./scripts/deploy-preview-netlify.mjs
|
||||
depends_on:
|
||||
- frontend-build-prod
|
||||
- build-prod
|
||||
when:
|
||||
event:
|
||||
include:
|
||||
|
@ -474,7 +475,7 @@ type: docker
|
|||
name: generate-swagger-docs
|
||||
|
||||
depends_on:
|
||||
- build-and-test
|
||||
- build-and-test-api
|
||||
|
||||
workspace:
|
||||
base: /go
|
||||
|
@ -518,7 +519,8 @@ type: docker
|
|||
name: release
|
||||
|
||||
depends_on:
|
||||
- build-and-test
|
||||
- build-and-test-api
|
||||
- build-and-test-frontend
|
||||
|
||||
workspace:
|
||||
base: /source
|
||||
|
@ -781,20 +783,6 @@ steps:
|
|||
- tag
|
||||
depends_on: [ build-os-packages-version ]
|
||||
|
||||
- name: gitea-release
|
||||
image: plugins/gitea-release
|
||||
pull: true
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: gitea_token
|
||||
base_url: https://kolaente.dev
|
||||
files: dist/zip/*
|
||||
prerelease: true
|
||||
title: ${DRONE_TAG##v}
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
depends_on: [ sign-release ]
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
|
@ -802,7 +790,8 @@ type: docker
|
|||
name: docker-release
|
||||
|
||||
depends_on:
|
||||
- build-and-test
|
||||
- build-and-test-api
|
||||
- build-and-test-frontend
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
|
@ -876,7 +865,7 @@ type: docker
|
|||
name: frontend-release-unstable
|
||||
|
||||
depends_on:
|
||||
- build-and-test
|
||||
- build-and-test-frontend
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
|
@ -939,7 +928,7 @@ type: docker
|
|||
name: frontend-release-version
|
||||
|
||||
depends_on:
|
||||
- build-and-test
|
||||
- build-and-test-frontend
|
||||
|
||||
trigger:
|
||||
event:
|
||||
|
@ -1210,21 +1199,8 @@ steps:
|
|||
# - '.cache'
|
||||
# depends_on:
|
||||
# - build
|
||||
|
||||
- name: rename-unstable
|
||||
image: bash
|
||||
pull: true
|
||||
commands:
|
||||
- cd desktop/dist
|
||||
- bash -c 'for file in Vikunja*; do suffix=".$${file##*.}"; if [[ ! -d $file ]]; then mv "$file" "Vikunja-Desktop-unstable$${suffix}"; fi; done'
|
||||
depends_on:
|
||||
- build
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: release-unstable
|
||||
- name: release-latest
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
|
@ -1237,14 +1213,13 @@ steps:
|
|||
region: fr-par
|
||||
path_style: true
|
||||
strip_prefix: desktop/dist/
|
||||
source: desktop/dist/Vikunja-Desktop*
|
||||
source: desktop/dist/*
|
||||
target: /desktop/unstable/
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
depends_on:
|
||||
- rename-unstable
|
||||
depends_on: [ build ]
|
||||
|
||||
- name: release-version
|
||||
image: plugins/s3
|
||||
|
@ -1362,7 +1337,8 @@ trigger:
|
|||
- "refs/tags/**"
|
||||
|
||||
depends_on:
|
||||
- build-and-test
|
||||
- build-and-test-api
|
||||
- build-and-test-frontend
|
||||
- release
|
||||
- deploy-docs
|
||||
- docker-release
|
||||
|
@ -1384,6 +1360,6 @@ steps:
|
|||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: 008b86263a8d03806da907c128a837a380901f1a2190a658c22d4e06cadc1b64
|
||||
hmac: 128214182f90834702da16ed7b0f1dec09dc61bbbe7f293aea7cb6c46e7edae2
|
||||
|
||||
...
|
||||
|
|
17
Dockerfile
17
Dockerfile
|
@ -33,15 +33,22 @@ RUN export PATH=$PATH:$GOPATH/bin && \
|
|||
# ┘└┘┘─┘┘└┘┘└┘┴─┘┘└┘
|
||||
|
||||
# The actual image
|
||||
FROM scratch
|
||||
# Note: I wanted to use the scratch image here, but unfortunatly the go-sqlite bindings require cgo and
|
||||
# because of this, the container would not start when I compiled the image without cgo.
|
||||
FROM alpine:3.19 AS runner
|
||||
LABEL maintainer="maintainers@vikunja.io"
|
||||
WORKDIR /app/vikunja
|
||||
ENTRYPOINT [ "/app/vikunja/vikunja" ]
|
||||
ENTRYPOINT [ "/sbin/tini", "-g", "--", "/entrypoint.sh" ]
|
||||
EXPOSE 3456
|
||||
USER 1000
|
||||
|
||||
ENV VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/
|
||||
ENV VIKUNJA_DATABASE_PATH=/db/vikunja.db
|
||||
ENV PUID 1000
|
||||
ENV PGID 1000
|
||||
|
||||
RUN apk --update --no-cache add tzdata tini shadow && \
|
||||
addgroup vikunja && \
|
||||
adduser -s /bin/sh -D -G vikunja vikunja -h /app/vikunja -H
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod 0755 /entrypoint.sh && mkdir files
|
||||
|
||||
COPY --from=apibuilder /build/vikunja-* vikunja
|
||||
COPY --from=apibuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
|
|
@ -11,9 +11,6 @@
|
|||
|
||||
> The Todo-app to organize your life.
|
||||
|
||||
If Vikunja is useful to you, please consider [buying me a coffee](https://www.buymeacoffee.com/kolaente), [sponsoring me on GitHub](https://github.com/sponsors/kolaente) or buying [a sticker pack](https://vikunja.cloud/stickers).
|
||||
I'm also offering [a hosted version of Vikunja](https://vikunja.cloud/) if you want a hassle-free solution for yourself or your team.
|
||||
|
||||
# Table of contents
|
||||
|
||||
* [Security Reports](#security-reports)
|
||||
|
|
|
@ -6,7 +6,7 @@ service:
|
|||
# The duration of the issued JWT tokens in seconds.
|
||||
# The default is 259200 seconds (3 Days).
|
||||
jwtttl: 259200
|
||||
# The duration of the "remember me" time in seconds. When the login request is made with
|
||||
# The duration of the "remember me" time in seconds. When the login request is made with
|
||||
# the long param set, the token returned will be valid for this period.
|
||||
# The default is 2592000 seconds (30 Days).
|
||||
jwtttllong: 2592000
|
||||
|
@ -48,7 +48,7 @@ service:
|
|||
# If enabled, vikunja will send an email to everyone who is either assigned to a task or created it when a task reminder
|
||||
# is due.
|
||||
enableemailreminders: true
|
||||
# If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
# If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
# it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
|
||||
# for user deletion.
|
||||
enableuserdeletion: true
|
||||
|
@ -76,7 +76,7 @@ sentry:
|
|||
frontenddsn: "https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480"
|
||||
|
||||
database:
|
||||
# Database type to use. Supported values are mysql, postgres and sqlite. Vikunja is able to run with MySQL 8.0+, Mariadb 10.2+, PostgreSQL 12+, and sqlite.
|
||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
type: "sqlite"
|
||||
# Database user which is used to connect to the database.
|
||||
user: "vikunja"
|
||||
|
@ -109,7 +109,7 @@ database:
|
|||
typesense:
|
||||
# Whether to enable the Typesense integration. If true, all tasks will be synced to the configured Typesense
|
||||
# instance and all search and filtering will run through Typesense instead of only through the database.
|
||||
# Typesense allows fast fulltext search including fuzzy matching support. It may return different results than
|
||||
# Typesense allows fast fulltext search including fuzzy matching support. It may return different results than
|
||||
# what you'd get with a database-only search.
|
||||
enabled: false
|
||||
# The url to the Typesense instance you want to use. Can be hosted locally or in Typesense Cloud as long
|
||||
|
@ -203,7 +203,7 @@ ratelimit:
|
|||
# Possible values are "keyvalue", "memory" or "redis".
|
||||
# When choosing "keyvalue" this setting follows the one configured in the "keyvalue" section.
|
||||
store: keyvalue
|
||||
# The number of requests a user can make from the same IP to all unauthenticated routes (login, register,
|
||||
# The number of requests a user can make from the same IP to all unauthenticated routes (login, register,
|
||||
# password confirmation, email verification, password reset request) per minute. This limit cannot be disabled.
|
||||
# You should only change this if you know what you're doing.
|
||||
noauthlimit: 10
|
||||
|
@ -301,11 +301,13 @@ 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:** Some openid providers (like Gitlab) only make the email of the user available through OpenID if they have set it to be publicly visible.
|
||||
# **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 the third party to rediect the user <frontend-url>/auth/openid/<auth key> after authentication. Please make sure to configure the redirect url in your third party auth service accordingly if you're using the default vikunja frontend.
|
||||
# The frontend will automatically provide the API with the redirect url, composed from the current url where it's hosted.
|
||||
# If you want to use the desktop client with OpenID, make sure to allow redirects to `127.0.0.1`.
|
||||
# **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 in your third party
|
||||
# auth service accordingly if you're using the default vikunja frontend.
|
||||
# The frontend will automatically provide the api with the redirect url, composed from the current url where it's hosted.
|
||||
# If you want to use the desktop client with openid, make sure to allow redirects to `127.0.0.1`.
|
||||
# Take a look at the [default config file](https://kolaente.dev/vikunja/vikunja/src/branch/main/config.yml.sample) for more information about how to configure openid authentication.
|
||||
openid:
|
||||
# Enable or disable OpenID Connect authentication
|
||||
|
@ -323,10 +325,6 @@ auth:
|
|||
clientid:
|
||||
# The client secret used to authenticate Vikunja at the OpenID Connect provider.
|
||||
clientsecret:
|
||||
# The scope necessary to use oidc.
|
||||
# If you want to use the Feature to create and assign to vikunja teams via oidc, you have to add the custom "vikunja_scope" and check [openid.md](https://vikunja.io/docs/openid/).
|
||||
# e.g. scope: openid email profile vikunja_scope
|
||||
scope: openid email profile
|
||||
|
||||
# Prometheus metrics endpoint
|
||||
metrics:
|
||||
|
|
|
@ -51,11 +51,11 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "29.1.0",
|
||||
"electron-builder": "24.12.0"
|
||||
"electron": "28.2.2",
|
||||
"electron-builder": "24.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"express": "4.18.3"
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"express": "^4.17.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -166,12 +166,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806"
|
||||
integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw==
|
||||
|
||||
"@types/node@^20.9.0":
|
||||
version "20.11.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195"
|
||||
integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
"@types/node@^18.11.18":
|
||||
version "18.15.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"
|
||||
integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==
|
||||
|
||||
"@types/plist@^3.0.1":
|
||||
version "3.0.2"
|
||||
|
@ -253,11 +251,12 @@ app-builder-bin@4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-4.0.0.tgz#1df8e654bd1395e4a319d82545c98667d7eed2f0"
|
||||
integrity sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==
|
||||
|
||||
app-builder-lib@24.12.0:
|
||||
version "24.12.0"
|
||||
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-24.12.0.tgz#2e985968c341d28fc887be3ecee658e6a240e147"
|
||||
integrity sha512-t/xinVrMbsEhwljLDoFOtGkiZlaxY1aceZbHERGAS02EkUHJp9lgs/+L8okXLlYCaDSqYdB05Yb8Co+krvguXA==
|
||||
app-builder-lib@24.9.1:
|
||||
version "24.9.1"
|
||||
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-24.9.1.tgz#bf3568529298b4de8595ed1acbb351fe27db5ba4"
|
||||
integrity sha512-Q1nYxZcio4r+W72cnIRVYofEAyjBd3mG47o+zms8HlD51zWtA/YxJb01Jei5F+jkWhge/PTQK+uldsPh6d0/4g==
|
||||
dependencies:
|
||||
"7zip-bin" "~5.2.0"
|
||||
"@develar/schema-utils" "~2.6.5"
|
||||
"@electron/notarize" "2.1.0"
|
||||
"@electron/osx-sign" "1.0.5"
|
||||
|
@ -266,12 +265,12 @@ app-builder-lib@24.12.0:
|
|||
"@types/fs-extra" "9.0.13"
|
||||
async-exit-hook "^2.0.1"
|
||||
bluebird-lst "^1.0.9"
|
||||
builder-util "24.9.4"
|
||||
builder-util "24.8.1"
|
||||
builder-util-runtime "9.2.3"
|
||||
chromium-pickle-js "^0.2.0"
|
||||
debug "^4.3.4"
|
||||
ejs "^3.1.8"
|
||||
electron-publish "24.9.4"
|
||||
electron-publish "24.8.1"
|
||||
form-data "^4.0.0"
|
||||
fs-extra "^10.1.0"
|
||||
hosted-git-info "^4.1.0"
|
||||
|
@ -348,13 +347,13 @@ bluebird@^3.5.5:
|
|||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
|
||||
body-parser@1.20.2:
|
||||
version "1.20.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
|
||||
integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
|
||||
body-parser@1.20.1:
|
||||
version "1.20.1"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
|
||||
integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
content-type "~1.0.5"
|
||||
content-type "~1.0.4"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "1.2.0"
|
||||
|
@ -362,7 +361,7 @@ body-parser@1.20.2:
|
|||
iconv-lite "0.4.24"
|
||||
on-finished "2.4.1"
|
||||
qs "6.11.0"
|
||||
raw-body "2.5.2"
|
||||
raw-body "2.5.1"
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
|
@ -417,10 +416,10 @@ builder-util-runtime@9.2.3:
|
|||
debug "^4.3.4"
|
||||
sax "^1.2.4"
|
||||
|
||||
builder-util@24.9.4:
|
||||
version "24.9.4"
|
||||
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.9.4.tgz#8cde880e7c719285e9cb30e6850ddd5bf475ac04"
|
||||
integrity sha512-YNon3rYjPSm4XDDho9wD6jq7vLRJZUy9FR+yFZnHoWvvdVCnZakL4BctTlPABP41MvIH5yk2cTZ2YfkOhGistQ==
|
||||
builder-util@24.8.1:
|
||||
version "24.8.1"
|
||||
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.8.1.tgz#594d45b0c86d1d17f5c7bebbb77405080b2571c2"
|
||||
integrity sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==
|
||||
dependencies:
|
||||
"7zip-bin" "~5.2.0"
|
||||
"@types/debug" "^4.1.6"
|
||||
|
@ -559,7 +558,7 @@ config-file-ts@^0.2.4:
|
|||
glob "^7.1.6"
|
||||
typescript "^4.0.2"
|
||||
|
||||
connect-history-api-fallback@2.0.0:
|
||||
connect-history-api-fallback@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8"
|
||||
integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==
|
||||
|
@ -571,7 +570,7 @@ content-disposition@0.5.4:
|
|||
dependencies:
|
||||
safe-buffer "5.2.1"
|
||||
|
||||
content-type@~1.0.4, content-type@~1.0.5:
|
||||
content-type@~1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||
|
@ -689,13 +688,13 @@ dir-compare@^3.0.0:
|
|||
buffer-equal "^1.0.0"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
dmg-builder@24.12.0:
|
||||
version "24.12.0"
|
||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-24.12.0.tgz#62a08162f2b3160a286d03ebb6db65c36a3711c7"
|
||||
integrity sha512-nS22OyHUIYcK40UnILOtqC5Qffd1SN1Ljqy/6e+QR2H1wM3iNBrKJoEbDRfEmYYaALKNFRkKPqSbZKRsGUBdPw==
|
||||
dmg-builder@24.9.1:
|
||||
version "24.9.1"
|
||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-24.9.1.tgz#04bf6c0dcd235f6214511f2358a78ed2b9379421"
|
||||
integrity sha512-huC+O6hvHd24Ubj3cy2GMiGLe2xGFKN3klqVMLAdcbB6SWMd1yPSdZvV8W1O01ICzCCRlZDHiv4VrNUgnPUfbQ==
|
||||
dependencies:
|
||||
app-builder-lib "24.12.0"
|
||||
builder-util "24.9.4"
|
||||
app-builder-lib "24.9.1"
|
||||
builder-util "24.8.1"
|
||||
builder-util-runtime "9.2.3"
|
||||
fs-extra "^10.1.0"
|
||||
iconv-lite "^0.6.2"
|
||||
|
@ -739,16 +738,16 @@ ejs@^3.1.8:
|
|||
dependencies:
|
||||
jake "^10.8.5"
|
||||
|
||||
electron-builder@24.12.0:
|
||||
version "24.12.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-24.12.0.tgz#95c41d14b3b1cc177db62715e42ef9fd27344491"
|
||||
integrity sha512-dH4O9zkxFxFbBVFobIR5FA71yJ1TZSCvjZ2maCskpg7CWjBF+SNRSQAThlDyUfRuB+jBTMwEMzwARywmap0CSw==
|
||||
electron-builder@24.9.1:
|
||||
version "24.9.1"
|
||||
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-24.9.1.tgz#4aee03947963b829a7f48a850fe02c219311ef63"
|
||||
integrity sha512-v7BuakDuY6sKMUYM8mfQGrwyjBpZ/ObaqnenU0H+igEL10nc6ht049rsCw2HghRBdEwJxGIBuzs3jbEhNaMDmg==
|
||||
dependencies:
|
||||
app-builder-lib "24.12.0"
|
||||
builder-util "24.9.4"
|
||||
app-builder-lib "24.9.1"
|
||||
builder-util "24.8.1"
|
||||
builder-util-runtime "9.2.3"
|
||||
chalk "^4.1.2"
|
||||
dmg-builder "24.12.0"
|
||||
dmg-builder "24.9.1"
|
||||
fs-extra "^10.1.0"
|
||||
is-ci "^3.0.0"
|
||||
lazy-val "^1.0.5"
|
||||
|
@ -756,26 +755,26 @@ electron-builder@24.12.0:
|
|||
simple-update-notifier "2.0.0"
|
||||
yargs "^17.6.2"
|
||||
|
||||
electron-publish@24.9.4:
|
||||
version "24.9.4"
|
||||
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-24.9.4.tgz#70db542763a78e4980e4e6409c203aef320d0d05"
|
||||
integrity sha512-FghbeVMfxHneHjsG2xUSC0NMZYWOOWhBxfZKPTbibcJ0CjPH0Ph8yb5CUO62nqywXfA5u1Otq6K8eOdOixxmNg==
|
||||
electron-publish@24.8.1:
|
||||
version "24.8.1"
|
||||
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-24.8.1.tgz#4216740372bf4297a429543402a1a15ce8c3560b"
|
||||
integrity sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==
|
||||
dependencies:
|
||||
"@types/fs-extra" "^9.0.11"
|
||||
builder-util "24.9.4"
|
||||
builder-util "24.8.1"
|
||||
builder-util-runtime "9.2.3"
|
||||
chalk "^4.1.2"
|
||||
fs-extra "^10.1.0"
|
||||
lazy-val "^1.0.5"
|
||||
mime "^2.5.2"
|
||||
|
||||
electron@29.1.0:
|
||||
version "29.1.0"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-29.1.0.tgz#37f0e4915226db3c87bc54b187795272bf61fc39"
|
||||
integrity sha512-giJVIm0sWVp+8V1GXrKqKTb+h7no0P3ooYqEd34AD9wMJzGnAeL+usj+R0155/0pdvvP1mgydnA7lcaFA2M9lw==
|
||||
electron@28.2.2:
|
||||
version "28.2.2"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.2.tgz#d5aa4a33c00927d83ca893f8726f7c62aad98c41"
|
||||
integrity sha512-8UcvIGFcjplHdjPFNAHVFg5bS0atDyT3Zx21WwuE4iLfxcAMsyMEOgrQX3im5LibA8srwsUZs7Cx0JAUfcQRpw==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^20.9.0"
|
||||
"@types/node" "^18.11.18"
|
||||
extract-zip "^2.0.1"
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
|
@ -830,14 +829,14 @@ etag@~1.8.1:
|
|||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
|
||||
express@4.18.3:
|
||||
version "4.18.3"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.3.tgz#6870746f3ff904dee1819b82e4b51509afffb0d4"
|
||||
integrity sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==
|
||||
express@^4.17.1:
|
||||
version "4.18.2"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
|
||||
integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
|
||||
dependencies:
|
||||
accepts "~1.3.8"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "1.20.2"
|
||||
body-parser "1.20.1"
|
||||
content-disposition "0.5.4"
|
||||
content-type "~1.0.4"
|
||||
cookie "0.5.0"
|
||||
|
@ -1569,10 +1568,10 @@ range-parser@~1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
|
||||
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
|
||||
raw-body@2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
|
||||
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
http-errors "2.0.0"
|
||||
|
@ -1904,11 +1903,6 @@ typescript@^4.0.2:
|
|||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
|
|
15
docker/entrypoint.sh
Normal file
15
docker/entrypoint.sh
Normal file
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
if [ -n "$PUID" ] && [ "$PUID" -ne 0 ] && \
|
||||
[ -n "$PGID" ] && [ "$PGID" -ne 0 ] ; then
|
||||
echo "info: creating the new user vikunja with $PUID:$PGID"
|
||||
groupmod -g "$PGID" -o vikunja
|
||||
usermod -u "$PUID" -o vikunja
|
||||
chown -R vikunja:vikunja ./files/
|
||||
chown vikunja:vikunja .
|
||||
exec su vikunja -c /app/vikunja/vikunja "$@"
|
||||
else
|
||||
echo "info: creation of non-root user is skipped"
|
||||
exec /app/vikunja/vikunja "$@"
|
||||
fi
|
|
@ -41,13 +41,24 @@ There are multiple categories of subcommands in the magefile:
|
|||
|
||||
These tasks are automatically run in our CI every time someone pushes to main or you update a pull request:
|
||||
|
||||
* `mage lint`
|
||||
* `mage check:lint`
|
||||
* `mage check:fmt`
|
||||
* `mage check:ineffassign`
|
||||
* `mage check:misspell`
|
||||
* `mage check:goconst`
|
||||
* `mage build:generate`
|
||||
* `mage build:build`
|
||||
|
||||
## Build
|
||||
|
||||
### Build Vikunja
|
||||
|
||||
```
|
||||
mage build:build
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
mage build
|
||||
```
|
||||
|
@ -68,9 +79,16 @@ All check sub-commands exit with a status code of 1 if the check fails.
|
|||
|
||||
Various code-checks are available:
|
||||
|
||||
* `mage check:all`: Runs golangci and swagger documentation check
|
||||
* `mage lint`: Checks if the code follows the rules as defined in the `.golangci.yml` config file.
|
||||
* `mage lint:fix`: Fixes all code style issues which are easily fixable.
|
||||
* `mage check:all`: Runs fmt-check, lint, got-swag, misspell-check, ineffasign-check, gocyclo-check, static-check, gosec-check, goconst-check all in parallel
|
||||
* `mage check:fmt`: Checks if the code is properly formatted with go fmt
|
||||
* `mage check:go-sec`: Checks the source code for potential security issues by scanning the Go AST using the [gosec tool](https://github.com/securego/gosec)
|
||||
* `mage check:goconst`: Checks for repeated strings that could be replaced by a constant using [goconst](https://github.com/jgautheron/goconst/)
|
||||
* `mage check:gocyclo`: Checks for the cyclomatic complexity of the source code using [gocyclo](https://github.com/fzipp/gocyclo)
|
||||
* `mage check:got-swag`: Checks if the swagger docs need to be re-generated from the code annotations
|
||||
* `mage check:ineffassign`: Checks the source code for ineffectual assigns using [ineffassign](https://github.com/gordonklaus/ineffassign)
|
||||
* `mage check:lint`: Runs golint on all packages
|
||||
* `mage check:misspell`: Checks the source code for misspellings
|
||||
* `mage check:static`: Statically analyzes the source code about a range of different problems using [staticcheck](https://staticcheck.io/docs/)
|
||||
|
||||
## Release
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ Environment path: `VIKUNJA_SERVICE_JWTTTL`
|
|||
|
||||
### jwtttllong
|
||||
|
||||
The duration of the "remember me" time in seconds. When the login request is made with
|
||||
The duration of the "remember me" time in seconds. When the login request is made with
|
||||
the long param set, the token returned will be valid for this period.
|
||||
The default is 2592000 seconds (30 Days).
|
||||
|
||||
|
@ -289,7 +289,7 @@ Environment path: `VIKUNJA_SERVICE_ENABLEEMAILREMINDERS`
|
|||
|
||||
### enableuserdeletion
|
||||
|
||||
If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
|
||||
for user deletion.
|
||||
|
||||
|
@ -406,7 +406,7 @@ Environment path: `VIKUNJA_SENTRY_FRONTENDDSN`
|
|||
|
||||
### type
|
||||
|
||||
Database type to use. Supported values are mysql, postgres and sqlite. Vikunja is able to run with MySQL 8.0+, Mariadb 10.2+, PostgreSQL 12+, and sqlite.
|
||||
Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
|
||||
Default: `sqlite`
|
||||
|
||||
|
@ -569,7 +569,7 @@ Environment path: `VIKUNJA_DATABASE_TLS`
|
|||
|
||||
Whether to enable the Typesense integration. If true, all tasks will be synced to the configured Typesense
|
||||
instance and all search and filtering will run through Typesense instead of only through the database.
|
||||
Typesense allows fast fulltext search including fuzzy matching support. It may return different results than
|
||||
Typesense allows fast fulltext search including fuzzy matching support. It may return different results than
|
||||
what you'd get with a database-only search.
|
||||
|
||||
Default: `false`
|
||||
|
@ -1024,7 +1024,7 @@ Environment path: `VIKUNJA_RATELIMIT_STORE`
|
|||
|
||||
### noauthlimit
|
||||
|
||||
The number of requests a user can make from the same IP to all unauthenticated routes (login, register,
|
||||
The number of requests a user can make from the same IP to all unauthenticated routes (login, register,
|
||||
password confirmation, email verification, password reset request) per minute. This limit cannot be disabled.
|
||||
You should only change this if you know what you're doing.
|
||||
|
||||
|
@ -1209,11 +1209,13 @@ Environment path: `VIKUNJA_AUTH_LOCAL`
|
|||
|
||||
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:** Some openid providers (like Gitlab) only make the email of the user available through OpenID if they have set it to be publicly visible.
|
||||
**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 the third party to rediect the user <frontend-url>/auth/openid/<auth key> after authentication. Please make sure to configure the redirect url in your third party auth service accordingly if you're using the default vikunja frontend.
|
||||
The frontend will automatically provide the API with the redirect url, composed from the current url where it's hosted.
|
||||
If you want to use the desktop client with OpenID, make sure to allow redirects to `127.0.0.1`.
|
||||
**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 in your third party
|
||||
auth service accordingly if you're using the default vikunja frontend.
|
||||
The frontend will automatically provide the api with the redirect url, composed from the current url where it's hosted.
|
||||
If you want to use the desktop client with openid, make sure to allow redirects to `127.0.0.1`.
|
||||
Take a look at the [default config file](https://kolaente.dev/vikunja/vikunja/src/branch/main/config.yml.sample) for more information about how to configure openid authentication.
|
||||
|
||||
Default: `<empty>`
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
title: "Desktop Packages"
|
||||
date: 2024-02-11T15:58:18+01:00
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# Desktop Packages
|
||||
|
||||
Vikunja is available as an electron-based desktop application for Linux and Windows.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download the latest release for your platform from [the download page](https://dl.vikunja.io/desktop/).
|
||||
* For Windows, choose the file with the `.exe` or `.msi` file ending
|
||||
* For a Linux-based operating system, choose a file with an ending for your operating system - we have builds for Alpine, AppImage, Arch Linux, Debian-based systems, FreeBSD, Fedora and Snap.
|
||||
2. Run the downloaded package in the same way you would normally install a package for your OS.
|
||||
|
||||
## Flatpack
|
||||
|
||||
Vikunja Desktop can be installed via the [Flathub](https://flathub.org/apps/io.vikunja.Vikunja).
|
||||
|
||||
To install it, run the following command:
|
||||
|
||||
```
|
||||
flatpak install flathub io.vikunja.Vikunja
|
||||
```
|
|
@ -27,6 +27,7 @@ Create a directory for the project where all data and the compose file will live
|
|||
|
||||
Create a `docker-compose.yml` file with the following contents in your directory:
|
||||
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
|
@ -36,7 +37,7 @@ services:
|
|||
environment:
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public url where vikunja is reachable>
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: changeme
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
|
@ -46,24 +47,19 @@ services:
|
|||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
- db
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: mariadb:10
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_ROOT_PASSWORD: supersupersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: changeme
|
||||
MYSQL_PASSWORD: supersecret
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER --password=$$MYSQL_PASSWORD"]
|
||||
interval: 2s
|
||||
start_period: 30s
|
||||
```
|
||||
|
||||
This defines two services, each with their own container:
|
||||
|
@ -79,18 +75,6 @@ The number before the colon is the host port - This is where you can reach vikun
|
|||
|
||||
You'll need to change the value of the `VIKUNJA_SERVICE_PUBLICURL` environment variable to the public port or hostname where Vikunja is reachable.
|
||||
|
||||
## Ensure adequate file permissions
|
||||
|
||||
Vikunja runs as user `1000` and no group by default.
|
||||
|
||||
To be able to upload task attachments or change the background of project, Vikunja must be able to write into the `files` directory.
|
||||
To do this, create the folder and chown it before starting the stack:
|
||||
|
||||
```
|
||||
mkdir $PWD/files
|
||||
chown 1000 $PWD/files
|
||||
```
|
||||
|
||||
## Run it
|
||||
|
||||
Run `sudo docker-compose up` in your directory and take a look at the output you get.
|
||||
|
|
|
@ -15,6 +15,8 @@ It uses a proxy configuration to make it available under a domain.
|
|||
|
||||
For all available configuration options, see [configuration]({{< ref "config.md">}}).
|
||||
|
||||
Once deployed, you might want to change the [`PUID` and `GUID` settings]({{< ref "install.md">}}#setting-user-and-group-id-of-the-user-running-vikunja) or [set the time zone]({{< ref "config.md">}}#timezone).
|
||||
|
||||
After registering all your users, you might also want to [disable the user registration]({{<ref "config.md">}}#enableregistration).
|
||||
|
||||
<div class="notification is-warning">
|
||||
|
@ -25,23 +27,6 @@ All examples on this page already reflect this and do not require additional wor
|
|||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## File permissions
|
||||
|
||||
Vikunja runs as user `1000` and no group by default.
|
||||
You can use Docker's [`--user`](https://docs.docker.com/engine/reference/run/#user) flag to change that.
|
||||
|
||||
You must ensure Vikunja is able to write into the `files` directory.
|
||||
To do this, create the folder and chown it before starting the stack:
|
||||
|
||||
```
|
||||
mkdir $PWD/files
|
||||
chown 1000 $PWD/files
|
||||
```
|
||||
|
||||
You'll need to do this before running any of the examples on this page.
|
||||
|
||||
Vikunja will not try to aquire ownership of the files folder, as that would mean it had to run as root.
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
Vikunja supports postgres, mysql and sqlite as a database backend. The examples on this page use mysql with a mariadb container.
|
||||
|
@ -49,20 +34,21 @@ To use postgres as a database backend, change the `db` section of the examples t
|
|||
|
||||
```yaml
|
||||
db:
|
||||
image: postgres:16
|
||||
image: postgres:13
|
||||
environment:
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_PASSWORD: secret
|
||||
POSTGRES_USER: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -h localhost -U $$POSTGRES_USER"]
|
||||
interval: 2s
|
||||
```
|
||||
|
||||
You'll also need to change the `VIKUNJA_DATABASE_TYPE` to `postgres` on the api container declaration.
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> The mariadb container can sometimes take a while to initialize, especially on the first run. During this time, the api container will fail to start at all. It will automatically restart every few seconds.
|
||||
</div>
|
||||
|
||||
## Sqlite
|
||||
|
||||
Vikunja supports postgres, mysql and sqlite as a database backend. The examples on this page use mysql with a mariadb container.
|
||||
|
@ -93,13 +79,6 @@ You'll also need to remove or change the `VIKUNJA_DATABASE_TYPE` to `sqlite` on
|
|||
|
||||
You can also remove the db section.
|
||||
|
||||
To run the container, you need to create the directories first and make sure they have all required permissions:
|
||||
|
||||
```
|
||||
mkdir $PWD/files $PWD/db
|
||||
chown 1000 $PWD/files $PWD/db
|
||||
```
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> If you'll use your instance with more than a handful of users, we recommend using mysql or postgres.
|
||||
</div>
|
||||
|
@ -110,13 +89,8 @@ This example lets you host Vikunja without any reverse proxy in front of it.
|
|||
This is the absolute minimum configuration you need to get something up and running.
|
||||
If you want to make Vikunja available on a domain or need tls termination, check out one of the other examples.
|
||||
|
||||
Note that you need to change the [`VIKUNJA_SERVICE_PUBLICURL`]({{< ref "config.md" >}}#publicurl) environment variable to the public ip or hostname including the port (the docker host you're running this on) is reachable at, prefixed with `http://`.
|
||||
Because the browser you'll use to access the Vikunja frontend uses that url to make the requests, it has to be able to reach it from the outside.
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> You must ensure Vikunja has write permissions on the `files` directory before starting the stack.
|
||||
To do this, <a href="#file-permissions">check out the related commands here</a>.
|
||||
</div>
|
||||
Note that you need to change the [`VIKUNJA_SERVICE_PUBLICURL`]({{< ref "config.md" >}}#publicurl) environment variable to the ip (the docker host you're running this on) is reachable at.
|
||||
Because the browser you'll use to access the Vikunja frontend uses that url to make the requests, it has to be able to reach that ip + port from the outside.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
@ -125,9 +99,9 @@ services:
|
|||
vikunja:
|
||||
image: vikunja/vikunja
|
||||
environment:
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public ip or host where vikunja is reachable>
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public url where vikunja is reachable>
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: changeme
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
|
@ -137,24 +111,19 @@ services:
|
|||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
- db
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: mariadb:10
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_ROOT_PASSWORD: supersupersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: changeme
|
||||
MYSQL_PASSWORD: supersecret
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER --password=$$MYSQL_PASSWORD"]
|
||||
interval: 2s
|
||||
start_period: 30s
|
||||
```
|
||||
|
||||
## Example with Traefik 2
|
||||
|
@ -167,11 +136,6 @@ We also make a few assumptions here which you'll most likely need to adjust for
|
|||
* The entrypoint you want to make vikunja available from is called `https`
|
||||
* The tls cert resolver is called `acme`
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> You must ensure Vikunja has write permissions on the `files` directory before starting the stack.
|
||||
To do this, <a href="#file-permissions">check out the related commands here</a>.
|
||||
</div>
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
|
@ -181,7 +145,7 @@ services:
|
|||
environment:
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public url where vikunja is reachable>
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: changeme
|
||||
VIKUNJA_DATABASE_PASSWORD: supersecret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
|
@ -192,12 +156,10 @@ services:
|
|||
- web
|
||||
- default
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
- db
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web"
|
||||
- "traefik.http.routers.vikunja.rule=Host(`vikunja.example.com`)"
|
||||
- "traefik.http.routers.vikunja.entrypoints=https"
|
||||
- "traefik.http.routers.vikunja.tls.certResolver=acme"
|
||||
|
@ -207,15 +169,11 @@ services:
|
|||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersupersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: changeme
|
||||
MYSQL_PASSWORD: supersecret
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER --password=$$MYSQL_PASSWORD"]
|
||||
interval: 2s
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
web:
|
||||
|
@ -235,11 +193,6 @@ vikunja.example.com {
|
|||
Note that you need to change the [`VIKUNJA_SERVICE_PUBLICURL`]({{< ref "config.md" >}}#publicurl) environment variable to the ip (the docker host you're running this on) is reachable at.
|
||||
Because the browser you'll use to access the Vikunja frontend uses that url to make the requests, it has to be able to reach that ip + port from the outside.
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> You must ensure Vikunja has write permissions on the `files` directory before starting the stack.
|
||||
To do this, <a href="#file-permissions">check out the related commands here</a>.
|
||||
</div>
|
||||
|
||||
Docker Compose config:
|
||||
|
||||
```yaml
|
||||
|
@ -251,7 +204,7 @@ services:
|
|||
environment:
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public url where vikunja is reachable>
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: changeme
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
|
@ -261,24 +214,19 @@ services:
|
|||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
- db
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: mariadb:10
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_ROOT_PASSWORD: supersupersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: changeme
|
||||
MYSQL_PASSWORD: supersecret
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER --password=$$MYSQL_PASSWORD"]
|
||||
interval: 2s
|
||||
start_period: 30s
|
||||
caddy:
|
||||
image: caddy
|
||||
restart: unless-stopped
|
||||
|
@ -323,12 +271,9 @@ The docker-compose file we're going to use is exactly the same from the [example
|
|||
|
||||
You may want to change the volumes to match the rest of your setup.
|
||||
|
||||
After registering all your users, you might also want to [disable the user registration]({{<ref "config.md">}}#enableregistration).
|
||||
Once deployed, you might want to change the [`PUID` and `GUID` settings]({{< ref "install.md">}}#setting-user-and-group-id-of-the-user-running-vikunja) or [set the time zone]({{< ref "config.md">}}#timezone).
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> You must ensure Vikunja has write permissions on the `files` directory before starting the stack.
|
||||
To do this, <a href="#file-permissions">check out the related commands here</a>.
|
||||
</div>
|
||||
After registering all your users, you might also want to [disable the user registration]({{<ref "config.md">}}#enableregistration).
|
||||
|
||||
## Redis
|
||||
|
||||
|
|
|
@ -29,11 +29,10 @@ You can also:
|
|||
Vikunja can be installed in various ways.
|
||||
This document provides an overview and instructions for the different methods:
|
||||
|
||||
* [Installing from binary (manual)](#install-from-binary)
|
||||
* [Installing from binary](#install-from-binary)
|
||||
* [Build from source]({{< ref "build-from-source.md">}})
|
||||
* [Docker](#docker)
|
||||
* [Debian](#debian-packages)
|
||||
* [RPM](#rpm)
|
||||
* [Debian packages](#debian-packages)
|
||||
* [FreeBSD](#freebsd--freenas)
|
||||
* [Kubernetes]({{< ref "k8s.md" >}})
|
||||
|
||||
|
@ -140,23 +139,17 @@ It will automatically run all necessary database migrations.
|
|||
To get up and running quickly, use this command:
|
||||
|
||||
```
|
||||
mkdir $PWD/files $PWD/db
|
||||
chown 1000 $PWD/files $PWD/db
|
||||
docker run -p 3456:3456 -v $PWD/files:/app/vikunja/files -v $PWD/db:/db vikunja/vikunja
|
||||
touch vikunja.db
|
||||
docker run -p 3456:3456 -v $PWD/files:/app/vikunja/files -v $PWD/vikunja.db:/app/vikunja/vikunja.db vikunja/vikunja
|
||||
```
|
||||
|
||||
This will expose vikunja on port `3456` on the host running the container and use sqlite as database backend.
|
||||
|
||||
**Note**: The container runs as the user `1000` and no group by default.
|
||||
You can use Docker's [`--user`](https://docs.docker.com/engine/reference/run/#user) flag to change that.
|
||||
Make sure the new user has required permissions on the `db` and `files` folder.
|
||||
|
||||
You can mount a local configuration like so:
|
||||
|
||||
```
|
||||
mkdir $PWD/files $PWD/db
|
||||
chown 1000 $PWD/files $PWD/db
|
||||
docker run -p 3456:3456 -v /path/to/config/on/host.yml:/app/vikunja/config.yml:ro -v $PWD/files:/app/vikunja/files -v $PWD/db:/db vikunja/vikunja
|
||||
touch vikunja.db
|
||||
docker run -p 3456:3456 -v /path/to/config/on/host.yml:/app/vikunja/config.yml:ro -v $PWD/files:/app/vikunja/files -v $PWD/vikunja.db:/app/vikunja/vikunja.db vikunja/vikunja
|
||||
```
|
||||
|
||||
Though it is recommended to use environment variables or `.env` files to configure Vikunja in docker.
|
||||
|
@ -169,13 +162,20 @@ Check out the [docker examples]({{<ref "full-docker-example.md">}}) for more adv
|
|||
By default, the container stores all files uploaded and used through vikunja inside of `/app/vikunja/files` which is created as a docker volume.
|
||||
You should mount the volume somewhere to the host to permanently store the files and don't lose them if the container restarts.
|
||||
|
||||
### Setting user and group id of the user running vikunja
|
||||
|
||||
You can set the user and group id of the user running vikunja with the `PUID` and `PGID` environment variables.
|
||||
This follows the pattern used by [the linuxserver.io](https://docs.linuxserver.io/general/understanding-puid-and-pgid) docker images.
|
||||
|
||||
This is useful to solve general permission problems when host-mounting volumes such as the volume used for task attachments.
|
||||
|
||||
### Docker compose
|
||||
|
||||
Check out the [docker examples]({{<ref "full-docker-example.md">}}) for more advanced configuration using docker compose.
|
||||
|
||||
## Debian packages
|
||||
|
||||
Vikunja is available as deb package for installation on debian-like systems.
|
||||
Vikunja is available as debian packages.
|
||||
|
||||
To install these, grab a `.deb` file from [the download page](https://dl.vikunja.io/vikunja) and run
|
||||
|
||||
|
@ -186,18 +186,6 @@ dpkg -i vikunja.deb
|
|||
This will install Vikunja to `/opt/vikunja`.
|
||||
To configure it, use the config file in `/etc/vikunja/config.yml`.
|
||||
|
||||
## RPM
|
||||
|
||||
Vikunja is available as rpm package for installation on Fedora, CentOS and others.
|
||||
|
||||
To install these, grab a `.rpm` file from [the download page](https://dl.vikunja.io/vikunja) and run
|
||||
|
||||
```
|
||||
rpm -i vikunja.rpm
|
||||
```
|
||||
|
||||
To configure Vikunja, use the config file in `/etc/vikunja/config.yml`.
|
||||
|
||||
## FreeBSD / FreeNAS
|
||||
|
||||
Unfortunately, we currently can't provide pre-built binaries for FreeBSD.
|
||||
|
|
|
@ -10,7 +10,7 @@ menu:
|
|||
|
||||
# OpenID example configurations
|
||||
|
||||
On this page you will find examples about how to set up Vikunja with a third-party OAuth 2.0 provider using OpenID Connect.
|
||||
On this page you will find examples about how to set up Vikunja with a third-party OpenID provider.
|
||||
To add another example, please [edit this document](https://kolaente.dev/vikunja/vikunja/src/branch/main/docs/content/doc/setup/openid-examples.md) and send a PR.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
@ -67,7 +67,7 @@ Google config:
|
|||
|
||||
Note that there currently seems to be no way to stop creation of new users, even when `enableregistration` is `false` in the configuration. This means that this approach works well only with an "Internal Organization" app for Google Workspace, which limits the allowed users to organizational accounts only. External / public applications will potentially allow every Google user to register.
|
||||
|
||||
## Keycloak
|
||||
## Keycloak
|
||||
|
||||
Vikunja Config:
|
||||
```yaml
|
||||
|
@ -111,7 +111,4 @@ auth:
|
|||
clientsecret: "" # copy from Authentik
|
||||
```
|
||||
|
||||
**Note:** The `authurl` that Vikunja requires is not the `Authorize URL` that you can see in the Provider.
|
||||
OpenID Discovery is used to find the correct endpoint to use automatically, by accessing the `OpenID Configuration URL` (usually `https://authentik.mydomain.com/application/o/vikunja/.well-known/openid-configuration`).
|
||||
Use this URL without the `.well-known/openid-configuration` as the `authurl`.
|
||||
Typically this URL can be found in the metadata section within your identity provider.
|
||||
**Note:** The `authurl` that Vikunja requires is not the `Authorize URL` that you can see in the Provider. Vikunja uses Open ID Discovery to find the correct endpoint to use. Vikunja does this by automatically accessing the `OpenID Configuration URL` (usually `https://authentik.mydomain.com/application/o/vikunja/.well-known/openid-configuration`). Use this URL without the `.well-known/openid-configuration` as the `authurl`.
|
||||
|
|
|
@ -1,180 +0,0 @@
|
|||
---
|
||||
date: "2022-08-09:00:00+02:00"
|
||||
title: "OpenID"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# OpenID
|
||||
|
||||
Vikunja allows for authentication with an external identity source such as Authentik, Keycloak or similar via the
|
||||
[OpenID Connect](https://openid.net/developers/specs/) standard.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## OpenID Connect Overview
|
||||
|
||||
OpenID Connect is a standardized identity layer built on top of the more generic OAuth 2.0 specification, simplying interaction between the involved parties significantly.
|
||||
While the [OpenID specification](https://openid.net/specs/openid-connect-core-1_0.html#Overview) is worth a read, we summarize the most important basics here.
|
||||
|
||||
The involved parties are:
|
||||
|
||||
- **Resource Owner:** typically the end-user
|
||||
- **Resource Server:** the application server handling requests from the client, the Vikunja API in our case
|
||||
- **Client:** the application or client accessing the RS on behalf of the RO. Vikunja web frontend or any of the apps
|
||||
- **Authorization Server:** the server verifying the user identity and issuing tokens. These docs also use the words `OAuth 2.0 provider`, `Identity Provider` interchangeably.
|
||||
|
||||
After the user is authenticated, the provider issues a token to the user, containing various claims.
|
||||
There's different types of tokens (ID token, access token, refresh token), and all of them are created as [JSON Web Token](https://www.rfc-editor.org/info/rfc7519).
|
||||
Claims in turn are assertions containing information about the token bearer, usually the user.
|
||||
|
||||
**Scopes** are requested by the client when redirecting the end-user to the Authorization Server for authentication, and indirectly control which claims are included in the resulting tokens.
|
||||
There's certain default scopes, but its also possible to define custom scopes, which are used by the feature assigning users to Teams automatically.
|
||||
|
||||
## Configuring OIDC Authentication
|
||||
|
||||
To achieve authentication via an external provider, it is required to (a) configure a confidential Client on your OAuth 2.0 provider and (b) configure Vikunja to authenticate against this provider.
|
||||
[Example configurations]({{< ref "openid-examples.md">}}) are provided for various different identity providers, below you can find generic guides though.
|
||||
|
||||
OpenID Connect defines various flow types indicating how exactly the interaction between the involved parties work, Vikunja makes use of the standard **Authorization Code Flow**.
|
||||
|
||||
### Step 1: Configure your Authorization Server
|
||||
|
||||
The first step is to configure the Authorization Server to correctly handle requests coming from Vikunja.
|
||||
In general, this involves the following steps at a minimum:
|
||||
|
||||
- Create a confidential client and obtain the client ID and client secret
|
||||
- Configure (whitelist) redirect URLs that can be used by Vikunja
|
||||
- Make sure the required scopes (`openid profile email` are the default scopes used by Vikunja) are supported
|
||||
- Optional: configure an additional scope for automatic team assignment, see below for details
|
||||
|
||||
More detailled instructions for various different identity providers can be [found here]({{< ref "openid-examples.md">}})
|
||||
|
||||
### Step 2: Configure Vikunja
|
||||
|
||||
Vikunja has to be configured to use the identity provider. Please note that there is currently no option to configure these settings via environment variables, they have to be defined using the configuration file. The configuration schema is as follows:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
openid:
|
||||
enabled: true
|
||||
redirecturl: https://vikunja.mydomain.com/auth/openid/ <---- slash at the end is important
|
||||
providers:
|
||||
- name: <provider-name>
|
||||
authurl: <auth-url>
|
||||
clientid: <vikunja client-id>
|
||||
clientsecret: <vikunja client-secret>
|
||||
scope: openid profile email
|
||||
```
|
||||
|
||||
The values for `authurl` can be obtained from the Metadata of your provider, while `clientid` and `clientsecret` are obtained when configuring the client.
|
||||
The scope usually doesn't need to be specified or changed, unless you want to configure the automatic team assignment.
|
||||
|
||||
Optionally it is possible to disable local authentication and therefore forcing users to login via OpenID connect:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
local:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
## Automatically assign users to teams
|
||||
|
||||
Vikunja is capable of automatically adding users to a team based on OIDC claims added by the identity provider.
|
||||
If configured, Vikunja will sync teams, automatically create new ones and make sure the members are part of the configured teams.
|
||||
Teams which exist only because they were created from oidc attributes are not editable in Vikunja.
|
||||
|
||||
To distinguish between teams created in Vikunja and teams generated automatically via oidc, generated teams have an `oidcID` assigned internally.
|
||||
Within the UI, the teams created through OIDC get a `(OIDC)` suffix to make them distinguishable from locally created teams.
|
||||
|
||||
On a high level, you need to make sure that the **ID token** issued by your identity provider contains a `vikunja_groups` claim, following the structure defined below.
|
||||
It depends on the provider being used as well as the preferences of the administrator how this is achieved.
|
||||
Typically you'd want to request an additional scope (e.g. `vikunja_scope`) which then triggers the identity provider to add the claim.
|
||||
If the `vikunja_groups` is part of the **ID token**, Vikunja will start the procedure and import teams and team memberships.
|
||||
|
||||
The claim structure expexted by Vikunja is as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"vikunja_groups": [
|
||||
{
|
||||
"name": "team 1",
|
||||
"oidcID": 33349
|
||||
},
|
||||
{
|
||||
"name": "team 2",
|
||||
"oidcID": 35933
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For each team, you need to define a team `name` and an `oidcID`, where the `oidcID` can be any string with a length of less than 250 characters.
|
||||
The `oidcID` is used to uniquely identify the team, so please make sure to keep this unique.
|
||||
|
||||
Below you'll find two example implementations for Authentik and Keycloak.
|
||||
If you've successfully implemented this with another identity provider, please let us know and submit a PR to improve the docs.
|
||||
|
||||
### Setup in Authentik
|
||||
|
||||
To configure automatic team management through Authentik, we assume you have already [set up Authentik]({{< ref "openid-examples.md">}}#authentik) as an OIDC provider for authentication with Vikunja.
|
||||
|
||||
To use Authentik's group assignment feature, follow these steps:
|
||||
|
||||
1. Edit [your config]({{< ref "config.md">}}) to include the following scopes: `openid profile email vikunja_scope`
|
||||
2. Open `<your authentik url>/if/admin/#/core/property-mappings`
|
||||
3. Create a new property mapping called `vikunja_scope` as scope mapping. There is a field `expression` to enter python expressions that will be delivered with the oidc token.
|
||||
4. Write a small script like the following to add group information to `vikunja_scope`:
|
||||
|
||||
```python
|
||||
groupsDict = {"vikunja_groups": []}
|
||||
for group in request.user.ak_groups.all():
|
||||
groupsDict["vikunja_groups"].append({"name": group.name, "oidcID": group.num_pk})
|
||||
return groupsDict
|
||||
```
|
||||
|
||||
5. In Authentik's menu on the left, go to Applications > Providers > Select the Vikunja provider. Then click on "Edit", on the bottom open "Advanced protocol settings", select the newly created property mapping under "Scopes". Save the provider.
|
||||
|
||||
Now when you log into Vikunja via Authentik it will show you a list of scopes you are claiming.
|
||||
You should see the description you entered on the OIDC provider's admin area.
|
||||
|
||||
Proceed to vikunja and open the teams page in the sidebar menu.
|
||||
You should see "(OIDC)" written next to each team you were assigned through OIDC.
|
||||
|
||||
### Setup in Keycloak
|
||||
|
||||
The kind people from Makerspace Darmstadt e.V. have written [a guide on how to create a mapper for Vikunja here](https://github.com/makerspace-darmstadt/keycloak-vikunja-mapper).
|
||||
|
||||
## Use cases
|
||||
|
||||
All examples assume one team called "Team 1" to be configured within your provider.
|
||||
|
||||
* *Token delivers team.name +team.oidcID and Vikunja team does not exist:* \
|
||||
New team will be created called "Team 1" with attribute oidcID: "33929"
|
||||
|
||||
2. *In Vikunja Team with name "team 1" already exists in vikunja, but has no oidcID set:* \
|
||||
new team will be created called "team 1" with attribute oidcID: "33929"
|
||||
|
||||
3. *In Vikunja Team with name "team 1" already exists in vikunja, but has different oidcID set:* \
|
||||
new team will be created called "team 1" with attribute oidcID: "33929"
|
||||
|
||||
4. *In Vikunja Team with oidcID "33929" already exists in vikunja, but has different name than "team1":* \
|
||||
new team will be created called "team 1" with attribute oidcID: "33929"
|
||||
|
||||
5. *Scope vikunja_scope is not set:* \
|
||||
nothing happens
|
||||
|
||||
6. *oidcID is not set:* \
|
||||
You'll get error.
|
||||
Custom Scope malformed
|
||||
"The custom scope set by the OIDC provider is malformed. Please make sure the openid provider sets the data correctly for your scope. Check especially to have set an oidcID."
|
||||
|
||||
7. *In Vikunja I am in "team 3" with oidcID "", but the token does not deliver any data for "team 3":* \
|
||||
You will stay in team 3 since it was not set by the oidc provider
|
||||
|
||||
8. *In Vikunja I am in "team 3" with oidcID "12345", but the token does not deliver any data for "team 3"*:\
|
||||
You will be signed out of all teams, which have an oidcID set and are not contained in the token.
|
||||
Especially if you've been the last team member, the team will be deleted.
|
|
@ -44,7 +44,6 @@ This document describes the different errors Vikunja can return.
|
|||
| 1020 | 412 | This user account is disabled. |
|
||||
| 1021 | 412 | This account is managed by a third-party authentication provider. |
|
||||
| 1021 | 412 | The username must not contain spaces. |
|
||||
| 1022 | 412 | The custom scope set by the OIDC provider is malformed. Please make sure the openid provider sets the data correctly for your scope. Check especially to have set an oidcID. |
|
||||
|
||||
## Validation
|
||||
|
||||
|
@ -107,9 +106,6 @@ This document describes the different errors Vikunja can return.
|
|||
| 6005 | 409 | The user is already a member of that team. |
|
||||
| 6006 | 400 | Cannot delete the last team member. |
|
||||
| 6007 | 403 | The team does not have access to the project to perform that action. |
|
||||
| 6008 | 400 | There are no teams found with that team name. |
|
||||
| 6009 | 400 | There is no oidc team with that team name and oidcId. |
|
||||
| 6010 | 400 | There are no oidc teams found for the user. |
|
||||
|
||||
## User Project Access
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@8.15.4",
|
||||
"packageManager": "pnpm@8.15.1",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
|
@ -28,15 +28,13 @@
|
|||
"serve": "pnpm run dev",
|
||||
"preview": "vite preview --port 4173",
|
||||
"preview:dev": "vite preview --outDir dist-dev --mode development --port 4173",
|
||||
"preview:test": "vite preview --port 4173 --outDir dist-test",
|
||||
"build": "vite build && workbox copyLibraries dist/",
|
||||
"build:test": "vite build --outDir dist-test && workbox copyLibraries dist-dev/",
|
||||
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
|
||||
"build:dev": "vite build --mode development --outDir dist-dev/",
|
||||
"lint": "eslint 'src/**/*.{js,ts,vue}'",
|
||||
"lint:fix": "pnpm run lint --fix",
|
||||
"test:e2e": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome'",
|
||||
"test:e2e-record-test": "start-server-and-test preview:test http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:e2e-dev": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:unit": "vitest --dir ./src",
|
||||
|
@ -57,53 +55,53 @@
|
|||
"@github/hotkey": "3.1.0",
|
||||
"@infectoone/vue-ganttastic": "2.2.0",
|
||||
"@intlify/unplugin-vue-i18n": "2.0.0",
|
||||
"@kyvg/vue3-notification": "3.2.0",
|
||||
"@sentry/tracing": "7.103.0",
|
||||
"@sentry/vue": "7.103.0",
|
||||
"@tiptap/core": "2.2.4",
|
||||
"@tiptap/extension-blockquote": "2.2.4",
|
||||
"@tiptap/extension-bold": "2.2.4",
|
||||
"@tiptap/extension-bullet-list": "2.2.4",
|
||||
"@tiptap/extension-code": "2.2.4",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.4",
|
||||
"@tiptap/extension-document": "2.2.4",
|
||||
"@tiptap/extension-dropcursor": "2.2.4",
|
||||
"@tiptap/extension-gapcursor": "2.2.4",
|
||||
"@tiptap/extension-hard-break": "2.2.4",
|
||||
"@tiptap/extension-heading": "2.2.4",
|
||||
"@tiptap/extension-history": "2.2.4",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.4",
|
||||
"@tiptap/extension-image": "2.2.4",
|
||||
"@tiptap/extension-italic": "2.2.4",
|
||||
"@tiptap/extension-link": "2.2.4",
|
||||
"@tiptap/extension-list-item": "2.2.4",
|
||||
"@tiptap/extension-ordered-list": "2.2.4",
|
||||
"@tiptap/extension-paragraph": "2.2.4",
|
||||
"@tiptap/extension-placeholder": "2.2.4",
|
||||
"@tiptap/extension-strike": "2.2.4",
|
||||
"@tiptap/extension-table": "2.2.4",
|
||||
"@tiptap/extension-table-cell": "2.2.4",
|
||||
"@tiptap/extension-table-header": "2.2.4",
|
||||
"@tiptap/extension-table-row": "2.2.4",
|
||||
"@tiptap/extension-task-item": "2.2.4",
|
||||
"@tiptap/extension-task-list": "2.2.4",
|
||||
"@tiptap/extension-text": "2.2.4",
|
||||
"@tiptap/extension-typography": "2.2.4",
|
||||
"@tiptap/extension-underline": "2.2.4",
|
||||
"@tiptap/pm": "2.2.4",
|
||||
"@tiptap/suggestion": "2.2.4",
|
||||
"@tiptap/vue-3": "2.2.4",
|
||||
"@kyvg/vue3-notification": "3.1.4",
|
||||
"@sentry/tracing": "7.100.1",
|
||||
"@sentry/vue": "7.100.1",
|
||||
"@tiptap/core": "2.2.1",
|
||||
"@tiptap/extension-blockquote": "2.2.1",
|
||||
"@tiptap/extension-bold": "2.2.1",
|
||||
"@tiptap/extension-bullet-list": "2.2.1",
|
||||
"@tiptap/extension-code": "2.2.1",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.1",
|
||||
"@tiptap/extension-document": "2.2.1",
|
||||
"@tiptap/extension-dropcursor": "2.2.1",
|
||||
"@tiptap/extension-gapcursor": "2.2.1",
|
||||
"@tiptap/extension-hard-break": "2.2.1",
|
||||
"@tiptap/extension-heading": "2.2.1",
|
||||
"@tiptap/extension-history": "2.2.1",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.1",
|
||||
"@tiptap/extension-image": "2.2.1",
|
||||
"@tiptap/extension-italic": "2.2.1",
|
||||
"@tiptap/extension-link": "2.2.1",
|
||||
"@tiptap/extension-list-item": "2.2.1",
|
||||
"@tiptap/extension-ordered-list": "2.2.1",
|
||||
"@tiptap/extension-paragraph": "2.2.1",
|
||||
"@tiptap/extension-placeholder": "2.2.1",
|
||||
"@tiptap/extension-strike": "2.2.1",
|
||||
"@tiptap/extension-table": "2.2.1",
|
||||
"@tiptap/extension-table-cell": "2.2.1",
|
||||
"@tiptap/extension-table-header": "2.2.1",
|
||||
"@tiptap/extension-table-row": "2.2.1",
|
||||
"@tiptap/extension-task-item": "2.2.1",
|
||||
"@tiptap/extension-task-list": "2.2.1",
|
||||
"@tiptap/extension-text": "2.2.1",
|
||||
"@tiptap/extension-typography": "2.2.1",
|
||||
"@tiptap/extension-underline": "2.2.1",
|
||||
"@tiptap/pm": "2.2.1",
|
||||
"@tiptap/suggestion": "2.2.1",
|
||||
"@tiptap/vue-3": "2.2.1",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@vueuse/core": "10.9.0",
|
||||
"@vueuse/router": "10.9.0",
|
||||
"@vueuse/core": "10.7.2",
|
||||
"@vueuse/router": "10.7.2",
|
||||
"axios": "1.6.7",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"date-fns": "3.3.1",
|
||||
"dayjs": "1.11.10",
|
||||
"dompurify": "3.0.9",
|
||||
"dompurify": "3.0.8",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.31",
|
||||
|
@ -118,11 +116,11 @@
|
|||
"sortablejs": "1.15.2",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.4.0",
|
||||
"vue": "3.4.21",
|
||||
"vue": "3.4.18",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.4",
|
||||
"vue-flatpickr-component": "11.0.3",
|
||||
"vue-i18n": "9.9.1",
|
||||
"vue-router": "4.3.0",
|
||||
"vue-router": "4.2.5",
|
||||
"workbox-precaching": "7.0.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
|
@ -130,9 +128,9 @@
|
|||
"@4tw/cypress-drag-drop": "2.2.5",
|
||||
"@cypress/vite-dev-server": "5.0.7",
|
||||
"@cypress/vue": "6.0.0",
|
||||
"@faker-js/faker": "8.4.1",
|
||||
"@faker-js/faker": "8.4.0",
|
||||
"@histoire/plugin-screenshot": "0.17.8",
|
||||
"@histoire/plugin-vue": "0.17.12",
|
||||
"@histoire/plugin-vue": "0.17.9",
|
||||
"@rushstack/eslint-patch": "1.7.2",
|
||||
"@tsconfig/node18": "18.2.2",
|
||||
"@types/codemirror": "5.60.15",
|
||||
|
@ -141,43 +139,43 @@
|
|||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.debounce": "4.0.9",
|
||||
"@types/marked": "5.0.2",
|
||||
"@types/node": "20.11.22",
|
||||
"@types/node": "20.11.10",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.0",
|
||||
"@typescript-eslint/parser": "7.1.0",
|
||||
"@vitejs/plugin-legacy": "5.3.1",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@types/sortablejs": "1.15.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.20.0",
|
||||
"@typescript-eslint/parser": "6.20.0",
|
||||
"@vitejs/plugin-legacy": "5.3.0",
|
||||
"@vitejs/plugin-vue": "5.0.3",
|
||||
"@vue/eslint-config-typescript": "12.0.0",
|
||||
"@vue/test-utils": "2.4.4",
|
||||
"@vue/tsconfig": "0.5.1",
|
||||
"autoprefixer": "10.4.17",
|
||||
"browserslist": "4.23.0",
|
||||
"caniuse-lite": "1.0.30001591",
|
||||
"css-has-pseudo": "6.0.2",
|
||||
"browserslist": "4.22.3",
|
||||
"caniuse-lite": "1.0.30001581",
|
||||
"css-has-pseudo": "6.0.1",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.6.6",
|
||||
"esbuild": "0.20.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-vue": "9.22.0",
|
||||
"happy-dom": "13.6.2",
|
||||
"cypress": "13.6.3",
|
||||
"esbuild": "0.20.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-vue": "9.20.1",
|
||||
"happy-dom": "13.3.5",
|
||||
"histoire": "0.17.9",
|
||||
"postcss": "8.4.35",
|
||||
"postcss": "8.4.33",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
"postcss-focus-within": "8.0.1",
|
||||
"postcss-preset-env": "9.4.0",
|
||||
"rollup": "4.12.0",
|
||||
"postcss-preset-env": "9.3.0",
|
||||
"rollup": "4.9.6",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass": "1.71.1",
|
||||
"sass": "1.70.0",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.1.4",
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-inject-preload": "1.3.3",
|
||||
"vite-plugin-pwa": "0.19.1",
|
||||
"vite-plugin-sentry": "1.4.0",
|
||||
"vite-plugin-pwa": "0.17.5",
|
||||
"vite-plugin-sentry": "1.3.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "1.3.1",
|
||||
"vitest": "1.2.2",
|
||||
"vue-tsc": "1.8.27",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.0.0"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -51,12 +51,10 @@ Hi ${process.env.DRONE_COMMIT_AUTHOR}!
|
|||
|
||||
Thank you for creating a PR!
|
||||
|
||||
I've deployed the frontend changes of this PR on a preview environment under this URL: ${fullPreviewUrl}
|
||||
I've deployed the changes of this PR on a preview environment under this URL: ${fullPreviewUrl}
|
||||
|
||||
You can use this url to view the changes live and test them out.
|
||||
You will need to manually connect this to an api running somewhere. The easiest to use is https://try.vikunja.io/.
|
||||
|
||||
This preview does not contain any changes made to the api, only the frontend.
|
||||
You will need to manually connect this to an api running somehwere. The easiest to use is https://try.vikunja.io/.
|
||||
|
||||
Have a nice day!
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
2ba5ae4c831fd749296d92f92c5f89339030e22b80be62b1253dc26982e8fd0082e354f884a3ba15293e0b96317ec758 ./scripts/deploy-preview-netlify.mjs
|
||||
d58cd9ebc135407aa29d093b046d84b72ec7073b3f08cedfdbb936318a0ad3e272fab921d3ff91a82c1a7059fcdecd7b ./scripts/deploy-preview-netlify.mjs
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
class="base-button"
|
||||
:href="href"
|
||||
rel="noreferrer noopener nofollow"
|
||||
:target="openExternalInNewTab ? '_blank' : undefined"
|
||||
target="_blank"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
|
@ -69,7 +69,6 @@ export interface BaseButtonProps extends /* @vue-ignore */ HTMLAttributes {
|
|||
disabled?: boolean
|
||||
to?: RouteLocationRaw
|
||||
href?: string
|
||||
openExternalInNewTab?: boolean
|
||||
}
|
||||
|
||||
export interface BaseButtonEmits {
|
||||
|
@ -79,7 +78,6 @@ export interface BaseButtonEmits {
|
|||
const {
|
||||
type = BASE_BUTTON_TYPES_MAP.BUTTON,
|
||||
disabled = false,
|
||||
openExternalInNewTab = true,
|
||||
} = defineProps<BaseButtonProps>()
|
||||
|
||||
const emit = defineEmits<BaseButtonEmits>()
|
||||
|
|
|
@ -84,10 +84,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
import {computed, ref} from 'vue'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useStorage} from '@vueuse/core'
|
||||
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
|
@ -113,18 +112,7 @@ const projectStore = useProjectStore()
|
|||
const baseStore = useBaseStore()
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
|
||||
// Persist open state across browser reloads. Using a seperate ref for the state
|
||||
// allows us to use only one entry in local storage instead of one for every project id.
|
||||
type openState = { [key: number]: boolean }
|
||||
const childProjectsOpenState = useStorage<openState>('navigation-child-projects-open', {})
|
||||
const childProjectsOpen = computed({
|
||||
get() {
|
||||
return childProjectsOpenState.value[project.id] ?? true
|
||||
},
|
||||
set(open) {
|
||||
childProjectsOpenState.value[project.id] = open
|
||||
},
|
||||
})
|
||||
const childProjectsOpen = ref(true)
|
||||
|
||||
const childProjects = computed(() => {
|
||||
return projectStore.getChildProjects(project.id)
|
||||
|
|
|
@ -122,7 +122,7 @@ const labelStore = useLabelStore()
|
|||
labelStore.loadAllLabels()
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
projectStore.loadAllProjects()
|
||||
projectStore.loadProjects()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -594,21 +594,27 @@ function clickTasklistCheckbox(event) {
|
|||
|
||||
watch(
|
||||
() => isEditing.value,
|
||||
async editing => {
|
||||
await nextTick()
|
||||
|
||||
let checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
|
||||
if (typeof checkboxes === 'undefined' || checkboxes.length === 0) {
|
||||
// For some reason, this works when we check a second time.
|
||||
await nextTick()
|
||||
|
||||
checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
|
||||
editing => {
|
||||
nextTick(() => {
|
||||
const checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
|
||||
if (typeof checkboxes === 'undefined' || checkboxes.length === 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
if (editing) {
|
||||
checkboxes.forEach(check => {
|
||||
if (check.children.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
// We assume the first child contains the label element with the checkbox and the second child the actual label
|
||||
// When the actual label is clicked, we forward that click to the checkbox.
|
||||
check.children[1].removeEventListener('click', clickTasklistCheckbox)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
checkboxes.forEach(check => {
|
||||
if (check.children.length < 2) {
|
||||
return
|
||||
|
@ -616,21 +622,8 @@ watch(
|
|||
|
||||
// We assume the first child contains the label element with the checkbox and the second child the actual label
|
||||
// When the actual label is clicked, we forward that click to the checkbox.
|
||||
check.children[1].removeEventListener('click', clickTasklistCheckbox)
|
||||
check.children[1].addEventListener('click', clickTasklistCheckbox)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
checkboxes.forEach(check => {
|
||||
if (check.children.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
// We assume the first child contains the label element with the checkbox and the second child the actual label
|
||||
// When the actual label is clicked, we forward that click to the checkbox.
|
||||
check.children[1].removeEventListener('click', clickTasklistCheckbox)
|
||||
check.children[1].addEventListener('click', clickTasklistCheckbox)
|
||||
})
|
||||
},
|
||||
{immediate: true},
|
||||
|
@ -788,7 +781,6 @@ watch(
|
|||
|
||||
.ProseMirror {
|
||||
/* Table-specific styling */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
|
@ -844,7 +836,6 @@ watch(
|
|||
}
|
||||
|
||||
// Lists
|
||||
|
||||
ul {
|
||||
margin-left: .5rem;
|
||||
margin-top: 0 !important;
|
||||
|
|
|
@ -10,8 +10,7 @@
|
|||
autocomplete="current-password"
|
||||
:tabindex="props.tabindex"
|
||||
@keyup.enter="e => $emit('submit', e)"
|
||||
@focusout="() => {validate(); validateAfterFirst = true}"
|
||||
@keyup="() => {validateAfterFirst ? validate() : null}"
|
||||
@focusout="validate"
|
||||
@input="handleInput"
|
||||
>
|
||||
<BaseButton
|
||||
|
@ -24,17 +23,16 @@
|
|||
</BaseButton>
|
||||
</div>
|
||||
<p
|
||||
v-if="isValid !== true"
|
||||
v-if="!isValid"
|
||||
class="help is-danger"
|
||||
>
|
||||
{{ isValid }}
|
||||
{{ $t('user.auth.passwordRequired') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, watch} from 'vue'
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -42,17 +40,13 @@ const props = defineProps({
|
|||
modelValue: String,
|
||||
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
|
||||
validateInitially: Boolean,
|
||||
validateMinLength: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['submit', 'update:modelValue'])
|
||||
const {t} = useI18n()
|
||||
|
||||
const passwordFieldType = ref('password')
|
||||
const password = ref('')
|
||||
const isValid = ref<true | string>(props.validateInitially === true ? true : '')
|
||||
const validateAfterFirst = ref(false)
|
||||
const isValid = ref(!props.validateInitially)
|
||||
|
||||
watch(
|
||||
() => props.validateInitially,
|
||||
|
@ -61,22 +55,7 @@ watch(
|
|||
)
|
||||
|
||||
const validate = useDebounceFn(() => {
|
||||
if (password.value === '') {
|
||||
isValid.value = t('user.auth.passwordRequired')
|
||||
return
|
||||
}
|
||||
|
||||
if (props.validateMinLength && password.value.length < 8) {
|
||||
isValid.value = t('user.auth.passwordNotMin')
|
||||
return
|
||||
}
|
||||
|
||||
if (props.validateMinLength && password.value.length > 250) {
|
||||
isValid.value = t('user.auth.passwordNotMax')
|
||||
return
|
||||
}
|
||||
|
||||
isValid.value = true
|
||||
isValid.value = password.value !== ''
|
||||
}, 100)
|
||||
|
||||
function togglePasswordFieldType() {
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<BaseButton
|
||||
class="dropdown-item"
|
||||
:class="{'is-disabled': disabled}"
|
||||
>
|
||||
<BaseButton class="dropdown-item">
|
||||
<span
|
||||
v-if="icon"
|
||||
class="icon is-small"
|
||||
|
@ -24,7 +21,6 @@ import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
|||
export interface DropDownItemProps extends /* @vue-ignore */ BaseButtonProps {
|
||||
icon?: IconProp,
|
||||
iconClass?: object | string,
|
||||
disabled?: boolean,
|
||||
}
|
||||
|
||||
defineProps<DropDownItemProps>()
|
||||
|
|
|
@ -67,10 +67,8 @@
|
|||
{{ $t('menu.duplicate') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-tooltip="isDefaultProject ? $t('menu.cantArchiveIsDefault') : ''"
|
||||
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
||||
icon="archive"
|
||||
:disabled="isDefaultProject"
|
||||
>
|
||||
{{ $t('menu.archive') }}
|
||||
</DropdownItem>
|
||||
|
@ -97,11 +95,9 @@
|
|||
{{ $t('menu.createProject') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-tooltip="isDefaultProject ? $t('menu.cantDeleteIsDefault') : ''"
|
||||
:to="{ name: 'project.settings.delete', params: { projectId: project.id } }"
|
||||
icon="trash-alt"
|
||||
class="has-text-danger"
|
||||
:disabled="isDefaultProject"
|
||||
>
|
||||
{{ $t('menu.delete') }}
|
||||
</DropdownItem>
|
||||
|
@ -110,7 +106,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, type PropType, ref, watchEffect} from 'vue'
|
||||
import {ref, computed, watchEffect, type PropType} from 'vue'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
|
@ -122,7 +118,6 @@ import type {ISubscription} from '@/modelTypes/ISubscription'
|
|||
import {isSavedFilter} from '@/services/savedFilter'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
|
@ -151,7 +146,4 @@ function setSubscriptionInStore(sub: ISubscription) {
|
|||
}
|
||||
projectStore.setProject(updatedProject)
|
||||
}
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const isDefaultProject = computed(() => props.project?.id === authStore.settings.defaultProjectId)
|
||||
</script>
|
|
@ -17,7 +17,6 @@
|
|||
bar-end="endDate"
|
||||
:grid="true"
|
||||
:width="ganttChartWidth"
|
||||
:color-scheme="GANTT_COLOR_SCHEME"
|
||||
@dragendBar="updateGanttTask"
|
||||
@dblclickBar="openTask"
|
||||
>
|
||||
|
@ -60,7 +59,7 @@ import {
|
|||
extendDayjs,
|
||||
GGanttChart,
|
||||
GGanttRow,
|
||||
type GanttBarObject, type ColorScheme,
|
||||
type GanttBarObject,
|
||||
} from '@infectoone/vue-ganttastic'
|
||||
|
||||
import Loading from '@/components/misc/loading.vue'
|
||||
|
@ -114,16 +113,6 @@ const ganttChartWidth = computed(() => {
|
|||
|
||||
const ganttBars = ref<GanttBarObject[][]>([])
|
||||
|
||||
const GANTT_COLOR_SCHEME: ColorScheme = {
|
||||
primary: 'var(--grey-100)',
|
||||
secondary: 'var(--grey-300)',
|
||||
ternary: 'var(--grey-500)',
|
||||
quartenary: 'var(--grey-600)',
|
||||
hoverHighlight: 'var(--grey-700)',
|
||||
text: 'var(--grey-800)',
|
||||
background: 'var(--white)',
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ganttBars when tasks change
|
||||
*/
|
||||
|
|
|
@ -59,7 +59,7 @@ const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
|
|||
}
|
||||
|
||||
&:hover .assignee:not(:first-child) {
|
||||
margin-left: -0.5rem;
|
||||
margin-left: -1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
|
|||
transition: all $transition;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: -1rem;
|
||||
margin-left: -1.5rem;
|
||||
}
|
||||
|
||||
:deep(.user img) {
|
||||
|
|
|
@ -27,8 +27,7 @@ defineProps({
|
|||
display: inline;
|
||||
|
||||
:deep(.tag) {
|
||||
margin-top: .125rem;
|
||||
margin-bottom: .125rem;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -261,18 +261,13 @@ const foundTasks = ref<ITask[]>([])
|
|||
|
||||
async function findTasks(newQuery: string) {
|
||||
query.value = newQuery
|
||||
const result = await taskService.getAll({}, {
|
||||
s: newQuery,
|
||||
sort_by: 'done',
|
||||
})
|
||||
|
||||
foundTasks.value = mapRelatedTasks(result)
|
||||
foundTasks.value = await taskService.getAll({}, {s: newQuery})
|
||||
}
|
||||
|
||||
function mapRelatedTasks(tasks: ITask[]) {
|
||||
return tasks.map(task => {
|
||||
// by doing this here once we can save a lot of duplicate calls in the template
|
||||
const project = projectStore.projects[task.projectId]
|
||||
const project = projectStore.projects[task.ProjectId]
|
||||
|
||||
return {
|
||||
...task,
|
||||
|
|
|
@ -103,7 +103,7 @@ import {getHexColor} from '@/models/task'
|
|||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
||||
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
|
||||
import Labels from '@/components/tasks/partials/labels.vue'
|
||||
import Labels from '@/components/tasks/partials//labels.vue'
|
||||
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
|
||||
|
||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||
|
@ -197,7 +197,6 @@ const project = computed(() => projectStore.projects[task.projectId])
|
|||
span.parent-tasks {
|
||||
color: var(--grey-500);
|
||||
width: auto;
|
||||
margin-left: .25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -21,7 +21,6 @@ export interface SortBy {
|
|||
percent_done?: Order
|
||||
created?: Order
|
||||
updated?: Order
|
||||
done_at?: Order,
|
||||
}
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in filters.vue
|
||||
|
|
|
@ -11,17 +11,14 @@ export function getRedirectUrlFromCurrentFrontendPath(provider: IProvider): stri
|
|||
|
||||
export const redirectToProvider = (provider: IProvider) => {
|
||||
|
||||
console.log({provider})
|
||||
|
||||
const redirectUrl = getRedirectUrlFromCurrentFrontendPath(provider)
|
||||
const state = createRandomID(24)
|
||||
localStorage.setItem('state', state)
|
||||
|
||||
let scope = 'openid email profile'
|
||||
if (provider.scope !== null){
|
||||
scope = provider.scope
|
||||
}
|
||||
window.location.href = `${provider.authUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}&state=${state}`
|
||||
window.location.href = `${provider.authUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=openid email profile&state=${state}`
|
||||
}
|
||||
|
||||
export const redirectToProviderOnLogout = (provider: IProvider) => {
|
||||
if (provider.logoutUrl.length > 0) {
|
||||
window.location.href = `${provider.logoutUrl}`
|
||||
|
|
|
@ -57,11 +57,7 @@
|
|||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
|
@ -716,8 +712,7 @@
|
|||
"repeat": "Repeat",
|
||||
"startDate": "Start Date",
|
||||
"title": "Title",
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
"updated": "Updated"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -978,9 +973,7 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
"createProject": "Create project"
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
|
|
@ -9,7 +9,6 @@ export interface ITeam extends IAbstract {
|
|||
description: string
|
||||
members: ITeamMember[]
|
||||
right: Right
|
||||
oidcId: string
|
||||
|
||||
createdBy: IUser
|
||||
created: Date
|
||||
|
|
|
@ -13,7 +13,6 @@ export default class TeamModel extends AbstractModel<ITeam> implements ITeam {
|
|||
description = ''
|
||||
members: ITeamMember[] = []
|
||||
right: Right = RIGHTS.READ
|
||||
oidcId = ''
|
||||
|
||||
createdBy: IUser = {} // FIXME: seems wrong
|
||||
created: Date = null
|
||||
|
|
|
@ -111,13 +111,13 @@ export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
|
|||
|
||||
async function createFilter() {
|
||||
filter.value = await filterService.create(filter.value)
|
||||
await projectStore.loadAllProjects()
|
||||
await projectStore.loadProjects()
|
||||
router.push({name: 'project.index', params: {projectId: getProjectId(filter.value)}})
|
||||
}
|
||||
|
||||
async function saveFilter() {
|
||||
const response = await filterService.update(filter.value)
|
||||
await projectStore.loadAllProjects()
|
||||
await projectStore.loadProjects()
|
||||
success({message: t('filters.edit.success')})
|
||||
response.filters = objectToSnakeCase(response.filters)
|
||||
filter.value = response
|
||||
|
@ -130,7 +130,7 @@ export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
|
|||
|
||||
async function deleteFilter() {
|
||||
await filterService.delete(filter.value)
|
||||
await projectStore.loadAllProjects()
|
||||
await projectStore.loadProjects()
|
||||
success({message: t('filters.delete.success')})
|
||||
router.push({name: 'projects.index'})
|
||||
}
|
||||
|
|
|
@ -246,9 +246,8 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||
}
|
||||
|
||||
async function loadNextTasksForBucket(
|
||||
projectId: IProject['id'],
|
||||
ps,
|
||||
bucketId: IBucket['id'],
|
||||
{projectId, ps = {}, bucketId} :
|
||||
{projectId: IProject['id'], ps, bucketId: IBucket['id']},
|
||||
) {
|
||||
const isLoading = bucketLoading.value[bucketId] ?? false
|
||||
if (isLoading) {
|
||||
|
|
|
@ -175,28 +175,20 @@ export const useProjectStore = defineStore('project', () => {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadAllProjects() {
|
||||
async function loadProjects() {
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
const projectService = new ProjectService()
|
||||
const loadedProjects: IProject[] = []
|
||||
let page = 1
|
||||
try {
|
||||
do {
|
||||
const newProjects = await projectService.getAll({}, {is_archived: true}, page) as IProject[]
|
||||
loadedProjects.push(...newProjects)
|
||||
page++
|
||||
} while (page <= projectService.totalPages)
|
||||
|
||||
const loadedProjects = await projectService.getAll({}, {is_archived: true}) as IProject[]
|
||||
projects.value = {}
|
||||
setProjects(loadedProjects)
|
||||
loadedProjects.forEach(p => add(p))
|
||||
|
||||
return loadedProjects
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
|
||||
projects.value = {}
|
||||
setProjects(loadedProjects)
|
||||
loadedProjects.forEach(p => add(p))
|
||||
|
||||
return loadedProjects
|
||||
}
|
||||
|
||||
function getAncestors(project: IProject): IProject[] {
|
||||
|
@ -230,7 +222,7 @@ export const useProjectStore = defineStore('project', () => {
|
|||
setProjects,
|
||||
removeProjectById,
|
||||
toggleProjectFavorite,
|
||||
loadAllProjects,
|
||||
loadProjects,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
|
|
|
@ -473,7 +473,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
task = await taskService.update(task)
|
||||
|
||||
// reloading the projects list so that the Favorites project shows up or is hidden when there are (or are not) favorite tasks
|
||||
await projectStore.loadAllProjects()
|
||||
await projectStore.loadProjects()
|
||||
|
||||
return task
|
||||
}
|
||||
|
|
|
@ -4,5 +4,4 @@ export interface IProvider {
|
|||
authUrl: string;
|
||||
clientId: string;
|
||||
logoutUrl: string;
|
||||
scope: string;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<h1>{{ $t('migrate.titleService', {name: migrator.name}) }}</h1>
|
||||
<p>{{ $t('migrate.descriptionDo') }}</p>
|
||||
|
||||
<template v-if="message === '' && lastMigrationStartedAt === null">
|
||||
<template v-if="message === '' && lastMigrationFinishedAt === null">
|
||||
<template v-if="isMigrating === false">
|
||||
<template v-if="migrator.isFileMigrator">
|
||||
<p>{{ $t('migrate.importUpload', {name: migrator.name}) }}</p>
|
||||
|
@ -27,7 +27,6 @@
|
|||
:loading="migrationService.loading"
|
||||
:disabled="migrationService.loading || undefined"
|
||||
:href="authUrl"
|
||||
:open-external-in-new-tab="false"
|
||||
>
|
||||
{{ $t('migrate.getStarted') }}
|
||||
</x-button>
|
||||
|
@ -54,10 +53,10 @@
|
|||
<p>{{ $t('migrate.inProgress') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="!migrationJustStarted && lastMigrationStartedAt && lastMigrationFinishedAt === null">
|
||||
<Message class="mb-4">
|
||||
<div v-else-if="lastMigrationStartedAt && lastMigrationFinishedAt === null">
|
||||
<p>
|
||||
{{ $t('migrate.migrationInProgress') }}
|
||||
</Message>
|
||||
</p>
|
||||
<x-button :to="{name: 'home'}">
|
||||
{{ $t('home.goToOverview') }}
|
||||
</x-button>
|
||||
|
@ -146,7 +145,6 @@ const lastMigrationFinishedAt = ref<Date | null>(null)
|
|||
const lastMigrationStartedAt = ref<Date | null>(null)
|
||||
const message = ref('')
|
||||
const migratorAuthCode = ref('')
|
||||
const migrationJustStarted = ref(false)
|
||||
|
||||
const migrator = computed<Migrator>(() => MIGRATORS[props.service])
|
||||
|
||||
|
@ -172,18 +170,19 @@ async function initMigration() {
|
|||
if (!migratorAuthCode.value) {
|
||||
return
|
||||
}
|
||||
const {started_at, finished_at} = await migrationService.getStatus()
|
||||
if (started_at) {
|
||||
lastMigrationStartedAt.value = parseDateOrNull(started_at)
|
||||
const {startedAt, finishedAt} = await migrationService.getStatus()
|
||||
if (startedAt) {
|
||||
lastMigrationStartedAt.value = parseDateOrNull(startedAt)
|
||||
}
|
||||
if (finished_at) {
|
||||
lastMigrationFinishedAt.value = parseDateOrNull(finished_at)
|
||||
if (finishedAt) {
|
||||
lastMigrationFinishedAt.value = parseDateOrNull(finishedAt)
|
||||
if (lastMigrationFinishedAt.value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (lastMigrationStartedAt.value && lastMigrationFinishedAt.value === null) {
|
||||
// Migration already in progress
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -209,17 +208,12 @@ async function migrate() {
|
|||
}
|
||||
|
||||
try {
|
||||
if (migrator.value.isFileMigrator) {
|
||||
const result = await migrationFileService.migrate(migrationConfig as File)
|
||||
message.value = result.message
|
||||
const projectStore = useProjectStore()
|
||||
return projectStore.loadAllProjects()
|
||||
}
|
||||
|
||||
await migrationService.migrate(migrationConfig as MigrationConfig)
|
||||
migrationJustStarted.value = true
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
const result = migrator.value.isFileMigrator
|
||||
? await migrationFileService.migrate(migrationConfig as File)
|
||||
: await migrationService.migrate(migrationConfig as MigrationConfig)
|
||||
message.value = result.message
|
||||
const projectStore = useProjectStore()
|
||||
return projectStore.loadProjects()
|
||||
} finally {
|
||||
isMigrating.value = false
|
||||
}
|
||||
|
|
|
@ -82,15 +82,14 @@
|
|||
>
|
||||
<div class="control">
|
||||
<input
|
||||
ref="bucketLimitInputRef"
|
||||
v-focus.always
|
||||
:value="bucket.limit"
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
@keyup.esc="() => showSetLimitInput = false"
|
||||
@keyup.enter="() => {setBucketLimit(bucket.id, true); showSetLimitInput = false}"
|
||||
@input="setBucketLimit(bucket.id)"
|
||||
@keyup.enter="() => showSetLimitInput = false"
|
||||
@input="(event) => setBucketLimit(bucket.id, parseInt((event.target as HTMLInputElement).value))"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
|
@ -99,7 +98,6 @@
|
|||
:disabled="bucket.limit < 0"
|
||||
:icon="['far', 'save']"
|
||||
:shadow="false"
|
||||
@click="setBucketLimit(bucket.id, true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -322,7 +320,6 @@ const taskStore = useTaskStore()
|
|||
const projectStore = useProjectStore()
|
||||
|
||||
const taskContainerRefs = ref<{[id: IBucket['id']]: HTMLElement}>({})
|
||||
const bucketLimitInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const drag = ref(false)
|
||||
const dragBucket = ref(false)
|
||||
|
@ -416,11 +413,11 @@ function handleTaskContainerScroll(id: IBucket['id'], projectId: IProject['id'],
|
|||
return
|
||||
}
|
||||
|
||||
kanbanStore.loadNextTasksForBucket(
|
||||
projectId,
|
||||
params.value,
|
||||
id,
|
||||
)
|
||||
kanbanStore.loadNextTasksForBucket({
|
||||
projectId: projectId,
|
||||
params: params.value,
|
||||
bucketId: id,
|
||||
})
|
||||
}
|
||||
|
||||
function updateTasks(bucketId: IBucket['id'], tasks: IBucket['tasks']) {
|
||||
|
@ -618,7 +615,7 @@ function updateBucketPosition(e: {newIndex: number}) {
|
|||
})
|
||||
}
|
||||
|
||||
async function saveBucketLimit(bucketId: IBucket['id'], limit: number) {
|
||||
async function setBucketLimit(bucketId: IBucket['id'], limit: number) {
|
||||
if (limit < 0) {
|
||||
return
|
||||
}
|
||||
|
@ -630,22 +627,6 @@ async function saveBucketLimit(bucketId: IBucket['id'], limit: number) {
|
|||
success({message: t('project.kanban.bucketLimitSavedSuccess')})
|
||||
}
|
||||
|
||||
const setBucketLimitCancel = ref<number|null>(null)
|
||||
|
||||
async function setBucketLimit(bucketId: IBucket['id'], now: boolean = false) {
|
||||
const limit = parseInt(bucketLimitInputRef.value?.value || '')
|
||||
|
||||
if (setBucketLimitCancel.value !== null) {
|
||||
clearTimeout(setBucketLimitCancel.value)
|
||||
}
|
||||
|
||||
if (now) {
|
||||
return saveBucketLimit(bucketId, limit)
|
||||
}
|
||||
|
||||
setBucketLimitCancel.value = setTimeout(saveBucketLimit, 2500, bucketId, limit)
|
||||
}
|
||||
|
||||
function shouldAcceptDrop(bucket: IBucket) {
|
||||
return (
|
||||
// When dragging from a bucket who has its limit reached, dragging should still be possible
|
||||
|
|
|
@ -52,9 +52,6 @@
|
|||
<Fancycheckbox v-model="activeColumns.percentDone">
|
||||
{{ $t('task.attributes.percentDone') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.doneAt">
|
||||
{{ $t('task.attributes.doneAt') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.created">
|
||||
{{ $t('task.attributes.created') }}
|
||||
</Fancycheckbox>
|
||||
|
@ -147,13 +144,6 @@
|
|||
@click="sort('percent_done')"
|
||||
/>
|
||||
</th>
|
||||
<th v-if="activeColumns.doneAt">
|
||||
{{ $t('task.attributes.doneAt') }}
|
||||
<Sort
|
||||
:order="sortBy.done_at"
|
||||
@click="sort('done_at')"
|
||||
/>
|
||||
</th>
|
||||
<th v-if="activeColumns.created">
|
||||
{{ $t('task.attributes.created') }}
|
||||
<Sort
|
||||
|
@ -233,10 +223,6 @@
|
|||
<td v-if="activeColumns.percentDone">
|
||||
{{ t.percentDone * 100 }}%
|
||||
</td>
|
||||
<DateTableCell
|
||||
v-if="activeColumns.doneAt"
|
||||
:date="t.doneAt"
|
||||
/>
|
||||
<DateTableCell
|
||||
v-if="activeColumns.created"
|
||||
:date="t.created"
|
||||
|
@ -311,7 +297,6 @@ const ACTIVE_COLUMNS_DEFAULT = {
|
|||
created: false,
|
||||
updated: false,
|
||||
createdBy: false,
|
||||
doneAt: false,
|
||||
}
|
||||
|
||||
const SORT_BY_DEFAULT: SortBy = {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
:class="{ 'is-loading': teamService.loading }"
|
||||
>
|
||||
<card
|
||||
v-if="userIsAdmin && !team.oidcId"
|
||||
v-if="userIsAdmin"
|
||||
class="is-fullwidth"
|
||||
:title="title"
|
||||
>
|
||||
|
@ -77,7 +77,7 @@
|
|||
:padding="false"
|
||||
>
|
||||
<div
|
||||
v-if="userIsAdmin && !team.oidcId"
|
||||
v-if="userIsAdmin"
|
||||
class="p-4"
|
||||
>
|
||||
<div class="field has-addons">
|
||||
|
|
|
@ -17,13 +17,11 @@
|
|||
class="teams box"
|
||||
>
|
||||
<li
|
||||
v-for="t in teams"
|
||||
:key="t.id"
|
||||
v-for="team in teams"
|
||||
:key="team.id"
|
||||
>
|
||||
<router-link :to="{name: 'teams.edit', params: {id: t.id}}">
|
||||
<p>
|
||||
{{ t.name }}
|
||||
</p>
|
||||
<router-link :to="{name: 'teams.edit', params: {id: team.id}}">
|
||||
{{ team.name }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -65,7 +63,7 @@ ul.teams {
|
|||
li {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--grey-200);
|
||||
border-bottom: 1px solid $border;
|
||||
|
||||
a {
|
||||
color: var(--text);
|
||||
|
|
|
@ -66,7 +66,6 @@
|
|||
v-model="password"
|
||||
tabindex="2"
|
||||
:validate-initially="validatePasswordInitially"
|
||||
:validate-min-length="false"
|
||||
@submit="submit"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -28,24 +28,21 @@
|
|||
type="text"
|
||||
autocomplete="username"
|
||||
@keyup.enter="submit"
|
||||
@focusout="() => {validateUsername(); validateUsernameAfterFirst = true}"
|
||||
@keyup="() => {validateUsernameAfterFirst ? validateUsername() : null}"
|
||||
@focusout="validateUsername"
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
v-if="usernameValid !== true"
|
||||
v-if="!usernameValid"
|
||||
class="help is-danger"
|
||||
>
|
||||
{{ usernameValid }}
|
||||
{{ $t('user.auth.usernameRequired') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
class="label"
|
||||
for="email"
|
||||
>
|
||||
{{ $t('user.auth.email') }}
|
||||
</label>
|
||||
>{{ $t('user.auth.email') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="email"
|
||||
|
@ -56,8 +53,7 @@
|
|||
required
|
||||
type="email"
|
||||
@keyup.enter="submit"
|
||||
@focusout="() => {validateEmail(); validateEmailAfterFirst = true}"
|
||||
@keyup="() => {validateEmailAfterFirst ? validateEmail() : null}"
|
||||
@focusout="validateEmail"
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
|
@ -88,7 +84,7 @@
|
|||
>
|
||||
{{ $t('user.auth.createAccount') }}
|
||||
</x-button>
|
||||
|
||||
|
||||
<Message
|
||||
v-if="configStore.demoModeEnabled"
|
||||
variant="warning"
|
||||
|
@ -98,7 +94,7 @@
|
|||
{{ $t('demo.accountWillBeDeleted') }}<br>
|
||||
<strong class="is-uppercase">{{ $t('demo.everythingWillBeDeleted') }}</strong>
|
||||
</Message>
|
||||
|
||||
|
||||
<p class="mt-2">
|
||||
{{ $t('user.auth.alreadyHaveAnAccount') }}
|
||||
<router-link :to="{ name: 'user.login' }">
|
||||
|
@ -111,8 +107,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
import {computed, onBeforeMount, reactive, ref, toRaw} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
|
||||
|
||||
import router from '@/router'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
|
@ -122,7 +117,6 @@ import Password from '@/components/input/password.vue'
|
|||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
const {t} = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
|
@ -148,30 +142,13 @@ const DEBOUNCE_TIME = 100
|
|||
|
||||
// debouncing to prevent error messages when clicking on the log in button
|
||||
const emailValid = ref(true)
|
||||
const validateEmailAfterFirst = ref(false)
|
||||
const validateEmail = useDebounceFn(() => {
|
||||
emailValid.value = isEmail(credentials.email)
|
||||
}, DEBOUNCE_TIME)
|
||||
|
||||
const usernameValid = ref<true | string>(true)
|
||||
const validateUsernameAfterFirst = ref(false)
|
||||
const usernameValid = ref(true)
|
||||
const validateUsername = useDebounceFn(() => {
|
||||
if (credentials.username === '') {
|
||||
usernameValid.value = t('user.auth.usernameRequired')
|
||||
return
|
||||
}
|
||||
|
||||
if(credentials.username.indexOf(' ') !== -1) {
|
||||
usernameValid.value = t('user.auth.usernameMustNotContainSpace')
|
||||
return
|
||||
}
|
||||
|
||||
if(credentials.username.indexOf('://') !== -1) {
|
||||
usernameValid.value = t('user.auth.usernameMustNotLookLikeUrl')
|
||||
return
|
||||
}
|
||||
|
||||
usernameValid.value = true
|
||||
usernameValid.value = credentials.username !== ''
|
||||
}, DEBOUNCE_TIME)
|
||||
|
||||
const everythingValid = computed(() => {
|
||||
|
|
|
@ -286,15 +286,16 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
|
|||
:key="group"
|
||||
class="mb-2"
|
||||
>
|
||||
<strong class="is-capitalized">{{ formatPermissionTitle(group) }}</strong><br>
|
||||
<template
|
||||
v-if="Object.keys(routes).length >= 1"
|
||||
v-if="Object.keys(routes).length > 1"
|
||||
>
|
||||
<Fancycheckbox
|
||||
v-model="newTokenPermissionsGroup[group]"
|
||||
class="mr-2 is-capitalized has-text-weight-bold"
|
||||
class="mr-2 is-italic"
|
||||
@update:modelValue="checked => selectPermissionGroup(group, checked)"
|
||||
>
|
||||
{{ formatPermissionTitle(group) }}
|
||||
{{ $t('user.settings.apiTokens.selectAll') }}
|
||||
</Fancycheckbox>
|
||||
<br>
|
||||
</template>
|
||||
|
@ -304,7 +305,7 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
|
|||
>
|
||||
<Fancycheckbox
|
||||
v-model="newTokenPermissions[group][route]"
|
||||
class="ml-4 mr-2 is-capitalized"
|
||||
class="mr-2 is-capitalized"
|
||||
@update:modelValue="checked => toggleGroupPermissionsFromChild(group, checked)"
|
||||
>
|
||||
{{ formatPermissionTitle(route) }}
|
||||
|
|
36
go.mod
36
go.mod
|
@ -21,7 +21,7 @@ require (
|
|||
dario.cat/mergo v1.0.0
|
||||
github.com/ThreeDotsLabs/watermill v1.3.5
|
||||
github.com/adlio/trello v1.10.0
|
||||
github.com/arran4/golang-ical v0.2.6
|
||||
github.com/arran4/golang-ical v0.2.4
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/bbrks/go-blurhash v1.1.1
|
||||
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
|
||||
|
@ -33,7 +33,7 @@ require (
|
|||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/getsentry/sentry-go v0.27.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.10.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.9.0
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
|
@ -50,10 +50,10 @@ require (
|
|||
github.com/magefile/mage v1.15.0
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/op/go-logging v1
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/redis/go-redis/v9 v9.4.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/spf13/afero v1.11.0
|
||||
|
@ -66,12 +66,12 @@ require (
|
|||
github.com/ulule/limiter/v3 v3.11.2
|
||||
github.com/wneessen/go-mail v0.4.0
|
||||
github.com/yuin/goldmark v1.7.0
|
||||
golang.org/x/crypto v0.20.0
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/oauth2 v0.16.0
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/sys v0.17.0
|
||||
golang.org/x/term v0.17.0
|
||||
golang.org/x/term v0.16.0
|
||||
golang.org/x/text v0.14.0
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
@ -83,10 +83,10 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/ClickHouse/ch-go v0.58.2 // indirect
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.18.0 // indirect
|
||||
github.com/ClickHouse/ch-go v0.55.0 // indirect
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.9.1 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
@ -142,13 +142,13 @@ require (
|
|||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/onsi/gomega v1.16.0 // indirect
|
||||
github.com/paulmach/orb v0.11.1 // indirect
|
||||
github.com/paulmach/orb v0.9.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
|
@ -168,17 +168,17 @@ require (
|
|||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect
|
||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel v1.15.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.15.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
|
|
41
go.sum
41
go.sum
|
@ -11,12 +11,8 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/ClickHouse/ch-go v0.55.0 h1:jw4Tpx887YXrkyL5DfgUome/po8MLz92nz2heOQ6RjQ=
|
||||
github.com/ClickHouse/ch-go v0.55.0/go.mod h1:kQT2f+yp2p+sagQA/7kS6G3ukym+GQ5KAu1kuFAFDiU=
|
||||
github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0=
|
||||
github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.9.1 h1:IeE2bwVvAba7Yw5ZKu98bKI4NpDmykEy6jUaQdJJCk8=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.9.1/go.mod h1:teXfZNM90iQ99Jnuht+dxQXCuhDZ8nvvMoTJOFrcmcg=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.18.0 h1:O1LicIeg2JS2V29fKRH4+yT3f6jvvcJBm506dpVQ4mQ=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.18.0/go.mod h1:ztQvX6wm7kAbhJslS87EXEhOVNY/TObXwyURnGju5FQ=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
|
@ -30,8 +26,6 @@ github.com/adlio/trello v1.10.0 h1:ia/rzoBwJJKr4IqnMlrU6n09CVqeyaahSkEVcV5/gPc=
|
|||
github.com/adlio/trello v1.10.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
|
@ -39,10 +33,6 @@ github.com/arran4/golang-ical v0.2.3 h1:C4Vj7+BjJBIrAJhHgi6Ku+XUkQVugRq4re5Cqj5Q
|
|||
github.com/arran4/golang-ical v0.2.3/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/arran4/golang-ical v0.2.4 h1:0/rTXn2qqEekLKec3SzRRy+z7pCLtniMb0KD/dPogUo=
|
||||
github.com/arran4/golang-ical v0.2.4/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/arran4/golang-ical v0.2.5 h1:zaAdee/cOnOCeSuxUSgkWnF9jZl/oYq2ZgDk+LU3wGs=
|
||||
github.com/arran4/golang-ical v0.2.5/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/arran4/golang-ical v0.2.6 h1:WRpbLKSIMjujycCNKGAjOALyj6evvklVpWXH+Hp72G4=
|
||||
github.com/arran4/golang-ical v0.2.6/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/bbrks/go-blurhash v1.1.1 h1:uoXOxRPDca9zHYabUTwvS4KnY++KKUbwFo+Yxb8ME4M=
|
||||
|
@ -170,8 +160,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
|
|||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.9.0 h1:938g5V+GWLVejm3Hc+nWCuEXRlcglZDDlN/t1gWzcSY=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.9.0/go.mod h1:cdsKD2ApFBjdog9jRsz6EJqF+LClq/hrwE9K/1Dzo4s=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.10.0 h1:BrBwN7AuC+74g5qtk9D59TLGOaEa8Bw1WmIsf+SyzWc=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.10.0/go.mod h1:z8RoleoNtibi6Ar8ziCW7e6PQ+jWiqbUWvuv8AMe4lo=
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA=
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
|
@ -426,15 +414,11 @@ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0C
|
|||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/paulmach/orb v0.9.0 h1:MwA1DqOKtvCgm7u9RZ/pnYejTeDJPnr0+0oFajBbJqk=
|
||||
github.com/paulmach/orb v0.9.0/go.mod h1:SudmOk85SXtmXAB3sLGyJ6tZy/8pdfrV0o6ef98Xc30=
|
||||
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
||||
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
|
@ -450,8 +434,6 @@ github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1
|
|||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
|
@ -460,8 +442,6 @@ github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdO
|
|||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
|
@ -470,10 +450,6 @@ github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21M
|
|||
github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/redis/go-redis/v9 v9.5.0 h1:Xe9TKMmZv939gwTBcvc0n1tzK5l2re0pKw/W/tN3amw=
|
||||
github.com/redis/go-redis/v9 v9.5.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
|
||||
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
|
@ -584,15 +560,10 @@ github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRla
|
|||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
|
||||
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||
go.opentelemetry.io/otel v1.15.0 h1:NIl24d4eiLJPM0vKn4HjLYM+UZf6gSfi9Z+NmCxkWbk=
|
||||
go.opentelemetry.io/otel v1.15.0/go.mod h1:qfwLEbWhLPk5gyWrne4XnF0lC8wtywbuJbgfAE3zbek=
|
||||
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
|
||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
||||
go.opentelemetry.io/otel/trace v1.15.0 h1:5Fwje4O2ooOxkfyqI/kJwxWotggDLix4BSAvpE1wlpo=
|
||||
go.opentelemetry.io/otel/trace v1.15.0/go.mod h1:CUsmE2Ht1CRkvE8OsMESvraoZrrcgD1J2W8GV1ev0Y4=
|
||||
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
|
@ -630,10 +601,6 @@ golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
|||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
|
||||
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
|
@ -672,14 +639,10 @@ golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
|||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -733,8 +696,6 @@ golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
|||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -784,8 +745,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
|||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
@ -33,8 +34,6 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
|
||||
"github.com/magefile/mage/mg"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
@ -72,8 +71,7 @@ var (
|
|||
"dev:make-listener": Dev.MakeListener,
|
||||
"dev:make-notification": Dev.MakeNotification,
|
||||
"generate-docs": GenerateDocs,
|
||||
"lint": Check.Golangci,
|
||||
"lint:fix": Check.GolangciFix,
|
||||
"check:golangci-fix": Check.GolangciFix,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -159,7 +157,7 @@ func setRootPath() {
|
|||
|
||||
func setGoFiles() {
|
||||
// GOFILES := $(shell find . -name "*.go" -type f ! -path "*/bindata.go")
|
||||
files, err := runCmdWithOutput("find", "./pkg", "-name", "*.go", "-type", "f", "!", "-path", "*/bindata.go")
|
||||
files, err := runCmdWithOutput("find", ".", "-name", "*.go", "-type", "f", "!", "-path", "*/bindata.go")
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting go files: %s\n", err)
|
||||
os.Exit(1)
|
||||
|
|
|
@ -179,10 +179,6 @@ func initSqliteEngine() (engine *xorm.Engine, err error) {
|
|||
path = "./db.db"
|
||||
}
|
||||
|
||||
if path == "memory" {
|
||||
return xorm.NewEngine("sqlite3", "file::memory:?cache=shared")
|
||||
}
|
||||
|
||||
// Try opening the db file to return a better error message if that does not work
|
||||
var exists = true
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
|
|
|
@ -234,7 +234,6 @@
|
|||
title: Test25
|
||||
owner_id: 6
|
||||
parent_project_id: 12
|
||||
position: 25
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -242,7 +241,6 @@
|
|||
title: Test26
|
||||
owner_id: 6
|
||||
parent_project_id: 25
|
||||
position: 26
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
|
|
@ -55,7 +55,3 @@
|
|||
team_id: 13
|
||||
user_id: 10
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
team_id: 14
|
||||
user_id: 10
|
||||
created: 2018-12-01 15:13:12
|
|
@ -28,8 +28,4 @@
|
|||
created_by_id: 7
|
||||
- id: 13
|
||||
name: testteam13
|
||||
created_by_id: 7
|
||||
- id: 14
|
||||
name: testteam14
|
||||
created_by_id: 7
|
||||
oidc_id: 14
|
||||
created_by_id: 7
|
|
@ -17,9 +17,7 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
gofs "io/fs"
|
||||
)
|
||||
|
||||
// Dump dumps all saved files
|
||||
|
@ -33,13 +31,8 @@ func Dump() (allFiles map[int64]io.ReadCloser, err error) {
|
|||
|
||||
allFiles = make(map[int64]io.ReadCloser, len(files))
|
||||
for _, file := range files {
|
||||
err = file.LoadFileByID()
|
||||
if err != nil {
|
||||
var pathError *gofs.PathError
|
||||
if errors.As(err, &pathError) {
|
||||
continue
|
||||
}
|
||||
return
|
||||
if err := file.LoadFileByID(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allFiles[file.ID] = file.File
|
||||
}
|
||||
|
|
|
@ -368,7 +368,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("by priority", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
|
||||
require.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","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"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":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by priority desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
|
||||
|
@ -378,7 +378,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
t.Run("by priority asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
|
||||
require.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","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"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":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
// should equal duedate asc
|
||||
t.Run("by due_date", func(t *testing.T) {
|
||||
|
|
|
@ -69,7 +69,7 @@ func init() {
|
|||
func setupActiveUsersMetric() {
|
||||
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_active_users",
|
||||
Help: "The number of users active within the last 30 seconds",
|
||||
Help: "The number of shares active within the last 30 seconds",
|
||||
}, func() float64 {
|
||||
allActiveUsers := activeUsersMap{}
|
||||
_, err := keyvalue.GetWithValue(activeUsersKey, &allActiveUsers)
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present 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 teams20230104152903 struct {
|
||||
OidcID string `xorm:"varchar(250) null" maxLength:"250" json:"oidc_id"`
|
||||
}
|
||||
|
||||
func (teams20230104152903) TableName() string {
|
||||
return "teams"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20230104152903",
|
||||
Description: "Adding OidcID to teams",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(teams20230104152903{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -69,7 +69,7 @@ func getRouteGroupName(path string) string {
|
|||
// CollectRoutesForAPITokenUsage gets called for every added APITokenRoute and builds a list of all routes we can use for the api tokens.
|
||||
func CollectRoutesForAPITokenUsage(route echo.Route) {
|
||||
|
||||
if !strings.Contains(route.Name, "(*WebHandler)") && !strings.Contains(route.Name, "Attachment") {
|
||||
if !strings.Contains(route.Name, "(*WebHandler)") {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -116,21 +116,6 @@ func CollectRoutesForAPITokenUsage(route echo.Route) {
|
|||
Method: route.Method,
|
||||
}
|
||||
}
|
||||
|
||||
if routeGroupName == "tasks_attachments" {
|
||||
if strings.Contains(route.Name, "UploadTaskAttachment") {
|
||||
apiTokenRoutes[routeGroupName].Create = &RouteDetail{
|
||||
Path: route.Path,
|
||||
Method: route.Method,
|
||||
}
|
||||
}
|
||||
if strings.Contains(route.Name, "GetTaskAttachment") {
|
||||
apiTokenRoutes[routeGroupName].ReadOne = &RouteDetail{
|
||||
Path: route.Path,
|
||||
Method: route.Method,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetAvailableAPIRoutesForToken returns a list of all API routes which are available for token usage.
|
||||
|
|
|
@ -1059,6 +1059,7 @@ func (err ErrTeamNameCannotBeEmpty) HTTPError() web.HTTPError {
|
|||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTeamNameCannotBeEmpty, Message: "The team name cannot be empty"}
|
||||
}
|
||||
|
||||
// ErrTeamDoesNotExist represents an error where a team does not exist
|
||||
type ErrTeamDoesNotExist struct {
|
||||
TeamID int64
|
||||
}
|
||||
|
@ -1177,54 +1178,6 @@ func (err ErrTeamDoesNotHaveAccessToProject) HTTPError() web.HTTPError {
|
|||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeTeamDoesNotHaveAccessToProject, Message: "This team does not have access to the project."}
|
||||
}
|
||||
|
||||
// ErrOIDCTeamDoesNotExist represents an error where a team with specified name and specified oidcId property does not exist
|
||||
type ErrOIDCTeamDoesNotExist struct {
|
||||
OidcID string
|
||||
Name string
|
||||
}
|
||||
|
||||
// IsErrOIDCTeamDoesNotExist checks if an error is ErrOIDCTeamDoesNotExist.
|
||||
func IsErrOIDCTeamDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrOIDCTeamDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ErrTeamDoesNotExist represents an error where a team does not exist
|
||||
func (err ErrOIDCTeamDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("No team with that name and valid oidcId could be found. [Team Name: %v] [OidcID : %v] ", err.Name, err.OidcID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeOIDCTeamDoesNotExist = 6008
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrOIDCTeamDoesNotExist) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No team with that name and valid oidcId could be found."}
|
||||
}
|
||||
|
||||
// ErrOIDCTeamsDoNotExistForUser represents an error where an oidcTeam does not exist for the user
|
||||
type ErrOIDCTeamsDoNotExistForUser struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrOIDCTeamsDoNotExistForUser checks if an error is ErrOIDCTeamsDoNotExistForUser.
|
||||
func IsErrOIDCTeamsDoNotExistForUser(err error) bool {
|
||||
_, ok := err.(ErrOIDCTeamsDoNotExistForUser)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrOIDCTeamsDoNotExistForUser) Error() string {
|
||||
return fmt.Sprintf("No teams with property oidcId could be found for user [User ID: %d]", err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeTeamDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeOIDCTeamsDoNotExistForUser = 6009
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrOIDCTeamsDoNotExistForUser) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTeamDoesNotExist, Message: "No Teams with property oidcId could be found for User."}
|
||||
}
|
||||
|
||||
// ====================
|
||||
// User <-> Project errors
|
||||
// ====================
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -316,8 +315,8 @@ func GetProjectSimplByTaskID(s *xorm.Session, taskID int64) (l *Project, err err
|
|||
return &project, nil
|
||||
}
|
||||
|
||||
// GetProjectsMapSimplByTaskIDs gets a list of projects by a task ids
|
||||
func GetProjectsMapSimplByTaskIDs(s *xorm.Session, taskIDs []int64) (ps map[int64]*Project, err error) {
|
||||
// GetProjectsSimplByTaskIDs gets a list of projects by a task ids
|
||||
func GetProjectsSimplByTaskIDs(s *xorm.Session, taskIDs []int64) (ps map[int64]*Project, err error) {
|
||||
ps = make(map[int64]*Project)
|
||||
err = s.
|
||||
Select("projects.*").
|
||||
|
@ -328,18 +327,8 @@ func GetProjectsMapSimplByTaskIDs(s *xorm.Session, taskIDs []int64) (ps map[int6
|
|||
return
|
||||
}
|
||||
|
||||
func GetProjectsSimplByTaskIDs(s *xorm.Session, taskIDs []int64) (ps []*Project, err error) {
|
||||
err = s.
|
||||
Select("projects.*").
|
||||
Table(Project{}).
|
||||
Join("INNER", "tasks", "projects.id = tasks.project_id").
|
||||
In("tasks.id", taskIDs).
|
||||
Find(&ps)
|
||||
return
|
||||
}
|
||||
|
||||
// GetProjectsMapByIDs returns a map of projects from a slice with project ids
|
||||
func GetProjectsMapByIDs(s *xorm.Session, projectIDs []int64) (projects map[int64]*Project, err error) {
|
||||
// GetProjectsByIDs returns a map of projects from a slice with project ids
|
||||
func GetProjectsByIDs(s *xorm.Session, projectIDs []int64) (projects map[int64]*Project, err error) {
|
||||
projects = make(map[int64]*Project, len(projectIDs))
|
||||
|
||||
if len(projectIDs) == 0 {
|
||||
|
@ -350,17 +339,6 @@ func GetProjectsMapByIDs(s *xorm.Session, projectIDs []int64) (projects map[int6
|
|||
return
|
||||
}
|
||||
|
||||
func GetProjectsByIDs(s *xorm.Session, projectIDs []int64) (projects []*Project, err error) {
|
||||
projects = make([]*Project, 0, len(projectIDs))
|
||||
|
||||
if len(projectIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.In("id", projectIDs).Find(&projects)
|
||||
return
|
||||
}
|
||||
|
||||
type projectOptions struct {
|
||||
search string
|
||||
user *user.User
|
||||
|
@ -440,49 +418,67 @@ func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search str
|
|||
parentCondition,
|
||||
builder.NotIn("l.id", parentProjectIDs),
|
||||
)).
|
||||
OrderBy("position").
|
||||
GroupBy("l.id")
|
||||
}
|
||||
|
||||
func getAllProjectsForUser(s *xorm.Session, userID int64, opts *projectOptions) (projects []*Project, totalCount int64, err error) {
|
||||
func getAllProjectsForUser(s *xorm.Session, userID int64, parentProjectIDs []int64, opts *projectOptions, projects *[]*Project, oldTotalCount int64, archivedProjects map[int64]bool) (resultCount int, totalCount int64, err error) {
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
query := getUserProjectsStatement(nil, userID, opts.search, opts.getArchived)
|
||||
|
||||
querySQLString, args, err := query.ToSQL()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var limitSQL string
|
||||
query := getUserProjectsStatement(parentProjectIDs, userID, opts.search, opts.getArchived)
|
||||
if limit > 0 {
|
||||
limitSQL = fmt.Sprintf("LIMIT %d OFFSET %d", limit, start)
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
|
||||
baseQuery := querySQLString + `
|
||||
UNION ALL
|
||||
SELECT p.* FROM projects p
|
||||
INNER JOIN all_projects ap ON p.parent_project_id = ap.id`
|
||||
|
||||
currentProjects := []*Project{}
|
||||
err = s.SQL(`WITH RECURSIVE all_projects as (`+baseQuery+`)
|
||||
SELECT DISTINCT * FROM all_projects ORDER BY position `+limitSQL, args...).Find(¤tProjects)
|
||||
err = s.SQL(query).Find(¤tProjects)
|
||||
if err != nil {
|
||||
return
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if len(currentProjects) == 0 {
|
||||
return nil, 0, err
|
||||
return 0, oldTotalCount, err
|
||||
}
|
||||
|
||||
query = getUserProjectsStatement(parentProjectIDs, userID, opts.search, opts.getArchived)
|
||||
totalCount, err = s.
|
||||
SQL(`WITH RECURSIVE all_projects as (`+baseQuery+`)
|
||||
SELECT COUNT(DISTINCT all_projects.id) FROM all_projects`, args...).
|
||||
SQL(query.Select("count(*)")).
|
||||
Count(&Project{})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return currentProjects, totalCount, err
|
||||
parentIDsMap := make(map[int64]bool, len(parentProjectIDs))
|
||||
for _, id := range parentProjectIDs {
|
||||
parentIDsMap[id] = true
|
||||
}
|
||||
|
||||
for _, project := range currentProjects {
|
||||
parentIDsMap[project.ID] = true
|
||||
}
|
||||
|
||||
newParentIDs := []int64{}
|
||||
for _, project := range currentProjects {
|
||||
if project.IsArchived {
|
||||
archivedProjects[project.ID] = true
|
||||
}
|
||||
if archivedProjects[project.ParentProjectID] {
|
||||
project.IsArchived = true
|
||||
}
|
||||
// Filter out parent project ids which we're not looking for to avoid leaking
|
||||
// information about parent projects
|
||||
if !parentIDsMap[project.ParentProjectID] {
|
||||
project.ParentProjectID = 0
|
||||
}
|
||||
newParentIDs = append(newParentIDs, project.ID)
|
||||
}
|
||||
|
||||
*projects = append(*projects, currentProjects...)
|
||||
|
||||
// If we don't reset the limit for subprojects, it will be impossible to fetch all subprojects.
|
||||
opts.page = -1
|
||||
|
||||
return getAllProjectsForUser(s, userID, newParentIDs, opts, projects, oldTotalCount+totalCount, archivedProjects)
|
||||
}
|
||||
|
||||
// Gets the projects with their children without any tasks
|
||||
|
@ -492,7 +488,9 @@ func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*P
|
|||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
allProjects, totalItems, err := getAllProjectsForUser(s, fullUser.ID, opts)
|
||||
allProjects := []*Project{}
|
||||
archivedProjects := make(map[int64]bool)
|
||||
resultCount, totalItems, err = getAllProjectsForUser(s, fullUser.ID, nil, opts, &allProjects, 0, archivedProjects)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -580,7 +578,7 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
|
|||
return err
|
||||
}
|
||||
|
||||
subscriptions, err := GetSubscriptionsForProjects(s, projects, a)
|
||||
subscriptions, err := GetSubscriptions(s, SubscriptionEntityProject, projectIDs, a)
|
||||
if err != nil {
|
||||
log.Errorf("An error occurred while getting project subscriptions for a project: %s", err.Error())
|
||||
subscriptions = make(map[int64][]*Subscription)
|
||||
|
|
|
@ -349,9 +349,11 @@ func TestProject_ReadAll(t *testing.T) {
|
|||
t.Run("all", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
projects, _, err := getAllProjectsForUser(s, 6, &projectOptions{})
|
||||
projects := []*Project{}
|
||||
archivedProjects := make(map[int64]bool)
|
||||
_, _, err := getAllProjectsForUser(s, 1, nil, &projectOptions{}, &projects, 0, archivedProjects)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, projects, 25)
|
||||
assert.Len(t, projects, 24)
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("only child projects for one project", func(t *testing.T) {
|
||||
|
@ -367,12 +369,12 @@ func TestProject_ReadAll(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Equal(t, reflect.Slice, reflect.TypeOf(projects3).Kind())
|
||||
ls := projects3.([]*Project)
|
||||
assert.Len(t, ls, 28)
|
||||
assert.Len(t, ls, 26)
|
||||
assert.Equal(t, int64(3), ls[0].ID) // Project 3 has a position of 1 and should be sorted first
|
||||
assert.Equal(t, int64(1), ls[1].ID)
|
||||
assert.Equal(t, int64(6), ls[2].ID)
|
||||
assert.Equal(t, int64(-1), ls[26].ID)
|
||||
assert.Equal(t, int64(-2), ls[27].ID)
|
||||
assert.Equal(t, int64(-1), ls[24].ID)
|
||||
assert.Equal(t, int64(-2), ls[25].ID)
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("projects for nonexistant user", func(t *testing.T) {
|
||||
|
|
|
@ -223,11 +223,7 @@ func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entity
|
|||
|
||||
switch entityType {
|
||||
case SubscriptionEntityProject:
|
||||
projects, err := GetProjectsByIDs(s, entityIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return GetSubscriptionsForProjects(s, projects, u)
|
||||
return getSubscriptionsForProjects(s, entityIDs, u)
|
||||
case SubscriptionEntityTask:
|
||||
subs, err := getSubscriptionsForTasks(s, entityIDs, u)
|
||||
if err != nil {
|
||||
|
@ -236,34 +232,22 @@ func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entity
|
|||
|
||||
// If the task does not have a subscription directly or from its project, get the one
|
||||
// from the parent and return it instead.
|
||||
var taskIDsWithoutSubscription []int64
|
||||
for _, eID := range entityIDs {
|
||||
if _, has := subs[eID]; has {
|
||||
continue
|
||||
}
|
||||
|
||||
taskIDsWithoutSubscription = append(taskIDsWithoutSubscription, eID)
|
||||
}
|
||||
|
||||
projects, err := GetProjectsSimplByTaskIDs(s, taskIDsWithoutSubscription)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tasks, err := GetTasksSimpleByIDs(s, taskIDsWithoutSubscription)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectSubscriptions, err := GetSubscriptionsForProjects(s, projects, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
sub, has := projectSubscriptions[task.ProjectID]
|
||||
if has {
|
||||
subs[task.ID] = sub
|
||||
task, err := GetTaskByIDSimple(s, eID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projectSubscriptions, err := getSubscriptionsForProjects(s, []int64{task.ProjectID}, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, subscription := range projectSubscriptions {
|
||||
subs[eID] = subscription // The first project subscription is the subscription we're looking for
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,59 +257,48 @@ func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entity
|
|||
return
|
||||
}
|
||||
|
||||
func GetSubscriptionsForProjects(s *xorm.Session, projects []*Project, a web.Auth) (projectsToSubscriptions map[int64][]*Subscription, err error) {
|
||||
u, is := a.(*user.User)
|
||||
if u != nil && !is {
|
||||
return
|
||||
}
|
||||
|
||||
func getSubscriptionsForProjects(s *xorm.Session, projectIDs []int64, u *user.User) (projectsToSubscriptions map[int64][]*Subscription, err error) {
|
||||
origEntityIDs := projectIDs
|
||||
var ps = make(map[int64]*Project)
|
||||
origProjectIDs := make([]int64, 0, len(projects))
|
||||
allProjectIDs := make([]int64, 0, len(projects))
|
||||
|
||||
for _, p := range projects {
|
||||
ps[p.ID] = p
|
||||
origProjectIDs = append(origProjectIDs, p.ID)
|
||||
allProjectIDs = append(allProjectIDs, p.ID)
|
||||
}
|
||||
|
||||
// We can't just use the projects we have, we need to fetch the parents
|
||||
// because they may not be loaded in the same object
|
||||
|
||||
for _, p := range projects {
|
||||
if p.ParentProjectID == 0 {
|
||||
for _, eID := range projectIDs {
|
||||
if eID < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, has := ps[p.ParentProjectID]; has {
|
||||
ps[eID], err = GetProjectSimpleByID(s, eID)
|
||||
if err != nil && IsErrProjectDoesNotExist(err) {
|
||||
// If the project does not exist, it might got deleted. There could still be subscribers though.
|
||||
delete(ps, eID)
|
||||
continue
|
||||
}
|
||||
|
||||
err = ps[p.ID].GetAllParentProjects(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ps[eID].GetAllParentProjects(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parentIDs := []int64{}
|
||||
var parent = ps[p.ID].ParentProject
|
||||
var parent = ps[eID].ParentProject
|
||||
for parent != nil {
|
||||
parentIDs = append(parentIDs, parent.ID)
|
||||
parent = parent.ParentProject
|
||||
}
|
||||
|
||||
// Now we have all parent ids
|
||||
allProjectIDs = append(allProjectIDs, parentIDs...) // the child project id is already in there
|
||||
projectIDs = append(projectIDs, parentIDs...) // the child project id is already in there
|
||||
}
|
||||
|
||||
var subscriptions []*Subscription
|
||||
if u != nil {
|
||||
err = s.
|
||||
Where("user_id = ?", u.ID).
|
||||
And(getSubscriberCondForEntities(SubscriptionEntityProject, allProjectIDs)).
|
||||
And(getSubscriberCondForEntities(SubscriptionEntityProject, projectIDs)).
|
||||
Find(&subscriptions)
|
||||
} else {
|
||||
err = s.
|
||||
And(getSubscriberCondForEntities(SubscriptionEntityProject, allProjectIDs)).
|
||||
And(getSubscriberCondForEntities(SubscriptionEntityProject, projectIDs)).
|
||||
Find(&subscriptions)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -340,7 +313,7 @@ func GetSubscriptionsForProjects(s *xorm.Session, projects []*Project, a web.Aut
|
|||
|
||||
// Rearrange so that subscriptions trickle down
|
||||
|
||||
for _, eID := range origProjectIDs {
|
||||
for _, eID := range origEntityIDs {
|
||||
// If the current project does not have a subscription, climb up the tree until a project has one,
|
||||
// then use that subscription for all child projects
|
||||
_, has := projectsToSubscriptions[eID]
|
||||
|
|
|
@ -656,18 +656,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
}
|
||||
task39 := &Task{
|
||||
ID: 39,
|
||||
Title: "task #39",
|
||||
Identifier: "#0",
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ProjectID: 25,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 0,
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
ProjectID int64
|
||||
|
@ -740,7 +728,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
task32,
|
||||
task33,
|
||||
task35,
|
||||
task39,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
@ -785,7 +772,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
task3,
|
||||
task1,
|
||||
task2,
|
||||
task39,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
@ -957,7 +943,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
task32, // has nil dates
|
||||
task33, // has nil dates
|
||||
task35, // has nil dates
|
||||
task39, // has nil dates
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
@ -1217,7 +1202,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
task32,
|
||||
task33,
|
||||
task35,
|
||||
task39,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1234,7 +1218,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
task6,
|
||||
task5,
|
||||
// The other ones don't have a due date
|
||||
task39,
|
||||
task35,
|
||||
task33,
|
||||
task32,
|
||||
|
|
|
@ -139,7 +139,7 @@ func RegisterOverdueReminderCron() {
|
|||
}
|
||||
}
|
||||
|
||||
projects, err := GetProjectsMapSimplByTaskIDs(s, taskIDs)
|
||||
projects, err := GetProjectsSimplByTaskIDs(s, taskIDs)
|
||||
if err != nil {
|
||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not get projects for tasks: %s", err)
|
||||
return
|
||||
|
|
|
@ -173,7 +173,7 @@ func getTasksWithRemindersDueAndTheirUsers(s *xorm.Session, now time.Time) (remi
|
|||
|
||||
seen := make(map[int64]map[int64]bool)
|
||||
|
||||
projects, err := GetProjectsMapSimplByTaskIDs(s, taskIDs)
|
||||
projects, err := GetProjectsSimplByTaskIDs(s, taskIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -356,11 +356,6 @@ func GetTaskSimple(s *xorm.Session, t *Task) (task Task, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func GetTasksSimpleByIDs(s *xorm.Session, ids []int64) (tasks []*Task, err error) {
|
||||
err = s.In("id", ids).Find(&tasks)
|
||||
return
|
||||
}
|
||||
|
||||
// GetTasksByIDs returns all tasks for a project of ids
|
||||
func (bt *BulkTask) GetTasksByIDs(s *xorm.Session) (err error) {
|
||||
for _, id := range bt.IDs {
|
||||
|
@ -591,7 +586,7 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e
|
|||
}
|
||||
|
||||
// Get all identifiers
|
||||
projects, err := GetProjectsMapByIDs(s, projectIDs)
|
||||
projects, err := GetProjectsByIDs(s, projectIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
member, err := user2.GetUserByUsername(s, tm.Username)
|
||||
if err != nil {
|
||||
|
@ -108,12 +109,6 @@ func (tm *TeamMember) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (tm *TeamMember) MembershipExists(s *xorm.Session) (exists bool, err error) {
|
||||
return s.
|
||||
Where("team_id = ? AND user_id = ?", tm.TeamID, tm.UserID).
|
||||
Exist(&TeamMember{})
|
||||
}
|
||||
|
||||
// Update toggles a team member's admin status
|
||||
// @Summary Toggle a team member's admin status
|
||||
// @Description If a user is team admin, this will make them member and vise-versa.
|
||||
|
|
|
@ -38,8 +38,6 @@ type Team struct {
|
|||
// The team's description.
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
CreatedByID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
// The team's oidc id delivered by the oidc provider
|
||||
OidcID string `xorm:"varchar(250) null" maxLength:"250" json:"oidc_id"`
|
||||
|
||||
// The user who created this team.
|
||||
CreatedBy *user.User `xorm:"-" json:"created_by"`
|
||||
|
@ -88,18 +86,11 @@ func (*TeamMember) TableName() string {
|
|||
// TeamUser is the team member type
|
||||
type TeamUser struct {
|
||||
user.User `xorm:"extends"`
|
||||
// Whether the member is an admin of the team. See the docs for more about what a team admin can do
|
||||
// Whether or not the member is an admin of the team. See the docs for more about what a team admin can do
|
||||
Admin bool `json:"admin"`
|
||||
TeamID int64 `json:"-"`
|
||||
}
|
||||
|
||||
// OIDCTeam is the relevant data for a team and is delivered by oidc token
|
||||
type OIDCTeam struct {
|
||||
Name string
|
||||
OidcID string
|
||||
Description string
|
||||
}
|
||||
|
||||
// GetTeamByID gets a team by its ID
|
||||
func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) {
|
||||
if id < 1 {
|
||||
|
@ -129,34 +120,6 @@ func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// GetTeamByOidcIDAndName gets teams where oidc_id and name match parameters
|
||||
// For oidc team creation oidcID and Name need to be set
|
||||
func GetTeamByOidcIDAndName(s *xorm.Session, oidcID string, teamName string) (*Team, error) {
|
||||
team := &Team{}
|
||||
has, err := s.
|
||||
Table("teams").
|
||||
Where("oidc_id = ? AND name = ?", oidcID, teamName).
|
||||
Get(team)
|
||||
if !has || err != nil {
|
||||
return nil, ErrOIDCTeamDoesNotExist{teamName, oidcID}
|
||||
}
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func FindAllOidcTeamIDsForUser(s *xorm.Session, userID int64) (ts []int64, err error) {
|
||||
err = s.
|
||||
Table("team_members").
|
||||
Where("user_id = ? ", userID).
|
||||
Join("RIGHT", "teams", "teams.id = team_members.team_id").
|
||||
Where("teams.oidc_id != ? AND teams.oidc_id IS NOT NULL", "").
|
||||
Cols("teams.id").
|
||||
Find(&ts)
|
||||
if ts == nil || err != nil {
|
||||
return ts, err
|
||||
}
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) {
|
||||
|
||||
if len(teams) == 0 {
|
||||
|
@ -307,6 +270,7 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// Insert the current user as member and admin
|
||||
tm := TeamMember{TeamID: t.ID, Username: doer.Username, Admin: true}
|
||||
if err = tm.Create(s, doer); err != nil {
|
||||
return err
|
||||
|
|
|
@ -21,22 +21,21 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.vikunja.io/web/handler"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
petname "github.com/dustinkirkland/golang-petname"
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/oauth2"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// Callback contains the callback after an auth request was made and redirected
|
||||
|
@ -54,17 +53,16 @@ type Provider struct {
|
|||
AuthURL string `json:"auth_url"`
|
||||
LogoutURL string `json:"logout_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
Scope string `json:"scope"`
|
||||
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"`
|
||||
VikunjaGroups []map[string]interface{} `json:"vikunja_groups"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -98,7 +96,6 @@ func HandleCallback(c echo.Context) error {
|
|||
// Check if the provider exists
|
||||
providerKey := c.Param("provider")
|
||||
provider, err := GetProvider(providerKey)
|
||||
log.Debugf("Provider: %v", provider)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
|
@ -148,7 +145,6 @@ func HandleCallback(c echo.Context) error {
|
|||
|
||||
// Extract custom claims
|
||||
cl := &claims{}
|
||||
|
||||
err = idToken.Claims(cl)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting token claims for provider %s: %v", provider.Name, err)
|
||||
|
@ -202,182 +198,16 @@ func HandleCallback(c echo.Context) error {
|
|||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// does the oidc token contain well formed "vikunja_groups" through vikunja_scope
|
||||
log.Debugf("Checking for vikunja_groups in token %v", cl.VikunjaGroups)
|
||||
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, provider)
|
||||
if len(teamData) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Errorf("Error creating teams for user and vikunja groups %s: %v", cl.VikunjaGroups, err)
|
||||
}
|
||||
|
||||
// find old teams for user through oidc
|
||||
oldOidcTeams, err := models.FindAllOidcTeamIDsForUser(s, u.ID)
|
||||
if err != nil {
|
||||
log.Debugf("No oidc teams found for user %v", err)
|
||||
}
|
||||
oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData)
|
||||
if err != nil {
|
||||
log.Errorf("Could not proceed with group routine %v", err)
|
||||
}
|
||||
teamIDsToLeave := utils.NotIn(oldOidcTeams, oidcTeams)
|
||||
err = RemoveUserFromTeamsByIds(s, u, teamIDsToLeave)
|
||||
if err != nil {
|
||||
log.Errorf("Found error while leaving teams %v", err)
|
||||
}
|
||||
errs := RemoveEmptySSOTeams(s, teamIDsToLeave)
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Errorf("Found error while removing empty teams %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
err = s.Commit()
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Errorf("Error creating new team for provider %s: %v", provider.Name, err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// Create token
|
||||
return auth.NewUserAuthTokenResponse(u, c, false)
|
||||
}
|
||||
|
||||
func AssignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []*models.OIDCTeam) (oidcTeams []int64, err error) {
|
||||
if len(teamData) == 0 {
|
||||
return
|
||||
}
|
||||
// check if we have seen these teams before.
|
||||
// find or create Teams and assign user as teammember.
|
||||
teams, err := GetOrCreateTeamsByOIDCAndNames(s, teamData, u)
|
||||
if err != nil {
|
||||
log.Errorf("Error verifying team for %v, got %v. Error: %v", u.Name, teams, err)
|
||||
return nil, err
|
||||
}
|
||||
for _, team := range teams {
|
||||
tm := models.TeamMember{TeamID: team.ID, UserID: u.ID, Username: u.Username}
|
||||
exists, _ := tm.MembershipExists(s)
|
||||
if !exists {
|
||||
err = tm.Create(s, u)
|
||||
if err != nil {
|
||||
log.Errorf("Could not assign user %s to team %s: %v", u.Username, team.Name, err)
|
||||
}
|
||||
}
|
||||
oidcTeams = append(oidcTeams, team.ID)
|
||||
}
|
||||
return oidcTeams, err
|
||||
}
|
||||
|
||||
func RemoveEmptySSOTeams(s *xorm.Session, teamIDs []int64) (errs []error) {
|
||||
for _, teamID := range teamIDs {
|
||||
count, err := s.Where("team_id = ?", teamID).Count(&models.TeamMember{})
|
||||
if count == 0 && err == nil {
|
||||
log.Debugf("SSO team with id %v has no members. It will be deleted", teamID)
|
||||
_, _err := s.Where("id = ?", teamID).Delete(&models.Team{})
|
||||
if _err != nil {
|
||||
errs = append(errs, _err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func RemoveUserFromTeamsByIds(s *xorm.Session, u *user.User, teamIDs []int64) (err error) {
|
||||
|
||||
if len(teamIDs) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debugf("Removing team_member with user_id %v from team_ids %v", u.ID, teamIDs)
|
||||
_, err = s.In("team_id", teamIDs).And("user_id = ?", u.ID).Delete(&models.TeamMember{})
|
||||
return err
|
||||
}
|
||||
|
||||
func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) (teamData []*models.OIDCTeam, errs []error) {
|
||||
teamData = []*models.OIDCTeam{}
|
||||
errs = []error{}
|
||||
for _, team := range groups {
|
||||
var name string
|
||||
var description string
|
||||
var oidcID string
|
||||
_, exists := team["name"]
|
||||
if exists {
|
||||
name = team["name"].(string)
|
||||
}
|
||||
_, exists = team["description"]
|
||||
if exists {
|
||||
description = team["description"].(string)
|
||||
}
|
||||
_, exists = team["oidcID"]
|
||||
if exists {
|
||||
switch t := team["oidcID"].(type) {
|
||||
case string:
|
||||
oidcID = team["oidcID"].(string)
|
||||
case int64:
|
||||
oidcID = strconv.FormatInt(team["oidcID"].(int64), 10)
|
||||
case float64:
|
||||
oidcID = strconv.FormatFloat(team["oidcID"].(float64), 'f', -1, 64)
|
||||
default:
|
||||
log.Errorf("No oidcID assigned for %v or type %v not supported", team, t)
|
||||
}
|
||||
}
|
||||
if name == "" || oidcID == "" {
|
||||
log.Errorf("Claim of your custom scope does not hold name or oidcID for automatic group assignment through oidc provider. Please check %s", provider.Name)
|
||||
errs = append(errs, &user.ErrOpenIDCustomScopeMalformed{})
|
||||
continue
|
||||
}
|
||||
teamData = append(teamData, &models.OIDCTeam{Name: name, OidcID: oidcID, Description: description})
|
||||
}
|
||||
return teamData, errs
|
||||
}
|
||||
|
||||
func getOIDCTeamName(name string) string {
|
||||
return name + " (OIDC)"
|
||||
}
|
||||
|
||||
func CreateOIDCTeam(s *xorm.Session, teamData *models.OIDCTeam, u *user.User) (team *models.Team, err error) {
|
||||
team = &models.Team{
|
||||
Name: getOIDCTeamName(teamData.Name),
|
||||
Description: teamData.Description,
|
||||
OidcID: teamData.OidcID,
|
||||
}
|
||||
err = team.Create(s, u)
|
||||
return team, err
|
||||
}
|
||||
|
||||
// GetOrCreateTeamsByOIDCAndNames returns a slice of teams which were generated from the oidc data. If a team did not exist previously it is automatically created.
|
||||
func GetOrCreateTeamsByOIDCAndNames(s *xorm.Session, teamData []*models.OIDCTeam, u *user.User) (te []*models.Team, err error) {
|
||||
te = []*models.Team{}
|
||||
// Procedure can only be successful if oidcID is set
|
||||
for _, oidcTeam := range teamData {
|
||||
team, err := models.GetTeamByOidcIDAndName(s, oidcTeam.OidcID, oidcTeam.Name)
|
||||
if err != nil && !models.IsErrOIDCTeamDoesNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil && models.IsErrOIDCTeamDoesNotExist(err) {
|
||||
log.Debugf("Team with oidc_id %v and name %v does not exist. Creating team… ", oidcTeam.OidcID, oidcTeam.Name)
|
||||
newTeam, err := CreateOIDCTeam(s, oidcTeam, u)
|
||||
if err != nil {
|
||||
return te, err
|
||||
}
|
||||
te = append(te, newTeam)
|
||||
continue
|
||||
}
|
||||
|
||||
if team.Name != getOIDCTeamName(oidcTeam.Name) {
|
||||
team.Name = getOIDCTeamName(oidcTeam.Name)
|
||||
err = team.Update(s, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Team with oidc_id %v and name %v already exists.", team.OidcID, team.Name)
|
||||
te = append(te, team)
|
||||
}
|
||||
return te, err
|
||||
}
|
||||
|
||||
func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *user.User, err error) {
|
||||
|
||||
// Check if the user exists for that issuer and subject
|
||||
u, err = user.GetUserWithEmail(s, &user.User{
|
||||
Issuer: issuer,
|
||||
|
|
|
@ -20,9 +20,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -97,145 +95,4 @@ func TestGetOrCreateUser(t *testing.T) {
|
|||
"email": cl.Email,
|
||||
}, false)
|
||||
})
|
||||
t.Run("existing user, non existing team", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
team := "new sso team"
|
||||
oidcID := "47404"
|
||||
cl := &claims{
|
||||
Email: "other-email-address@some.service.com",
|
||||
VikunjaGroups: []map[string]interface{}{
|
||||
{"name": team, "oidcID": oidcID},
|
||||
},
|
||||
}
|
||||
|
||||
u, err := getOrCreateUser(s, cl, "https://some.service.com", "12345")
|
||||
require.NoError(t, err)
|
||||
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil)
|
||||
for _, err := range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData)
|
||||
require.NoError(t, err)
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "users", map[string]interface{}{
|
||||
"id": u.ID,
|
||||
"email": cl.Email,
|
||||
}, false)
|
||||
db.AssertExists(t, "teams", map[string]interface{}{
|
||||
"id": oidcTeams,
|
||||
"name": team + " (OIDC)",
|
||||
}, false)
|
||||
})
|
||||
|
||||
t.Run("existing user, assign to existing team", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
team := "testteam14"
|
||||
oidcID := "14"
|
||||
cl := &claims{
|
||||
Email: "other-email-address@some.service.com",
|
||||
VikunjaGroups: []map[string]interface{}{
|
||||
{"name": team, "oidcID": oidcID},
|
||||
},
|
||||
}
|
||||
|
||||
u := &user.User{ID: 10}
|
||||
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil)
|
||||
for _, err := range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData)
|
||||
require.NoError(t, err)
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "team_members", map[string]interface{}{
|
||||
"team_id": oidcTeams,
|
||||
"user_id": u.ID,
|
||||
}, false)
|
||||
})
|
||||
t.Run("existing user, remove from existing team", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
cl := &claims{
|
||||
Email: "other-email-address@some.service.com",
|
||||
VikunjaGroups: []map[string]interface{}{},
|
||||
}
|
||||
|
||||
u := &user.User{ID: 10}
|
||||
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil)
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
oldOidcTeams, err := models.FindAllOidcTeamIDsForUser(s, u.ID)
|
||||
require.NoError(t, err)
|
||||
oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData)
|
||||
require.NoError(t, err)
|
||||
teamIDsToLeave := utils.NotIn(oldOidcTeams, oidcTeams)
|
||||
require.NoError(t, err)
|
||||
err = RemoveUserFromTeamsByIds(s, u, teamIDsToLeave)
|
||||
require.NoError(t, err)
|
||||
errs = RemoveEmptySSOTeams(s, teamIDsToLeave)
|
||||
for _, err = range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
errs = RemoveEmptySSOTeams(s, teamIDsToLeave)
|
||||
for _, err = range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
|
||||
db.AssertMissing(t, "team_members", map[string]interface{}{
|
||||
"team_id": oidcTeams,
|
||||
"user_id": u.ID,
|
||||
})
|
||||
})
|
||||
t.Run("existing user, remove from existing team and delete team", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
cl := &claims{
|
||||
Email: "other-email-address@some.service.com",
|
||||
VikunjaGroups: []map[string]interface{}{},
|
||||
}
|
||||
|
||||
u := &user.User{ID: 10}
|
||||
teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil)
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
oldOidcTeams, err := models.FindAllOidcTeamIDsForUser(s, u.ID)
|
||||
require.NoError(t, err)
|
||||
oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData)
|
||||
require.NoError(t, err)
|
||||
teamIDsToLeave := utils.NotIn(oldOidcTeams, oidcTeams)
|
||||
require.NoError(t, err)
|
||||
err = RemoveUserFromTeamsByIds(s, u, teamIDsToLeave)
|
||||
require.NoError(t, err)
|
||||
errs = RemoveEmptySSOTeams(s, teamIDsToLeave)
|
||||
for _, err := range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
db.AssertMissing(t, "teams", map[string]interface{}{
|
||||
"id": oidcTeams,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -125,10 +125,6 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro
|
|||
logoutURL = ""
|
||||
}
|
||||
|
||||
scope, _ := pi["scope"].(string)
|
||||
if scope == "" {
|
||||
scope = "openid profile email"
|
||||
}
|
||||
provider = &Provider{
|
||||
Name: pi["name"].(string),
|
||||
Key: k,
|
||||
|
@ -136,7 +132,6 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro
|
|||
OriginalAuthURL: pi["authurl"].(string),
|
||||
ClientSecret: pi["clientsecret"].(string),
|
||||
LogoutURL: logoutURL,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
cl, is := pi["clientid"].(int)
|
||||
|
|
|
@ -30,14 +30,11 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/initialize"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/migration"
|
||||
vversion "code.vikunja.io/api/pkg/version"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
)
|
||||
|
@ -66,7 +63,6 @@ func Restore(filename string) error {
|
|||
// Find the configFile, database and files files
|
||||
var configFile *zip.File
|
||||
var dotEnvFile *zip.File
|
||||
var versionFile *zip.File
|
||||
dbfiles := make(map[string]*zip.File)
|
||||
filesFiles := make(map[string]*zip.File)
|
||||
for _, file := range r.File {
|
||||
|
@ -85,18 +81,7 @@ func Restore(filename string) error {
|
|||
}
|
||||
if strings.HasPrefix(file.Name, "files/") {
|
||||
filesFiles[strings.ReplaceAll(file.Name, "files/", "")] = file
|
||||
continue
|
||||
}
|
||||
if file.Name == "VERSION" {
|
||||
versionFile = file
|
||||
}
|
||||
}
|
||||
|
||||
///////
|
||||
// Check if we're restoring to the same version as the dump
|
||||
err = checkVikunjaVersion(versionFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
///////
|
||||
|
@ -293,38 +278,3 @@ func restoreConfig(configFile, dotEnvFile *zip.File) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkVikunjaVersion(versionFile *zip.File) error {
|
||||
if versionFile == nil {
|
||||
return fmt.Errorf("dump does not contain VERSION file, refusing to continue")
|
||||
}
|
||||
vf, err := versionFile.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open version file: %w", err)
|
||||
}
|
||||
|
||||
var bufVersion bytes.Buffer
|
||||
if _, err := bufVersion.ReadFrom(vf); err != nil {
|
||||
return fmt.Errorf("could not read version file: %w", err)
|
||||
}
|
||||
|
||||
versionString := bufVersion.String()
|
||||
if versionString == "dev" && vversion.Version == "dev" {
|
||||
log.Debugf("Importing from dev version")
|
||||
} else {
|
||||
dumpedVersion, err := version.NewVersion(bufVersion.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentVersion, err := version.NewVersion(vversion.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !dumpedVersion.Equal(currentVersion) {
|
||||
return fmt.Errorf("export was created with version %s but this is %s - please make sure you are running the same Vikunja version before restoring", dumpedVersion, currentVersion)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||
|
||||
err = handler.SaveBackgroundFile(s, user, &project.Project, backgroundFile, "", uint64(backgroundFile.Len()))
|
||||
if err != nil {
|
||||
log.Errorf("[creating structure] Could not create background for project %d, error was %v", project.ID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("[creating structure] Created a background file for project %d", project.ID)
|
||||
|
|
|
@ -73,8 +73,8 @@ func (mw *MigrationWeb) Migrate(c echo.Context) error {
|
|||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
if !stats.StartedAt.IsZero() && stats.FinishedAt.IsZero() {
|
||||
return c.JSON(http.StatusPreconditionFailed, map[string]string{
|
||||
if stats.FinishedAt.IsZero() {
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
"message": "Migration already running",
|
||||
"running_since": stats.StartedAt.String(),
|
||||
})
|
||||
|
@ -95,7 +95,7 @@ func (mw *MigrationWeb) Migrate(c echo.Context) error {
|
|||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{Message: "Migration was started successfully."})
|
||||
return c.JSON(http.StatusOK, models.Message{Message: "Everything was migrated successfully."})
|
||||
}
|
||||
|
||||
// Status returns whether or not a user has already done this migration
|
||||
|
|
|
@ -151,7 +151,7 @@ func setupSentry(e *echo.Echo) {
|
|||
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
||||
// Only capture errors not already handled by echo
|
||||
var herr *echo.HTTPError
|
||||
if errors.As(err, &herr) && herr.Code > 499 {
|
||||
if errors.As(err, &herr) && herr.Code > 403 {
|
||||
hub := sentryecho.GetHubFromContext(c)
|
||||
if hub != nil {
|
||||
hub.WithScope(func(scope *sentry.Scope) {
|
||||
|
|
|
@ -126,11 +126,12 @@ func serveIndexFile(c echo.Context, assetFs http.FileSystem) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
//etag, err := generateEtag(index, info.Name())
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
return serveFile(c, reader, info, "")
|
||||
etag, err := generateEtag(index, info.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return serveFile(c, reader, info, etag)
|
||||
}
|
||||
|
||||
// Copied from echo's middleware.StaticWithConfig simplified and adjusted for caching
|
||||
|
@ -140,9 +141,6 @@ func static() echo.MiddlewareFunc {
|
|||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) (err error) {
|
||||
p := c.Request().URL.Path
|
||||
if strings.HasPrefix(p, "/api/") {
|
||||
return next(c)
|
||||
}
|
||||
if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`.
|
||||
p = c.Param("*")
|
||||
}
|
||||
|
@ -270,9 +268,7 @@ func serveFile(c echo.Context, file io.ReadSeeker, info os.FileInfo, etag string
|
|||
|
||||
c.Response().Header().Set("Server", "Vikunja")
|
||||
c.Response().Header().Set("Vary", "Accept-Encoding")
|
||||
if etag != "" {
|
||||
c.Response().Header().Set("Etag", etag)
|
||||
}
|
||||
c.Response().Header().Set("Etag", etag)
|
||||
|
||||
cacheControl, err := getCacheControlHeader(info, file)
|
||||
if err != nil {
|
||||
|
|
|
@ -8300,11 +8300,6 @@ const docTemplate = `{
|
|||
"maxLength": 250,
|
||||
"minLength": 1
|
||||
},
|
||||
"oidc_id": {
|
||||
"description": "The team's oidc id delivered by the oidc provider",
|
||||
"type": "string",
|
||||
"maxLength": 250
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this relation was last updated. You cannot change this value.",
|
||||
"type": "string"
|
||||
|
@ -8367,7 +8362,7 @@ const docTemplate = `{
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"admin": {
|
||||
"description": "Whether the member is an admin of the team. See the docs for more about what a team admin can do",
|
||||
"description": "Whether or not the member is an admin of the team. See the docs for more about what a team admin can do",
|
||||
"type": "boolean"
|
||||
},
|
||||
"created": {
|
||||
|
@ -8435,11 +8430,6 @@ const docTemplate = `{
|
|||
"maxLength": 250,
|
||||
"minLength": 1
|
||||
},
|
||||
"oidc_id": {
|
||||
"description": "The team's oidc id delivered by the oidc provider",
|
||||
"type": "string",
|
||||
"maxLength": 250
|
||||
},
|
||||
"right": {
|
||||
"$ref": "#/definitions/models.Right"
|
||||
},
|
||||
|
@ -8583,9 +8573,6 @@ const docTemplate = `{
|
|||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -8292,11 +8292,6 @@
|
|||
"maxLength": 250,
|
||||
"minLength": 1
|
||||
},
|
||||
"oidc_id": {
|
||||
"description": "The team's oidc id delivered by the oidc provider",
|
||||
"type": "string",
|
||||
"maxLength": 250
|
||||
},
|
||||
"updated": {
|
||||
"description": "A timestamp when this relation was last updated. You cannot change this value.",
|
||||
"type": "string"
|
||||
|
@ -8359,7 +8354,7 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"admin": {
|
||||
"description": "Whether the member is an admin of the team. See the docs for more about what a team admin can do",
|
||||
"description": "Whether or not the member is an admin of the team. See the docs for more about what a team admin can do",
|
||||
"type": "boolean"
|
||||
},
|
||||
"created": {
|
||||
|
@ -8427,11 +8422,6 @@
|
|||
"maxLength": 250,
|
||||
"minLength": 1
|
||||
},
|
||||
"oidc_id": {
|
||||
"description": "The team's oidc id delivered by the oidc provider",
|
||||
"type": "string",
|
||||
"maxLength": 250
|
||||
},
|
||||
"right": {
|
||||
"$ref": "#/definitions/models.Right"
|
||||
},
|
||||
|
@ -8575,9 +8565,6 @@
|
|||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -904,10 +904,6 @@ definitions:
|
|||
maxLength: 250
|
||||
minLength: 1
|
||||
type: string
|
||||
oidc_id:
|
||||
description: The team's oidc id delivered by the oidc provider
|
||||
maxLength: 250
|
||||
type: string
|
||||
updated:
|
||||
description: A timestamp when this relation was last updated. You cannot change
|
||||
this value.
|
||||
|
@ -958,8 +954,8 @@ definitions:
|
|||
models.TeamUser:
|
||||
properties:
|
||||
admin:
|
||||
description: Whether the member is an admin of the team. See the docs for
|
||||
more about what a team admin can do
|
||||
description: Whether or not the member is an admin of the team. See the docs
|
||||
for more about what a team admin can do
|
||||
type: boolean
|
||||
created:
|
||||
description: A timestamp when this task was created. You cannot change this
|
||||
|
@ -1011,10 +1007,6 @@ definitions:
|
|||
maxLength: 250
|
||||
minLength: 1
|
||||
type: string
|
||||
oidc_id:
|
||||
description: The team's oidc id delivered by the oidc provider
|
||||
maxLength: 250
|
||||
type: string
|
||||
right:
|
||||
$ref: '#/definitions/models.Right'
|
||||
updated:
|
||||
|
@ -1124,8 +1116,6 @@ definitions:
|
|||
type: string
|
||||
name:
|
||||
type: string
|
||||
scope:
|
||||
type: string
|
||||
type: object
|
||||
todoist.Migration:
|
||||
properties:
|
||||
|
|
|
@ -426,32 +426,6 @@ func (err *ErrNoOpenIDEmailProvided) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// ErrNoOpenIDEmailProvided represents a "NoEmailProvided" kind of error.
|
||||
type ErrOpenIDCustomScopeMalformed struct {
|
||||
}
|
||||
|
||||
// IsErrNoEmailProvided checks if an error is a ErrNoOpenIDEmailProvided.
|
||||
func IsErrOpenIDCustomScopeMalformed(err error) bool {
|
||||
_, ok := err.(*ErrOpenIDCustomScopeMalformed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrOpenIDCustomScopeMalformed) Error() string {
|
||||
return "Custom Scope malformed"
|
||||
}
|
||||
|
||||
// ErrCodeNoOpenIDEmailProvided holds the unique world-error code of this error
|
||||
const ErrCodeOpenIDCustomScopeMalformed = 1022
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrOpenIDCustomScopeMalformed) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeOpenIDCustomScopeMalformed,
|
||||
Message: "The custom scope set by the OIDC provider is malformed. Please make sure the openid provider sets the data correctly for your scope. Check especially to have set an oidcID",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrAccountDisabled represents a "AccountDisabled" kind of error.
|
||||
type ErrAccountDisabled struct {
|
||||
UserID int64
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present 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
|
||||
|
||||
// find the elements which appear in slice1, but not in slice2
|
||||
func NotIn(slice1 []int64, slice2 []int64) []int64 {
|
||||
var diff []int64
|
||||
|
||||
for _, s1 := range slice1 {
|
||||
found := false
|
||||
for _, s2 := range slice2 {
|
||||
if s1 == s2 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// int64 not found. We add it to return slice
|
||||
if !found {
|
||||
diff = append(diff, s1)
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
publiccode-yml-version: '0.2'
|
||||
name: 'Vikunja'
|
||||
url: 'https://code.vikunja.io/vikunja'
|
||||
landingURL: 'https://vikunja.io'
|
||||
isBasedOn: 'https://go.dev/'
|
||||
softwareVersion: 'v0.23.0'
|
||||
releaseDate: '2024-02-10'
|
||||
logo: 'https://vikunja.io/images/vikunja-logo.svg'
|
||||
platforms:
|
||||
- 'web'
|
||||
- 'linux'
|
||||
- 'windows'
|
||||
- 'mac'
|
||||
- 'lxc'
|
||||
- 'docker'
|
||||
- 'k8s'
|
||||
- 'debian'
|
||||
categories:
|
||||
- 'agile-project-management'
|
||||
- 'enterprise-project-management'
|
||||
- 'project-management'
|
||||
- 'task-management'
|
||||
- 'workflow-management'
|
||||
usedBy:
|
||||
- 'City of Treuchtlingen'
|
||||
roadmap: 'https://my.vikunja.cloud/projects/16/list#share-auth-token=QFyzYEmEYfSyQfTOmIRSwLUpkFjboaBqQCnaPmWd'
|
||||
developmentStatus: 'stable'
|
||||
softwareType: 'standalone/web'
|
||||
intendedAudience:
|
||||
scope:
|
||||
- 'Providers of standard based workflow management services who wish or are required to use Open Source solutions'
|
||||
description:
|
||||
en: ' The open-source, self-hostable to-do app. Organize everything, on all platforms.'
|
||||
documentation: 'https://vikunja.io/docs/'
|
||||
apiDocumentation: 'https://vikunja.io/docs/api-documentation/'
|
||||
features:
|
||||
- 'workflow management'
|
||||
- 'CalDAV provider'
|
||||
- 'kanban tool'
|
||||
- 'gantt display tool'
|
||||
- 'ticket system'
|
||||
screenshots:
|
||||
- 'https://vikunja.io/images/vikunja/09-task-detail-dark.webp'
|
||||
videos:
|
||||
- 'none'
|
||||
legal:
|
||||
license: 'GNU AFFERO GENERAL PUBLIC LICENSE Version 3'
|
||||
mainCopyrightOwner: 'Konrad Langenberg Software'
|
||||
repoOwner: 'Konrad Langenberg Software'
|
||||
authorsFile: 'hello@vikunja.io'
|
||||
maintenance:
|
||||
type: 'https://vikunja.cloud/'
|
||||
contractors:
|
||||
- name: 'Vikunja Cloud '
|
||||
localisation:
|
||||
localisationReady: 'true'
|
||||
availableLanguages:
|
||||
- 'en'
|
||||
- 'de-de'
|
||||
- 'ru-ru'
|
||||
- 'fr-fr'
|
||||
- 'vi-vn'
|
||||
- 'it-it'
|
||||
- 'cs-cz'
|
||||
- 'pl-pl'
|
||||
- 'nl-nl'
|
||||
- 'pt-pt'
|
||||
- 'zh-cn'
|
||||
- 'no-no'
|
||||
- 'es-es'
|
||||
- 'da-dk'
|
||||
- 'ja-jp'
|
||||
- 'hu-hu'
|
||||
- 'sl-si'
|
||||
dependsOn:
|
||||
open:
|
||||
- name: 'CalDAV'
|
||||
- name: 'GoLANG'
|
||||
- name: 'Vue.js'
|
||||
- name: 'HTTP-Proxy'
|
||||
- name: 'SSL'
|
||||
- name: 'Unit file (SystemD)'
|
||||
- name: 'Firewall Rules (Web: 443)'
|
Loading…
Reference in New Issue
Block a user