Compare commits

...
Author SHA1 Message Date
Maksim Eltyshev 0c1c7e2192 chore: Update version 2024-02-01 00:34:31 +01:00
Maksim Eltyshev b8d262f745 feat: Add ability to enforce SSO
Closes #543, closes #545
2024-02-01 00:31:15 +01:00
Maksim Eltyshev a1fd694248 chore: Update version 2024-01-31 15:58:57 +01:00
Maksim Eltyshev 634d6ceab1 feat: Add ability to map OIDC attributes and ignore username
Closes #554
2024-01-25 23:01:59 +01:00
Maksim Eltyshev 31d4d5f38d chore: Update version 2024-01-18 11:00:20 +01:00
Maksim Eltyshev 3f99438402 fix: Fix images becoming black and white when resizing
Closes #574, closes #585
2024-01-18 10:58:26 +01:00
Maksim Eltyshev 7e7727e3e1 chore: Bump sharp version 2024-01-03 13:59:14 +05:00
Daniel Hiller 616304a1fa Update branch 2023-12-21 00:51:17 +01:00
Daniel Hiller 1e01148138 ci: Add retry on failure 2023-12-20 23:50:51 +01:00
Daniel Hiller ede56e1ae3 ci: Switch to matrix workflow 2023-12-20 23:28:35 +01:00
Maksim Eltyshev b2e0c45539 chore: Update version 2023-12-20 13:49:07 +01:00
Maksim Eltyshev 389648b546 fix: Add missing date formats to Ukrainian translation 2023-12-20 12:58:43 +01:00
Denis Polishchuk f4789c28db feat: Add Ukrainian translation (#571) 2023-12-20 12:39:51 +01:00
ossdate 668092de7d fix: Update Chinese translation (#569) 2023-12-20 12:33:37 +01:00
Yu Inoue 920f9c097d fix: Display year when date is outside of current year (#567) 2023-12-20 12:26:01 +01:00
Maksim Eltyshev da63598de3 chore: Update version 2023-12-11 00:18:14 +01:00
Gilberto Vidal 517cb34cfe fix: Fix eslint rules for Windows (#508) 2023-12-11 00:13:04 +01:00
NavyStack 079b8eba53 fix: Fine-tune Korean and Japanese translations, fix full stops (#561) 2023-12-10 23:47:04 +01:00
Maksim Eltyshev af02cdd8bf docs: Little readme fixes 2023-12-10 22:20:23 +01:00
Maksim Eltyshev e1bc189b88 chore: Bump chart-releaser-action version 2023-12-10 17:55:15 +01:00
Maksim Eltyshev 5f2723623a chore: Update version 2023-12-09 17:23:01 +01:00
Maksim Eltyshev fee6841d0a fix: Attempt to fix error that occurs during reconnection 2023-12-09 17:19:40 +01:00
Maksim Eltyshev ca6fb3b962 fix: Add state parameter to OIDC authorization url
Closes #558
2023-12-09 17:06:45 +01:00
Maksim Eltyshev 28c3f28e01 fix: Add issuer to OIDC callback parameters
Closes #562
2023-12-09 16:28:24 +01:00
Maksim Eltyshev 80d12aaeeb fix: Fix Korean translation linting 2023-12-04 11:42:15 +01:00
NavyStack 89af209a2e fix: Update Korean translation (#560) 2023-12-04 11:40:04 +01:00
Maksim Eltyshev f1fed3e533 build: Stop using base image 2023-12-01 18:02:19 +01:00
Maksim Eltyshev eddeb96b16 fix: Fix Romanian translation linting 2023-11-24 12:43:00 +01:00
Maksim Eltyshev 9e8ff3b579 chore: Revert remark-gfm update 2023-11-23 14:40:44 +01:00
Maksim Eltyshev fc1278011c build: Use pnpm import before installing dependencies 2023-11-23 11:54:28 +01:00
Andrei Covali 24163463fc feat: Add Romanian translation (#555) 2023-11-22 14:58:13 +01:00
Yu Inoue fdef87e3b9 fix: Add support for OIDC configuration thru Helm (#550)
Closes #548
2023-11-20 14:16:19 +01:00
Maksim Eltyshev 964498dbaa chore: Use Node v18 2023-11-18 09:26:31 +01:00
Maksim Eltyshev 5b519cb62f chore: Update version 2023-11-17 22:13:04 +01:00
Yu Inoue 11fc3b4485 fix: Separate service port and container port in Helm (#549) 2023-11-17 22:11:35 +01:00
Maksim Eltyshev 37c0b59f82 chore: Update vips version 2023-11-17 21:48:26 +01:00
Maksim Eltyshev 239611ad51 chore: Update dependencies 2023-11-17 14:34:10 +01:00
Yu Inoue 0575e665fe fix: Fix time format in Japanese locale (#546)
Closes #542
2023-11-13 12:51:00 +01:00
Maksim Eltyshev 019766dced chore: Update version 2023-10-25 23:41:48 +02:00
Maksim Eltyshev 6dc9e4ed99 fix: Disable role change when OIDC roles are not ignored 2023-10-25 23:39:34 +02:00
Balthasar Hofer d4b64b90fc feat: Add ability to ignore roles when logging in with SSO (#534)
Closes #533
2023-10-26 02:01:35 +05:00
Daniel Hiller 1a49826b85 docs: adjust filenames to match the docs 2023-10-24 13:17:49 +02:00
Daniel Hiller 815643eb2c ci: ignore some paths on push 2023-10-22 22:47:39 +02:00
Daniel Hiller bcc4c0d923 chore: update version 2023-10-22 22:31:10 +02:00
Daniel Hiller cf7810b067 fix: add admin user variables 2023-10-22 22:27:25 +02:00
Maksim Eltyshev f1c72df8b6 chore: Update version 2023-10-20 22:38:43 +02:00
Maksim Eltyshev 5056d38848 fix: Fix logging out with invalid token 2023-10-20 22:34:58 +02:00
Maksim Eltyshev 3ef5b3ead8 fix: Make default admin environment variables optional
Closes #526
2023-10-20 21:52:12 +02:00
Vishnu Kaushik ded58dc8b0 fix: Add task counter in card modal (#525)
Closes #467
2023-10-21 00:20:04 +05:00
Maksim Eltyshev d8ee1ecdd1 ci: Do not mark released chart as latest 2023-10-19 23:30:58 +02:00
Maksim Eltyshev fc2f4cd5d3 chore: Update version 2023-10-19 23:08:40 +02:00
Maksim Eltyshev 7ef457157e fix: Fallback to query if there is no fragment 2023-10-19 23:06:37 +02:00
Maksim Eltyshev 23ee815200 chore: Update version 2023-10-19 16:07:52 +02:00
Maksim Eltyshev 40c04c35ff ref: Refactoring 2023-10-19 16:05:34 +02:00
Lorenz Brun 743f2956c8 feat: Improve OIDC SSO (#524)
The OIDC implementation merged in https://github.com/plankanban/planka/pull/491 is flawed for multiple reasons.

It assumes that the access_token returned by the IDP has to be a JWT parseable by the RP which is not the case [1].
Many major IDPs do issue tokens which are not JWTs and RPs should not rely on the contents of these at all.
The only signed token which has a standardized format for direct RP consumption is the OIDC ID token (id_token), but this by default doesn't contain many claims, especially role claims are omitted from them by default for size reasons. To get these additional claims into the ID token, one needs an IDP with support for the "claims" parameter.

It requires manual specification of the JWKS URL which is mandatory in any OIDC discovery document and thus never needs to be manually specified.

It also makes the questionable decision to use a client-side code flow with PKCE where a normal code flow would be much more appropriate as all user data is processed in the backend which can securely hold a client secret (confidential client). This has far wider IDP support, is safer (due to direct involvement of the IDP in obtaining user information) and doesn't require working with ID tokens and claim parameters.

By using a server-side code flow we can also offload most complexity to the server alone, no longer requiring an additional OIDC library on the web client.

Also silent logout doesn't work on most IDPs for security reasons, one needs to actually redirect the user over to the IDP, which then prompts them once more if they actually want to log out.

This implementation should work with any OIDC-compliant IDP and even OAuth 2.0-only IDPs as long as they serve and OIDC discovery document.

[1] rfc-editor.org/rfc/rfc6749#section-5.1
2023-10-19 17:39:21 +05:00
96 changed files with 9536 additions and 25164 deletions
@@ -1,34 +1,107 @@
# https://docs.docker.com/build/ci/github-actions/multi-platform/
name: Build and push Docker DEV image
on:
push:
paths-ignore:
- '.github/**'
- 'charts/**'
- 'docker-*.sh'
- '*.md'
branches: [master]
env:
REGISTRY_IMAGE: ghcr.io/plankanban/planka
jobs:
build-and-push-docker-image-dev:
runs-on: self-hosted
build:
strategy:
fail-fast: false
matrix:
include:
- os: [self-hosted, x64]
platform: linux/amd64
- os: [self-hosted, arm64]
platform: linux/arm64
- os: [self-hosted, arm64]
platform: linux/arm/v7
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
- name: Build and push by digest
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
ghcr.io/plankanban/planka:dev
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v3
with:
name: digests
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: [self-hosted]
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v3
with:
name: digests
path: /tmp/digests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
rerun-failed-jobs:
runs-on: [self-hosted]
needs: [ build, merge]
if: failure()
steps:
- name: Rerun failed jobs in the current workflow
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh run rerun ${{ github.run_id }} --failed
+2 -1
View File
@@ -35,8 +35,9 @@ jobs:
done
- name: Run chart-releaser for stable
uses: helm/chart-releaser-action@v1.5.0
uses: helm/chart-releaser-action@v1.6.0
with:
charts_dir: charts
mark_as_latest: false
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+11 -3
View File
@@ -1,4 +1,8 @@
FROM ghcr.io/plankanban/planka:base-latest as server-dependencies
FROM node:18-alpine as server-dependencies
RUN apk -U upgrade \
&& apk add build-base python3 \
--no-cache
WORKDIR /app
@@ -6,6 +10,7 @@ COPY server/package.json server/package-lock.json .
RUN npm install npm@latest --global \
&& npm install pnpm --global \
&& pnpm import \
&& pnpm install --prod
FROM node:lts AS client
@@ -16,14 +21,17 @@ COPY client/package.json client/package-lock.json .
RUN npm install npm@latest --global \
&& npm install pnpm --global \
&& pnpm import \
&& pnpm install --prod
COPY client .
RUN DISABLE_ESLINT_PLUGIN=true npm run build
FROM ghcr.io/plankanban/planka:base-latest
FROM node:18-alpine
RUN apk del vips-dependencies --purge
RUN apk -U upgrade \
&& apk add bash \
--no-cache
USER node
WORKDIR /app
+12 -17
View File
@@ -1,27 +1,22 @@
FROM node:lts-alpine
FROM node:18-alpine
ARG ALPINE_VERSION=3.16
ARG VIPS_VERSION=8.13.3
ARG VIPS_VERSION=8.14.5
RUN apk -U upgrade \
&& apk add \
bash giflib glib lcms2 libexif \
libgsf libjpeg-turbo libpng librsvg libwebp \
orc pango tiff \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
bash pkgconf \
libjpeg-turbo libexif librsvg cgif tiff libspng libimagequant \
--no-cache \
&& apk add \
build-base giflib-dev glib-dev lcms2-dev libexif-dev \
libgsf-dev libjpeg-turbo-dev libpng-dev librsvg-dev libwebp-dev \
orc-dev pango-dev tiff-dev \
build-base gobject-introspection-dev meson \
libjpeg-turbo-dev libexif-dev librsvg-dev cgif-dev tiff-dev libspng-dev libimagequant-dev \
--virtual vips-dependencies \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp \
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz | tar xJC /tmp \
&& cd /tmp/vips-${VIPS_VERSION} \
&& ./configure \
&& make \
&& make install-strip \
&& meson setup build-dir \
&& cd build-dir \
&& ninja \
&& ninja test \
&& ninja install \
&& rm -rf /tmp/vips-${VIPS_VERSION}
+2 -5
View File
@@ -1,5 +1,5 @@
# Planka
#### Elegant open source project tracking
#### Elegant open source project tracking.
![David (path)](https://img.shields.io/github/package-json/v/plankanban/planka) ![Docker Pulls](https://img.shields.io/docker/pulls/meltyshev/planka) ![GitHub](https://img.shields.io/github/license/plankanban/planka)
@@ -20,10 +20,7 @@
## How to deploy Planka
There are 2 types of installation:
- [Without Docker](https://docs.planka.cloud/docs/installl-planka/Debian%20&%20Ubuntu) ([for Windows](https://docs.planka.cloud/docs/installl-planka/Windows))
- [Dockerized](https://docs.planka.cloud/docs/installl-planka/Docker%20Compose)
- [Automated installation](https://github.com/plankanban/planka-installer)
There are many ways to install Planka, [check them out](https://docs.planka.cloud/docs/intro).
For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration).
+2 -2
View File
@@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.4
version: 0.1.17
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.13.1"
appVersion: "1.15.6"
dependencies:
- alias: postgresql
+23 -1
View File
@@ -21,7 +21,11 @@ git clone https://github.com/Chris-Greaves/planka-helm-chart.git
cd planka-helm-chart
helm dependency build
export SECRETKEY=$(openssl rand -hex 64)
helm install planka . --set secretkey=$SECRETKEY
helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo"
```
> **NOTE:** The command `openssl rand -hex 64` is needed to create a random hexadecimal key for planka. On Windows you can use Git Bash to run that command.
@@ -39,11 +43,19 @@ To access Planka externally you can use the following configuration
```bash
# HTTP only
helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo"
--set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \
# HTTPS
helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo"
--set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \
--set ingress.tls[0].secretName=planka-tls \
@@ -54,6 +66,16 @@ or create a values.yaml file like:
```yaml
secretkey: "<InsertSecretKey>"
# The admin section needs to be present for new instances of Planka, after the first start you can remove the lines starting with admin_. If you want the admin user to be unchangeable admin_email: has to stay
# After changing the config you have to run ```helm upgrade planka . -f values.yaml```
# Admin user
admin_email: "demo@demo.demo" # Do not remove if you want to prevent this user from being edited/deleted
admin_password: "demo"
admin_name: "Demo Demo"
admin_username: "demo"
# Admin user
ingress:
enabled: true
hosts:
+36 -1
View File
@@ -35,7 +35,7 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
containerPort: {{ .Values.service.containerPort | default 1337 }}
protocol: TCP
livenessProbe:
httpGet:
@@ -75,10 +75,45 @@ spec:
value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }}
- name: TRUST_PROXY
value: "0"
- name: DEFAULT_ADMIN_EMAIL
value: {{ .Values.admin_email }}
- name: DEFAULT_ADMIN_PASSWORD
value: {{ .Values.admin_password }}
- name: DEFAULT_ADMIN_NAME
value: {{ .Values.admin_name }}
- name: DEFAULT_ADMIN_USERNAME
value: {{ .Values.admin_username }}
{{ range $k, $v := .Values.env }}
- name: {{ $k | quote }}
value: {{ $v | quote }}
{{- end }}
{{- if .Values.oidc.enabled }}
{{- $secretName := default (printf "%s-oidc" (include "planka.fullname" .)) .Values.oidc.existingSecret }}
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
key: clientId
name: {{ $secretName }}
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
key: clientSecret
name: {{ $secretName }}
- name: OIDC_ISSUER
value: {{ required "issuerUrl is required when configuring OIDC" .Values.oidc.issuerUrl | quote }}
- name: OIDC_SCOPES
value: {{ join " " .Values.oidc.scopes | default "openid profile email" | quote }}
{{- if .Values.oidc.admin.roles }}
- name: OIDC_ADMIN_ROLES
value: {{ join "," .Values.oidc.admin.roles | quote }}
{{- end }}
- name: OIDC_ROLES_ATTRIBUTE
value: {{ .Values.oidc.admin.rolesAttribute | default "groups" | quote }}
{{- if .Values.oidc.admin.ignoreRoles }}
- name: OIDC_IGNORE_ROLES
value: {{ .Values.oidc.admin.ignoreRoles | quote }}
{{- end }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
+17
View File
@@ -0,0 +1,17 @@
{{- if .Values.oidc.enabled }}
{{- if eq (and (not (empty .Values.oidc.clientId)) (not (empty .Values.oidc.clientSecret))) (not (empty .Values.oidc.existingSecret)) -}}
{{- fail "Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret`" -}}
{{- end }}
{{- if (and (and (not (empty .Values.oidc.clientId)) (not (empty .Values.oidc.clientSecret))) (empty .Values.oidc.existingSecret)) -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "planka.fullname" . }}-oidc
labels:
{{- include "planka.labels" . | nindent 4 }}
type: Opaque
data:
clientId: {{ .Values.oidc.clientId | b64enc | quote }}
clientSecret: {{ .Values.oidc.clientSecret | b64enc | quote }}
{{- end }}
{{- end }}
+69
View File
@@ -46,6 +46,10 @@ securityContext: {}
service:
type: ClusterIP
port: 1337
## @param service.containerPort Planka HTTP container port
## If empty will default to 1337
##
containerPort: 1337
ingress:
enabled: false
@@ -109,3 +113,68 @@ persistence:
accessMode: ReadWriteOnce
size: 10Gi
## OpenID Identity Management configuration
##
## Example:
## ---------------
## oidc:
## enabled: true
## clientId: sxxaAIAxVXlCxTmc1YLHBbQr8NL8MqLI2DUbt42d
## clientSecret: om4RTMRVHRszU7bqxB7RZNkHIzA8e4sGYWxeCwIMYQXPwEBWe4SY5a0wwCe9ltB3zrq5f0dnFnp34cEHD7QSMHsKvV9AiV5Z7eqDraMnv0I8IFivmuV5wovAECAYreSI
## issuerUrl: https://auth.local/application/o/planka/
## admin:
## roles:
## - planka-admin
##
## ---------------
## NOTE: A minimal configuration requires setting `clientId`, `clientSecret` and `issuerUrl`. (plus `admin.roles` for administrators)
## ref: https://docs.planka.cloud/docs/Configuration/OIDC
##
oidc:
## @param oidc.enabled Enable single sign-on (SSO) with OpenID Connect (OIDC)
##
enabled: false
## OIDC credentials
## @param oidc.clientId A string unique to the provider that identifies your app.
## @param oidc.clientSecret A secret string that the provider uses to confirm ownership of a client ID.
##
## NOTE: Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret`
##
clientId: ""
clientSecret: ""
## @param oidc.existingSecret Name of an existing secret containing OIDC credentials
## NOTE: Must contain key `clientId` and `clientSecret`
## NOTE: When it's set, the `clientId` and `clientSecret` parameters are ignored
##
existingSecret: ""
## @param oidc.issuerUrl The OpenID connect metadata document endpoint
##
issuerUrl: ""
## @param oidc.scopes A list of scopes required for OIDC client.
## If empty will default to `openid`, `profile` and `email`
## NOTE: Planka needs the email and name claims
##
scopes: []
## Admin permissions configuration
admin:
## @param oidc.admin.ignoreRoles If set to true, the admin roles will be ignored.
## It is useful if you want to use OIDC for authentication but not for authorization.
## If empty will default to `false`
##
ignoreRoles: false
## @param oidc.admin.rolesAttribute The name of a custom group claim that you have configured in your OIDC provider
## If empty will default to `groups`
##
rolesAttribute: groups
## @param oidc.admin.roles The names of the admin groups
##
roles: []
# - planka-admin
+1 -1
View File
@@ -1 +1 @@
REACT_APP_VERSION=1.13.1
REACT_APP_VERSION=1.15.6
+3379 -21979
View File
File diff suppressed because it is too large Load Diff
+36 -30
View File
@@ -49,66 +49,72 @@
"**/*.test.js"
]
}
],
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"dependencies": {
"@juggle/resize-observer": "^3.4.0",
"classnames": "^2.3.2",
"date-fns": "^2.29.3",
"date-fns": "^2.30.0",
"dequal": "^2.0.3",
"easymde": "^2.18.0",
"history": "^5.3.0",
"i18next": "^22.0.6",
"i18next-browser-languagedetector": "^7.0.1",
"i18next": "^23.7.6",
"i18next-browser-languagedetector": "^7.2.0",
"initials": "^3.1.2",
"js-cookie": "^3.0.1",
"jwt-decode": "^3.1.2",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"node-sass": "^8.0.0",
"oidc-client-ts": "^2.3.0",
"photoswipe": "^5.3.3",
"nanoid": "^5.0.3",
"node-sass": "^9.0.0",
"photoswipe": "^5.4.2",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-app-rewired": "^2.2.1",
"react-beautiful-dnd": "^13.1.1",
"react-datepicker": "^4.8.0",
"react-datepicker": "^4.21.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-i18next": "^12.0.0",
"react-i18next": "^13.5.0",
"react-input-mask": "^2.0.4",
"react-markdown": "^8.0.3",
"react-photoswipe-gallery": "^2.2.2",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.3",
"react-markdown": "^8.0.7",
"react-photoswipe-gallery": "^2.2.7",
"react-redux": "^8.1.3",
"react-router-dom": "^6.19.0",
"react-scripts": "5.0.1",
"react-simplemde-editor": "^5.2.0",
"react-textarea-autosize": "^8.4.0",
"redux": "^4.2.0",
"react-textarea-autosize": "^8.5.3",
"redux": "^4.2.1",
"redux-logger": "^3.0.6",
"redux-orm": "^0.16.2",
"redux-saga": "^1.2.1",
"remark-breaks": "^3.0.2",
"redux-saga": "^1.2.3",
"remark-breaks": "^4.0.0",
"remark-gfm": "^3.0.1",
"reselect": "^4.1.7",
"reselect": "^4.1.8",
"sails.io.js": "^1.2.1",
"semantic-ui-react": "^2.1.3",
"semantic-ui-react": "^2.1.4",
"socket.io-client": "^2.5.0",
"validator": "^13.7.0",
"whatwg-fetch": "^3.6.2",
"validator": "^13.11.0",
"whatwg-fetch": "^3.6.19",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.0",
"@testing-library/user-event": "^14.5.1",
"babel-preset-airbnb": "^5.0.0",
"chai": "^4.3.7",
"eslint": "^8.28.0",
"chai": "^4.3.10",
"eslint": "^8.53.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"react-test-renderer": "^18.2.0"
}
+8 -2
View File
@@ -1,7 +1,6 @@
import ActionTypes from '../constants/ActionTypes';
const initializeCore = (
config,
user,
board,
users,
@@ -21,7 +20,6 @@ const initializeCore = (
) => ({
type: ActionTypes.CORE_INITIALIZE,
payload: {
config,
user,
board,
users,
@@ -41,6 +39,14 @@ const initializeCore = (
},
});
// TODO: with success?
initializeCore.fetchConfig = (config) => ({
type: ActionTypes.CORE_INITIALIZE__CONFIG_FETCH,
payload: {
config,
},
});
const logout = () => ({
type: ActionTypes.LOGOUT,
payload: {},
+7 -7
View File
@@ -28,20 +28,20 @@ authenticate.failure = (error) => ({
},
});
const authenticateWithOidc = () => ({
type: ActionTypes.WITH_OIDC_AUTHENTICATE,
const authenticateUsingOidc = () => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE,
payload: {},
});
authenticateWithOidc.success = (accessToken) => ({
type: ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS,
authenticateUsingOidc.success = (accessToken) => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS,
payload: {
accessToken,
},
});
authenticateWithOidc.failure = (error) => ({
type: ActionTypes.WITH_OIDC_AUTHENTICATE__FAILURE,
authenticateUsingOidc.failure = (error) => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE__FAILURE,
payload: {
error,
},
@@ -55,6 +55,6 @@ const clearAuthenticateError = () => ({
export default {
initializeLogin,
authenticate,
authenticateWithOidc,
authenticateUsingOidc,
clearAuthenticateError,
};
+3 -3
View File
@@ -5,14 +5,14 @@ import socket from './socket';
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
const exchangeToAccessToken = (data, headers) =>
http.post('/access-tokens/exchange', data, headers);
const exchangeForAccessTokenUsingOidc = (data, headers) =>
http.post('/access-tokens/exchange-using-oidc', data, headers);
const deleteCurrentAccessToken = (headers) =>
socket.delete('/access-tokens/me', undefined, headers);
export default {
createAccessToken,
exchangeToAccessToken,
exchangeForAccessTokenUsingOidc,
deleteCurrentAccessToken,
};
@@ -4,6 +4,7 @@ import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next';
import { Comment } from 'semantic-ui-react';
import getDateFormat from '../../../utils/get-date-format';
import { ActivityTypes } from '../../../constants/Enums';
import ItemComment from './ItemComment';
import User from '../../User';
@@ -66,7 +67,7 @@ const Item = React.memo(({ type, data, createdAt, user }) => {
<div className={classNames(styles.content)}>
<div>{contentNode}</div>
<span className={styles.date}>
{t('format:longDateTime', {
{t(`format:${getDateFormat(createdAt)}`, {
postProcess: 'formatDate',
value: createdAt,
})}
@@ -6,6 +6,7 @@ import { Comment } from 'semantic-ui-react';
import { usePopup } from '../../../lib/popup';
import { Markdown } from '../../../lib/custom-ui';
import getDateFormat from '../../../utils/get-date-format';
import CommentEdit from './CommentEdit';
import User from '../../User';
import DeleteStep from '../../DeleteStep';
@@ -33,7 +34,7 @@ const ItemComment = React.memo(
<div className={styles.title}>
<span className={styles.author}>{user.name}</span>
<span className={styles.date}>
{t('format:longDateTime', {
{t(`format:${getDateFormat(createdAt)}`, {
postProcess: 'formatDate',
value: createdAt,
})}
@@ -217,6 +217,7 @@ const CardModal = React.memo(
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
@@ -266,6 +267,7 @@ const CardModal = React.memo(
onMove={onLabelMove}
onDelete={onLabelDelete}
>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
@@ -314,10 +316,11 @@ const CardModal = React.memo(
)}
</span>
{canEdit && (
// eslint-disable-next-line jsx-a11y/control-has-associated-label
<button
onClick={handleToggleStopwatchClick}
type="button"
className={classNames(styles.attachment, styles.dueDate)}
onClick={handleToggleStopwatchClick}
>
<Icon
name={stopwatch.startedAt ? 'pause' : 'play'}
@@ -48,14 +48,21 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete
return (
<>
{items.length > 0 && (
<Progress
autoSuccess
value={completedItems.length}
total={items.length}
color="blue"
size="tiny"
className={styles.progress}
/>
<>
<span className={styles.progressWrapper}>
<Progress
autoSuccess
value={completedItems.length}
total={items.length}
color="blue"
size="tiny"
className={styles.progress}
/>
</span>
<span className={styles.count}>
{completedItems.length}/{items.length}
</span>
</>
)}
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="tasks" type={DroppableTypes.TASK}>
@@ -3,6 +3,23 @@
margin: 0 0 16px;
}
.progressWrapper {
display: inline-block;
padding: 3px 0;
vertical-align: top;
width: calc(100% - 50px);
}
.count {
color: #8c8c8c;
display: inline-block;
font-size: 14px;
line-height: 14px;
text-align: right;
vertical-align: top;
width: 50px;
}
.taskButton {
background: transparent;
border: none;
+16 -2
View File
@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import getDateFormat from '../../utils/get-date-format';
import styles from './DueDate.module.scss';
const SIZES = {
@@ -12,15 +14,27 @@ const SIZES = {
MEDIUM: 'medium',
};
const FORMATS = {
const LONG_DATE_FORMAT_BY_SIZE = {
tiny: 'longDate',
small: 'longDate',
medium: 'longDateTime',
};
const FULL_DATE_FORMAT_BY_SIZE = {
tiny: 'fullDate',
small: 'fullDate',
medium: 'fullDateTime',
};
const DueDate = React.memo(({ value, size, isDisabled, onClick }) => {
const [t] = useTranslation();
const dateFormat = getDateFormat(
value,
LONG_DATE_FORMAT_BY_SIZE[size],
FULL_DATE_FORMAT_BY_SIZE[size],
);
const contentNode = (
<span
className={classNames(
@@ -29,7 +43,7 @@ const DueDate = React.memo(({ value, size, isDisabled, onClick }) => {
onClick && styles.wrapperHoverable,
)}
>
{t(`format:${FORMATS[size]}`, {
{t(`format:${dateFormat}`, {
value,
postProcess: 'formatDate',
})}
+57 -47
View File
@@ -65,11 +65,12 @@ const Login = React.memo(
({
defaultData,
isSubmitting,
isSubmittingWithOidc,
isSubmittingUsingOidc,
error,
withOidc,
isOidcEnforced,
onAuthenticate,
onAuthenticateWithOidc,
onAuthenticateUsingOidc,
onMessageDismiss,
}) => {
const [t] = useTranslation();
@@ -107,8 +108,10 @@ const Login = React.memo(
}, [onAuthenticate, data]);
useEffect(() => {
emailOrUsernameField.current.focus();
}, []);
if (!isOidcEnforced) {
emailOrUsernameField.current.focus();
}
}, [isOidcEnforced]);
useEffect(() => {
if (wasSubmitting && !isSubmitting && error) {
@@ -159,51 +162,57 @@ const Login = React.memo(
onDismiss={onMessageDismiss}
/>
)}
<Form size="large" onSubmit={handleSubmit}>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.emailOrUsername')}</div>
<Input
fluid
ref={emailOrUsernameField}
name="emailOrUsername"
value={data.emailOrUsername}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
{!isOidcEnforced && (
<Form size="large" onSubmit={handleSubmit}>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.emailOrUsername')}</div>
<Input
fluid
ref={emailOrUsernameField}
name="emailOrUsername"
value={data.emailOrUsername}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
/>
</div>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.password')}</div>
<Input.Password
fluid
ref={passwordField}
name="password"
value={data.password}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
/>
</div>
<Form.Button
primary
size="large"
icon="right arrow"
labelPosition="right"
content={t('action.logIn')}
floated="right"
loading={isSubmitting}
disabled={isSubmitting || isSubmittingUsingOidc}
/>
</div>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.password')}</div>
<Input.Password
fluid
ref={passwordField}
name="password"
value={data.password}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
/>
</div>
<Form.Button
primary
size="large"
icon="right arrow"
labelPosition="right"
content={t('action.logIn')}
floated="right"
loading={isSubmitting}
disabled={isSubmitting || isSubmittingWithOidc}
/>
</Form>
</Form>
)}
{withOidc && (
<Button
type="button"
loading={isSubmittingWithOidc}
disabled={isSubmitting || isSubmittingWithOidc}
onClick={onAuthenticateWithOidc}
>
{t('action.logInWithSSO')}
</Button>
fluid={isOidcEnforced}
primary={isOidcEnforced}
size={isOidcEnforced ? 'large' : undefined}
icon={isOidcEnforced ? 'right arrow' : undefined}
labelPosition={isOidcEnforced ? 'right' : undefined}
content={t('action.logInWithSSO')}
loading={isSubmittingUsingOidc}
disabled={isSubmitting || isSubmittingUsingOidc}
onClick={onAuthenticateUsingOidc}
/>
)}
</div>
</div>
@@ -239,11 +248,12 @@ Login.propTypes = {
defaultData: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
isSubmitting: PropTypes.bool.isRequired,
isSubmittingWithOidc: PropTypes.bool.isRequired,
isSubmittingUsingOidc: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
withOidc: PropTypes.bool.isRequired,
isOidcEnforced: PropTypes.bool.isRequired,
onAuthenticate: PropTypes.func.isRequired,
onAuthenticateWithOidc: PropTypes.func.isRequired,
onAuthenticateUsingOidc: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
};
@@ -24,6 +24,7 @@ const AccountPane = React.memo(
organization,
language,
isLocked,
isUsernameLocked,
isAvatarUpdating,
usernameUpdateForm,
emailUpdateForm,
@@ -104,7 +105,7 @@ const AccountPane = React.memo(
value={language || 'auto'}
onChange={handleLanguageChange}
/>
{!isLocked && (
{(!isLocked || !isUsernameLocked) && (
<>
<Divider horizontal section>
<Header as="h4">
@@ -113,56 +114,62 @@ const AccountPane = React.memo(
})}
</Header>
</Divider>
<div className={styles.action}>
<UserUsernameEditPopup
usePasswordConfirmation
defaultData={usernameUpdateForm.data}
username={username}
isSubmitting={usernameUpdateForm.isSubmitting}
error={usernameUpdateForm.error}
onUpdate={onUsernameUpdate}
onMessageDismiss={onUsernameUpdateMessageDismiss}
>
<Button className={styles.actionButton}>
{t('action.editUsername', {
context: 'title',
})}
</Button>
</UserUsernameEditPopup>
</div>
<div className={styles.action}>
<UserEmailEditPopup
usePasswordConfirmation
defaultData={emailUpdateForm.data}
email={email}
isSubmitting={emailUpdateForm.isSubmitting}
error={emailUpdateForm.error}
onUpdate={onEmailUpdate}
onMessageDismiss={onEmailUpdateMessageDismiss}
>
<Button className={styles.actionButton}>
{t('action.editEmail', {
context: 'title',
})}
</Button>
</UserEmailEditPopup>
</div>
<div className={styles.action}>
<UserPasswordEditPopup
usePasswordConfirmation
defaultData={passwordUpdateForm.data}
isSubmitting={passwordUpdateForm.isSubmitting}
error={passwordUpdateForm.error}
onUpdate={onPasswordUpdate}
onMessageDismiss={onPasswordUpdateMessageDismiss}
>
<Button className={styles.actionButton}>
{t('action.editPassword', {
context: 'title',
})}
</Button>
</UserPasswordEditPopup>
</div>
{!isUsernameLocked && (
<div className={styles.action}>
<UserUsernameEditPopup
defaultData={usernameUpdateForm.data}
username={username}
isSubmitting={usernameUpdateForm.isSubmitting}
error={usernameUpdateForm.error}
usePasswordConfirmation={!isLocked} // FIXME: hack
onUpdate={onUsernameUpdate}
onMessageDismiss={onUsernameUpdateMessageDismiss}
>
<Button className={styles.actionButton}>
{t('action.editUsername', {
context: 'title',
})}
</Button>
</UserUsernameEditPopup>
</div>
)}
{!isLocked && (
<>
<div className={styles.action}>
<UserEmailEditPopup
usePasswordConfirmation
defaultData={emailUpdateForm.data}
email={email}
isSubmitting={emailUpdateForm.isSubmitting}
error={emailUpdateForm.error}
onUpdate={onEmailUpdate}
onMessageDismiss={onEmailUpdateMessageDismiss}
>
<Button className={styles.actionButton}>
{t('action.editEmail', {
context: 'title',
})}
</Button>
</UserEmailEditPopup>
</div>
<div className={styles.action}>
<UserPasswordEditPopup
usePasswordConfirmation
defaultData={passwordUpdateForm.data}
isSubmitting={passwordUpdateForm.isSubmitting}
error={passwordUpdateForm.error}
onUpdate={onPasswordUpdate}
onMessageDismiss={onPasswordUpdateMessageDismiss}
>
<Button className={styles.actionButton}>
{t('action.editPassword', {
context: 'title',
})}
</Button>
</UserPasswordEditPopup>
</div>
</>
)}
</>
)}
</Tab.Pane>
@@ -179,6 +186,7 @@ AccountPane.propTypes = {
organization: PropTypes.string,
language: PropTypes.string,
isLocked: PropTypes.bool.isRequired,
isUsernameLocked: PropTypes.bool.isRequired,
isAvatarUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
usernameUpdateForm: PropTypes.object.isRequired,
@@ -18,6 +18,7 @@ const UserSettingsModal = React.memo(
language,
subscribeToOwnCards,
isLocked,
isUsernameLocked,
isAvatarUpdating,
usernameUpdateForm,
emailUpdateForm,
@@ -50,6 +51,7 @@ const UserSettingsModal = React.memo(
organization={organization}
language={language}
isLocked={isLocked}
isUsernameLocked={isUsernameLocked}
isAvatarUpdating={isAvatarUpdating}
usernameUpdateForm={usernameUpdateForm}
emailUpdateForm={emailUpdateForm}
@@ -108,6 +110,7 @@ UserSettingsModal.propTypes = {
language: PropTypes.string,
subscribeToOwnCards: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
isUsernameLocked: PropTypes.bool.isRequired,
isAvatarUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
usernameUpdateForm: PropTypes.object.isRequired,
@@ -136,13 +136,15 @@ const ActionsStep = React.memo(
context: 'title',
})}
</Menu.Item>
{!user.isUsernameLocked && (
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
{t('action.editUsername', {
context: 'title',
})}
</Menu.Item>
)}
{!user.isLocked && (
<>
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
{t('action.editUsername', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}>
{t('action.editEmail', {
context: 'title',
@@ -155,7 +157,7 @@ const ActionsStep = React.memo(
</Menu.Item>
</>
)}
{!user.isLockedAdmin && (
{!user.isDeletionLocked && (
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteUser', {
context: 'title',
@@ -18,7 +18,9 @@ const Item = React.memo(
phone,
isAdmin,
isLocked,
isLockedAdmin,
isRoleLocked,
isUsernameLocked,
isDeletionLocked,
emailUpdateForm,
passwordUpdateForm,
usernameUpdateForm,
@@ -48,7 +50,7 @@ const Item = React.memo(
<Table.Cell>{username || '-'}</Table.Cell>
<Table.Cell>{email}</Table.Cell>
<Table.Cell>
<Radio toggle checked={isAdmin} disabled={isLockedAdmin} onChange={handleIsAdminChange} />
<Radio toggle checked={isAdmin} disabled={isRoleLocked} onChange={handleIsAdminChange} />
</Table.Cell>
<Table.Cell textAlign="right">
<ActionsPopup
@@ -60,7 +62,8 @@ const Item = React.memo(
phone,
isAdmin,
isLocked,
isLockedAdmin,
isUsernameLocked,
isDeletionLocked,
emailUpdateForm,
passwordUpdateForm,
usernameUpdateForm,
@@ -93,7 +96,9 @@ Item.propTypes = {
phone: PropTypes.string,
isAdmin: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
isLockedAdmin: PropTypes.bool.isRequired,
isRoleLocked: PropTypes.bool.isRequired,
isUsernameLocked: PropTypes.bool.isRequired,
isDeletionLocked: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
emailUpdateForm: PropTypes.object.isRequired,
passwordUpdateForm: PropTypes.object.isRequired,
@@ -10,6 +10,7 @@ import Item from './Item';
const UsersModal = React.memo(
({
items,
canAdd,
onUpdate,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
@@ -111,7 +112,9 @@ const UsersModal = React.memo(
phone={item.phone}
isAdmin={item.isAdmin}
isLocked={item.isLocked}
isLockedAdmin={item.isLockedAdmin}
isRoleLocked={item.isRoleLocked}
isUsernameLocked={item.isUsernameLocked}
isDeletionLocked={item.isDeletionLocked}
emailUpdateForm={item.emailUpdateForm}
passwordUpdateForm={item.passwordUpdateForm}
usernameUpdateForm={item.usernameUpdateForm}
@@ -128,11 +131,13 @@ const UsersModal = React.memo(
</Table.Body>
</Table>
</Modal.Content>
<Modal.Actions>
<UserAddPopupContainer>
<Button positive content={t('action.addUser')} />
</UserAddPopupContainer>
</Modal.Actions>
{canAdd && (
<Modal.Actions>
<UserAddPopupContainer>
<Button positive content={t('action.addUser')} />
</UserAddPopupContainer>
</Modal.Actions>
)}
</Modal>
);
},
@@ -140,6 +145,7 @@ const UsersModal = React.memo(
UsersModal.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canAdd: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
+4 -3
View File
@@ -16,14 +16,15 @@ export default {
AUTHENTICATE: 'AUTHENTICATE',
AUTHENTICATE__SUCCESS: 'AUTHENTICATE__SUCCESS',
AUTHENTICATE__FAILURE: 'AUTHENTICATE__FAILURE',
WITH_OIDC_AUTHENTICATE: 'WITH_OIDC_AUTHENTICATE',
WITH_OIDC_AUTHENTICATE__SUCCESS: 'WITH_OIDC_AUTHENTICATE__SUCCESS',
WITH_OIDC_AUTHENTICATE__FAILURE: 'WITH_OIDC_AUTHENTICATE__FAILURE',
USING_OIDC_AUTHENTICATE: 'USING_OIDC_AUTHENTICATE',
USING_OIDC_AUTHENTICATE__SUCCESS: 'USING_OIDC_AUTHENTICATE__SUCCESS',
USING_OIDC_AUTHENTICATE__FAILURE: 'USING_OIDC_AUTHENTICATE__FAILURE',
AUTHENTICATE_ERROR_CLEAR: 'AUTHENTICATE_ERROR_CLEAR',
/* Core */
CORE_INITIALIZE: 'CORE_INITIALIZE',
CORE_INITIALIZE__CONFIG_FETCH: 'CORE_INITIALIZE__CONFIG_FETCH',
LOGOUT: 'LOGOUT',
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
+1 -1
View File
@@ -11,7 +11,7 @@ export default {
/* Login */
AUTHENTICATE: `${PREFIX}/AUTHENTICATE`,
WITH_OIDC_AUTHENTICATE: `${PREFIX}/WITH_OIDC_AUTHENTICATE`,
USING_OIDC_AUTHENTICATE: `${PREFIX}/USING_OIDC_AUTHENTICATE`,
AUTHENTICATE_ERROR_CLEAR: `${PREFIX}/AUTHENTICATE_ERROR_CLEAR`,
/* Core */
+4 -3
View File
@@ -10,16 +10,17 @@ const mapStateToProps = (state) => {
const {
ui: {
authenticateForm: { data: defaultData, isSubmitting, isSubmittingWithOidc, error },
authenticateForm: { data: defaultData, isSubmitting, isSubmittingUsingOidc, error },
},
} = state;
return {
defaultData,
isSubmitting,
isSubmittingWithOidc,
isSubmittingUsingOidc,
error,
withOidc: !!oidcConfig,
isOidcEnforced: oidcConfig && oidcConfig.isEnforced,
};
};
@@ -27,7 +28,7 @@ const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
onAuthenticate: entryActions.authenticate,
onAuthenticateWithOidc: entryActions.authenticateWithOidc,
onAuthenticateUsingOidc: entryActions.authenticateUsingOidc,
onMessageDismiss: entryActions.clearAuthenticateError,
},
dispatch,
@@ -16,6 +16,7 @@ const mapStateToProps = (state) => {
language,
subscribeToOwnCards,
isLocked,
isUsernameLocked,
isAvatarUpdating,
emailUpdateForm,
passwordUpdateForm,
@@ -32,6 +33,7 @@ const mapStateToProps = (state) => {
language,
subscribeToOwnCards,
isLocked,
isUsernameLocked,
isAvatarUpdating,
emailUpdateForm,
passwordUpdateForm,
@@ -6,10 +6,12 @@ import entryActions from '../entry-actions';
import UsersModal from '../components/UsersModal';
const mapStateToProps = (state) => {
const oidcConfig = selectors.selectOidcConfig(state);
const users = selectors.selectUsersExceptCurrent(state);
return {
items: users,
canAdd: !oidcConfig || !oidcConfig.isEnforced,
};
};
+3 -3
View File
@@ -7,8 +7,8 @@ const authenticate = (data) => ({
},
});
const authenticateWithOidc = () => ({
type: EntryActionTypes.WITH_OIDC_AUTHENTICATE,
const authenticateUsingOidc = () => ({
type: EntryActionTypes.USING_OIDC_AUTHENTICATE,
payload: {},
});
@@ -19,6 +19,6 @@ const clearAuthenticateError = () => ({
export default {
authenticate,
authenticateWithOidc,
authenticateUsingOidc,
clearAuthenticateError,
};
+9 -7
View File
@@ -9,6 +9,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'v' p",
fullDate: 'd MMM, y',
fullDateTime: "d MMMM, y 'v' p",
},
translation: {
@@ -24,7 +26,7 @@ export default {
administrator: 'Administrátor',
all: 'Vše',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Všechny změny budou automaticky uloženy<br />po obnově spojení',
'Všechny změny budou automaticky uloženy<br />po obnově spojení.',
areYouSureYouWantToDeleteThisAttachment: 'Opravdu chcete smazat tuto přílohu?',
areYouSureYouWantToDeleteThisBoard: 'Opravdu chcete smazat tuto tabuli?',
areYouSureYouWantToDeleteThisCard: 'Opravdu chcete smazat tuto kartu?',
@@ -47,15 +49,15 @@ export default {
board: 'Tabule',
boardNotFound_title: 'Tabule nenalezena',
canComment: 'Může komentovat',
canEditContentOfBoard: 'Může upravovat obsah tabule',
canOnlyViewBoard: 'Může zobrazit pouze tabuli',
canEditContentOfBoard: 'Může upravovat obsah tabule.',
canOnlyViewBoard: 'Může zobrazit pouze tabuli.',
cardActions_title: 'Akce na kartě',
cardNotFound_title: 'Karta nenalezena',
cardOrActionAreDeleted: 'Karta nebo akce je smazána',
cardOrActionAreDeleted: 'Karta nebo akce je smazána.',
color: 'Barva',
createBoard_title: 'Vytvořit tabuli',
createLabel_title: 'Vytvořit štítek',
createNewOneOrSelectExistingOne: 'Vytvořit nový nebo vyberte<br />již existující',
createNewOneOrSelectExistingOne: 'Vytvořit nový nebo vyberte<br />již existující.',
createProject_title: 'Vytvořit projekt',
createTextFile_title: 'Vytvořit textový soubor',
currentPassword: 'Aktuální heslo',
@@ -128,7 +130,7 @@ export default {
phone: 'Telefon',
preferences: 'Volby',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky',
'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky.',
project: 'Projekt',
projectNotFound_title: 'Projekt nenalezen',
removeManager_title: 'Odstranit vedoucího',
@@ -146,7 +148,7 @@ export default {
subscribeToMyOwnCardsByDefault: 'Ve výchozím nastavení odebírat vlastní karty',
taskActions_title: 'Akce na úkolu',
tasks: 'Úkoly',
thereIsNoPreviewAvailableForThisAttachment: 'Pro tuto přílohu není k dispozici žádný náhled',
thereIsNoPreviewAvailableForThisAttachment: 'Pro tuto přílohu není k dispozici žádný náhled.',
time: 'Čas',
title: 'Titulek',
userActions_title: 'Akce na uživateli',
+7 -5
View File
@@ -9,6 +9,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'at' p",
fullDate: 'MMM d, y',
fullDateTime: "MMMM d, y 'a' p",
},
translation: {
@@ -23,7 +25,7 @@ export default {
administrator: 'Administrator',
all: 'Alle',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Alle ændringer vil automatisk blive gemt<br />ved genforbindelse',
'Alle ændringer vil automatisk blive gemt<br />ved genforbindelse.',
areYouSureYouWantToDeleteThisAttachment: 'Vil du slette den vedhæftede fil?',
areYouSureYouWantToDeleteThisBoard: 'Vil du slette dette bræt?',
areYouSureYouWantToDeleteThisCard: 'Vil du slette dette kort?',
@@ -46,11 +48,11 @@ export default {
boardNotFound_title: 'Bræt ikke fundet',
cardActions_title: 'Korthandlinger',
cardNotFound_title: 'Kort ikke fundet',
cardOrActionAreDeleted: 'Kort eller handling er slettet',
cardOrActionAreDeleted: 'Kort eller handling er slettet.',
color: 'Farve',
createBoard_title: 'Opret bræt',
createLabel_title: 'Opret mærkat',
createNewOneOrSelectExistingOne: 'Lav en ny eller marker<br />en eksisterende',
createNewOneOrSelectExistingOne: 'Lav en ny eller marker<br />en eksisterende.',
createProject_title: 'Opret projekt',
createTextFile_title: 'Opret tekstfil',
currentPassword: 'Nuværende løsen',
@@ -109,14 +111,14 @@ export default {
noLists: 'Ingen lister',
noProjects: 'Ingen projekter',
notifications: 'Notifikationer',
noUnreadNotifications: 'Ingen ulæste notifikationer',
noUnreadNotifications: 'Ingen ulæste notifikationer.',
openBoard_title: 'Åben bræt',
optional_inline: 'valgfri',
organization: 'Organisation',
phone: 'Telefon',
preferences: 'Præferencer',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: Tryk Ctrl-V (Cmd-V på Mac) for at vedhæfte direkte fra udklipsholder',
'Tip: Tryk Ctrl-V (Cmd-V på Mac) for at vedhæfte direkte fra udklipsholder.',
project: 'Projekt',
projectNotFound_title: 'Projekt ej fundet',
removeManager_title: 'Fjern projektleder',
+5 -3
View File
@@ -9,6 +9,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd. MMM',
longDateTime: "d. MMMM 'um' p",
fullDate: 'd. MMM. y',
fullDateTime: "d. MMMM. y 'um' p",
},
translation: {
@@ -51,7 +53,7 @@ export default {
boardNotFound_title: 'Board nicht gefunden',
cardActions_title: 'Kartenaktionen',
cardNotFound_title: 'Karte nicht gefunden',
cardOrActionAreDeleted: 'Karte oder Aktion wurden gelöscht',
cardOrActionAreDeleted: 'Karte oder Aktion wurden gelöscht.',
color: 'Farbe',
createBoard_title: 'Board erstellen',
createLabel_title: 'Label erstellen',
@@ -117,14 +119,14 @@ export default {
noLists: 'Keine Listen',
noProjects: 'Keine Projekte',
notifications: 'Benachrichtigungen',
noUnreadNotifications: 'Keine ungelesenen Benachrichtigungen',
noUnreadNotifications: 'Keine ungelesenen Benachrichtigungen.',
openBoard_title: 'Board öffnen',
optional_inline: 'Optional',
organization: 'Organisation',
phone: 'Telefon',
preferences: 'Voreinstellungen',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen',
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen.',
project: 'Projekt',
projectNotFound_title: 'Projekt nicht gefunden',
removeManager_title: 'Manager entfernen',
+11 -9
View File
@@ -5,6 +5,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'at' p",
fullDate: 'MMM d, y',
fullDateTime: "MMMM d, y 'at' p",
},
translation: {
@@ -20,7 +22,7 @@ export default {
administrator: 'Administrator',
all: 'All',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'All changes will be automatically saved<br />after connection restored',
'All changes will be automatically saved<br />after connection restored.',
areYouSureYouWantToDeleteThisAttachment: 'Are you sure you want to delete this attachment?',
areYouSureYouWantToDeleteThisBoard: 'Are you sure you want to delete this board?',
areYouSureYouWantToDeleteThisCard: 'Are you sure you want to delete this card?',
@@ -43,15 +45,15 @@ export default {
board: 'Board',
boardNotFound_title: 'Board Not Found',
canComment: 'Can comment',
canEditContentOfBoard: 'Can edit the content of the board',
canOnlyViewBoard: 'Can only view the board',
canEditContentOfBoard: 'Can edit the content of the board.',
canOnlyViewBoard: 'Can only view the board.',
cardActions_title: 'Card Actions',
cardNotFound_title: 'Card Not Found',
cardOrActionAreDeleted: 'Card or action are deleted',
cardOrActionAreDeleted: 'Card or action are deleted.',
color: 'Color',
createBoard_title: 'Create Board',
createLabel_title: 'Create Label',
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one',
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one.',
createProject_title: 'Create Project',
createTextFile_title: 'Create Text File',
currentPassword: 'Current password',
@@ -84,7 +86,7 @@ export default {
editUsername_title: 'Edit Username',
email: 'E-mail',
emailAlreadyInUse: 'E-mail already in use',
enterCardTitle: 'Enter card title... [Ctrl+Enter] to auto-open',
enterCardTitle: 'Enter card title... [Ctrl+Enter] to auto-open.',
enterDescription: 'Enter description...',
enterFilename: 'Enter filename',
enterListTitle: 'Enter list title...',
@@ -117,14 +119,14 @@ export default {
noLists: 'No lists',
noProjects: 'No projects',
notifications: 'Notifications',
noUnreadNotifications: 'No unread notifications',
noUnreadNotifications: 'No unread notifications.',
openBoard_title: 'Open Board',
optional_inline: 'optional',
organization: 'Organization',
phone: 'Phone',
preferences: 'Preferences',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard',
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard.',
project: 'Project',
projectNotFound_title: 'Project Not Found',
removeManager_title: 'Remove Manager',
@@ -143,7 +145,7 @@ export default {
taskActions_title: 'Task Actions',
tasks: 'Tasks',
thereIsNoPreviewAvailableForThisAttachment:
'There is no preview available for this attachment',
'There is no preview available for this attachment.',
time: 'Time',
title: 'Title',
userActions_title: 'User Actions',
+6 -4
View File
@@ -9,6 +9,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'a' p",
fullDate: 'MMM d, y',
fullDateTime: "MMMM d, y 'a' p",
},
translation: {
@@ -22,7 +24,7 @@ export default {
administrator: 'Administrador',
all: 'Todos',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Todos los cambios serán automáticamente guardados<br />después de que se restaure la conexión',
'Todos los cambios serán automáticamente guardados<br />después de que se restaure la conexión.',
areYouSureYouWantToDeleteThisAttachment: '¿Estás seguro de que quieres borrar este adjunto?',
areYouSureYouWantToDeleteThisBoard: '¿Estás seguro de que quieres borrar este tablero?',
areYouSureYouWantToDeleteThisCard: '¿Estás seguro de que quieres borrar esta tarjeta?',
@@ -41,11 +43,11 @@ export default {
boardNotFound_title: 'Tablero no encontrado',
cardActions_title: 'Acciones de las tarjetas',
cardNotFound_title: 'tarjeta no encontrada',
cardOrActionAreDeleted: 'La tarjeta o la acción está eliminada',
cardOrActionAreDeleted: 'La tarjeta o la acción está eliminada.',
color: 'Color',
createBoard_title: 'Crear Tablero',
createLabel_title: 'Crear Etiqueta',
createNewOneOrSelectExistingOne: 'Crear uno nuevo o seleccionar<br /> uno existente',
createNewOneOrSelectExistingOne: 'Crear uno nuevo o seleccionar<br />uno existente.',
createProject_title: 'Crear Proyecto',
createTextFile_title: 'Crear archivo de Texto',
currentPassword: 'Contraseña Actual',
@@ -106,7 +108,7 @@ export default {
phone: 'Teléfono',
preferences: 'Preferencias',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles',
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles.',
project: 'Proyecto',
projectNotFound_title: 'Proyecto no encontrado',
removeMember_title: 'Remover Miembro',
+10 -8
View File
@@ -9,6 +9,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'à' p",
fullDate: 'd MMM y',
fullDateTime: "d MMMM y 'à' p",
},
translation: {
@@ -22,7 +24,7 @@ export default {
administrator: 'Administrateur',
all: 'Tout',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Toutes les modifications seront automatiquement enregistrées <br /> une fois la connexion rétablie',
'Toutes les modifications seront automatiquement enregistrées<br />une fois la connexion rétablie.',
areYouSureYouWantToDeleteThisAttachment: 'Voulez-vous vraiment supprimer cette pièce jointe?',
areYouSureYouWantToDeleteThisBoard: 'Êtes-vous sûr de vouloir supprimer ce forum?',
areYouSureYouWantToDeleteThisCard: 'Voulez-vous vraiment supprimer cette carte?',
@@ -41,11 +43,11 @@ export default {
boardNotFound_title: 'Carte non trouvée',
cardActions_title: 'Actions sur la carte',
cardNotFound_title: 'Carte non trouvée',
cardOrActionAreDeleted: "La carte ou l'action sont supprimées",
cardOrActionAreDeleted: "La carte ou l'action sont supprimées.",
color: 'Couleur',
createBoard_title: 'Créer un tableau',
createLabel_title: 'Créer une étiquette',
createNewOneOrSelectExistingOne: 'Créez-en un nouveau ou sélectionnez <br /> un existant',
createNewOneOrSelectExistingOne: 'Créez-en un nouveau ou sélectionnez<br />un existant.',
createProject_title: 'Créer un projet',
createTextFile_title: 'Créer un fichier texte',
currentPassword: 'Mot de passe actuel',
@@ -73,12 +75,12 @@ export default {
editUsername_title: "Modifier le nom d'utilisateur",
email: 'E-mail',
emailAlreadyInUse: 'Email déjà utilisé',
enterCardTitle: 'Entrer le titre de la carte ...',
enterDescription: 'Entrez la description ...',
enterCardTitle: 'Entrer le titre de la carte...',
enterDescription: 'Entrez la description...',
enterFilename: 'Entrez le nom du fichier',
enterListTitle: 'Entrer le titre de la liste ...',
enterListTitle: 'Entrer le titre de la liste...',
enterProjectTitle: 'Saisir le titre du projet',
enterTaskDescription: 'Saisir la description de la tâche ...',
enterTaskDescription: 'Saisir la description de la tâche...',
filterByLabels_title: 'Filtrer par libellés',
filterByMembers_title: 'Filtrer par membres',
fromComputer_title: "Depuis l'ordinateur",
@@ -99,7 +101,7 @@ export default {
noLists: 'Pas de listes',
noProjects: 'Pas de projets',
notifications: 'Notifications',
noUnreadNotifications: 'Aucune notification non lue',
noUnreadNotifications: 'Aucune notification non lue.',
openBoard_title: 'Open Board',
optional_inline: 'optionnel',
organization: 'Organisation',
+3 -1
View File
@@ -8,14 +8,16 @@ import it from './it';
import ja from './ja';
import ko from './ko';
import pl from './pl';
import ro from './ro';
import ru from './ru';
import sk from './sk';
import sv from './sv';
import tr from './tr';
import ua from './ua';
import uz from './uz';
import zh from './zh';
const locales = [cs, da, de, en, es, fr, it, ja, ko, pl, ru, sk, sv, tr, uz, zh];
const locales = [cs, da, de, en, es, fr, it, ja, ko, pl, ro, ru, sk, sv, tr, ua, uz, zh];
export default locales;
+10 -8
View File
@@ -5,6 +5,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'at' p",
fullDate: 'MMM d, y',
fullDateTime: "MMMM d, y 'at' p",
},
translation: {
@@ -19,7 +21,7 @@ export default {
administrator: 'Amministratore',
all: 'tutto',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'All changes will be automatically saved<br />after connection restored',
'All changes will be automatically saved<br />after connection restored.',
areYouSureYouWantToDeleteThisAttachment: 'Sei sicuro di voler eliminare questo allegato?',
areYouSureYouWantToDeleteThisBoard: 'Sei sicuro di voler eliminare questa bacheca?',
areYouSureYouWantToDeleteThisCard: 'Sei sicuro di voler eliminare questa card?',
@@ -42,15 +44,15 @@ export default {
board: 'Bacheca',
boardNotFound_title: 'Bacheca non trovata',
canComment: 'Può commentare',
canEditContentOfBoard: 'Può modificare il contenuto della bacheca',
canOnlyViewBoard: 'Può solo vedere la bacheca',
canEditContentOfBoard: 'Può modificare il contenuto della bacheca.',
canOnlyViewBoard: 'Può solo vedere la bacheca.',
cardActions_title: 'Card Azioni',
cardNotFound_title: 'Card non trovata',
cardOrActionAreDeleted: "La card o l'azione vengono eliminate",
cardOrActionAreDeleted: "La card o l'azione vengono eliminate.",
color: 'Colore',
createBoard_title: 'Crea Bacheca',
createLabel_title: 'Crea Etichetta',
createNewOneOrSelectExistingOne: 'Crea nuovo o seleziona <br /> esistente',
createNewOneOrSelectExistingOne: 'Crea nuovo o seleziona<br />esistente.',
createProject_title: 'Crea Progetto',
createTextFile_title: 'Create File di Testo',
currentPassword: 'Password corrente',
@@ -114,14 +116,14 @@ export default {
noLists: 'No liste',
noProjects: 'No progetti',
notifications: 'Notifiche',
noUnreadNotifications: 'Nessuna notifica non letta',
noUnreadNotifications: 'Nessuna notifica non letta.',
openBoard_title: 'Bacheca aperta',
optional_inline: 'opzionale',
organization: 'Organizazzione',
phone: 'Telefono',
preferences: 'Preferenze',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: prmi Ctrl-V (Cmd-V on Mac) per aggiungere un allegato dalla clipboard',
'Tip: prmi Ctrl-V (Cmd-V on Mac) per aggiungere un allegato dalla clipboard.',
project: 'Progetto',
projectNotFound_title: 'Progetto non trovato',
removeManager_title: 'Rimuovi Manager',
@@ -140,7 +142,7 @@ export default {
taskActions_title: 'Task Azioni',
tasks: 'Tasks',
thereIsNoPreviewAvailableForThisAttachment:
'Non è disponibile alcuna anteprima per questo allegato',
'Non è disponibile alcuna anteprima per questo allegato.',
time: 'Tempo',
title: 'Titolo',
userActions_title: 'Azioni Utente',
+42 -16
View File
@@ -8,11 +8,14 @@ export default {
time: 'HH:mm',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMMMd日',
longDateTime: "MMMMd'日 ' HH:MM",
longDateTime: "MMMMd'日 ' HH:mm",
fullDate: 'yyyy年M月d日',
fullDateTime: 'yyyy年M月d日 HH:mm',
},
translation: {
common: {
aboutPlanka: 'Plankaについて',
account: 'アカウント',
actions: 'アクション',
addAttachment_title: '添付ファイルを追加',
@@ -23,7 +26,7 @@ export default {
administrator: '管理者',
all: 'すべて',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'全ての変更は接続回復後自動的に保存されます',
'全ての変更は接続回復後自動的に保存されます。',
areYouSureYouWantToDeleteThisAttachment: 'この添付ファイルを削除してもよろしいですか?',
areYouSureYouWantToDeleteThisBoard: 'このボードを削除してもよろしいですか?',
areYouSureYouWantToDeleteThisCard: 'このカードを削除してもよろしいですか?',
@@ -45,13 +48,16 @@ export default {
background: '背景',
board: 'ボード',
boardNotFound_title: 'ボードが見つかりません',
canComment: 'コメントを作成することができます',
canEditContentOfBoard: 'ボードコンテンツを修正することができます。',
canOnlyViewBoard: 'ボードの読み取りのみ可能です。',
cardActions_title: 'カードのアクション',
cardNotFound_title: 'カードが見つかりません',
cardOrActionAreDeleted: 'カードやアクションが削除されます',
cardOrActionAreDeleted: 'カードやアクションが削除されます。',
color: '色',
createBoard_title: 'ボードを作成',
createLabel_title: 'ラベルを作成',
createNewOneOrSelectExistingOne: '新規作成もしくは既存のものから選択',
createNewOneOrSelectExistingOne: '新規作成もしくは<br />既存のものから選択。',
createProject_title: 'プロジェクトを作成',
createTextFile_title: 'テキストファイルを作成',
currentPassword: '現在のパスワード',
@@ -68,31 +74,38 @@ export default {
deleteTask_title: 'タスクを削除',
deleteUser_title: 'ユーザーを削除',
description: '説明',
detectAutomatically: '自動的に検知',
dropFileToUpload: 'ファイルをドロップしてアップロード',
editor: 'エディター',
editAttachment_title: '添付ファイルを編集',
editAvatar_title: 'アバターを編集',
editBoard_title: 'ボードを編集',
editDueDate_title: '期限を編集',
editEmail_title: 'Eメールを編集',
editInformation_title: '情報を編集',
editLabel_title: 'ラベルを編集',
editPassword_title: 'パスワードを編集',
editPermissions_title: '権限を編集',
editStopwatch_title: 'タイマーを編集',
editUsername_title: 'ユーザー名を編集',
email: 'Eメール',
emailAlreadyInUse: 'Eメールは既に使われています',
enterCardTitle: 'カードのタイトルを入力...',
enterDescription: '説明を入力...',
enterFilename: 'ファイル名を入力...',
enterListTitle: 'リストのタイトルを入力...',
enterCardTitle: 'カードのタイトルを入力… [Ctrl+Enter]で自動的に開く。',
enterDescription: '説明を入力…',
enterFilename: 'ファイル名を入力',
enterListTitle: 'リストのタイトルを入力…',
enterProjectTitle: 'プロジェクトのタイトルを入力',
enterTaskDescription: 'タスクの説明を入力...',
enterTaskDescription: 'タスクの説明を入力…',
filterByLabels_title: 'ラベルで絞り込む',
filterByMembers_title: 'メンバーで絞り込む',
fromComputer_title: 'コンピューターから',
fromTrello: 'Trelloから',
general: '一般',
hours: '時間',
importBoard_title: 'インポートボード',
invalidCurrentPassword: '現在のパスワードが無効',
labels: 'ラベル',
language: '使用言語',
leaveBoard_title: 'ボードから退出',
leaveProject_title: 'プロジェクトから退出',
list: 'リスト',
@@ -110,41 +123,48 @@ export default {
noLists: 'リストがありません',
noProjects: 'プロジェクトがありません',
notifications: '通知',
noUnreadNotifications: '未読の通知はありません',
noUnreadNotifications: '未読の通知はありません。',
openBoard_title: 'ボードを開く',
optional_inline: '任意',
organization: '組織',
phone: '電話番号',
preferences: '環境設定',
pressPasteShortcutToAddAttachmentFromClipboard:
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します',
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します。',
project: 'プロジェクト',
projectNotFound_title: 'プロジェクトがありません',
removeManager_title: 'マネージャーを削除',
removeMember_title: 'メンバーを削除',
searchLabels: 'ラベルから探す…',
searchMembers: 'メンバーから探す…',
searchUsers: 'ユーザーから探す…',
seconds: '秒',
selectBoard: 'ボードを選択',
selectList: 'リストを選択',
selectPermissions_title: '権限を選択',
selectProject: 'プロジェクトを選択',
settings: '設定',
stopwatch: 'タイマー',
subscribeToMyOwnCardsByDefault: '自分のカードをデフォルトで購読する',
taskActions_title: 'タスクのアクション',
tasks: 'タスク',
thereIsNoPreviewAvailableForThisAttachment: 'この添付ファイルにはプレビューがありません。',
time: '時間',
title: 'タイトル',
userActions_title: 'ユーザーのアクション',
userAddedThisCardToList: '<0>{{user}}</0> は <1>{{list}} をこのカードに追加しました</1>',
userAddedThisCardToList: '<0>{{user}}</0> 様が <1>{{list}} をこのカードに追加しました</1>',
userLeftNewCommentToCard:
'{{user}} は <2> {{card}} </2> に新しいコメント «{{comment}}» を残しました',
'{{user}} 様が <2> {{card}} </2> に新しいコメント «{{comment}}» を残しました',
userMovedCardFromListToList:
'{{user}} は <2> {{card}} </2> を {{fromList}} から {{toList}} に移動しました',
'{{user}} 様が <2> {{card}} </2> を {{fromList}} から {{toList}} に移動しました',
userMovedThisCardFromListToList:
'<0>{{user}}</0> <1>はこのカードを {{fromList}} から {{toList}} に移動しました</1>',
'<0>{{user}}</0> <1>様がこのカードを {{fromList}} から {{toList}} に移動しました</1>',
username: 'ユーザー名',
usernameAlreadyInUse: 'ユーザー名は既に使われています',
users: 'ユーザー',
writeComment: 'コメントを書く...',
version: 'バージョン',
viewer: 'ビューア',
writeComment: 'コメントを書く…',
},
action: {
@@ -155,6 +175,7 @@ export default {
addCard_title: 'カードを追加',
addComment: 'コメントを追加',
addList: 'リストを追加',
addMember: 'メンバーを追加',
addMoreDetailedDescription: 'より詳細な説明を追加',
addTask: 'タスクを追加',
addToCard: 'カードに追加',
@@ -184,10 +205,14 @@ export default {
editDueDate_title: '期限を編集',
editDescription_title: '説明を編集',
editEmail_title: 'Eメールを編集',
editInformation_title: '情報を編集',
editPassword_title: 'パスワードの編集',
editPermissions: '権限を編集',
editStopwatch_title: 'タイマーの編集',
editTitle_title: 'タイトルの編集',
editUsername_title: 'ユーザー名の編集',
hideDetails: '詳細を隠す',
import: 'インポート',
leaveBoard: 'ボードから退出',
leaveProject: 'プロジェクトから退出',
logOut_title: 'ログアウト',
@@ -203,6 +228,7 @@ export default {
removeMember: 'メンバーを削除',
save: '保存',
showAllAttachments: '全ての添付ファイルを表示する({{hidden}} 非表示)',
showDetails: '詳細を表示',
showFewerAttachments: 'テンプファイルの表示数を減らす',
start: 'スタート',
stop: 'ストップ',
+4 -2
View File
@@ -5,16 +5,18 @@ export default {
invalidEmailOrUsername: 'Eメールまたはユーザー名が無効',
invalidPassword: 'パスワードが無効',
logInToPlanka: 'Planka にログインする',
noInternetConnection: 'インターネットに接続されていません。',
pageNotFound_title: 'ページが見つかりません。',
noInternetConnection: 'インターネットに接続されていません',
pageNotFound_title: 'ページが見つかりません',
password: 'パスワード',
projectManagement: 'プロジェクト管理',
serverConnectionFailed: 'サーバーの接続に失敗',
unknownError: '不明なエラーです。後でもう一度試してください。',
useSingleSignOn: 'SSOを使用',
},
action: {
logIn: 'ログイン',
logInWithSSO: 'SSOでログイン',
},
},
};
+148 -128
View File
@@ -1,167 +1,183 @@
import dateFns from 'date-fns/locale/ko';
/* Translated by Park Seok Ho */
export default {
dateFns,
format: {
date: 'yyyy/M/d',
time: 'HH:mm',
time: 'a hh시 mm분',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMMMd일',
longDateTime: "MMMMd'일 ' HH:MM",
longDate: "MMMMd'일'",
longDateTime: "MMMMd'일 ' a hh시 mm분",
fullDate: 'yyyy년M월d일',
fullDateTime: 'yyyy년M월d일 a hh시 mm분',
},
translation: {
common: {
aboutPlanka: 'Planka에 대해서',
account: '계정',
actions: '활동',
addAttachment_title: '첨부파일 추가',
addAttachment_title: '첨부 파일 추가',
addComment: '댓글',
addManager_title: '관리자 추가',
addMember_title: '회원 추가',
addManager_title: '매니저 추가',
addMember_title: '멤버 추가',
addUser_title: '사용자 추가',
administrator: '시스템 관리자',
all: 'All',
administrator: '관리자',
all: '모두',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'모든 변경 사항은 연결이 복원된 후 자동으로 저장됩니다',
areYouSureYouWantToDeleteThisAttachment: '첨부 파일을 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisBoard: '보드를 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisCard: '항목카드를 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisComment: '댓글을 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisLabel: '라벨을 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisList: '리스트를 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisProject: '프로젝트를 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisTask: '태스트를 삭제하겠습니까?',
areYouSureYouWantToDeleteThisUser: '이 유저를 삭제하겠습니까?',
'연결이 복원되면<br />모든 변경 사항이 자동으로 저장됩니다.',
areYouSureYouWantToDeleteThisAttachment: '해당하는 첨부 파일을 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisBoard: '해당하는 보드를 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisCard: '해당하는 항목카드를 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisComment: '해당하는 댓글을 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisLabel: '해당하는 라벨을 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisList: '해당하는 목록를 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisProject: '해당하는 프로젝트를 삭제하시겠습니까?',
areYouSureYouWantToDeleteThisTask: '해당하는 업무를 삭제하겠습니까?',
areYouSureYouWantToDeleteThisUser: '해당하는 사용자를 삭제하겠습니까?',
areYouSureYouWantToLeaveBoard: '이 보드에서 나가시겠습니까?',
areYouSureYouWantToLeaveProject: '이 프로젝트에서 나가시겠습니까?',
areYouSureYouWantToRemoveThisManagerFromProject: '프로젝트에서 관리자를 제거하시겠습니까?',
areYouSureYouWantToRemoveThisMemberFromBoard: '이 회원을 보드에서 탈퇴시키겠습니까?',
attachment: '첨부파일',
attachments: '첨부파일',
authentication: '계정 관리',
background: 'Background',
areYouSureYouWantToRemoveThisManagerFromProject:
'해당하는 프로젝트에서 매니저를 제거하시겠습니까?',
areYouSureYouWantToRemoveThisMemberFromBoard: '해당하는 멤버를 보드에서 삭제하시겠습니까?',
attachment: '첨부',
attachments: '첨부 파일',
authentication: '인증',
background: '배경',
board: '보드',
boardNotFound_title: '보드를 찾을 수 없습니다.',
cardActions_title: 'Card Actions',
cardNotFound_title: '항목카드를 찾을 수 없습니다.',
cardOrActionAreDeleted: '항목카드 혹은 활동내역이 삭제되었습니다.',
color: 'Color',
createBoard_title: '보드명칭을 입력하세요.',
boardNotFound_title: '보드를 찾을 수 없습니다',
canComment: '댓글을 작성할 수 있습니다',
canEditContentOfBoard: '보드 콘텐츠를 수정할 수 있습니다.',
canOnlyViewBoard: '보드를 읽기만 할 수 있습니다.',
cardActions_title: '카드 작업',
cardNotFound_title: '카드를 찾을 수 없습니다',
cardOrActionAreDeleted: '카드 또는 작업이 삭제되었습니다.',
color: '색상',
createBoard_title: '보드 생성',
createLabel_title: '라벨 생성',
createNewOneOrSelectExistingOne: '새 항목을 만들거나<br />기존 항목을 선택하세요.',
createNewOneOrSelectExistingOne: '새로 생성하거나<br />기존 항목을 선택하세요.',
createProject_title: '프로젝트 생성',
createTextFile_title: 'Text File 생성',
createTextFile_title: '텍스트 파일 생성',
currentPassword: '현재 비밀번호',
dangerZone_title: 'Danger Zone',
dangerZone_title: '위험 구역',
date: '날짜',
dueDate_title: '완료일',
deleteAttachment_title: '첨부파일 삭제',
dueDate_title: '마감일',
deleteAttachment_title: '첨부 파일 삭제',
deleteBoard_title: '보드 삭제',
deleteCard_title: '항목 카드 삭제',
deleteCard_title: '카드 삭제',
deleteComment_title: '댓글 삭제',
deleteLabel_title: '라벨 삭제',
deleteList_title: '리스트 삭제',
deleteList_title: '목록 삭제',
deleteProject_title: '프로젝트 삭제',
deleteTask_title: '태스크 삭제',
deleteTask_title: '업무 삭제',
deleteUser_title: '사용자 삭제',
description: '설명',
detectAutomatically: '자동으로 감지',
dropFileToUpload: '업로드 하기 위해서 파일을 Drop 하세요.',
dropFileToUpload: '업로드할 파일 놓기',
editor: '편집기',
editAttachment_title: '첨부 수정',
editAvatar_title: '프로필사진 수정',
editAvatar_title: '아바타 수정',
editBoard_title: '보드 수정',
editDueDate_title: '완료일 수정',
editEmail_title: '메일 수정',
editDueDate_title: '마감일 수정',
editEmail_title: '이메일 수정',
editInformation_title: '정보 수정',
editLabel_title: '라벨 수정',
editPassword_title: '비밀번호 수정',
editStopwatch_title: '타이머 수정',
editUsername_title: '사용자이름 수정',
email: '메일',
emailAlreadyInUse: '기 가입된 이메일이 존재합니다.',
enterCardTitle: '항목카드명을 입력하세요...',
enterDescription: '자세한 설명을 작성하세요...',
enterFilename: '파일이름을 입력하세요...',
enterListTitle: '리스트명을 입력 (예,todo,완료,이슈등...)',
enterProjectTitle: '프로젝트명칭을 입력하세요.',
enterTaskDescription: '태스크 설명을 입력하세요...',
filterByLabels_title: '라벨 검색',
filterByMembers_title: '회원으로 검색',
fromComputer_title: 'PC로 부터',
general: 'General',
editPermissions_title: '권한 수정',
editStopwatch_title: '스톱워치 수정',
editUsername_title: '사용자 이름 수정',
email: '이메일',
emailAlreadyInUse: '이미 사용 중인 이메일입니다',
enterCardTitle: '카드 제목 입력... [Ctrl+Enter]로 자동으로 열 수 있습니다.',
enterDescription: '설명 입력...',
enterFilename: '파일 이름 입력',
enterListTitle: '목록 제목 입력...',
enterProjectTitle: '프로젝트 제목 입력',
enterTaskDescription: '업무 설명 입력...',
filterByLabels_title: '라벨로 필터링',
filterByMembers_title: '멤버로 필터링',
fromComputer_title: 'PC에서',
fromTrello: '트렐로에서',
general: '일반',
hours: '시',
invalidCurrentPassword: '현재 패스워드는 유효하지 않습니다.',
importBoard_title: '보드 가져오기',
invalidCurrentPassword: '현재 비밀번호가 일치하지 않습니다',
labels: '라벨',
language: '언어 선택',
leaveBoard_title: '보드 탈퇴',
leaveProject_title: '프로젝트 탈퇴',
list: '내역',
listActions_title: '활용 내역',
managers: '관리자',
members: '팀원',
language: '언어',
leaveBoard_title: '보드 떠나기',
leaveProject_title: '프로젝트 떠나기',
list: '목록',
listActions_title: '목록 항목 조율',
managers: '매니저',
members: '멤버',
minutes: '분',
moveCard_title: '항목카드 이동',
moveCard_title: '카드 옮기기',
name: '이름',
newEmail: '새로운 메일',
newPassword: '새로운 패스워드',
newEmail: '새로운 이메일',
newPassword: '새로운 비밀번호',
newUsername: '새로운 사용자 이름',
noConnectionToServer: '서버에 연결할수 없습니다.',
noBoards: '보드가 존재하지 않습니다.',
noLists: '리스트가 존재하지 않습니다.',
noProjects: '프로젝트가 존재하지 않습니다.',
noConnectionToServer: '서버에 연결할 수 없습니다',
noBoards: '보드가 존재하지 않습니다',
noLists: '목록이 존재하지 않습니다',
noProjects: '프로젝트가 존재하지 않습니다',
notifications: '알림',
noUnreadNotifications: '알림을 모두 확인했습니다.',
openBoard_title: '보드열기',
noUnreadNotifications: '확인하지 않은 알림이 없습니다.',
openBoard_title: '보드 열기',
optional_inline: '옵션',
organization: '조직',
phone: '전화번호',
preferences: '환경설정',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: 클립보드에서 첨부 파일을 추가하려면 Ctrl-V(Mac의 경우 Cmd-V) 입니다..',
'팁: 클립보드에서 첨부 파일을 추가하려면 Ctrl-V (Mac의 경우 Cmd-V)를 누릅니다.',
project: '프로젝트',
projectNotFound_title: '프로젝트가 존재하지 않습니다.',
removeManager_title: '관리자 삭제',
removeMember_title: '맴버 삭제',
projectNotFound_title: '프로젝트를 찾을 수 없습니다',
removeManager_title: '매니저 삭제',
removeMember_title: '멤버 삭제',
searchLabels: '라벨 검색...',
searchMembers: '멤버 검색...',
searchUsers: '사용자 검색...',
seconds: '초',
selectBoard: '보드 선택',
selectList: '리스트 선택',
selectList: '목록 선택',
selectPermissions_title: '권한 선택',
selectProject: '프로젝트 선택',
settings: '설정',
stopwatch: '활동시간',
subscribeToMyOwnCardsByDefault: '내 항목카드는 기본으로 구독',
taskActions_title: '태스크 활동',
tasks: '태스크',
thereIsNoPreviewAvailableForThisAttachment:
'이 첨부 파일에 사용할 수 있는 미리보기가 없습니다.',
stopwatch: '스톱워치',
subscribeToMyOwnCardsByDefault: '기본적으로 내 카드를 구독합니다',
taskActions_title: '업무 조율',
tasks: '업무',
thereIsNoPreviewAvailableForThisAttachment: '이 첨부 파일은 미리 보기를 사용할 수 없습니다.',
time: '시간',
title: '제목',
userActions_title: '',
userAddedThisCardToList: '<0>{{user}}</0><1>이 카드를 {{list}}</1>에 추가했습니다.',
userActions_title: '사용자 조율',
userAddedThisCardToList: '<0>{{user}}</0>님이 <1>{{list}}에 카드를 추가했습니다</1>',
userLeftNewCommentToCard:
'{{user}}이(가) <2>{{card}}</2>에 «{{comment}}»라는 댓글을 남겼습니다',
userMovedCardFromListToList: '{{user}} moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
'{{user}}님이 <2>{{card}}</2>에 «{{comment}}»라는 댓글을 남겼습니다',
userMovedCardFromListToList:
'{{user}}님이 {{fromList}}에서 {{toList}}로 <2>{{card}}</2>를 옮겼습니다',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> 이 항목카드를 {{fromList}} 에서 {{toList}} 으로 이동했습니다. </1>',
username: '아이디',
usernameAlreadyInUse: '이미 사용중인 사용자입니다.',
'<0>{{user}}</0><1> 님이 {{fromList}}에서 {{toList}}로 이 카드를 옮겼습니다</1>',
username: '사용자 이름',
usernameAlreadyInUse: '이미 사용중인 사용자 이름입니다',
users: '사용자',
writeComment: '댓글 작성...',
version: '버전',
viewer: '열람자',
writeComment: '댓글 쓰기...',
},
action: {
addAnotherCard: '다른 항목카드 추가',
addAnotherList: '다른 리스트 추가',
addAnotherTask: '다른 TASK 추가',
addCard: '항목카드 추가',
addCard_title: '항목카드 추가',
addAnotherCard: '다른 카드 추가',
addAnotherList: '다른 목록 추가',
addAnotherTask: '다른 업무 추가',
addCard: '카드 추가',
addCard_title: '카드 추가',
addComment: '댓글 추가',
addList: '리스트 추가',
addList: '목록 추가',
addMember: '멤버 추가',
addMoreDetailedDescription: '자세한 설명 추가',
addTask: '태스트 추가',
addToCard: '항목카드 추가',
addTask: '업무 추가',
addToCard: '카드에 추가',
addUser: '사용자 추가',
createBoard: '보드 생성',
createFile: '파일 생성',
@@ -169,51 +185,55 @@ export default {
createNewLabel: '새로운 라벨 생성',
createProject: '프로젝트 생성',
delete: '삭제',
deleteAttachment: '첨부삭제',
deleteAvatar: '프로필 사진 삭제',
deleteAttachment: '첨부 삭제',
deleteAvatar: '아바타 삭제',
deleteBoard: '보드 삭제',
deleteCard: '항목카드 삭제',
deleteCard_title: '항목카드 삭제',
deleteCard: '카드 삭제',
deleteCard_title: '카드 삭제',
deleteComment: '댓글 삭제',
deleteImage: '이미지 삭제',
deleteLabel: '라벨 삭제',
deleteList: '리스트 삭제',
deleteList_title: '리스트 삭제',
deleteList: '목록 삭제',
deleteList_title: '목록 삭제',
deleteProject: '프로젝트 삭제',
deleteProject_title: '프로젝트 삭제',
deleteTask: '태스트 삭제',
deleteTask_title: '태스트 삭제',
deleteTask: '업무 삭제',
deleteTask_title: '업무 삭제',
deleteUser: '사용자 삭제',
edit: '수정',
editDueDate_title: '완료일 수정',
editDueDate_title: '마감일 수정',
editDescription_title: '설명 수정',
editEmail_title: '메일 수정',
editEmail_title: '이메일 수정',
editInformation_title: '정보 수정',
editPassword_title: '패스워드 수정',
editStopwatch_title: '시간 수정',
editPassword_title: '비밀번호 수정',
editPermissions: '권한 수정',
editStopwatch_title: '스톱워치 수정',
editTitle_title: '제목 수정',
editUsername_title: '이름 수정',
leaveBoard: '보드에서 나가기',
leaveProject: '프로젝트에서 나가기',
editUsername_title: '사용자 이름 수정',
hideDetails: '세부 사항 숨기기',
import: '가져오기',
leaveBoard: '보드 나가기',
leaveProject: '프로젝트 나가기',
logOut_title: '로그아웃',
makeCover_title: '커버 만들기',
move: '이동',
moveCard_title: '항목카드 이동',
moveCard_title: '카드 이동',
remove: '제거',
removeBackground: '백그라운드 제거',
removeBackground: '배경 제거',
removeCover_title: '커버 제거',
removeFromBoard: '보드에서 제거',
removeFromProject: '프로젝트에서 제거',
removeManager: '관리자 삭제',
removeMember: '멤버 삭제',
removeFromBoard: '보드 제거',
removeFromProject: '프로젝트 제거',
removeManager: '매니저 제거',
removeMember: '멤버 제거',
save: '저장',
showAllAttachments: '모든 첨부파일 표시 ({{hidden}} hidden)',
showFewerAttachments: '일부 첨부파일만 표시',
showAllAttachments: '모든 첨부 파일 표시 ({{hidden}} 숨겨짐)',
showDetails: '세부 사항 표시',
showFewerAttachments: '일부 첨부 파일만 표시',
start: '시작',
stop: '끝',
subscribe: '구독하기',
stop: '종료',
subscribe: '구독',
unsubscribe: '구독 취소',
uploadNewAvatar: '새로운 프로필 사진 업로드',
uploadNewAvatar: '새로운 아바타 업로드',
uploadNewImage: '새로운 이미지 업로드',
},
},
+10 -10
View File
@@ -1,22 +1,22 @@
/* Translated by Park Seok Ho */
export default {
translation: {
common: {
emailOrUsername: '이메일 혹은 아이디',
invalidEmailOrUsername: '이메일이나 사용자 이름이 유효하지 않습니다.',
invalidPassword: '잘못된 비밀번호',
emailOrUsername: '이메일 혹은 사용자 이름',
invalidEmailOrUsername: '이메일 혹은 사용자 이름이 유효하지 않습니다',
invalidPassword: '유효하지 않은 비밀번호',
logInToPlanka: 'Planka에 로그인',
noInternetConnection: '인터넷에 연결되지 않음',
pageNotFound_title: '페이지를 찾을수 없습니다.',
password: '패스워드',
noInternetConnection: '인터넷 연결이 없음',
pageNotFound_title: '페이지를 찾을 수 없습니다',
password: '비밀번호',
projectManagement: '프로젝트 관리',
serverConnectionFailed: '서버 연결 실패',
unknownError: '알 수 없는 오류가 발생하였습니다, 나중에 다시 시도하세요.',
serverConnectionFailed: '서버 연결에 실패함',
unknownError: '알 수 없는 오류, 나중에 다시 시도하십시오',
useSingleSignOn: 'single sign-on(SSO) 사용',
},
action: {
logIn: '로그인',
logInWithSSO: 'single sign-on(SSO)으로 로그인',
},
},
};
+7 -5
View File
@@ -9,6 +9,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'o' p",
fullDate: 'd MMM y',
fullDateTime: "d MMMM y 'o' p",
},
translation: {
@@ -22,7 +24,7 @@ export default {
administrator: 'Administrator',
all: 'Wszyscy',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Wszystkie zmiany zostaną automatycznie zapisane<br />po przywróceniu połączenia',
'Wszystkie zmiany zostaną automatycznie zapisane<br />po przywróceniu połączenia.',
areYouSureYouWantToDeleteThisAttachment: 'Jesteś pewien że chcesz usunąć ten załącznik?',
areYouSureYouWantToDeleteThisBoard: 'Jesteś pewien że chcesz usunąć tę tablicę?',
areYouSureYouWantToDeleteThisCard: 'Jesteś pewien że chcesz usunąć tę kartę?',
@@ -41,11 +43,11 @@ export default {
boardNotFound_title: 'Tablica Nie Znaleziona',
cardActions_title: 'Akcje Karty',
cardNotFound_title: 'Karta Nie Znaleziona',
cardOrActionAreDeleted: 'Karta lub akcja została usunięta',
cardOrActionAreDeleted: 'Karta lub akcja została usunięta.',
color: 'Kolor',
createBoard_title: 'Utwórz Tablicę',
createLabel_title: 'Utwórz Oznaczenie',
createNewOneOrSelectExistingOne: 'Utwórz nowy lub wybierz<br />istniejący',
createNewOneOrSelectExistingOne: 'Utwórz nowy lub wybierz<br />istniejący.',
createProject_title: 'Utwórz Projekt',
createTextFile_title: 'Utwórz Plik Tekstowy',
currentPassword: 'Aktualne hasło',
@@ -99,14 +101,14 @@ export default {
noLists: 'Brak list',
noProjects: 'Brak projektów',
notifications: 'Powiadomienia',
noUnreadNotifications: 'Brak nieprzeczytanych powiadomień',
noUnreadNotifications: 'Brak nieprzeczytanych powiadomień.',
openBoard_title: 'Otwórz Tablicę',
optional_inline: 'opcjonalny',
organization: 'Organizacja',
phone: 'Telefon',
preferences: 'Preferencje',
pressPasteShortcutToAddAttachmentFromClipboard:
'Podpowiedź: naciśnij Ctrl-V (Cmd-V na Macu) aby dodać załącznik ze schowka',
'Podpowiedź: naciśnij Ctrl-V (Cmd-V na Macu) aby dodać załącznik ze schowka.',
project: 'Projekt',
projectNotFound_title: 'Projekt Nie Znaleziony',
removeMember_title: 'Usuń Członka',
+242
View File
@@ -0,0 +1,242 @@
import dateFns from 'date-fns/locale/ro';
export default {
dateFns,
format: {
date: 'P',
time: 'HH:mm',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'в' p",
fullDate: 'd MMM y',
fullDateTime: "d MMMM y 'в' p",
},
translation: {
common: {
aboutPlanka: 'Despre Planka',
account: 'Cont',
actions: 'Acțiuni',
addAttachment_title: 'Adauga atasament',
addComment: 'Adaugă comentariu',
addManager_title: 'Adaugă Manager',
addMember_title: 'Adaugă Membru',
addUser_title: 'Adaugă Utilizator',
administrator: 'Administrator',
all: 'Toate',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Toate modificarile vor fi salvate automat<br />dupa restabilirea conexiunii.',
areYouSureYouWantToDeleteThisAttachment: 'Sigur doriți să ștergeți acest atașament?',
areYouSureYouWantToDeleteThisBoard: 'Sigur doriți să ștergeți acest panou?',
areYouSureYouWantToDeleteThisCard: 'Sigur doriți să ștergeți acest card?',
areYouSureYouWantToDeleteThisComment: 'Sigur doriți să ștergeți acest comentariu?',
areYouSureYouWantToDeleteThisLabel: 'Sigur doriți să ștergeți această etichetă?',
areYouSureYouWantToDeleteThisList: 'Sigur doriți să ștergeți această listă?',
areYouSureYouWantToDeleteThisProject: 'Sigur doriți să ștergeți acest proiect?',
areYouSureYouWantToDeleteThisTask: 'Sigur doriți să ștergeți această sarcină?',
areYouSureYouWantToDeleteThisUser: 'Sigur doriți să ștergeți acest utilizator?',
areYouSureYouWantToLeaveBoard: 'Ești sigur că vrei să părăsești tabla?',
areYouSureYouWantToLeaveProject: 'Ești sigur că vrei să părăsești proiectul?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Sigur doriți să eliminați acest manager din proiect?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Sigur doriți să eliminați acest membru din consiliu?',
attachment: 'Atașament',
attachments: 'Atasamente',
authentication: 'Autentificare',
background: 'Fundal',
board: 'Tabla',
boardNotFound_title: 'Tabla nu a fost găsită',
canComment: 'Poate comenta',
canEditContentOfBoard: 'Poate edita conținutul tablei.',
canOnlyViewBoard: 'Poate doar vizualiza tabla.',
cardActions_title: 'Acțiuni cu carduri',
cardNotFound_title: 'Cardul nu a fost găsit',
cardOrActionAreDeleted: 'Cardul sau acțiunea sunt șterse.',
color: 'Culoarea',
createBoard_title: 'Creare Tabla',
createLabel_title: 'Creați etichetă',
createNewOneOrSelectExistingOne: 'Crează unu nou sau selectează<br />unu deja existent.',
createProject_title: 'Crează Proiect',
createTextFile_title: 'Crează un Fișier Text',
currentPassword: 'Parola Curentă',
dangerZone_title: 'Zona periculoasă',
date: 'Data',
dueDate_title: 'Data scadentă',
deleteAttachment_title: 'Ștergeți atașamentul',
deleteBoard_title: 'Ștergeți Tabla',
deleteCard_title: 'Ștergeți cardul',
deleteComment_title: 'Șterge comentariul',
deleteLabel_title: 'Ștergeți eticheta',
deleteList_title: 'Șterge lista',
deleteProject_title: 'Șterge proiectul',
deleteTask_title: 'Ștergeți sarcina',
deleteUser_title: 'Șterge utilizator',
description: 'Descriere',
detectAutomatically: 'Detectează automat',
dropFileToUpload: 'Aruncă fișierul pentru a încărca',
editor: 'Editor',
editAttachment_title: 'Editați atașamentul',
editAvatar_title: 'Editați avatarul',
editBoard_title: 'Editați tabla',
editDueDate_title: 'Editați data scadentă',
editEmail_title: 'Editați E-mail',
editInformation_title: 'Editați informații',
editLabel_title: 'Editați eticheta',
editPassword_title: 'Editați parola',
editPermissions_title: 'Editați permisiunile',
editStopwatch_title: 'Editați cronometrul',
editUsername_title: 'Editați numele de utilizator',
email: 'E-mail',
emailAlreadyInUse: 'E-mail deja utilizat',
enterCardTitle: 'Introduceți titlul cardului... [Ctrl+Enter] pentru a se deschide automat.',
enterDescription: 'Introduceți descrierea...',
enterFilename: 'Introduceți numele fișierului',
enterListTitle: 'Introduceți titlul listei...',
enterProjectTitle: 'Introduceți titlul proiectului',
enterTaskDescription: 'Introduceți descrierea sarcinii...',
filterByLabels_title: 'Filtrați după etichete',
filterByMembers_title: 'Filtrați după membri',
fromComputer_title: 'De pe computer',
fromTrello: 'De pe Trello',
general: 'General',
hours: 'Ore',
importBoard_title: 'Import Tabla',
invalidCurrentPassword: 'Parolă actuală nevalidă',
labels: 'Etichete',
language: 'Limba',
leaveBoard_title: 'Părăsiți Tabla',
leaveProject_title: 'Părăsiți proiectul',
list: 'Lista',
listActions_title: 'Listează acțiuni',
managers: 'Managerii',
members: 'Membri',
minutes: 'Minute',
moveCard_title: 'Mutați cardul',
name: 'Nume',
newEmail: 'Email nou',
newPassword: 'Parolă Nouă',
newUsername: 'Nume de utilizator nou',
noConnectionToServer: 'Nicio conexiune la server',
noBoards: 'Fără Table',
noLists: 'Fără Liste',
noProjects: 'Fără proiecte',
notifications: 'Notificări',
noUnreadNotifications: 'Fără notificări necitite.',
openBoard_title: 'Tablă deschisă',
optional_inline: 'optional',
organization: 'Organizatia',
phone: 'Telefon',
preferences: 'Preferințe',
pressPasteShortcutToAddAttachmentFromClipboard:
'Sfat: apăsați Ctrl-V (Cmd-V pe Mac) pentru a adăuga un atașament din clipboard.',
project: 'Proiect',
projectNotFound_title: 'Proiectul nu a fost găsit',
removeManager_title: 'Eliminați Manager',
removeMember_title: 'Eliminați membru',
searchLabels: 'Căutați etichete...',
searchMembers: 'Căutați membri...',
searchUsers: 'Căutați utilizatori...',
seconds: 'Secunde',
selectBoard: 'Selectați tabla',
selectList: 'Selectați lista',
selectPermissions_title: 'Selectați Permisiuni',
selectProject: 'Selectați proiectul',
settings: 'Setări',
stopwatch: 'Cronometru',
subscribeToMyOwnCardsByDefault: 'Abonați-vă la propriile carduri în mod implicit',
taskActions_title: 'Acțiuni de sarcină',
tasks: 'Sarcini',
thereIsNoPreviewAvailableForThisAttachment:
'Nu există nicio previzualizare disponibilă pentru acest atașament.',
time: 'Timp',
title: 'Titlu',
userActions_title: 'Acțiunile utilizatorului',
userAddedThisCardToList: '<0>{{user}}</0><1> a adăugat acest card în {{list}}</1>',
userLeftNewCommentToCard:
'{{user}} a lăsat un nou comentariu «{{comment}}» în <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} a mutat <2>{{card}}</2> din {{fromList}} în {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> a mutat aceast card din {{fromList}} în {{toList}}</1>',
username: 'Nume utilizator',
usernameAlreadyInUse: 'Nume utilizator deja exista',
users: 'Utilizatori',
version: 'Versiunea',
viewer: 'Vizualizator',
writeComment: 'Scrie un comentariu...',
},
action: {
addAnotherCard: 'Adăugați un alt card',
addAnotherList: 'Adăugați o altă listă',
addAnotherTask: 'Adăugați o altă sarcină',
addCard: 'Adăugați card',
addCard_title: 'Adăugați card',
addComment: 'Adauga comentariu',
addList: 'Adăugați listă',
addMember: 'Adăugați membru',
addMoreDetailedDescription: 'Adăugați descrierea mai detaliata',
addTask: 'Adăugați sarcină',
addToCard: 'Adauga în card',
addUser: 'Adăugați utilizator',
createBoard: 'Creați tablă',
createFile: 'Creați fișier',
createLabel: 'Creați eticheta',
createNewLabel: 'Creați o nouă etichetă',
createProject: 'Creați proiect',
delete: 'Ștergeți',
deleteAttachment: 'Ștergeți atașamentul',
deleteAvatar: 'Ștergeți avatarul',
deleteBoard: 'Ștergeți tabla',
deleteCard: 'Ștergeți cardul',
deleteCard_title: 'Ștergeți Cardul',
deleteComment: 'Șterge comentariu',
deleteImage: 'Șterge imaginea',
deleteLabel: 'Ștergeți Eticheta',
deleteList: 'Ștergeți lista',
deleteList_title: 'Ștergeți Lista',
deleteProject: 'Ștergeți proiectul',
deleteProject_title: 'Ștergeți Proiectul',
deleteTask: 'Ștergeți sarcina',
deleteTask_title: 'Ștergeți Sarcina',
deleteUser: 'Ștergeți utilizatorul',
edit: 'Editați',
editDueDate_title: 'Editați data scadentă',
editDescription_title: 'Editați descrierea',
editEmail_title: 'Editați e-mailul',
editInformation_title: 'Editați informații',
editPassword_title: 'Editați parola',
editPermissions: 'Editați permisiunile',
editStopwatch_title: 'Editați cronometrul',
editTitle_title: 'Editați Titlul',
editUsername_title: 'Editați Numele Utilizator',
hideDetails: 'Ascunde detalii',
import: 'Import',
leaveBoard: 'Părăsiți bordul',
leaveProject: 'Părăsiți proiect',
logOut_title: 'Deconectați-vă',
makeCover_title: 'Faceți Coperta',
move: 'Mutați',
moveCard_title: 'Mutați cardul',
remove: 'Eliminați',
removeBackground: 'Eliminați fundalul',
removeCover_title: 'Eliminați Coperta',
removeFromBoard: 'Eliminați din Tabla',
removeFromProject: 'Eliminați din proiect',
removeManager: 'Eliminați managerul',
removeMember: 'Eliminați membrul',
save: 'Salveaza',
showAllAttachments: 'Afișați toate atașamentele ({{hidden}} ascunse)',
showDetails: 'Arată detalii',
showFewerAttachments: 'Afișați mai puține atașamente',
start: 'Start',
stop: 'Stop',
subscribe: 'Abonati-va',
unsubscribe: 'Dezabonați-vă',
uploadNewAvatar: 'Încărcați un avatar nou',
uploadNewImage: 'Încărcați o nouă imagine',
},
},
};
+8
View File
@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'ro',
country: 'ro',
name: 'Română',
embeddedLocale: login,
};
+22
View File
@@ -0,0 +1,22 @@
export default {
translation: {
common: {
emailOrUsername: 'E-mail sau nume de utilizator',
invalidEmailOrUsername: 'E-mail sau nume de utilizator introduse greșit',
invalidPassword: 'Parola greșita',
logInToPlanka: 'Autentificarea în Planka',
noInternetConnection: 'Lipsește conexiunea cu internet',
pageNotFound_title: 'Pagina n-a fost găsită',
password: 'Parola',
projectManagement: 'Gestionarea proiectului',
serverConnectionFailed: 'Conexiunea cu Server a eșuat',
unknownError: 'Eroarea necunoscuta, mai incercați',
useSingleSignOn: 'Folosiți autentificarea unica',
},
action: {
logIn: 'Autentificarea',
logInWithSSO: 'Autentificarea cu SSO',
},
},
};
+10 -8
View File
@@ -9,6 +9,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'в' p",
fullDate: 'd MMM y',
fullDateTime: "d MMMM y 'в' p",
},
translation: {
@@ -24,7 +26,7 @@ export default {
administrator: 'Администратор',
all: 'Все',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Все изменения сохранятся автоматически,<br />как только подключение восстановится',
'Все изменения сохранятся автоматически,<br />как только подключение восстановится.',
areYouSureYouWantToDeleteThisAttachment: 'Вы уверены, что хотите удалить это вложение?',
areYouSureYouWantToDeleteThisBoard: 'Вы уверены, что хотите удалить эту доску?',
areYouSureYouWantToDeleteThisCard: 'Вы уверены, что хотите удалить эту карточку?',
@@ -47,16 +49,16 @@ export default {
board: 'Доска',
boardNotFound: 'Доска не найдена',
canComment: 'Может комментировать',
canEditContentOfBoard: 'Может редактировать содержимое доски',
canOnlyViewBoard: 'Может только просматривать доску',
canEditContentOfBoard: 'Может редактировать содержимое доски.',
canOnlyViewBoard: 'Может только просматривать доску.',
cardActions: 'Действия с карточкой',
cardNotFound: 'Карточка не найдена',
cardOrActionAreDeleted: 'Карточка или действие удалены',
cardOrActionAreDeleted: 'Карточка или действие удалены.',
color: 'Цвет',
connectionRestored: 'Соединение восстановлено',
createBoard: 'Создание доски',
createLabel: 'Создание метки',
createNewOneOrSelectExistingOne: 'Создайте новую или выберите<br />уже существующую',
createNewOneOrSelectExistingOne: 'Создайте новую или выберите<br />уже существующую.',
createProject: 'Создание проекта',
createTextFile: 'Создание текстового файла',
currentPassword: 'Текущий пароль',
@@ -124,14 +126,14 @@ export default {
noLists: 'Списков нет',
noProjects: 'Проектов нет',
notifications: 'Уведомления',
noUnreadNotifications: 'Уведомлений нет',
noUnreadNotifications: 'Уведомлений нет.',
openBoard: 'Откройте доску',
optional_inline: 'необязательно',
organization: 'Организация',
phone: 'Телефон',
preferences: 'Предпочтения',
pressPasteShortcutToAddAttachmentFromClipboard:
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена',
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена.',
project: 'Проект',
projectActions: 'Действия с проектом',
projectNotFound: 'Проект не найден',
@@ -150,7 +152,7 @@ export default {
subscribeToMyOwnCardsByDefault: 'По умолчанию подписываться на мои собственные карточки',
taskActions: 'Действия с задачей',
tasks: 'Задачи',
thereIsNoPreviewAvailableForThisAttachment: 'Предпросмотр для этого вложения недоступен',
thereIsNoPreviewAvailableForThisAttachment: 'Предпросмотр для этого вложения недоступен.',
time: 'Время',
title: 'Название',
userActions: 'Действия с пользователем',
+7 -5
View File
@@ -9,6 +9,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'v' p",
fullDate: 'd MMM y',
fullDateTime: "d MMMM y 'v' p",
},
translation: {
@@ -23,7 +25,7 @@ export default {
administrator: 'Administrátor',
all: 'Všetko',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Všetky zmeny budú automaticky uložené<br />po obnovení spojenia',
'Všetky zmeny budú automaticky uložené<br />po obnovení spojenia.',
areYouSureYouWantToDeleteThisAttachment: 'Naozaj chcete zmazať túto prílohu?',
areYouSureYouWantToDeleteThisBoard: 'Naozaj chcete zmazať túto tabuľu?',
areYouSureYouWantToDeleteThisCard: 'Naozaj chcete zmazať túto kartu?',
@@ -47,11 +49,11 @@ export default {
boardNotFound_title: 'Tabuľa neexistuje',
cardActions_title: 'Akcie na karte',
cardNotFound_title: 'Karta neexistuje',
cardOrActionAreDeleted: 'Karta alebo akcia sú zmazané',
cardOrActionAreDeleted: 'Karta alebo akcia sú zmazané.',
color: 'Farba',
createBoard_title: 'Vytvoriť tabuľu',
createLabel_title: 'Vytvoriť štítok',
createNewOneOrSelectExistingOne: 'Vytvoriť nový alebo vyberte<br />už existujúci',
createNewOneOrSelectExistingOne: 'Vytvoriť nový alebo vyberte<br />už existujúci.',
createProject_title: 'Vytvoriť projekt',
createTextFile_title: 'Vytvoriť textový súbor',
currentPassword: 'Aktuálne heslo',
@@ -111,14 +113,14 @@ export default {
noLists: 'Žiadne zoznamy',
noProjects: 'Žiadne projekty',
notifications: 'Oznámenia',
noUnreadNotifications: 'Žiadne neprečítané oznámenia',
noUnreadNotifications: 'Žiadne neprečítané oznámenia.',
openBoard_title: 'Otvoriť tabuľu',
optional_inline: 'voliteľné',
organization: 'Spoločnosť',
phone: 'Telefón',
preferences: 'Voľby',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: stlačte Ctrl-V (Cmd-V na Mac) pre vloženie prílohy zo schránky',
'Tip: stlačte Ctrl-V (Cmd-V na Mac) pre vloženie prílohy zo schránky.',
project: 'Projekt',
projectNotFound_title: 'Projekt neexistuje',
removeManager_title: 'Odstrániť správcu',
+7 -5
View File
@@ -9,6 +9,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'at' p",
fullDate: 'MMM d, y',
fullDateTime: "MMMM d, y 'at' p",
},
translation: {
@@ -23,7 +25,7 @@ export default {
administrator: 'Administratör',
all: 'Alla',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Alla ändringar kommer att sparas automatiskt<br />så fort anslutningen är återställd',
'Alla ändringar kommer att sparas automatiskt<br />så fort anslutningen är återställd.',
areYouSureYouWantToDeleteThisAttachment:
'Är du säker på att du vill ta bort den här bilagan?',
areYouSureYouWantToDeleteThisBoard: 'Är du säker på att du vill ta bort den här tavlan?',
@@ -49,11 +51,11 @@ export default {
boardNotFound_title: 'Tavla Ej Funnen',
cardActions_title: 'Kortåtgärder',
cardNotFound_title: 'Kort Ej Funnet',
cardOrActionAreDeleted: 'Kort eller åtgärd är borttagen',
cardOrActionAreDeleted: 'Kort eller åtgärd är borttagen.',
color: 'Färg',
createBoard_title: 'Skapa Tavla',
createLabel_title: 'Skapa Etikett',
createNewOneOrSelectExistingOne: 'Skapa en ny eller välj<br />en redan existerande',
createNewOneOrSelectExistingOne: 'Skapa en ny eller välj<br />en redan existerande.',
createProject_title: 'Skapa Projekt',
createTextFile_title: 'Skapa Textfil',
currentPassword: 'Nuvarande lösenord',
@@ -112,14 +114,14 @@ export default {
noLists: 'Inga listor',
noProjects: 'Inga projekt',
notifications: 'Notifikationer',
noUnreadNotifications: 'Inga olästa notifikationer',
noUnreadNotifications: 'Inga olästa notifikationer.',
openBoard_title: 'Öppna Tavla',
optional_inline: 'valfri',
organization: 'Organisation',
phone: 'Telefon',
preferences: 'Preferenser',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tips: tryck på Ctrl-V (Cmd-V på Mac) för att lägga till en bilaga från urklipp',
'Tips: tryck på Ctrl-V (Cmd-V på Mac) för att lägga till en bilaga från urklipp.',
project: 'Projekt',
projectNotFound_title: 'Projekt Ej Funnet',
removeManager_title: 'Ta Bort Projektledare',
+11 -9
View File
@@ -9,6 +9,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd. MMM',
longDateTime: "d. MMMM 'Saat' p",
fullDate: 'd. MMM. y',
fullDateTime: "d. MMMM. y 'Saat' p",
},
translation: {
@@ -47,7 +49,7 @@ export default {
boardNotFound_title: 'Pano bulunamadı',
cardActions_title: 'Kart İşlemleri',
cardNotFound_title: 'Kart bulunamadı',
cardOrActionAreDeleted: 'Kart veya işlem silindi',
cardOrActionAreDeleted: 'Kart veya işlem silindi.',
color: 'renk',
createBoard_title: 'Pano Oluştur',
createLabel_title: 'Etiket Oluştur',
@@ -81,12 +83,12 @@ export default {
editUsername_title: 'Kullanıcı Adını Düzenle',
email: 'e-posta',
emailAlreadyInUse: 'E-posta adresi zaten kullanımda',
enterCardTitle: 'Kart başlığını girin',
enterDescription: 'Açıklamayı girin',
enterCardTitle: 'Kart başlığını girin...',
enterDescription: 'Açıklamayı girin...',
enterFilename: 'Dosya adını girin',
enterListTitle: 'Liste başlığını girin',
enterListTitle: 'Liste başlığını girin...',
enterProjectTitle: 'Proje başlığını girin',
enterTaskDescription: 'Görev açıklamasını girin',
enterTaskDescription: 'Görev açıklamasını girin...',
filterByLabels_title: 'Etikete Göre Filtrele',
filterByMembers_title: 'Üyelere göre filtrele',
fromComputer_title: 'Bilgisayardan',
@@ -112,14 +114,14 @@ export default {
noLists: 'Liste yok',
noProjects: 'Proje yok',
notifications: 'Bildirimler',
noUnreadNotifications: 'Okunmamış bildirim yok',
noUnreadNotifications: 'Okunmamış bildirim yok.',
openBoard_title: 'Açık Pano',
optional_inline: 'İsteğe bağlı',
organization: 'Organizasyon',
phone: 'telefon',
preferences: 'Tercihler',
pressPasteShortcutToAddAttachmentFromClipboard:
'İpucu: Panodan bir ek eklemek için CTRL-V ye (Macte Cmd-V) basın',
'İpucu: Panodan bir ek eklemek için CTRL-V ye (Macte Cmd-V) basın.',
project: 'Proje',
projectNotFound_title: 'Proje bulunamadı',
removeManager_title: 'Yöneticiyi Kaldır',
@@ -133,7 +135,7 @@ export default {
subscribeToMyOwnCardsByDefault: 'Varsayılan olarak kendi kartlarıma abone ol',
taskActions_title: 'Görev Eylemleri',
tasks: 'Görevler',
thereIsNoPreviewAvailableForThisAttachment: 'Bu ek için önizleme mevcut değil',
thereIsNoPreviewAvailableForThisAttachment: 'Bu ek için önizleme mevcut değil.',
time: 'zaman',
title: 'başlık',
userActions_title: 'Kullanıcı İşlemleri',
@@ -147,7 +149,7 @@ export default {
username: 'kullanıcı adı',
usernameAlreadyInUse: 'Kullanıcı adı zaten kullanımda',
users: 'kullanıcı',
writeComment: 'Yorum yazın',
writeComment: 'Yorum yazın...',
},
action: {
addAnotherCard: 'Başka bir kart ekle',
+237
View File
@@ -0,0 +1,237 @@
export default {
format: {
date: 'd/M/yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMMM',
longDateTime: "d MMMM 'о' p",
fullDate: 'd MMMM y',
fullDateTime: "d MMMM y 'о' p",
},
translation: {
common: {
aboutPlanka: 'Про Planka',
account: 'Обліковий запис',
actions: 'Дії',
addAttachment_title: 'Додати Вкладення',
addComment: 'Додати коментар',
addManager_title: 'Додати Менеджера',
addMember_title: 'Додати Учасника',
addUser_title: 'Додати Користувача',
administrator: 'Адміністратор',
all: 'Все',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Всі зміни будуть автоматично збережені<br />після відновлення підключення.',
areYouSureYouWantToDeleteThisAttachment: 'Ви впевнені, що хочете видалити це вкладення?',
areYouSureYouWantToDeleteThisBoard: 'Ви впевнені, що хочете видалити цю дошку?',
areYouSureYouWantToDeleteThisCard: 'Ви впевнені, що хочете видалити цю картку?',
areYouSureYouWantToDeleteThisComment: 'Ви впевнені, що хочете видалити цей коментар?',
areYouSureYouWantToDeleteThisLabel: 'Ви впевнені, що хочете видалити цю мітку?',
areYouSureYouWantToDeleteThisList: 'Ви впевнені, що хочете видалити цей список?',
areYouSureYouWantToDeleteThisProject: 'Ви впевнені, що хочете видалити цей проект?',
areYouSureYouWantToDeleteThisTask: 'Ви впевнені, що хочете видалити це завдання?',
areYouSureYouWantToDeleteThisUser: 'Ви впевнені, що хочете видалити цього користувача?',
areYouSureYouWantToLeaveBoard: 'Ви впевнені, що хочете залишити дошку?',
areYouSureYouWantToLeaveProject: 'Ви впевнені, що хочете залишити проект?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Ви впевнені, що хочете видалити цього менеджера з проекту?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Ви впевнені, що хочете видалити цього учасника з дошки?',
attachment: 'Вкладення',
attachments: 'Вкладення',
authentication: 'Аутентифікація',
background: 'Фон',
board: 'Дошка',
boardNotFound_title: 'Дошку не знайдено',
canComment: 'Може коментувати',
canEditContentOfBoard: 'Має право редагувати вміст дошки.',
canOnlyViewBoard: 'Може лише переглядати дошку.',
cardActions_title: 'Дії з карткою',
cardNotFound_title: 'Картку не знайдено',
cardOrActionAreDeleted: 'Картка чи дія видалена.',
color: 'Колір',
createBoard_title: 'Створити Дошку',
createLabel_title: 'Створити Мітку',
createNewOneOrSelectExistingOne: 'Створіть нову або виберіть<br />існуючу.',
createProject_title: 'Створити Проект',
createTextFile_title: 'Створити Текстовий Файл',
currentPassword: 'Поточний пароль',
dangerZone_title: 'Небезпечна Зона',
date: 'Дата',
dueDate_title: 'Крайній Термін',
deleteAttachment_title: 'Видалити Вкладення',
deleteBoard_title: 'Видалити Дошку',
deleteCard_title: 'Видалити Картку',
deleteComment_title: 'Видалити Коментар',
deleteLabel_title: 'Видалити Мітку',
deleteList_title: 'Видалити Список',
deleteProject_title: 'Видалити Проект',
deleteTask_title: 'Видалити Завдання',
deleteUser_title: 'Видалити Користувача',
description: 'Опис',
detectAutomatically: 'Визначити автоматично',
dropFileToUpload: 'Перетягніть файл для завантаження',
editor: 'Редактор',
editAttachment_title: 'Редагувати Вкладення',
editAvatar_title: 'Редагувати Аватар',
editBoard_title: 'Редагувати Дошку',
editDueDate_title: 'Редагувати Крайній Термін',
editEmail_title: 'Редагувати Електронну пошту',
editInformation_title: 'Редагувати Інформацію',
editLabel_title: 'Редагувати Мітку',
editPassword_title: 'Редагувати Пароль',
editPermissions_title: 'Редагувати Дозволи',
editStopwatch_title: 'Редагувати Секундомір',
editUsername_title: "Редагувати Ім'я користувача",
email: 'Електронна пошта',
emailAlreadyInUse: 'Електронна пошта вже використовується',
enterCardTitle: 'Введіть назву картки... [Ctrl+Enter] для автоматичного відкриття.',
enterDescription: 'Введіть опис...',
enterFilename: "Введіть ім'я файлу",
enterListTitle: 'Введіть назву списку...',
enterProjectTitle: 'Введіть назву проекту',
enterTaskDescription: 'Введіть опис завдання...',
filterByLabels_title: 'Фільтрувати за Мітками',
filterByMembers_title: 'Фільтрувати за Учасниками',
fromComputer_title: "З комп'ютера",
fromTrello: 'З Trello',
general: 'Загальне',
hours: 'Години',
importBoard_title: 'Імпортувати Дошку',
invalidCurrentPassword: 'Невірний поточний пароль',
labels: 'Мітки',
language: 'Мова',
leaveBoard_title: 'Покинути Дошку',
leaveProject_title: 'Покинути Проект',
list: 'Список',
listActions_title: 'Дії зі Списком',
managers: 'Менеджери',
members: 'Учасники',
minutes: 'Хвилини',
moveCard_title: 'Перемістити Картку',
name: 'Назва',
newEmail: 'Нова електронна пошта',
newPassword: 'Новий пароль',
newUsername: "Нове ім'я користувача",
noConnectionToServer: 'Відсутнє підключення до сервера',
noBoards: 'Немає дошок',
noLists: 'Немає списків',
noProjects: 'Немає проектів',
notifications: 'Сповіщення',
noUnreadNotifications: 'Немає непрочитаних сповіщень.',
openBoard_title: 'Відкрити Дошку',
optional_inline: 'опціонально',
organization: 'Організація',
phone: 'Телефон',
preferences: 'Уподобання',
pressPasteShortcutToAddAttachmentFromClipboard:
'Порада: натисніть Ctrl-V (⌘V на Mac), щоб додати вкладення з буфера обміну.',
project: 'Проект',
projectNotFound_title: 'Проект не знайдено',
removeManager_title: 'Видалити Менеджера',
removeMember_title: 'Видалити Учасника',
searchLabels: 'Пошук міток...',
searchMembers: 'Пошук учасників...',
searchUsers: 'Пошук користувачів...',
seconds: 'Секунди',
selectBoard: 'Вибрати дошку',
selectList: 'Вибрати список',
selectPermissions_title: 'Вибрати Дозволи',
selectProject: 'Вибрати проект',
settings: 'Налаштування',
stopwatch: 'Секундомір',
subscribeToMyOwnCardsByDefault: 'Підписатися на свої картки за замовчуванням',
taskActions_title: 'Дії з завданням',
tasks: 'Завдання',
thereIsNoPreviewAvailableForThisAttachment: 'Для цього вкладення немає доступного перегляду.',
time: 'Час',
title: 'Назва',
userActions_title: 'Дії користувача',
userAddedThisCardToList: '<0>{{user}}</0><1> додав(ла) цю картку до {{list}}</1>',
userLeftNewCommentToCard:
'{{user}} залишив(ла) новий коментар «{{comment}}» до <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} перемістив(ла) <2>{{card}}</2> з {{fromList}} в {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> перемістив(ла) цю картку з {{fromList}} в {{toList}}</1>',
username: "Ім'я користувача",
usernameAlreadyInUse: "Ім'я користувача вже використовується",
users: 'Користувачі',
version: 'Версія',
viewer: 'Переглядач',
writeComment: 'Написати коментар...',
},
action: {
addAnotherCard: 'Додати іншу картку',
addAnotherList: 'Додати інший список',
addAnotherTask: 'Додати інше завдання',
addCard: 'Додати картку',
addCard_title: 'Додати Картку',
addComment: 'Додати коментар',
addList: 'Додати список',
addMember: 'Додати учасника',
addMoreDetailedDescription: 'Додати більш детальний опис',
addTask: 'Додати завдання',
addToCard: 'Додати до картки',
addUser: 'Додати користувача',
createBoard: 'Створити дошку',
createFile: 'Створити файл',
createLabel: 'Створити мітку',
createNewLabel: 'Створити нову мітку',
createProject: 'Створити проект',
delete: 'Видалити',
deleteAttachment: 'Видалити вкладення',
deleteAvatar: 'Видалити аватар',
deleteBoard: 'Видалити дошку',
deleteCard: 'Видалити картку',
deleteCard_title: 'Видалити Картку',
deleteComment: 'Видалити коментар',
deleteImage: 'Видалити зображення',
deleteLabel: 'Видалити мітку',
deleteList: 'Видалити список',
deleteList_title: 'Видалити Список',
deleteProject: 'Видалити проект',
deleteProject_title: 'Видалити Проект',
deleteTask: 'Видалити завдання',
deleteTask_title: 'Видалити Завдання',
deleteUser: 'Видалити користувача',
edit: 'Редагувати',
editDueDate_title: 'Редагувати термін виконання',
editDescription_title: 'Редагувати опис',
editEmail_title: 'Редагувати E-mail',
editInformation_title: 'Редагувати інформацію',
editPassword_title: 'Редагувати пароль',
editPermissions: 'Редагувати дозволи',
editStopwatch_title: 'Редагувати секундомір',
editTitle_title: 'Редагувати Заголовок',
editUsername_title: "Редагувати Ім'я користувача",
hideDetails: 'Сховати деталі',
import: 'Імпортувати',
leaveBoard: 'Залишити дошку',
leaveProject: 'Залишити проект',
logOut_title: 'Вийти',
makeCover_title: 'Встановити обкладинку',
move: 'Перемістити',
moveCard_title: 'Перемістити Картку',
remove: 'Видалити',
removeBackground: 'Видалити фон',
removeCover_title: 'Видалити обкладинку',
removeFromBoard: 'Вилучити з дошки',
removeFromProject: 'Вилучити з проекту',
removeManager: 'Вилучити керівника',
removeMember: 'Вилучити учасника',
save: 'Зберегти',
showAllAttachments: 'Показати всі вкладення ({{hidden}} приховані)',
showDetails: 'Показати деталі',
showFewerAttachments: 'Показати менше вкладень',
start: 'Почати',
stop: 'Зупинити',
subscribe: 'Підписатися',
unsubscribe: 'Відписатися',
uploadNewAvatar: 'Завантажити новий аватар',
uploadNewImage: 'Завантажити нове зображення',
},
},
};
+8
View File
@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'ua',
country: 'ua',
name: 'Українська',
embeddedLocale: login,
};
+22
View File
@@ -0,0 +1,22 @@
export default {
translation: {
common: {
emailOrUsername: "Електронна пошта або ім'я користувача",
invalidEmailOrUsername: "Неправильна електронна пошта або ім'я користувача",
invalidPassword: 'Неправильний пароль',
logInToPlanka: 'Увійти в Planka',
noInternetConnection: 'Відсутнє підключення до Інтернету',
pageNotFound_title: 'Сторінку не знайдено',
password: 'Пароль',
projectManagement: 'Управління проектами',
serverConnectionFailed: 'Не вдалося підключитися до сервера',
unknownError: 'Невідома помилка, спробуйте ще раз пізніше',
useSingleSignOn: 'Використовувати одночасний вхід',
},
action: {
logIn: 'Увійти',
logInWithSSO: 'Увійти за допомогою SSO',
},
},
};
+7 -5
View File
@@ -5,6 +5,8 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'at' p",
fullDate: 'MMM d, y',
fullDateTime: "MMMM d, y 'at' p",
},
translation: {
@@ -19,7 +21,7 @@ export default {
administrator: 'Administrator',
all: 'Barchasi',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
"Barcha o'zgarishlar tarmoq ulanishi tiklangandan so'ng<br /> avtomatik saqlanadi",
"Barcha o'zgarishlar tarmoq ulanishi tiklangandan so'ng<br />avtomatik saqlanadi.",
areYouSureYouWantToDeleteThisAttachment: "Ushbu biriktirmani o'chirmoqchimisiz?",
areYouSureYouWantToDeleteThisBoard: "Ushbu doskani o'chirmoqchimisiz?",
areYouSureYouWantToDeleteThisCard: "Ushbu kartani o'chirmoqchimisiz?",
@@ -42,11 +44,11 @@ export default {
boardNotFound_title: 'Doska Topilmadi',
cardActions_title: 'Karta Amallari',
cardNotFound_title: 'Karta Topilmadi',
cardOrActionAreDeleted: "Karta yoki amal o'chirilgan",
cardOrActionAreDeleted: "Karta yoki amal o'chirilgan.",
color: 'Rang',
createBoard_title: 'Doska Yaratish',
createLabel_title: 'Yorliq Yaratish',
createNewOneOrSelectExistingOne: "Yangisini tanlash yoki<br /> mavjud bo'lganini tanlash",
createNewOneOrSelectExistingOne: "Yangisini tanlash yoki<br />mavjud bo'lganini tanlash.",
createProject_title: 'Loyiha Yaratish',
createTextFile_title: 'Matnli fayl yaratish',
currentPassword: 'Hozirgi parol',
@@ -105,14 +107,14 @@ export default {
noLists: "Ro'yxatlar yo'q",
noProjects: "Loyihalar yo'q",
notifications: 'Bildirishnomalar',
noUnreadNotifications: "O'qilmagan bildirishnomalar yo'q",
noUnreadNotifications: "O'qilmagan bildirishnomalar yo'q.",
openBoard_title: 'Doskani Ochish',
optional_inline: 'ixtiyoriy',
organization: 'Tashkilot',
phone: 'Telefon',
preferences: 'Afzalliklar',
pressPasteShortcutToAddAttachmentFromClipboard:
"Tip: Buferdan ilova qo'shish uchun Ctrl-V (Mac da Cmd-V) ni bosing",
"Tip: Buferdan ilova qo'shish uchun Ctrl-V (Mac da Cmd-V) ni bosing.",
project: 'Loyiha',
projectNotFound_title: 'Loyiha Topilmadi',
removeManager_title: "Boshqaruvchini O'chirish",
+27 -1
View File
@@ -5,10 +5,13 @@ export default {
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'at' p",
fullDate: 'MMM d, y',
fullDateTime: "MMMM d, y 'at' p",
},
translation: {
common: {
aboutPlanka: '关于 Planka',
account: '账号',
actions: '操作',
addAttachment_title: '添加附件',
@@ -38,6 +41,9 @@ export default {
background: '背景',
board: '面板',
boardNotFound_title: '面板不存在',
canComment: '可以评论',
canEditContentOfBoard: '可以编辑面板内容',
canOnlyViewBoard: '仅可查看面板',
cardActions_title: '卡片操作',
cardNotFound_title: '卡片不存在',
cardOrActionAreDeleted: '卡片或操作已经被删除',
@@ -61,14 +67,18 @@ export default {
deleteTask_title: '删除任务',
deleteUser_title: '删除用户',
description: '描述',
detectAutomatically: '自动删除',
dropFileToUpload: '拖放文件以上传',
editor: '编辑器',
editAttachment_title: '编辑附件',
editAvatar_title: '编辑头像',
editBoard_title: '编辑面板',
editDueDate_title: '编辑截止时间',
editEmail_title: '编辑邮箱',
editInformation_title: '编辑信息',
editLabel_title: '编辑标签',
editPassword_title: '修改密码',
editPermissions_title: '修改权限',
editStopwatch_title: '修改时间',
editUsername_title: '修改用户名',
email: '邮箱',
@@ -81,11 +91,14 @@ export default {
enterTaskDescription: '输入任务描述...',
filterByLabels_title: '通过标签筛选',
filterByMembers_title: '通过成员筛选',
fromComputer_title: '从本机',
fromComputer_title: '来自计算机',
fromTrello: '来自 Trello',
general: '全体',
hours: '小时',
importBoard_title: '导入面板',
invalidCurrentPassword: '当前密码错误',
labels: '标签',
language: '语言',
leaveBoard_title: '离开面板',
leaveProject_title: '离开项目',
list: '列表',
@@ -115,15 +128,20 @@ export default {
projectNotFound_title: '项目未找到',
removeManager_title: '删除管理员',
removeMember_title: '删除成员',
searchLabels: '搜索标签...',
searchMembers: '搜索成员...',
searchUsers: '搜索用户...',
seconds: '秒',
selectBoard: '选择面板',
selectList: '选择列表',
selectPermissions_title: '选择权限',
selectProject: '选择项目',
settings: '设置',
stopwatch: '计时器',
subscribeToMyOwnCardsByDefault: '默认关注自己创建的卡片',
taskActions_title: '任务操作',
tasks: '任务',
thereIsNoPreviewAvailableForThisAttachment: '此附件无法预览',
time: '时间',
title: '标题',
userActions_title: '用户操作',
@@ -136,6 +154,8 @@ export default {
username: '用户名',
usernameAlreadyInUse: '用户名已被占用',
users: '用户',
version: '版本',
viewer: '视图',
writeComment: '编写评论...',
},
@@ -147,6 +167,7 @@ export default {
addCard_title: '添加卡片',
addComment: '添加评论',
addList: '添加列表',
addMember: 'Add member',
addMoreDetailedDescription: '添加更多详细描述',
addTask: '添加任务',
addToCard: '添加到卡片',
@@ -176,10 +197,14 @@ export default {
editDueDate_title: '编辑到期时间',
editDescription_title: '编辑描述',
editEmail_title: '编辑邮箱',
editInformation_title: '编辑信息',
editPassword_title: '编辑密码',
editPermissions: '编辑权限',
editStopwatch_title: '编辑时间',
editTitle_title: '编辑标题',
editUsername_title: '编辑用户名',
hideDetails: '隐藏详情',
import: '导入',
leaveBoard: '离开面板',
leaveProject: '离开项目',
logOut_title: '退出',
@@ -195,6 +220,7 @@ export default {
removeMember: '删除成员',
save: '保存',
showAllAttachments: '显示所有的附件 ({{hidden}} 隐藏)',
showDetails: '显示详情',
showFewerAttachments: '显示较少的附件',
start: '开始',
stop: '结束',
+5 -3
View File
@@ -2,19 +2,21 @@ export default {
translation: {
common: {
emailOrUsername: '邮箱或用户名',
invalidEmailOrUsername: '错误的邮箱或用户名',
invalidEmailOrUsername: '无效的邮箱或用户名',
invalidPassword: '密码错误',
logInToPlanka: '登录至 Planka',
noInternetConnection: '没有网络连接',
pageNotFound_title: '页面找不到',
pageNotFound_title: '找不到页面',
password: '密码',
projectManagement: '项目管理',
serverConnectionFailed: '服务连接失败',
serverConnectionFailed: '服务器连接失败',
unknownError: '未知错误,请稍后重试',
useSingleSignOn: '使用单点登录',
},
action: {
logIn: '登录',
logInWithSSO: '使用SSO登录',
},
},
};
+3 -1
View File
@@ -45,7 +45,9 @@ export default class extends BaseModel {
subscribeToOwnCards: attr(),
isAdmin: attr(),
isLocked: attr(),
isLockedAdmin: attr(),
isRoleLocked: attr(),
isUsernameLocked: attr(),
isDeletionLocked: attr(),
deletedAt: attr(),
createdAt: attr({
getDefault: () => new Date(),
+1 -1
View File
@@ -10,7 +10,7 @@ const initialState = {
export default (state = initialState, { type, payload }) => {
switch (type) {
case ActionTypes.AUTHENTICATE__SUCCESS:
case ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS:
case ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS:
return {
...state,
accessToken: payload.accessToken,
+6 -4
View File
@@ -15,7 +15,7 @@ export default (state = initialState, { type, payload }) => {
config: payload.config,
};
case ActionTypes.AUTHENTICATE__SUCCESS:
case ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS:
case ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS:
return {
...state,
isInitializing: true,
@@ -24,9 +24,11 @@ export default (state = initialState, { type, payload }) => {
return {
...state,
isInitializing: false,
...(payload.config && {
config: payload.config,
}),
};
case ActionTypes.CORE_INITIALIZE__CONFIG_FETCH:
return {
...state,
config: payload.config,
};
default:
return state;
+5 -5
View File
@@ -9,7 +9,7 @@ const initialState = {
password: '',
},
isSubmitting: false,
isSubmittingWithOidc: false,
isSubmittingUsingOidc: false,
error: null,
};
@@ -20,7 +20,7 @@ export default (state = initialState, { type, payload }) => {
if (payload.location.pathname === Paths.OIDC_CALLBACK) {
return {
...state,
isSubmittingWithOidc: true,
isSubmittingUsingOidc: true,
};
}
@@ -35,7 +35,7 @@ export default (state = initialState, { type, payload }) => {
isSubmitting: true,
};
case ActionTypes.AUTHENTICATE__SUCCESS:
case ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS:
case ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS:
return initialState;
case ActionTypes.AUTHENTICATE__FAILURE:
return {
@@ -43,10 +43,10 @@ export default (state = initialState, { type, payload }) => {
isSubmitting: false,
error: payload.error,
};
case ActionTypes.WITH_OIDC_AUTHENTICATE__FAILURE:
case ActionTypes.USING_OIDC_AUTHENTICATE__FAILURE:
return {
...state,
isSubmittingWithOidc: false,
isSubmittingUsingOidc: false,
error: payload.error,
};
case ActionTypes.AUTHENTICATE_ERROR_CLEAR:
+10 -2
View File
@@ -1,7 +1,8 @@
import { all, apply, fork, take } from 'redux-saga/effects';
import { all, apply, fork, select, take } from 'redux-saga/effects';
import watchers from './watchers';
import services from './services';
import selectors from '../../selectors';
import { socket } from '../../api';
import ActionTypes from '../../constants/ActionTypes';
import Paths from '../../constants/Paths';
@@ -14,5 +15,12 @@ export default function* coreSaga() {
yield take(ActionTypes.LOGOUT);
window.location.href = Paths.LOGIN;
const oidcConfig = yield select(selectors.selectOidcConfig);
if (oidcConfig && oidcConfig.endSessionUrl !== null) {
// Redirect the user to the IDP to log out.
window.location.href = oidcConfig.endSessionUrl;
} else {
window.location.href = Paths.LOGIN;
}
}
+3 -13
View File
@@ -1,4 +1,4 @@
import { apply, call, put, select, take } from 'redux-saga/effects';
import { call, put, select, take } from 'redux-saga/effects';
import request from '../request';
import requests from '../requests';
@@ -6,7 +6,6 @@ import selectors from '../../../selectors';
import actions from '../../../actions';
import api from '../../../api';
import i18n from '../../../i18n';
import { createOidcManager } from '../../../utils/oidc-manager';
import { removeAccessToken } from '../../../utils/access-token-storage';
export function* initializeCore() {
@@ -15,6 +14,8 @@ export function* initializeCore() {
let config;
if (!currentConfig) {
({ item: config } = yield call(api.getConfig)); // TODO: handle error
yield put(actions.initializeCore.fetchConfig(config));
}
const {
@@ -41,7 +42,6 @@ export function* initializeCore() {
yield put(
actions.initializeCore(
config,
user,
board,
users,
@@ -84,16 +84,6 @@ export function* logout(invalidateAccessToken = true) {
} catch (error) {} // eslint-disable-line no-empty
}
const oidcConfig = yield select(selectors.selectOidcConfig);
if (oidcConfig) {
const oidcManager = createOidcManager(oidcConfig);
try {
yield apply(oidcManager, oidcManager.logout);
} catch (error) {} // eslint-disable-line no-empty
}
yield put(actions.logout());
yield take();
}
+39 -18
View File
@@ -14,24 +14,45 @@ export function* handleSocketReconnect() {
yield put(actions.handleSocketReconnect.fetchCore(currentUserId, boardId));
const {
user,
board,
users,
projects,
projectManagers,
boards,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
activities,
notifications,
} = yield call(requests.fetchCore); // TODO: handle error
let user;
let board;
let users;
let projects;
let projectManagers;
let boards;
let boardMemberships;
let labels;
let lists;
let cards;
let cardMemberships;
let cardLabels;
let tasks;
let attachments;
let activities;
let notifications;
try {
({
user,
board,
users,
projects,
projectManagers,
boards,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
activities,
notifications,
} = yield call(requests.fetchCore));
} catch (error) {
return;
}
yield put(
actions.handleSocketReconnect(
+1 -1
View File
@@ -9,7 +9,7 @@ export default function* loginSaga() {
yield fork(services.initializeLogin);
yield take([ActionTypes.AUTHENTICATE__SUCCESS, ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS]);
yield take([ActionTypes.AUTHENTICATE__SUCCESS, ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS]);
yield cancel(watcherTasks);
yield call(services.goToRoot);
+73 -29
View File
@@ -1,10 +1,10 @@
import { apply, call, put, select } from 'redux-saga/effects';
import { nanoid } from 'nanoid';
import { call, put, select } from 'redux-saga/effects';
import { replace } from '../../../lib/redux-router';
import selectors from '../../../selectors';
import actions from '../../../actions';
import api from '../../../api';
import { createOidcManager } from '../../../utils/oidc-manager';
import { setAccessToken } from '../../../utils/access-token-storage';
import Paths from '../../../constants/Paths';
@@ -29,40 +29,84 @@ export function* authenticate(data) {
yield put(actions.authenticate.success(accessToken));
}
export function* authenticateWithOidc() {
export function* authenticateUsingOidc() {
const oidcConfig = yield select(selectors.selectOidcConfig);
const oidcManager = createOidcManager(oidcConfig);
yield apply(oidcManager, oidcManager.login);
const state = nanoid();
window.sessionStorage.setItem('oidc-state', state);
const nonce = nanoid();
window.sessionStorage.setItem('oidc-nonce', nonce);
let redirectUrl = `${oidcConfig.authorizationUrl}`;
redirectUrl += `&state=${encodeURIComponent(state)}`;
redirectUrl += `&nonce=${encodeURIComponent(nonce)}`;
window.location.href = redirectUrl;
}
export function* authenticateWithOidcCallback() {
const oidcConfig = yield select(selectors.selectOidcConfig);
const oidcManager = createOidcManager(oidcConfig);
export function* authenticateUsingOidcCallback() {
// https://github.com/plankanban/planka/issues/511#issuecomment-1771385639
const params = new URLSearchParams(window.location.hash.substring(1) || window.location.search);
let oidcToken;
try {
({ access_token: oidcToken } = yield apply(oidcManager, oidcManager.loginCallback));
} catch (error) {
yield put(actions.authenticateWithOidc.failure(error));
}
const state = window.sessionStorage.getItem('oidc-state');
window.sessionStorage.removeItem('oidc-state');
const nonce = window.sessionStorage.getItem('oidc-nonce');
window.sessionStorage.removeItem('oidc-nonce');
yield put(replace(Paths.LOGIN));
if (oidcToken) {
let accessToken;
try {
({ item: accessToken } = yield call(api.exchangeToAccessToken, {
token: oidcToken,
}));
} catch (error) {
yield put(actions.authenticateWithOidc.failure(error));
return;
}
yield call(setAccessToken, accessToken);
yield put(actions.authenticateWithOidc.success(accessToken));
if (params.get('error') !== null) {
yield put(
actions.authenticateUsingOidc.failure(
new Error(
`OIDC Authorization error: ${params.get('error')}: ${params.get('error_description')}`,
),
),
);
return;
}
const code = params.get('code');
if (code === null) {
yield put(
actions.authenticateUsingOidc.failure(new Error('Invalid OIDC response: no code parameter')),
);
return;
}
if (params.get('state') !== state) {
yield put(
actions.authenticateUsingOidc.failure(
new Error('Unable to process OIDC response: state mismatch'),
),
);
return;
}
if (nonce === null) {
yield put(
actions.authenticateUsingOidc.failure(
new Error('Unable to process OIDC response: no nonce issued'),
),
);
return;
}
let accessToken;
try {
({ item: accessToken } = yield call(api.exchangeForAccessTokenUsingOidc, {
code,
nonce,
}));
} catch (error) {
yield put(actions.authenticateUsingOidc.failure(error));
return;
}
yield call(setAccessToken, accessToken);
yield put(actions.authenticateUsingOidc.success(accessToken));
}
export function* clearAuthenticateError() {
@@ -72,7 +116,7 @@ export function* clearAuthenticateError() {
export default {
initializeLogin,
authenticate,
authenticateWithOidc,
authenticateWithOidcCallback,
authenticateUsingOidc,
authenticateUsingOidcCallback,
clearAuthenticateError,
};
+2 -4
View File
@@ -1,7 +1,7 @@
import { call, put, select, take } from 'redux-saga/effects';
import { push } from '../../../lib/redux-router';
import { authenticateWithOidcCallback } from './login';
import { authenticateUsingOidcCallback } from './login';
import selectors from '../../../selectors';
import ActionTypes from '../../../constants/ActionTypes';
import Paths from '../../../constants/Paths';
@@ -36,9 +36,7 @@ export function* handleLocationChange() {
yield take(ActionTypes.LOGIN_INITIALIZE);
}
// TODO: check if OIDC is enabled
yield call(authenticateWithOidcCallback);
yield call(authenticateUsingOidcCallback);
break;
}
+1 -1
View File
@@ -8,7 +8,7 @@ export default function* loginWatchers() {
takeEvery(EntryActionTypes.AUTHENTICATE, ({ payload: { data } }) =>
services.authenticate(data),
),
takeEvery(EntryActionTypes.WITH_OIDC_AUTHENTICATE, () => services.authenticateWithOidc()),
takeEvery(EntryActionTypes.USING_OIDC_AUTHENTICATE, () => services.authenticateUsingOidc()),
takeEvery(EntryActionTypes.AUTHENTICATE_ERROR_CLEAR, () => services.clearAuthenticateError()),
]);
}
+1 -1
View File
@@ -1,5 +1,5 @@
import Cookies from 'js-cookie';
import jwtDecode from 'jwt-decode';
import { jwtDecode } from 'jwt-decode';
import Config from '../constants/Config';
+6
View File
@@ -0,0 +1,6 @@
export default (value, longDateFormat = 'longDateTime', fullDateFormat = 'fullDateTime') => {
const year = value.getFullYear();
const currentYear = new Date().getFullYear();
return year === currentYear ? longDateFormat : fullDateFormat;
};
-27
View File
@@ -1,27 +0,0 @@
import { UserManager } from 'oidc-client-ts';
export default class OidcManager {
constructor(config) {
this.userManager = new UserManager({
...config,
authority: config.issuer,
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: config.scopes,
});
}
login() {
return this.userManager.signinRedirect();
}
loginCallback() {
return this.userManager.signinRedirectCallback();
}
logout() {
return this.userManager.signoutSilent();
}
}
export const createOidcManager = (config) => new OidcManager(config);
+16
View File
@@ -0,0 +1,16 @@
version: '3'
services:
postgres:
image: postgres:alpine
restart: unless-stopped
volumes:
- db-data:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
- POSTGRES_DB=planka
- POSTGRES_HOST_AUTH_METHOD=trust
volumes:
db-data:
+52 -3
View File
@@ -1,16 +1,65 @@
version: '3'
services:
planka:
image: ghcr.io/plankanban/planka:master
command: >
bash -c
"for i in `seq 1 30`; do
./start.sh &&
s=$$? && break || s=$$?;
echo \"Tried $$i times. Waiting 5 seconds...\";
sleep 5;
done; (exit $$s)"
restart: unless-stopped
volumes:
- user-avatars:/app/public/user-avatars
- project-background-images:/app/public/project-background-images
- attachments:/app/private/attachments
ports:
- 3000:1337
environment:
- BASE_URL=http://localhost:3000
- DATABASE_URL=postgresql://postgres@postgres/planka
- SECRET_KEY=notsecretkey
# - TRUST_PROXY=0
# - TOKEN_EXPIRES_IN=365 # In days
# related: https://github.com/knex/knex/issues/2354
# As knex does not pass query parameters from the connection string we
# have to use environment variables in order to pass the desired values, e.g.
# - PGSSLMODE=<value>
# Configure knex to accept SSL certificates
# - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
# - DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
# - DEFAULT_ADMIN_PASSWORD=demo
# - DEFAULT_ADMIN_NAME=Demo Demo
# - DEFAULT_ADMIN_USERNAME=demo
# - OIDC_ISSUER=
# - OIDC_CLIENT_ID=
# - OIDC_CLIENT_SECRET=
# - OIDC_SCOPES=openid email profile
# - OIDC_ADMIN_ROLES=admin
# - OIDC_ROLES_ATTRIBUTE=groups
# - OIDC_IGNORE_ROLES=true
depends_on:
- postgres
postgres:
image: postgres:alpine
image: postgres:14-alpine
restart: unless-stopped
volumes:
- db-data:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
- POSTGRES_DB=planka
- POSTGRES_HOST_AUTH_METHOD=trust
volumes:
user-avatars:
project-background-images:
attachments:
db-data:
+14 -10
View File
@@ -20,15 +20,11 @@ services:
- 3000:1337
environment:
- BASE_URL=http://localhost:3000
- TRUST_PROXY=0
- DATABASE_URL=postgresql://postgres@postgres/planka
- SECRET_KEY=notsecretkey
# Can be removed after installation
- DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
- DEFAULT_ADMIN_PASSWORD=demo
- DEFAULT_ADMIN_NAME=Demo Demo
- DEFAULT_ADMIN_USERNAME=demo
# - TRUST_PROXY=0
# - TOKEN_EXPIRES_IN=365 # In days
# related: https://github.com/knex/knex/issues/2354
# As knex does not pass query parameters from the connection string we
@@ -38,15 +34,23 @@ services:
# Configure knex to accept SSL certificates
# - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
# - DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
# - DEFAULT_ADMIN_PASSWORD=demo
# - DEFAULT_ADMIN_NAME=Demo Demo
# - DEFAULT_ADMIN_USERNAME=demo
# - OIDC_ISSUER=
# - OIDC_CLIENT_ID=
# - OIDC_REDIRECT_URI=http://localhost:3000/oidc-callback
# - OIDC_CLIENT_SECRET=
# - OIDC_SCOPES=openid email profile
# - OIDC_JWKS_URI=
# - OIDC_AUDIENCE=
# - OIDC_ADMIN_ROLES=admin
# - OIDC_EMAIL_ATTRIBUTE=email
# - OIDC_NAME_ATTRIBUTE=name
# - OIDC_USERNAME_ATTRIBUTE=preferred_username
# - OIDC_ROLES_ATTRIBUTE=groups
# - OIDC_SKIP_USER_INFO=true
# - OIDC_IGNORE_USERNAME=true
# - OIDC_IGNORE_ROLES=true
# - OIDC_ENFORCED=true
depends_on:
- postgres
+1383 -826
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.13.1",
"version": "1.15.6",
"private": true,
"homepage": "https://plankanban.github.io/planka",
"repository": {
@@ -56,14 +56,14 @@
}
},
"dependencies": {
"concurrently": "^7.6.0",
"husky": "^8.0.2",
"lint-staged": "^13.0.3"
"concurrently": "^8.2.2",
"husky": "^8.0.3",
"lint-staged": "^15.1.0"
},
"devDependencies": {
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.7.1"
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.1.0"
}
}
+12 -11
View File
@@ -4,13 +4,6 @@ BASE_URL=http://localhost:1337
DATABASE_URL=postgresql://postgres@localhost/planka
SECRET_KEY=notsecretkey
## Can be removed after installation
DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
DEFAULT_ADMIN_PASSWORD=demo
DEFAULT_ADMIN_NAME=Demo Demo
DEFAULT_ADMIN_USERNAME=demo
## Optional
# TRUST_PROXY=0
@@ -24,15 +17,23 @@ DEFAULT_ADMIN_USERNAME=demo
# Configure knex to accept SSL certificates
# KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
# DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
# DEFAULT_ADMIN_PASSWORD=demo
# DEFAULT_ADMIN_NAME=Demo Demo
# DEFAULT_ADMIN_USERNAME=demo
# OIDC_ISSUER=
# OIDC_CLIENT_ID=
# OIDC_REDIRECT_URI=http://localhost:1337/oidc-callback
# OIDC_CLIENT_SECRET=
# OIDC_SCOPES=openid email profile
# OIDC_JWKS_URI=
# OIDC_AUDIENCE=
# OIDC_ADMIN_ROLES=admin
# OIDC_EMAIL_ATTRIBUTE=email
# OIDC_NAME_ATTRIBUTE=name
# OIDC_USERNAME_ATTRIBUTE=preferred_username
# OIDC_ROLES_ATTRIBUTE=groups
# OIDC_SKIP_USER_INFO=true
# OIDC_IGNORE_USERNAME=true
# OIDC_IGNORE_ROLES=true
# OIDC_ENFORCED=true
## Do not edit this
@@ -46,8 +46,11 @@ module.exports = {
},
async fn(inputs) {
const remoteAddress = getRemoteAddress(this.req);
if (sails.config.custom.oidcEnforced) {
throw Errors.USE_SINGLE_SIGN_ON;
}
const remoteAddress = getRemoteAddress(this.req);
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
if (!user) {
@@ -1,8 +1,8 @@
const { getRemoteAddress } = require('../../../utils/remoteAddress');
const Errors = {
INVALID_TOKEN: {
invalidToken: 'Invalid token',
INVALID_CODE_OR_NONCE: {
invalidCodeOrNonce: 'Invalid code or nonce',
},
EMAIL_ALREADY_IN_USE: {
emailAlreadyInUse: 'Email already in use',
@@ -17,14 +17,18 @@ const Errors = {
module.exports = {
inputs: {
token: {
code: {
type: 'string',
required: true,
},
nonce: {
type: 'string',
required: true,
},
},
exits: {
invalidToken: {
invalidCodeOrNonce: {
responseType: 'unauthorized',
},
emailAlreadyInUse: {
@@ -42,10 +46,10 @@ module.exports = {
const remoteAddress = getRemoteAddress(this.req);
const user = await sails.helpers.users
.getOrCreateOneByOidcToken(inputs.token)
.intercept('invalidToken', () => {
sails.log.warn(`Invalid token! (IP: ${remoteAddress})`);
return Errors.INVALID_TOKEN;
.getOrCreateOneUsingOidc(inputs.code, inputs.nonce)
.intercept('invalidCodeOrNonce', () => {
sails.log.warn(`Invalid code or nonce! (IP: ${remoteAddress})`);
return Errors.INVALID_CODE_OR_NONCE;
})
.intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE)
.intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE)
+15 -8
View File
@@ -1,15 +1,22 @@
module.exports = {
fn() {
let oidc = null;
if (sails.hooks.oidc.isActive()) {
const oidcClient = sails.hooks.oidc.getClient();
oidc = {
authorizationUrl: oidcClient.authorizationUrl({
scope: sails.config.custom.oidcScopes,
response_mode: 'fragment',
}),
endSessionUrl: oidcClient.issuer.end_session_endpoint ? oidcClient.endSessionUrl({}) : null,
isEnforced: sails.config.custom.oidcEnforced,
};
}
return {
item: {
oidc: sails.config.custom.oidcIssuer
? {
issuer: sails.config.custom.oidcIssuer,
clientId: sails.config.custom.oidcClientId,
redirectUri: sails.config.custom.oidcRedirectUri,
scopes: sails.config.custom.oidcScopes,
}
: null,
oidc,
},
};
},
+10
View File
@@ -1,6 +1,9 @@
const zxcvbn = require('zxcvbn');
const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
EMAIL_ALREADY_IN_USE: {
emailAlreadyInUse: 'Email already in use',
},
@@ -56,6 +59,9 @@ module.exports = {
},
exits: {
notEnoughRights: {
responseType: 'forbidden',
},
emailAlreadyInUse: {
responseType: 'conflict',
},
@@ -65,6 +71,10 @@ module.exports = {
},
async fn(inputs) {
if (sails.config.custom.oidcEnforced) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, [
'email',
'password',
+10 -11
View File
@@ -53,11 +53,7 @@ module.exports = {
async fn(inputs) {
const { currentUser } = this.req;
if (inputs.id === currentUser.id) {
if (!inputs.currentPassword) {
throw Errors.INVALID_CURRENT_PASSWORD;
}
} else if (!currentUser.isAdmin) {
if (inputs.id !== currentUser.id && !currentUser.isAdmin) {
throw Errors.USER_NOT_FOUND; // Forbidden
}
@@ -67,15 +63,18 @@ module.exports = {
throw Errors.USER_NOT_FOUND;
}
if (user.email === sails.config.custom.defaultAdminEmail || user.isSso) {
if (user.email === sails.config.custom.defaultAdminEmail) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
if (
inputs.id === currentUser.id &&
!bcrypt.compareSync(inputs.currentPassword, user.password)
) {
throw Errors.INVALID_CURRENT_PASSWORD;
if (user.isSso) {
if (!sails.config.custom.oidcIgnoreUsername) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
} else if (inputs.id === currentUser.id) {
if (!inputs.currentPassword || !bcrypt.compareSync(inputs.currentPassword, user.password)) {
throw Errors.INVALID_CURRENT_PASSWORD;
}
}
const values = _.pick(inputs, ['username']);
+4
View File
@@ -73,6 +73,10 @@ module.exports = {
delete inputs.name;
/* eslint-enable no-param-reassign */
} else if (user.isSso) {
if (!sails.config.custom.oidcIgnoreRoles) {
delete inputs.isAdmin; // eslint-disable-line no-param-reassign
}
delete inputs.name; // eslint-disable-line no-param-reassign
}
@@ -1,159 +0,0 @@
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const openidClient = require('openid-client');
const jwks = jwksClient({
jwksUri: sails.config.custom.oidcJwksUri,
});
const verifyOidcToken = async (oidcToken) => {
const signingKeys = await jwks.getSigningKeys();
const options = {
issuer: sails.config.custom.oidcIssuer,
};
if (sails.config.custom.oidcAudience) {
options.audience = sails.config.custom.oidcAudience;
}
let payload = null;
signingKeys.some((signingKey) => {
try {
const publicKey = signingKey.getPublicKey();
payload = jwt.verify(oidcToken, publicKey, options);
} catch (error) {
sails.log.error(error);
}
return !!payload;
});
return payload;
};
const getUserInfo = async (oidcToken) => {
const issuer = await openidClient.Issuer.discover(sails.config.custom.oidcIssuer);
const client = new issuer.Client({
client_id: 'irrelevant',
});
return client.userinfo(oidcToken);
};
module.exports = {
inputs: {
token: {
type: 'string',
required: true,
},
},
exits: {
invalidToken: {},
missingValues: {},
emailAlreadyInUse: {},
usernameAlreadyInUse: {},
},
async fn(inputs) {
const oidcUser = await verifyOidcToken(inputs.token);
if (!oidcUser) {
throw 'invalidToken';
}
if (!sails.config.custom.oidcSkipUserInfo) {
const userInfo = await getUserInfo(inputs.token);
Object.assign(oidcUser, userInfo);
}
if (!oidcUser.email || !oidcUser.name) {
throw 'missingValues';
}
let isAdmin = false;
if (sails.config.custom.oidcAdminRoles.includes('*')) {
isAdmin = true;
} else {
const roles = oidcUser[sails.config.custom.oidcRolesAttribute];
if (Array.isArray(roles)) {
isAdmin = sails.config.custom.oidcAdminRoles.some((role) => roles.includes(role));
}
}
const values = {
isAdmin,
email: oidcUser.email,
isSso: true,
name: oidcUser.name,
username: oidcUser.preferred_username,
subscribeToOwnCards: false,
};
let user;
/* eslint-disable no-constant-condition, no-await-in-loop */
while (true) {
let identityProviderUser = await IdentityProviderUser.findOne({
issuer: oidcUser.iss,
sub: oidcUser.sub,
});
if (identityProviderUser) {
user = await sails.helpers.users.getOne(identityProviderUser.userId);
} else {
while (true) {
user = await sails.helpers.users.getOne({
email: values.email,
});
if (!user) {
user = await sails.helpers.users
.createOne(values)
.tolerate('emailAlreadyInUse')
.intercept('usernameAlreadyInUse', 'usernameAlreadyInUse');
}
if (user) {
break;
}
}
identityProviderUser = await IdentityProviderUser.create({
userId: user.id,
issuer: oidcUser.iss,
sub: oidcUser.sub,
}).tolerate('E_UNIQUE');
}
if (identityProviderUser) {
break;
}
}
/* eslint-enable no-constant-condition, no-await-in-loop */
const updateFieldKeys = ['email', 'isAdmin', 'isSso', 'name', 'username'];
const updateValues = updateFieldKeys.reduce((result, fieldKey) => {
if (values[fieldKey] === user[fieldKey]) {
return result;
}
return {
...result,
[fieldKey]: values[fieldKey],
};
}, {});
if (Object.keys(updateValues).length > 0) {
user = await sails.helpers.users
.updateOne(user, updateValues, {}) // FIXME: hack for last parameter
.intercept('emailAlreadyInUse', 'emailAlreadyInUse')
.intercept('usernameAlreadyInUse', 'usernameAlreadyInUse');
}
return user;
},
};
@@ -0,0 +1,126 @@
module.exports = {
inputs: {
code: {
type: 'string',
required: true,
},
nonce: {
type: 'string',
required: true,
},
},
exits: {
invalidCodeOrNonce: {},
missingValues: {},
emailAlreadyInUse: {},
usernameAlreadyInUse: {},
},
async fn(inputs) {
const client = sails.hooks.oidc.getClient();
let userInfo;
try {
const tokenSet = await client.callback(
sails.config.custom.oidcRedirectUri,
{
iss: sails.config.custom.oidcIssuer,
code: inputs.code,
},
{
nonce: inputs.nonce,
},
);
userInfo = await client.userinfo(tokenSet);
} catch (e) {
sails.log.warn(`Error while exchanging OIDC code: ${e}`);
throw 'invalidCodeOrNonce';
}
if (
!userInfo[sails.config.custom.oidcEmailAttribute] ||
!userInfo[sails.config.custom.oidcNameAttribute]
) {
throw 'missingValues';
}
let isAdmin = false;
if (sails.config.custom.oidcAdminRoles.includes('*')) {
isAdmin = true;
} else {
const roles = userInfo[sails.config.custom.oidcRolesAttribute];
if (Array.isArray(roles)) {
// Use a Set here to avoid quadratic time complexity
const userRoles = new Set(userInfo[sails.config.custom.oidcRolesAttribute]);
isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1;
}
}
const values = {
isAdmin,
email: userInfo[sails.config.custom.oidcEmailAttribute],
isSso: true,
name: userInfo[sails.config.custom.oidcNameAttribute],
subscribeToOwnCards: false,
};
if (!sails.config.custom.oidcIgnoreUsername) {
values.username = userInfo[sails.config.custom.oidcUsernameAttribute];
}
let user;
// This whole block technically needs to be executed in a transaction
// with SERIALIZABLE isolation level (but Waterline does not support
// that), so this will result in errors if for example users are deleted
// concurrently with logging in via OIDC.
let identityProviderUser = await IdentityProviderUser.findOne({
issuer: sails.config.custom.oidcIssuer,
sub: userInfo.sub,
});
if (identityProviderUser) {
user = await sails.helpers.users.getOne(identityProviderUser.userId);
} else {
// If no IDP/User mapping exists, search for the user by email.
user = await sails.helpers.users.getOne({
email: values.email.toLowerCase(),
});
// Otherwise, create a new user.
if (!user) {
user = await sails.helpers.users
.createOne(values)
.intercept('usernameAlreadyInUse', 'usernameAlreadyInUse');
}
identityProviderUser = await IdentityProviderUser.create({
userId: user.id,
issuer: sails.config.custom.oidcIssuer,
sub: userInfo.sub,
});
}
const updateFieldKeys = ['email', 'isSso', 'name'];
if (!sails.config.custom.oidcIgnoreUsername) {
updateFieldKeys.push('username');
}
if (!sails.config.custom.oidcIgnoreRoles) {
updateFieldKeys.push('isAdmin');
}
const updateValues = {};
// eslint-disable-next-line no-restricted-syntax
for (const k of updateFieldKeys) {
if (values[k] !== user[k]) updateValues[k] = values[k];
}
if (Object.keys(updateValues).length > 0) {
user = await sails.helpers.users
.updateOne(user, updateValues, {}) // FIXME: hack for last parameter
.intercept('emailAlreadyInUse', 'emailAlreadyInUse')
.intercept('usernameAlreadyInUse', 'usernameAlreadyInUse');
}
return user;
},
};
+33
View File
@@ -0,0 +1,33 @@
const openidClient = require('openid-client');
module.exports = function oidcServiceHook(sails) {
let client = null;
return {
/**
* Runs when this Sails app loads/lifts.
*/
async initialize() {
if (sails.config.custom.oidcIssuer) {
const issuer = await openidClient.Issuer.discover(sails.config.custom.oidcIssuer);
client = new issuer.Client({
client_id: sails.config.custom.oidcClientId,
client_secret: sails.config.custom.oidcClientSecret,
redirect_uris: [sails.config.custom.oidcRedirectUri],
response_types: ['code'],
});
sails.log.info('OIDC hook has been loaded successfully');
}
},
getClient() {
return client;
},
isActive() {
return client !== null;
},
};
};
+5 -3
View File
@@ -110,12 +110,14 @@ module.exports = {
tableName: 'user_account',
customToJSON() {
const isLockedAdmin = this.email === sails.config.custom.defaultAdminEmail;
const isDefaultAdmin = this.email === sails.config.custom.defaultAdminEmail;
return {
..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']),
isLockedAdmin,
isLocked: this.isSso || isLockedAdmin,
isLocked: this.isSso || isDefaultAdmin,
isRoleLocked: (this.isSso && !sails.config.custom.oidcIgnoreRoles) || isDefaultAdmin,
isUsernameLocked: (this.isSso && !sails.config.custom.oidcIgnoreUsername) || isDefaultAdmin,
isDeletionLocked: isDefaultAdmin,
avatarUrl:
this.avatar &&
`${sails.config.custom.userAvatarsUrl}/${this.avatar.dirname}/square-100.${this.avatar.extension}`,
+12 -4
View File
@@ -35,11 +35,19 @@ module.exports.custom = {
oidcIssuer: process.env.OIDC_ISSUER,
oidcClientId: process.env.OIDC_CLIENT_ID,
oidcRedirectUri: process.env.OIDC_REDIRECT_URI,
oidcClientSecret: process.env.OIDC_CLIENT_SECRET,
oidcScopes: process.env.OIDC_SCOPES || 'openid email profile',
oidcJwksUri: process.env.OIDC_JWKS_URI,
oidcAudience: process.env.OIDC_AUDIENCE,
oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
oidcEmailAttribute: process.env.OIDC_EMAIL_ATTRIBUTE || 'email',
oidcNameAttribute: process.env.OIDC_NAME_ATTRIBUTE || 'name',
oidcUsernameAttribute: process.env.OIDC_USERNAME_ATTRIBUTE || 'preferred_username',
oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
oidcSkipUserInfo: process.env.OIDC_SKIP_USER_INFO === 'true',
oidcIgnoreUsername: process.env.OIDC_IGNORE_USERNAME === 'true',
oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true',
oidcEnforced: process.env.OIDC_ENFORCED === 'true',
// TODO: move client base url to environment variable?
oidcRedirectUri: `${
sails.config.environment === 'production' ? process.env.BASE_URL : 'http://localhost:3000'
}/oidc-callback`,
};
+1 -1
View File
@@ -25,5 +25,5 @@ module.exports.policies = {
'show-config': true,
'access-tokens/create': true,
'access-tokens/exchange': true,
'access-tokens/exchange-using-oidc': true,
};
+1 -1
View File
@@ -12,7 +12,7 @@ module.exports.routes = {
'GET /api/config': 'show-config',
'POST /api/access-tokens': 'access-tokens/create',
'POST /api/access-tokens/exchange': 'access-tokens/exchange',
'POST /api/access-tokens/exchange-using-oidc': 'access-tokens/exchange-using-oidc',
'DELETE /api/access-tokens/me': 'access-tokens/delete',
'GET /api/users': 'users/index',
-4
View File
@@ -34,10 +34,6 @@ exports.seed = async (knex) => {
createdAt: new Date().toISOString(),
});
} catch (error) {
if (Object.keys(data).length === 0) {
return;
}
await knex('user_account').update(data).where('email', process.env.DEFAULT_ADMIN_EMAIL);
}
};
+2847 -1533
View File
File diff suppressed because it is too large Load Diff
+19 -20
View File
@@ -27,39 +27,38 @@
}
},
"dependencies": {
"bcrypt": "^5.1.0",
"dotenv": "^16.0.3",
"dotenv-cli": "^6.0.0",
"bcrypt": "^5.1.1",
"dotenv": "^16.3.1",
"dotenv-cli": "^7.3.0",
"filenamify": "^4.3.0",
"jsonwebtoken": "^9.0.0",
"jwks-rsa": "^3.0.1",
"knex": "^2.3.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.0.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"move-file": "^2.1.0",
"openid-client": "^5.4.3",
"rimraf": "^3.0.2",
"openid-client": "^5.6.1",
"rimraf": "^5.0.5",
"sails": "^1.5.7",
"sails-hook-orm": "^4.0.2",
"sails-hook-sockets": "^2.0.3",
"sails-hook-sockets": "^2.0.4",
"sails-postgresql-redacted": "^1.0.2-9",
"sharp": "^0.31.2",
"sharp": "^0.33.2",
"stream-to-array": "^2.3.0",
"uuid": "^9.0.0",
"validator": "^13.7.0",
"winston": "^3.8.2",
"uuid": "^9.0.1",
"validator": "^13.11.0",
"winston": "^3.11.0",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"chai": "^4.3.7",
"eslint": "^8.28.0",
"chai": "^4.3.10",
"eslint": "^8.53.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.26.0",
"mocha": "^10.1.0",
"nodemon": "^2.0.20",
"supertest": "^6.3.1"
"eslint-plugin-import": "^2.29.0",
"mocha": "^10.2.0",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"engines": {
"node": "^12.10"
"node": ">=18"
}
}