Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f2723623a | ||
|
|
fee6841d0a | ||
|
|
ca6fb3b962 | ||
|
|
28c3f28e01 | ||
|
|
80d12aaeeb | ||
|
|
89af209a2e | ||
|
|
f1fed3e533 | ||
|
|
eddeb96b16 | ||
|
|
9e8ff3b579 | ||
|
|
fc1278011c | ||
|
|
24163463fc | ||
|
|
fdef87e3b9 | ||
|
|
964498dbaa | ||
|
|
5b519cb62f | ||
|
|
11fc3b4485 | ||
|
|
37c0b59f82 | ||
|
|
239611ad51 | ||
|
|
0575e665fe | ||
|
|
019766dced | ||
|
|
6dc9e4ed99 | ||
|
|
d4b64b90fc | ||
|
|
1a49826b85 | ||
|
|
815643eb2c | ||
|
|
bcc4c0d923 | ||
|
|
cf7810b067 | ||
|
|
f1c72df8b6 | ||
|
|
5056d38848 | ||
|
|
3ef5b3ead8 | ||
|
|
ded58dc8b0 | ||
|
|
d8ee1ecdd1 | ||
|
|
fc2f4cd5d3 | ||
|
|
7ef457157e | ||
|
|
23ee815200 | ||
|
|
40c04c35ff | ||
|
|
743f2956c8 | ||
|
|
5bf5db27d8 | ||
|
|
667c5daf25 | ||
|
|
6662b0a717 | ||
|
|
348bf4d6c4 | ||
|
|
3ac2d289d0 | ||
|
|
3bc73c43ea | ||
|
|
8e0c60f5be | ||
|
|
8a4bdbd1fe | ||
|
|
5bf42be786 |
@@ -2,6 +2,11 @@ name: Build and push Docker DEV image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- 'charts/**'
|
||||||
|
- 'docker-*.sh'
|
||||||
|
- '*.md'
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -38,5 +38,6 @@ jobs:
|
|||||||
uses: helm/chart-releaser-action@v1.5.0
|
uses: helm/chart-releaser-action@v1.5.0
|
||||||
with:
|
with:
|
||||||
charts_dir: charts
|
charts_dir: charts
|
||||||
|
mark_as_latest: false
|
||||||
env:
|
env:
|
||||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|||||||
+11
-3
@@ -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
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -6,6 +10,7 @@ COPY server/package.json server/package-lock.json .
|
|||||||
|
|
||||||
RUN npm install npm@latest --global \
|
RUN npm install npm@latest --global \
|
||||||
&& npm install pnpm --global \
|
&& npm install pnpm --global \
|
||||||
|
&& pnpm import \
|
||||||
&& pnpm install --prod
|
&& pnpm install --prod
|
||||||
|
|
||||||
FROM node:lts AS client
|
FROM node:lts AS client
|
||||||
@@ -16,14 +21,17 @@ COPY client/package.json client/package-lock.json .
|
|||||||
|
|
||||||
RUN npm install npm@latest --global \
|
RUN npm install npm@latest --global \
|
||||||
&& npm install pnpm --global \
|
&& npm install pnpm --global \
|
||||||
|
&& pnpm import \
|
||||||
&& pnpm install --prod
|
&& pnpm install --prod
|
||||||
|
|
||||||
COPY client .
|
COPY client .
|
||||||
RUN DISABLE_ESLINT_PLUGIN=true npm run build
|
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
|
USER node
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
+12
-17
@@ -1,27 +1,22 @@
|
|||||||
FROM node:lts-alpine
|
FROM node:18-alpine
|
||||||
|
|
||||||
ARG ALPINE_VERSION=3.16
|
ARG VIPS_VERSION=8.14.5
|
||||||
ARG VIPS_VERSION=8.13.3
|
|
||||||
|
|
||||||
RUN apk -U upgrade \
|
RUN apk -U upgrade \
|
||||||
&& apk add \
|
&& apk add \
|
||||||
bash giflib glib lcms2 libexif \
|
bash pkgconf \
|
||||||
libgsf libjpeg-turbo libpng librsvg libwebp \
|
libjpeg-turbo libexif librsvg cgif tiff libspng libimagequant \
|
||||||
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/ \
|
|
||||||
--no-cache \
|
--no-cache \
|
||||||
&& apk add \
|
&& apk add \
|
||||||
build-base giflib-dev glib-dev lcms2-dev libexif-dev \
|
build-base gobject-introspection-dev meson \
|
||||||
libgsf-dev libjpeg-turbo-dev libpng-dev librsvg-dev libwebp-dev \
|
libjpeg-turbo-dev libexif-dev librsvg-dev cgif-dev tiff-dev libspng-dev libimagequant-dev \
|
||||||
orc-dev pango-dev tiff-dev \
|
|
||||||
--virtual vips-dependencies \
|
--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 \
|
--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} \
|
&& cd /tmp/vips-${VIPS_VERSION} \
|
||||||
&& ./configure \
|
&& meson setup build-dir \
|
||||||
&& make \
|
&& cd build-dir \
|
||||||
&& make install-strip \
|
&& ninja \
|
||||||
|
&& ninja test \
|
||||||
|
&& ninja install \
|
||||||
&& rm -rf /tmp/vips-${VIPS_VERSION}
|
&& rm -rf /tmp/vips-${VIPS_VERSION}
|
||||||
|
|||||||
@@ -20,10 +20,9 @@
|
|||||||
|
|
||||||
## How to deploy Planka
|
## How to deploy Planka
|
||||||
|
|
||||||
There are 2 types of installation:
|
There are many ways to install Planka
|
||||||
- [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)
|
[Check them out](https://docs.planka.cloud/docs/intro)
|
||||||
- [Automated installation](https://github.com/plankanban/planka-installer)
|
|
||||||
|
|
||||||
For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration).
|
For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration).
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# 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.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.1.2
|
version: 0.1.12
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# 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
|
# 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.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "1.12.0"
|
appVersion: "1.15.1"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- alias: postgresql
|
- alias: postgresql
|
||||||
|
|||||||
+23
-1
@@ -21,7 +21,11 @@ git clone https://github.com/Chris-Greaves/planka-helm-chart.git
|
|||||||
cd planka-helm-chart
|
cd planka-helm-chart
|
||||||
helm dependency build
|
helm dependency build
|
||||||
export SECRETKEY=$(openssl rand -hex 64)
|
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.
|
> **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
|
```bash
|
||||||
# HTTP only
|
# HTTP only
|
||||||
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"
|
||||||
--set ingress.enabled=true \
|
--set ingress.enabled=true \
|
||||||
--set ingress.hosts[0].host=planka.example.dev \
|
--set ingress.hosts[0].host=planka.example.dev \
|
||||||
|
|
||||||
# HTTPS
|
# HTTPS
|
||||||
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"
|
||||||
--set ingress.enabled=true \
|
--set ingress.enabled=true \
|
||||||
--set ingress.hosts[0].host=planka.example.dev \
|
--set ingress.hosts[0].host=planka.example.dev \
|
||||||
--set ingress.tls[0].secretName=planka-tls \
|
--set ingress.tls[0].secretName=planka-tls \
|
||||||
@@ -54,6 +66,16 @@ or create a values.yaml file like:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
secretkey: "<InsertSecretKey>"
|
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:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
hosts:
|
hosts:
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ spec:
|
|||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: {{ .Values.service.port }}
|
containerPort: {{ .Values.service.containerPort | default 1337 }}
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
@@ -75,6 +75,45 @@ spec:
|
|||||||
value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }}
|
value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }}
|
||||||
- name: TRUST_PROXY
|
- name: TRUST_PROXY
|
||||||
value: "0"
|
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 }}
|
{{- with .Values.nodeSelector }}
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -46,6 +46,10 @@ securityContext: {}
|
|||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 1337
|
port: 1337
|
||||||
|
## @param service.containerPort Planka HTTP container port
|
||||||
|
## If empty will default to 1337
|
||||||
|
##
|
||||||
|
containerPort: 1337
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: false
|
enabled: false
|
||||||
@@ -109,3 +113,68 @@ persistence:
|
|||||||
|
|
||||||
accessMode: ReadWriteOnce
|
accessMode: ReadWriteOnce
|
||||||
size: 10Gi
|
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
@@ -1 +1 @@
|
|||||||
REACT_APP_VERSION=1.12.0
|
REACT_APP_VERSION=1.15.1
|
||||||
|
|||||||
Generated
+3376
-21998
File diff suppressed because it is too large
Load Diff
+30
-30
@@ -55,60 +55,60 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.30.0",
|
||||||
"dequal": "^2.0.3",
|
"dequal": "^2.0.3",
|
||||||
"easymde": "^2.18.0",
|
"easymde": "^2.18.0",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"i18next": "^22.0.6",
|
"i18next": "^23.7.6",
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
"initials": "^3.1.2",
|
"initials": "^3.1.2",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.5",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"node-sass": "^8.0.0",
|
"nanoid": "^5.0.3",
|
||||||
"photoswipe": "^5.3.3",
|
"node-sass": "^9.0.0",
|
||||||
|
"photoswipe": "^5.4.2",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-app-rewired": "^2.2.1",
|
"react-app-rewired": "^2.2.1",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-datepicker": "^4.8.0",
|
"react-datepicker": "^4.21.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-i18next": "^12.0.0",
|
"react-i18next": "^13.5.0",
|
||||||
"react-input-mask": "^2.0.4",
|
"react-input-mask": "^2.0.4",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.7",
|
||||||
"react-oidc-context": "^2.2.2",
|
"react-photoswipe-gallery": "^2.2.7",
|
||||||
"react-photoswipe-gallery": "^2.2.2",
|
"react-redux": "^8.1.3",
|
||||||
"react-redux": "^8.0.5",
|
"react-router-dom": "^6.19.0",
|
||||||
"react-router-dom": "^6.4.3",
|
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-simplemde-editor": "^5.2.0",
|
"react-simplemde-editor": "^5.2.0",
|
||||||
"react-textarea-autosize": "^8.4.0",
|
"react-textarea-autosize": "^8.5.3",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.1",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"redux-orm": "^0.16.2",
|
"redux-orm": "^0.16.2",
|
||||||
"redux-saga": "^1.2.1",
|
"redux-saga": "^1.2.3",
|
||||||
"remark-breaks": "^3.0.2",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"reselect": "^4.1.7",
|
"reselect": "^4.1.8",
|
||||||
"sails.io.js": "^1.2.1",
|
"sails.io.js": "^1.2.1",
|
||||||
"semantic-ui-react": "^2.1.3",
|
"semantic-ui-react": "^2.1.4",
|
||||||
"socket.io-client": "^2.5.0",
|
"socket.io-client": "^2.5.0",
|
||||||
"validator": "^13.7.0",
|
"validator": "^13.11.0",
|
||||||
"whatwg-fetch": "^3.6.2",
|
"whatwg-fetch": "^3.6.19",
|
||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^6.1.4",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^14.1.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^14.5.1",
|
||||||
"babel-preset-airbnb": "^5.0.0",
|
"babel-preset-airbnb": "^5.0.0",
|
||||||
"chai": "^4.3.7",
|
"chai": "^4.3.10",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.53.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.29.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
"eslint-plugin-react": "^7.31.11",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"react-test-renderer": "^18.2.0"
|
"react-test-renderer": "^18.2.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ const initializeCore = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: with success?
|
||||||
|
initializeCore.fetchConfig = (config) => ({
|
||||||
|
type: ActionTypes.CORE_INITIALIZE__CONFIG_FETCH,
|
||||||
|
payload: {
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const logout = () => ({
|
const logout = () => ({
|
||||||
type: ActionTypes.LOGOUT,
|
type: ActionTypes.LOGOUT,
|
||||||
payload: {},
|
payload: {},
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
const initializeLogin = (config) => ({
|
||||||
|
type: ActionTypes.LOGIN_INITIALIZE,
|
||||||
|
payload: {
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const authenticate = (data) => ({
|
const authenticate = (data) => ({
|
||||||
type: ActionTypes.AUTHENTICATE,
|
type: ActionTypes.AUTHENTICATE,
|
||||||
payload: {
|
payload: {
|
||||||
@@ -21,12 +28,33 @@ authenticate.failure = (error) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authenticateUsingOidc = () => ({
|
||||||
|
type: ActionTypes.USING_OIDC_AUTHENTICATE,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
authenticateUsingOidc.success = (accessToken) => ({
|
||||||
|
type: ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS,
|
||||||
|
payload: {
|
||||||
|
accessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
authenticateUsingOidc.failure = (error) => ({
|
||||||
|
type: ActionTypes.USING_OIDC_AUTHENTICATE__FAILURE,
|
||||||
|
payload: {
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const clearAuthenticateError = () => ({
|
const clearAuthenticateError = () => ({
|
||||||
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
|
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
initializeLogin,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
authenticateUsingOidc,
|
||||||
clearAuthenticateError,
|
clearAuthenticateError,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import socket from './socket';
|
|||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
|
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
|
||||||
const exchangeOidcToken = (accessToken, headers) =>
|
|
||||||
http.post('/access-tokens/exchange', { token: accessToken }, headers);
|
const exchangeForAccessTokenUsingOidc = (data, headers) =>
|
||||||
|
http.post('/access-tokens/exchange-using-oidc', data, headers);
|
||||||
|
|
||||||
const deleteCurrentAccessToken = (headers) =>
|
const deleteCurrentAccessToken = (headers) =>
|
||||||
socket.delete('/access-tokens/me', undefined, headers);
|
socket.delete('/access-tokens/me', undefined, headers);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createAccessToken,
|
createAccessToken,
|
||||||
|
exchangeForAccessTokenUsingOidc,
|
||||||
deleteCurrentAccessToken,
|
deleteCurrentAccessToken,
|
||||||
exchangeOidcToken,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import Config from '../constants/Config';
|
|||||||
const http = {};
|
const http = {};
|
||||||
|
|
||||||
// TODO: add all methods
|
// TODO: add all methods
|
||||||
['POST'].forEach((method) => {
|
['GET', 'POST'].forEach((method) => {
|
||||||
http[method.toLowerCase()] = (url, data, headers) => {
|
http[method.toLowerCase()] = (url, data, headers) => {
|
||||||
const formData = Object.keys(data).reduce((result, key) => {
|
const formData =
|
||||||
|
data &&
|
||||||
|
Object.keys(data).reduce((result, key) => {
|
||||||
result.append(key, data[key]);
|
result.append(key, data[key]);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import http from './http';
|
import http from './http';
|
||||||
import socket from './socket';
|
import socket from './socket';
|
||||||
|
import root from './root';
|
||||||
import accessTokens from './access-tokens';
|
import accessTokens from './access-tokens';
|
||||||
import users from './users';
|
import users from './users';
|
||||||
import projects from './projects';
|
import projects from './projects';
|
||||||
@@ -20,6 +21,7 @@ import notifications from './notifications';
|
|||||||
export { http, socket };
|
export { http, socket };
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
...root,
|
||||||
...accessTokens,
|
...accessTokens,
|
||||||
...users,
|
...users,
|
||||||
...projects,
|
...projects,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import http from './http';
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
|
||||||
|
const getConfig = (headers) => http.get('/config', undefined, headers);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getConfig,
|
||||||
|
};
|
||||||
@@ -217,6 +217,7 @@ const CardModal = React.memo(
|
|||||||
onUserSelect={onUserAdd}
|
onUserSelect={onUserAdd}
|
||||||
onUserDeselect={onUserRemove}
|
onUserDeselect={onUserRemove}
|
||||||
>
|
>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={classNames(styles.attachment, styles.dueDate)}
|
className={classNames(styles.attachment, styles.dueDate)}
|
||||||
@@ -266,6 +267,7 @@ const CardModal = React.memo(
|
|||||||
onMove={onLabelMove}
|
onMove={onLabelMove}
|
||||||
onDelete={onLabelDelete}
|
onDelete={onLabelDelete}
|
||||||
>
|
>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={classNames(styles.attachment, styles.dueDate)}
|
className={classNames(styles.attachment, styles.dueDate)}
|
||||||
@@ -314,10 +316,11 @@ const CardModal = React.memo(
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
|
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleStopwatchClick}
|
|
||||||
type="button"
|
type="button"
|
||||||
className={classNames(styles.attachment, styles.dueDate)}
|
className={classNames(styles.attachment, styles.dueDate)}
|
||||||
|
onClick={handleToggleStopwatchClick}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={stopwatch.startedAt ? 'pause' : 'play'}
|
name={stopwatch.startedAt ? 'pause' : 'play'}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{items.length > 0 && (
|
{items.length > 0 && (
|
||||||
|
<>
|
||||||
|
<span className={styles.progressWrapper}>
|
||||||
<Progress
|
<Progress
|
||||||
autoSuccess
|
autoSuccess
|
||||||
value={completedItems.length}
|
value={completedItems.length}
|
||||||
@@ -56,6 +58,11 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete
|
|||||||
size="tiny"
|
size="tiny"
|
||||||
className={styles.progress}
|
className={styles.progress}
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
|
<span className={styles.count}>
|
||||||
|
{completedItems.length}/{items.length}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||||
<Droppable droppableId="tasks" type={DroppableTypes.TASK}>
|
<Droppable droppableId="tasks" type={DroppableTypes.TASK}>
|
||||||
|
|||||||
@@ -3,6 +3,23 @@
|
|||||||
margin: 0 0 16px;
|
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 {
|
.taskButton {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Button, Icon, Menu } from 'semantic-ui-react';
|
import { Button, Icon, Menu } from 'semantic-ui-react';
|
||||||
import { useAuth } from 'react-oidc-context';
|
|
||||||
import { usePopup } from '../../lib/popup';
|
import { usePopup } from '../../lib/popup';
|
||||||
|
|
||||||
import Paths from '../../constants/Paths';
|
import Paths from '../../constants/Paths';
|
||||||
@@ -30,7 +29,6 @@ const Header = React.memo(
|
|||||||
onUserSettingsClick,
|
onUserSettingsClick,
|
||||||
onLogout,
|
onLogout,
|
||||||
}) => {
|
}) => {
|
||||||
const auth = useAuth();
|
|
||||||
const handleProjectSettingsClick = useCallback(() => {
|
const handleProjectSettingsClick = useCallback(() => {
|
||||||
if (canEditProject) {
|
if (canEditProject) {
|
||||||
onProjectSettingsClick();
|
onProjectSettingsClick();
|
||||||
@@ -40,11 +38,6 @@ const Header = React.memo(
|
|||||||
const NotificationsPopup = usePopup(NotificationsStep, POPUP_PROPS);
|
const NotificationsPopup = usePopup(NotificationsStep, POPUP_PROPS);
|
||||||
const UserPopup = usePopup(UserStep, POPUP_PROPS);
|
const UserPopup = usePopup(UserStep, POPUP_PROPS);
|
||||||
|
|
||||||
const onFullLogout = () => {
|
|
||||||
auth.signoutSilent();
|
|
||||||
onLogout();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{!project && (
|
{!project && (
|
||||||
@@ -95,7 +88,7 @@ const Header = React.memo(
|
|||||||
<UserPopup
|
<UserPopup
|
||||||
isLogouting={isLogouting}
|
isLogouting={isLogouting}
|
||||||
onSettingsClick={onUserSettingsClick}
|
onSettingsClick={onUserSettingsClick}
|
||||||
onLogout={onFullLogout}
|
onLogout={onLogout}
|
||||||
>
|
>
|
||||||
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
|
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
|
||||||
{user.name}
|
{user.name}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import isEmail from 'validator/lib/isEmail';
|
import isEmail from 'validator/lib/isEmail';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useAuth } from 'react-oidc-context';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Form, Grid, Header, Message } from 'semantic-ui-react';
|
import { Button, Form, Grid, Header, Message } from 'semantic-ui-react';
|
||||||
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
|
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
|
||||||
import { Input } from '../../lib/custom-ui';
|
import { Input } from '../../lib/custom-ui';
|
||||||
|
|
||||||
@@ -29,6 +28,21 @@ const createMessage = (error) => {
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
content: 'common.invalidPassword',
|
content: 'common.invalidPassword',
|
||||||
};
|
};
|
||||||
|
case 'Use single sign-on':
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
content: 'common.useSingleSignOn',
|
||||||
|
};
|
||||||
|
case 'Email already in use':
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
content: 'common.emailAlreadyInUse',
|
||||||
|
};
|
||||||
|
case 'Username already in use':
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
content: 'common.usernameAlreadyInUse',
|
||||||
|
};
|
||||||
case 'Failed to fetch':
|
case 'Failed to fetch':
|
||||||
return {
|
return {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@@ -48,8 +62,16 @@ const createMessage = (error) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Login = React.memo(
|
const Login = React.memo(
|
||||||
({ defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss }) => {
|
({
|
||||||
const auth = useAuth();
|
defaultData,
|
||||||
|
isSubmitting,
|
||||||
|
isSubmittingUsingOidc,
|
||||||
|
error,
|
||||||
|
withOidc,
|
||||||
|
onAuthenticate,
|
||||||
|
onAuthenticateUsingOidc,
|
||||||
|
onMessageDismiss,
|
||||||
|
}) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const wasSubmitting = usePrevious(isSubmitting);
|
const wasSubmitting = usePrevious(isSubmitting);
|
||||||
|
|
||||||
@@ -170,12 +192,19 @@ const Login = React.memo(
|
|||||||
content={t('action.logIn')}
|
content={t('action.logIn')}
|
||||||
floated="right"
|
floated="right"
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isSubmittingUsingOidc}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
<Form.Button type="button" onClick={() => auth.signinRedirect()}>
|
{withOidc && (
|
||||||
Log in with SSO
|
<Button
|
||||||
</Form.Button>
|
type="button"
|
||||||
|
loading={isSubmittingUsingOidc}
|
||||||
|
disabled={isSubmitting || isSubmittingUsingOidc}
|
||||||
|
onClick={onAuthenticateUsingOidc}
|
||||||
|
>
|
||||||
|
{t('action.logInWithSSO')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
@@ -206,10 +235,15 @@ const Login = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
Login.propTypes = {
|
Login.propTypes = {
|
||||||
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
/* eslint-disable react/forbid-prop-types */
|
||||||
|
defaultData: PropTypes.object.isRequired,
|
||||||
|
/* eslint-enable react/forbid-prop-types */
|
||||||
isSubmitting: PropTypes.bool.isRequired,
|
isSubmitting: PropTypes.bool.isRequired,
|
||||||
|
isSubmittingUsingOidc: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||||
|
withOidc: PropTypes.bool.isRequired,
|
||||||
onAuthenticate: PropTypes.func.isRequired,
|
onAuthenticate: PropTypes.func.isRequired,
|
||||||
|
onAuthenticateUsingOidc: PropTypes.func.isRequired,
|
||||||
onMessageDismiss: PropTypes.func.isRequired,
|
onMessageDismiss: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Loader } from 'semantic-ui-react';
|
||||||
|
|
||||||
|
import LoginContainer from '../containers/LoginContainer';
|
||||||
|
|
||||||
|
const LoginWrapper = React.memo(({ isInitializing }) => {
|
||||||
|
if (isInitializing) {
|
||||||
|
return <Loader active size="massive" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LoginContainer />;
|
||||||
|
});
|
||||||
|
|
||||||
|
LoginWrapper.propTypes = {
|
||||||
|
isInitializing: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginWrapper;
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { useAuth } from 'react-oidc-context';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
let isLoggingIn = true;
|
|
||||||
const OidcLogin = React.memo(({ onAuthenticate }) => {
|
|
||||||
const auth = useAuth();
|
|
||||||
if (isLoggingIn && auth.user) {
|
|
||||||
isLoggingIn = false;
|
|
||||||
const { user } = auth;
|
|
||||||
onAuthenticate(user);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
OidcLogin.propTypes = {
|
|
||||||
onAuthenticate: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OidcLogin;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import OidcLogin from './OidcLogin';
|
|
||||||
|
|
||||||
export default OidcLogin;
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { AuthProvider } from 'react-oidc-context';
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import { ReduxRouter } from '../lib/redux-router';
|
import { ReduxRouter } from '../lib/redux-router';
|
||||||
|
|
||||||
import Paths from '../constants/Paths';
|
import Paths from '../constants/Paths';
|
||||||
import LoginContainer from '../containers/LoginContainer';
|
import LoginWrapperContainer from '../containers/LoginWrapperContainer';
|
||||||
import CoreContainer from '../containers/CoreContainer';
|
import CoreContainer from '../containers/CoreContainer';
|
||||||
import NotFound from './NotFound';
|
import NotFound from './NotFound';
|
||||||
|
|
||||||
@@ -15,24 +14,14 @@ import 'photoswipe/dist/photoswipe.css';
|
|||||||
import 'easymde/dist/easymde.min.css';
|
import 'easymde/dist/easymde.min.css';
|
||||||
import '../lib/custom-ui/styles.css';
|
import '../lib/custom-ui/styles.css';
|
||||||
import '../styles.module.scss';
|
import '../styles.module.scss';
|
||||||
import OidcLoginContainer from '../containers/OidcLoginContainer';
|
|
||||||
|
|
||||||
function Root({ store, history, config }) {
|
function Root({ store, history }) {
|
||||||
return (
|
return (
|
||||||
<AuthProvider
|
|
||||||
authority={config.authority}
|
|
||||||
client_id={config.clientId}
|
|
||||||
redirect_uri={config.redirectUri}
|
|
||||||
scope={config.scopes}
|
|
||||||
onSigninCallback={() => {
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ReduxRouter history={history}>
|
<ReduxRouter history={history}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={Paths.LOGIN} element={<LoginContainer />} />
|
<Route path={Paths.LOGIN} element={<LoginWrapperContainer />} />
|
||||||
<Route path={Paths.OIDC_LOGIN} element={<OidcLoginContainer />} />
|
<Route path={Paths.OIDC_CALLBACK} element={<LoginWrapperContainer />} />
|
||||||
<Route path={Paths.ROOT} element={<CoreContainer />} />
|
<Route path={Paths.ROOT} element={<CoreContainer />} />
|
||||||
<Route path={Paths.PROJECTS} element={<CoreContainer />} />
|
<Route path={Paths.PROJECTS} element={<CoreContainer />} />
|
||||||
<Route path={Paths.BOARDS} element={<CoreContainer />} />
|
<Route path={Paths.BOARDS} element={<CoreContainer />} />
|
||||||
@@ -41,14 +30,13 @@ function Root({ store, history, config }) {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</ReduxRouter>
|
</ReduxRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</AuthProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Root.propTypes = {
|
Root.propTypes = {
|
||||||
/* eslint-disable react/forbid-prop-types */
|
/* eslint-disable react/forbid-prop-types */
|
||||||
store: PropTypes.object.isRequired,
|
store: PropTypes.object.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
config: PropTypes.object.isRequired,
|
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const UserSettingsModal = React.memo(
|
|||||||
phone,
|
phone,
|
||||||
organization,
|
organization,
|
||||||
language,
|
language,
|
||||||
isLocked,
|
|
||||||
subscribeToOwnCards,
|
subscribeToOwnCards,
|
||||||
|
isLocked,
|
||||||
isAvatarUpdating,
|
isAvatarUpdating,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
@@ -106,8 +106,8 @@ UserSettingsModal.propTypes = {
|
|||||||
phone: PropTypes.string,
|
phone: PropTypes.string,
|
||||||
organization: PropTypes.string,
|
organization: PropTypes.string,
|
||||||
language: PropTypes.string,
|
language: PropTypes.string,
|
||||||
isLocked: PropTypes.bool.isRequired,
|
|
||||||
subscribeToOwnCards: PropTypes.bool.isRequired,
|
subscribeToOwnCards: PropTypes.bool.isRequired,
|
||||||
|
isLocked: PropTypes.bool.isRequired,
|
||||||
isAvatarUpdating: PropTypes.bool.isRequired,
|
isAvatarUpdating: PropTypes.bool.isRequired,
|
||||||
/* eslint-disable react/forbid-prop-types */
|
/* eslint-disable react/forbid-prop-types */
|
||||||
usernameUpdateForm: PropTypes.object.isRequired,
|
usernameUpdateForm: PropTypes.object.isRequired,
|
||||||
|
|||||||
@@ -153,12 +153,14 @@ const ActionsStep = React.memo(
|
|||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!user.isDeletionLocked && (
|
||||||
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
||||||
{t('action.deleteUser', {
|
{t('action.deleteUser', {
|
||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</Popup.Content>
|
</Popup.Content>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const Item = React.memo(
|
|||||||
phone,
|
phone,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isLocked,
|
isLocked,
|
||||||
|
isRoleLocked,
|
||||||
|
isDeletionLocked,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
@@ -47,7 +49,7 @@ const Item = React.memo(
|
|||||||
<Table.Cell>{username || '-'}</Table.Cell>
|
<Table.Cell>{username || '-'}</Table.Cell>
|
||||||
<Table.Cell>{email}</Table.Cell>
|
<Table.Cell>{email}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Radio toggle checked={isAdmin} disabled={isLocked} onChange={handleIsAdminChange} />
|
<Radio toggle checked={isAdmin} disabled={isRoleLocked} onChange={handleIsAdminChange} />
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell textAlign="right">
|
<Table.Cell textAlign="right">
|
||||||
<ActionsPopup
|
<ActionsPopup
|
||||||
@@ -59,6 +61,7 @@ const Item = React.memo(
|
|||||||
phone,
|
phone,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isLocked,
|
isLocked,
|
||||||
|
isDeletionLocked,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
@@ -91,6 +94,8 @@ Item.propTypes = {
|
|||||||
phone: PropTypes.string,
|
phone: PropTypes.string,
|
||||||
isAdmin: PropTypes.bool.isRequired,
|
isAdmin: PropTypes.bool.isRequired,
|
||||||
isLocked: PropTypes.bool.isRequired,
|
isLocked: PropTypes.bool.isRequired,
|
||||||
|
isRoleLocked: PropTypes.bool.isRequired,
|
||||||
|
isDeletionLocked: PropTypes.bool.isRequired,
|
||||||
/* eslint-disable react/forbid-prop-types */
|
/* eslint-disable react/forbid-prop-types */
|
||||||
emailUpdateForm: PropTypes.object.isRequired,
|
emailUpdateForm: PropTypes.object.isRequired,
|
||||||
passwordUpdateForm: PropTypes.object.isRequired,
|
passwordUpdateForm: PropTypes.object.isRequired,
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ const UsersModal = React.memo(
|
|||||||
phone={item.phone}
|
phone={item.phone}
|
||||||
isAdmin={item.isAdmin}
|
isAdmin={item.isAdmin}
|
||||||
isLocked={item.isLocked}
|
isLocked={item.isLocked}
|
||||||
|
isRoleLocked={item.isRoleLocked}
|
||||||
|
isDeletionLocked={item.isDeletionLocked}
|
||||||
emailUpdateForm={item.emailUpdateForm}
|
emailUpdateForm={item.emailUpdateForm}
|
||||||
passwordUpdateForm={item.passwordUpdateForm}
|
passwordUpdateForm={item.passwordUpdateForm}
|
||||||
usernameUpdateForm={item.usernameUpdateForm}
|
usernameUpdateForm={item.usernameUpdateForm}
|
||||||
|
|||||||
@@ -12,14 +12,19 @@ export default {
|
|||||||
|
|
||||||
/* Login */
|
/* Login */
|
||||||
|
|
||||||
|
LOGIN_INITIALIZE: 'LOGIN_INITIALIZE',
|
||||||
AUTHENTICATE: 'AUTHENTICATE',
|
AUTHENTICATE: 'AUTHENTICATE',
|
||||||
AUTHENTICATE__SUCCESS: 'AUTHENTICATE__SUCCESS',
|
AUTHENTICATE__SUCCESS: 'AUTHENTICATE__SUCCESS',
|
||||||
AUTHENTICATE__FAILURE: 'AUTHENTICATE__FAILURE',
|
AUTHENTICATE__FAILURE: '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',
|
AUTHENTICATE_ERROR_CLEAR: 'AUTHENTICATE_ERROR_CLEAR',
|
||||||
|
|
||||||
/* Core */
|
/* Core */
|
||||||
|
|
||||||
CORE_INITIALIZE: 'CORE_INITIALIZE',
|
CORE_INITIALIZE: 'CORE_INITIALIZE',
|
||||||
|
CORE_INITIALIZE__CONFIG_FETCH: 'CORE_INITIALIZE__CONFIG_FETCH',
|
||||||
LOGOUT: 'LOGOUT',
|
LOGOUT: 'LOGOUT',
|
||||||
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
|
LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ export default {
|
|||||||
/* Login */
|
/* Login */
|
||||||
|
|
||||||
AUTHENTICATE: `${PREFIX}/AUTHENTICATE`,
|
AUTHENTICATE: `${PREFIX}/AUTHENTICATE`,
|
||||||
|
USING_OIDC_AUTHENTICATE: `${PREFIX}/USING_OIDC_AUTHENTICATE`,
|
||||||
AUTHENTICATE_ERROR_CLEAR: `${PREFIX}/AUTHENTICATE_ERROR_CLEAR`,
|
AUTHENTICATE_ERROR_CLEAR: `${PREFIX}/AUTHENTICATE_ERROR_CLEAR`,
|
||||||
|
|
||||||
/* Core */
|
/* Core */
|
||||||
|
|
||||||
CORE_INITIALIZE: `${PREFIX}/CORE_INITIALIZE`,
|
|
||||||
LOGOUT: `${PREFIX}/LOGOUT`,
|
LOGOUT: `${PREFIX}/LOGOUT`,
|
||||||
|
|
||||||
/* Modals */
|
/* Modals */
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Config from './Config';
|
|||||||
|
|
||||||
const ROOT = `${Config.BASE_PATH}/`;
|
const ROOT = `${Config.BASE_PATH}/`;
|
||||||
const LOGIN = `${Config.BASE_PATH}/login`;
|
const LOGIN = `${Config.BASE_PATH}/login`;
|
||||||
const OIDC_LOGIN = `${Config.BASE_PATH}/oidclogin`;
|
const OIDC_CALLBACK = `${Config.BASE_PATH}/oidc-callback`;
|
||||||
const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
|
const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
|
||||||
const BOARDS = `${Config.BASE_PATH}/boards/:id`;
|
const BOARDS = `${Config.BASE_PATH}/boards/:id`;
|
||||||
const CARDS = `${Config.BASE_PATH}/cards/:id`;
|
const CARDS = `${Config.BASE_PATH}/cards/:id`;
|
||||||
@@ -10,8 +10,8 @@ const CARDS = `${Config.BASE_PATH}/cards/:id`;
|
|||||||
export default {
|
export default {
|
||||||
ROOT,
|
ROOT,
|
||||||
LOGIN,
|
LOGIN,
|
||||||
|
OIDC_CALLBACK,
|
||||||
PROJECTS,
|
PROJECTS,
|
||||||
BOARDS,
|
BOARDS,
|
||||||
CARDS,
|
CARDS,
|
||||||
OIDC_LOGIN,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import selectors from '../selectors';
|
|||||||
import Core from '../components/Core';
|
import Core from '../components/Core';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
const isCoreInitializing = selectors.selectIsCoreInitializing(state);
|
const isInitializing = selectors.selectIsInitializing(state);
|
||||||
const isSocketDisconnected = selectors.selectIsSocketDisconnected(state);
|
const isSocketDisconnected = selectors.selectIsSocketDisconnected(state);
|
||||||
const currentModal = selectors.selectCurrentModal(state);
|
const currentModal = selectors.selectCurrentModal(state);
|
||||||
const currentProject = selectors.selectCurrentProject(state);
|
const currentProject = selectors.selectCurrentProject(state);
|
||||||
const currentBoard = selectors.selectCurrentBoard(state);
|
const currentBoard = selectors.selectCurrentBoard(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isInitializing,
|
||||||
isSocketDisconnected,
|
isSocketDisconnected,
|
||||||
currentModal,
|
currentModal,
|
||||||
currentProject,
|
currentProject,
|
||||||
currentBoard,
|
currentBoard,
|
||||||
isInitializing: isCoreInitializing,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import selectors from '../selectors';
|
||||||
import entryActions from '../entry-actions';
|
import entryActions from '../entry-actions';
|
||||||
import Login from '../components/Login';
|
import Login from '../components/Login';
|
||||||
|
|
||||||
const mapStateToProps = ({
|
const mapStateToProps = (state) => {
|
||||||
|
const oidcConfig = selectors.selectOidcConfig(state);
|
||||||
|
|
||||||
|
const {
|
||||||
ui: {
|
ui: {
|
||||||
authenticateForm: { data: defaultData, isSubmitting, error },
|
authenticateForm: { data: defaultData, isSubmitting, isSubmittingUsingOidc, error },
|
||||||
},
|
},
|
||||||
}) => ({
|
} = state;
|
||||||
|
|
||||||
|
return {
|
||||||
defaultData,
|
defaultData,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
|
isSubmittingUsingOidc,
|
||||||
error,
|
error,
|
||||||
});
|
withOidc: !!oidcConfig,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) =>
|
const mapDispatchToProps = (dispatch) =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{
|
{
|
||||||
onAuthenticate: entryActions.authenticate,
|
onAuthenticate: entryActions.authenticate,
|
||||||
|
onAuthenticateUsingOidc: entryActions.authenticateUsingOidc,
|
||||||
onMessageDismiss: entryActions.clearAuthenticateError,
|
onMessageDismiss: entryActions.clearAuthenticateError,
|
||||||
},
|
},
|
||||||
dispatch,
|
dispatch,
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import selectors from '../selectors';
|
||||||
|
import LoginWrapper from '../components/LoginWrapper';
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
const isInitializing = selectors.selectIsInitializing(state);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInitializing,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(LoginWrapper);
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import entryActions from '../entry-actions';
|
|
||||||
import OidcLogin from '../components/OIDC';
|
|
||||||
|
|
||||||
const mapStateToProps = ({
|
|
||||||
ui: {
|
|
||||||
authenticateForm: { data: defaultData, isSubmitting, error },
|
|
||||||
},
|
|
||||||
}) => ({
|
|
||||||
defaultData,
|
|
||||||
isSubmitting,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) =>
|
|
||||||
bindActionCreators(
|
|
||||||
{
|
|
||||||
onAuthenticate: entryActions.authenticate,
|
|
||||||
onMessageDismiss: entryActions.clearAuthenticateError,
|
|
||||||
},
|
|
||||||
dispatch,
|
|
||||||
);
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(OidcLogin);
|
|
||||||
@@ -14,8 +14,8 @@ const mapStateToProps = (state) => {
|
|||||||
phone,
|
phone,
|
||||||
organization,
|
organization,
|
||||||
language,
|
language,
|
||||||
isLocked,
|
|
||||||
subscribeToOwnCards,
|
subscribeToOwnCards,
|
||||||
|
isLocked,
|
||||||
isAvatarUpdating,
|
isAvatarUpdating,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
@@ -30,8 +30,8 @@ const mapStateToProps = (state) => {
|
|||||||
phone,
|
phone,
|
||||||
organization,
|
organization,
|
||||||
language,
|
language,
|
||||||
isLocked,
|
|
||||||
subscribeToOwnCards,
|
subscribeToOwnCards,
|
||||||
|
isLocked,
|
||||||
isAvatarUpdating,
|
isAvatarUpdating,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import EntryActionTypes from '../constants/EntryActionTypes';
|
import EntryActionTypes from '../constants/EntryActionTypes';
|
||||||
|
|
||||||
const initializeCore = () => ({
|
|
||||||
type: EntryActionTypes.CORE_INITIALIZE,
|
|
||||||
payload: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const logout = () => ({
|
const logout = () => ({
|
||||||
type: EntryActionTypes.LOGOUT,
|
type: EntryActionTypes.LOGOUT,
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
initializeCore,
|
|
||||||
logout,
|
logout,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ const authenticate = (data) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authenticateUsingOidc = () => ({
|
||||||
|
type: EntryActionTypes.USING_OIDC_AUTHENTICATE,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
const clearAuthenticateError = () => ({
|
const clearAuthenticateError = () => ({
|
||||||
type: EntryActionTypes.AUTHENTICATE_ERROR_CLEAR,
|
type: EntryActionTypes.AUTHENTICATE_ERROR_CLEAR,
|
||||||
payload: {},
|
payload: {},
|
||||||
@@ -14,5 +19,6 @@ const clearAuthenticateError = () => ({
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
authenticate,
|
authenticate,
|
||||||
|
authenticateUsingOidc,
|
||||||
clearAuthenticateError,
|
clearAuthenticateError,
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-6
@@ -1,15 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
import Config from './constants/Config';
|
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import history from './history';
|
import history from './history';
|
||||||
import Root from './components/Root';
|
import Root from './components/Root';
|
||||||
|
|
||||||
import './i18n';
|
import './i18n';
|
||||||
|
|
||||||
fetch(`${Config.SERVER_BASE_URL}/api/appconfig`).then((response) => {
|
|
||||||
response.json().then((config) => {
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(React.createElement(Root, { store, history, config }));
|
root.render(React.createElement(Root, { store, history }));
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ export default {
|
|||||||
projectManagement: 'Project management',
|
projectManagement: 'Project management',
|
||||||
serverConnectionFailed: 'Server connection failed',
|
serverConnectionFailed: 'Server connection failed',
|
||||||
unknownError: 'Unknown error, try again later',
|
unknownError: 'Unknown error, try again later',
|
||||||
|
useSingleSignOn: 'Use single sign-on',
|
||||||
},
|
},
|
||||||
|
|
||||||
action: {
|
action: {
|
||||||
logIn: 'Log in',
|
logIn: 'Log in',
|
||||||
|
logInWithSSO: 'Log in with SSO',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import it from './it';
|
|||||||
import ja from './ja';
|
import ja from './ja';
|
||||||
import ko from './ko';
|
import ko from './ko';
|
||||||
import pl from './pl';
|
import pl from './pl';
|
||||||
|
import ro from './ro';
|
||||||
import ru from './ru';
|
import ru from './ru';
|
||||||
import sk from './sk';
|
import sk from './sk';
|
||||||
import sv from './sv';
|
import sv from './sv';
|
||||||
@@ -15,7 +16,7 @@ import tr from './tr';
|
|||||||
import uz from './uz';
|
import uz from './uz';
|
||||||
import zh from './zh';
|
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, uz, zh];
|
||||||
|
|
||||||
export default locales;
|
export default locales;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default {
|
|||||||
time: 'HH:mm',
|
time: 'HH:mm',
|
||||||
dateTime: '$t(format:date) $t(format:time)',
|
dateTime: '$t(format:date) $t(format:time)',
|
||||||
longDate: 'MMMMd日',
|
longDate: 'MMMMd日',
|
||||||
longDateTime: "MMMMd'日 ' HH:MM",
|
longDateTime: "MMMMd'日 ' HH:mm",
|
||||||
},
|
},
|
||||||
|
|
||||||
translation: {
|
translation: {
|
||||||
|
|||||||
+135
-117
@@ -1,7 +1,5 @@
|
|||||||
import dateFns from 'date-fns/locale/ko';
|
import dateFns from 'date-fns/locale/ko';
|
||||||
|
|
||||||
/* Translated by Park Seok Ho */
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
dateFns,
|
dateFns,
|
||||||
|
|
||||||
@@ -15,205 +13,225 @@ export default {
|
|||||||
|
|
||||||
translation: {
|
translation: {
|
||||||
common: {
|
common: {
|
||||||
|
aboutPlanka: 'Planka에 대해서',
|
||||||
account: '계정',
|
account: '계정',
|
||||||
actions: '활동',
|
actions: '활동',
|
||||||
addAttachment_title: '첨부 파일 추가',
|
addAttachment_title: '첨부 파일 추가',
|
||||||
addComment: '댓글',
|
addComment: '댓글',
|
||||||
addManager_title: '관리자 추가',
|
addManager_title: '매니저 추가',
|
||||||
addMember_title: '회원 추가',
|
addMember_title: '멤버 추가',
|
||||||
addUser_title: '사용자 추가',
|
addUser_title: '사용자 추가',
|
||||||
administrator: '시스템 관리자',
|
administrator: '관리자',
|
||||||
all: 'All',
|
all: '모두',
|
||||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||||
'모든 변경 사항은 연결이 복원된 후 자동으로 저장됩니다',
|
'연결이 복원되면 모든 변경 사항이 자동으로 저장됩니다.',
|
||||||
areYouSureYouWantToDeleteThisAttachment: '첨부 파일을 삭제하시겠습니까?',
|
areYouSureYouWantToDeleteThisAttachment: '해당하는 첨부 파일을 삭제하시겠습니까?',
|
||||||
areYouSureYouWantToDeleteThisBoard: '보드를 삭제하시겠습니까?',
|
areYouSureYouWantToDeleteThisBoard: '해당하는 보드를 삭제하시겠습니까?',
|
||||||
areYouSureYouWantToDeleteThisCard: '항목카드를 삭제하시겠습니까?',
|
areYouSureYouWantToDeleteThisCard: '해당하는 항목카드를 삭제하시겠습니까?',
|
||||||
areYouSureYouWantToDeleteThisComment: '댓글을 삭제하시겠습니까?',
|
areYouSureYouWantToDeleteThisComment: '해당하는 댓글을 삭제하시겠습니까?',
|
||||||
areYouSureYouWantToDeleteThisLabel: '라벨을 삭제하시겠습니까?',
|
areYouSureYouWantToDeleteThisLabel: '해당하는 레이블을 삭제하시겠습니까?',
|
||||||
areYouSureYouWantToDeleteThisList: '리스트를 삭제하시겠습니까?',
|
areYouSureYouWantToDeleteThisList: '해당하는 리스트를 삭제하시겠습니까?',
|
||||||
areYouSureYouWantToDeleteThisProject: '프로젝트를 삭제하시겠습니까?',
|
areYouSureYouWantToDeleteThisProject: '해당하는 프로젝트를 삭제하시겠습니까?',
|
||||||
areYouSureYouWantToDeleteThisTask: '태스트를 삭제하겠습니까?',
|
areYouSureYouWantToDeleteThisTask: '해당하는 태스크를 삭제하겠습니까?',
|
||||||
areYouSureYouWantToDeleteThisUser: '이 유저를 삭제하겠습니까?',
|
areYouSureYouWantToDeleteThisUser: '해당하는 사용자를 삭제하겠습니까?',
|
||||||
areYouSureYouWantToLeaveBoard: '이 보드에서 나가시겠습니까?',
|
areYouSureYouWantToLeaveBoard: '이 보드에서 나가시겠습니까?',
|
||||||
areYouSureYouWantToLeaveProject: '이 프로젝트에서 나가시겠습니까?',
|
areYouSureYouWantToLeaveProject: '이 프로젝트에서 나가시겠습니까?',
|
||||||
areYouSureYouWantToRemoveThisManagerFromProject: '프로젝트에서 관리자를 제거하시겠습니까?',
|
areYouSureYouWantToRemoveThisManagerFromProject:
|
||||||
areYouSureYouWantToRemoveThisMemberFromBoard: '이 회원을 보드에서 탈퇴시키겠습니까?',
|
'해당하는 프로젝트에서 매니저를 제거하시겠습니까?',
|
||||||
attachment: '첨부파일',
|
areYouSureYouWantToRemoveThisMemberFromBoard: '해당하는 멤버를 보드에서 삭제하시겠습니까?',
|
||||||
|
attachment: '첨부',
|
||||||
attachments: '첨부 파일',
|
attachments: '첨부 파일',
|
||||||
authentication: '계정 관리',
|
authentication: '인증',
|
||||||
background: 'Background',
|
background: '배경',
|
||||||
board: '보드',
|
board: '보드',
|
||||||
boardNotFound_title: '보드를 찾을 수 없습니다.',
|
boardNotFound_title: '보드를 찾을 수 없음',
|
||||||
cardActions_title: 'Card Actions',
|
canComment: '댓글 달기 가능',
|
||||||
cardNotFound_title: '항목카드를 찾을 수 없습니다.',
|
canEditContentOfBoard: '보드 콘텐츠 수정 가능',
|
||||||
cardOrActionAreDeleted: '항목카드 혹은 활동내역이 삭제되었습니다.',
|
canOnlyViewBoard: '보드를 보는 것만 가능합니다.',
|
||||||
color: 'Color',
|
cardActions_title: '카드 액션',
|
||||||
createBoard_title: '보드명칭을 입력하세요.',
|
cardNotFound_title: '카드를 찾을 수 없음',
|
||||||
createLabel_title: '라벨 생성',
|
cardOrActionAreDeleted: '카드 또는 액션이 삭제됨',
|
||||||
createNewOneOrSelectExistingOne: '새 항목을 만들거나<br />기존 항목을 선택하세요.',
|
color: '색상',
|
||||||
|
createBoard_title: '보드 생성',
|
||||||
|
createLabel_title: '레이블 생성',
|
||||||
|
createNewOneOrSelectExistingOne: '새로 생성하거나<br />기존 항목을 선택하세요.',
|
||||||
createProject_title: '프로젝트 생성',
|
createProject_title: '프로젝트 생성',
|
||||||
createTextFile_title: 'Text File 생성',
|
createTextFile_title: '텍스트 파일 생성',
|
||||||
currentPassword: '현재 비밀번호',
|
currentPassword: '현재 비밀번호',
|
||||||
dangerZone_title: 'Danger Zone',
|
dangerZone_title: '위험 구역',
|
||||||
date: '날짜',
|
date: '날짜',
|
||||||
dueDate_title: '완료일',
|
dueDate_title: '마감일',
|
||||||
deleteAttachment_title: '첨부 파일 삭제',
|
deleteAttachment_title: '첨부 파일 삭제',
|
||||||
deleteBoard_title: '보드 삭제',
|
deleteBoard_title: '보드 삭제',
|
||||||
deleteCard_title: '항목 카드 삭제',
|
deleteCard_title: '카드 삭제',
|
||||||
deleteComment_title: '댓글 삭제',
|
deleteComment_title: '댓글 삭제',
|
||||||
deleteLabel_title: '라벨 삭제',
|
deleteLabel_title: '레이블 삭제',
|
||||||
deleteList_title: '리스트 삭제',
|
deleteList_title: '리스트 삭제',
|
||||||
deleteProject_title: '프로젝트 삭제',
|
deleteProject_title: '프로젝트 삭제',
|
||||||
deleteTask_title: '태스크 삭제',
|
deleteTask_title: '태스크 삭제',
|
||||||
deleteUser_title: '사용자 삭제',
|
deleteUser_title: '사용자 삭제',
|
||||||
description: '설명',
|
description: '설명',
|
||||||
detectAutomatically: '자동으로 감지',
|
detectAutomatically: '자동으로 감지',
|
||||||
dropFileToUpload: '업로드 하기 위해서 파일을 Drop 하세요.',
|
dropFileToUpload: '업로드할 파일 놓기',
|
||||||
|
editor: '편집기',
|
||||||
editAttachment_title: '첨부 수정',
|
editAttachment_title: '첨부 수정',
|
||||||
editAvatar_title: '프로필사진 수정',
|
editAvatar_title: '아바타 수정',
|
||||||
editBoard_title: '보드 수정',
|
editBoard_title: '보드 수정',
|
||||||
editDueDate_title: '완료일 수정',
|
editDueDate_title: '마감일 수정',
|
||||||
editEmail_title: '메일 수정',
|
editEmail_title: '이메일 수정',
|
||||||
editInformation_title: '정보 수정',
|
editInformation_title: '정보 수정',
|
||||||
editLabel_title: '라벨 수정',
|
editLabel_title: '레이블 수정',
|
||||||
editPassword_title: '비밀번호 수정',
|
editPassword_title: '비밀번호 수정',
|
||||||
editStopwatch_title: '타이머 수정',
|
editPermissions_title: '권한 수정',
|
||||||
|
editStopwatch_title: '스톱워치 수정',
|
||||||
editUsername_title: '사용자 이름 수정',
|
editUsername_title: '사용자 이름 수정',
|
||||||
email: '메일',
|
email: '이메일',
|
||||||
emailAlreadyInUse: '기 가입된 이메일이 존재합니다.',
|
emailAlreadyInUse: '이미 사용 중인 이메일입니다.',
|
||||||
enterCardTitle: '항목카드명을 입력하세요...',
|
enterCardTitle: '카드 제목 입력... [Ctrl+Enter]로 자동으로 열기',
|
||||||
enterDescription: '자세한 설명을 작성하세요...',
|
enterDescription: '설명 입력...',
|
||||||
enterFilename: '파일이름을 입력하세요...',
|
enterFilename: '파일 이름 입력...',
|
||||||
enterListTitle: '리스트명을 입력 (예,todo,완료,이슈등...)',
|
enterListTitle: '리스트 제목 입력...',
|
||||||
enterProjectTitle: '프로젝트명칭을 입력하세요.',
|
enterProjectTitle: '프로젝트 제목 입력',
|
||||||
enterTaskDescription: '태스크 설명을 입력하세요...',
|
enterTaskDescription: '태스크 설명 입력...',
|
||||||
filterByLabels_title: '라벨 검색',
|
filterByLabels_title: '레이블로 필터링',
|
||||||
filterByMembers_title: '회원으로 검색',
|
filterByMembers_title: '멤버로 필터링',
|
||||||
fromComputer_title: 'PC로 부터',
|
fromComputer_title: 'PC에서',
|
||||||
general: 'General',
|
fromTrello: '트렐로에서',
|
||||||
|
general: '일반',
|
||||||
hours: '시',
|
hours: '시',
|
||||||
invalidCurrentPassword: '현재 패스워드는 유효하지 않습니다.',
|
importBoard_title: '보드 가져오기',
|
||||||
labels: '라벨',
|
invalidCurrentPassword: '현재 비밀번호가 일치하지 않습니다.',
|
||||||
language: '언어 선택',
|
labels: '레이블',
|
||||||
leaveBoard_title: '보드 탈퇴',
|
language: '언어',
|
||||||
leaveProject_title: '프로젝트 탈퇴',
|
leaveBoard_title: '보드 떠나기',
|
||||||
list: '내역',
|
leaveProject_title: '프로젝트 떠나기',
|
||||||
listActions_title: '활용 내역',
|
list: '리스트',
|
||||||
managers: '관리자',
|
listActions_title: '리스트 액션',
|
||||||
members: '팀원',
|
managers: '매니저',
|
||||||
|
members: '멤버',
|
||||||
minutes: '분',
|
minutes: '분',
|
||||||
moveCard_title: '항목카드 이동',
|
moveCard_title: '카드 옮기기',
|
||||||
name: '이름',
|
name: '이름',
|
||||||
newEmail: '새로운 메일',
|
newEmail: '새로운 이메일',
|
||||||
newPassword: '새로운 패스워드',
|
newPassword: '새로운 비밀번호',
|
||||||
newUsername: '새로운 사용자 이름',
|
newUsername: '새로운 사용자 이름',
|
||||||
noConnectionToServer: '서버에 연결할수 없습니다.',
|
noConnectionToServer: '서버에 연결되지 않음',
|
||||||
noBoards: '보드가 존재하지 않습니다.',
|
noBoards: '보드가 존재하지 않음',
|
||||||
noLists: '리스트가 존재하지 않습니다.',
|
noLists: '리스트가 존재하지 않음',
|
||||||
noProjects: '프로젝트가 존재하지 않습니다.',
|
noProjects: '프로젝트가 존재하지 않음',
|
||||||
notifications: '알림',
|
notifications: '알림',
|
||||||
noUnreadNotifications: '알림을 모두 확인했습니다.',
|
noUnreadNotifications: '읽지 않은 알림 없음',
|
||||||
openBoard_title: '보드 열기',
|
openBoard_title: '보드 열기',
|
||||||
optional_inline: '옵션',
|
optional_inline: '옵션',
|
||||||
organization: '조직',
|
organization: '조직',
|
||||||
phone: '전화번호',
|
phone: '전화번호',
|
||||||
preferences: '환경설정',
|
preferences: '기본설정',
|
||||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||||
'Tip: 클립보드에서 첨부 파일을 추가하려면 Ctrl-V(Mac의 경우 Cmd-V) 입니다..',
|
'팁: 클립보드에서 첨부 파일을 추가하려면 Ctrl-V(Mac의 경우 Cmd-V)를 누릅니다.',
|
||||||
project: '프로젝트',
|
project: '프로젝트',
|
||||||
projectNotFound_title: '프로젝트가 존재하지 않습니다.',
|
projectNotFound_title: '프로젝트를 찾을 수 없음',
|
||||||
removeManager_title: '관리자 삭제',
|
removeManager_title: '매니저 삭제',
|
||||||
removeMember_title: '맴버 삭제',
|
removeMember_title: '멤버 삭제',
|
||||||
|
searchLabels: '레이블 검색하기...',
|
||||||
|
searchMembers: '멤버 검색하기...',
|
||||||
|
searchUsers: '사용자 검색하기...',
|
||||||
seconds: '초',
|
seconds: '초',
|
||||||
selectBoard: '보드 선택',
|
selectBoard: '보드 선택',
|
||||||
selectList: '리스트 선택',
|
selectList: '리스트 선택',
|
||||||
|
selectPermissions_title: '권한 선택',
|
||||||
selectProject: '프로젝트 선택',
|
selectProject: '프로젝트 선택',
|
||||||
settings: '설정',
|
settings: '설정',
|
||||||
stopwatch: '활동시간',
|
stopwatch: '스톱워치',
|
||||||
subscribeToMyOwnCardsByDefault: '내 항목카드는 기본으로 구독',
|
subscribeToMyOwnCardsByDefault: '기본적으로 내 카드 구독하기',
|
||||||
taskActions_title: '태스크 활동',
|
taskActions_title: '태스크 액션',
|
||||||
tasks: '태스크',
|
tasks: '태스크',
|
||||||
thereIsNoPreviewAvailableForThisAttachment:
|
thereIsNoPreviewAvailableForThisAttachment: '이 첨부 파일은 미리 보기를 사용할 수 없습니다.',
|
||||||
'이 첨부 파일에 사용할 수 있는 미리보기가 없습니다.',
|
|
||||||
time: '시간',
|
time: '시간',
|
||||||
title: '제목',
|
title: '제목',
|
||||||
userActions_title: '',
|
userActions_title: '사용자 액션',
|
||||||
userAddedThisCardToList: '<0>{{user}}</0><1>이 카드를 {{list}}</1>에 추가했습니다.',
|
userAddedThisCardToList: '<0>{{user}}</0><1>님이 {{list}}</1>에 카드를 추가했습니다.',
|
||||||
userLeftNewCommentToCard:
|
userLeftNewCommentToCard:
|
||||||
'{{user}}이(가) <2>{{card}}</2>에 «{{comment}}»라는 댓글을 남겼습니다',
|
'{{user}}님이 <2>{{card}}</2>에 «{{comment}}»라는 댓글을 남겼습니다.',
|
||||||
userMovedCardFromListToList: '{{user}} moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
|
userMovedCardFromListToList:
|
||||||
|
'{{user}}님이 {{fromList}}에서 {{toList}}로 <2>{{card}}</2>를 옮겼습니다.',
|
||||||
userMovedThisCardFromListToList:
|
userMovedThisCardFromListToList:
|
||||||
'<0>{{user}}</0><1> 이 항목카드를 {{fromList}} 에서 {{toList}} 으로 이동했습니다. </1>',
|
'<0>{{user}}</0><1> 님이 {{fromList}}에서 {{toList}}로 이 카드를 옮겼습니다.</1>',
|
||||||
username: '아이디',
|
username: '사용자 이름',
|
||||||
usernameAlreadyInUse: '이미 사용중인 사용자입니다.',
|
usernameAlreadyInUse: '이미 사용중인 사용자 이름입니다.',
|
||||||
users: '사용자',
|
users: '사용자',
|
||||||
writeComment: '댓글 작성...',
|
version: '버전',
|
||||||
|
viewer: '열람자',
|
||||||
|
writeComment: '댓글 쓰기...',
|
||||||
},
|
},
|
||||||
|
|
||||||
action: {
|
action: {
|
||||||
addAnotherCard: '다른 항목카드 추가',
|
addAnotherCard: '다른 카드 추가',
|
||||||
addAnotherList: '다른 리스트 추가',
|
addAnotherList: '다른 리스트 추가',
|
||||||
addAnotherTask: '다른 TASK 추가',
|
addAnotherTask: '다른 태스크 추가',
|
||||||
addCard: '항목카드 추가',
|
addCard: '카드 추가',
|
||||||
addCard_title: '항목카드 추가',
|
addCard_title: '카드 추가',
|
||||||
addComment: '댓글 추가',
|
addComment: '댓글 추가',
|
||||||
addList: '리스트 추가',
|
addList: '리스트 추가',
|
||||||
|
addMember: '멤버 추가',
|
||||||
addMoreDetailedDescription: '자세한 설명 추가',
|
addMoreDetailedDescription: '자세한 설명 추가',
|
||||||
addTask: '태스트 추가',
|
addTask: '태스크 추가',
|
||||||
addToCard: '항목카드 추가',
|
addToCard: '카드에 추가',
|
||||||
addUser: '사용자 추가',
|
addUser: '사용자 추가',
|
||||||
createBoard: '보드 생성',
|
createBoard: '보드 생성',
|
||||||
createFile: '파일 생성',
|
createFile: '파일 생성',
|
||||||
createLabel: '라벨 생성',
|
createLabel: '레이블 생성',
|
||||||
createNewLabel: '새로운 라벨 생성',
|
createNewLabel: '새로운 레이블 생성',
|
||||||
createProject: '프로젝트 생성',
|
createProject: '프로젝트 생성',
|
||||||
delete: '삭제',
|
delete: '삭제',
|
||||||
deleteAttachment: '첨부 삭제',
|
deleteAttachment: '첨부 삭제',
|
||||||
deleteAvatar: '프로필 사진 삭제',
|
deleteAvatar: '아바타 삭제',
|
||||||
deleteBoard: '보드 삭제',
|
deleteBoard: '보드 삭제',
|
||||||
deleteCard: '항목카드 삭제',
|
deleteCard: '카드 삭제',
|
||||||
deleteCard_title: '항목카드 삭제',
|
deleteCard_title: '카드 삭제',
|
||||||
deleteComment: '댓글 삭제',
|
deleteComment: '댓글 삭제',
|
||||||
deleteImage: '이미지 삭제',
|
deleteImage: '이미지 삭제',
|
||||||
deleteLabel: '라벨 삭제',
|
deleteLabel: '레이블 삭제',
|
||||||
deleteList: '리스트 삭제',
|
deleteList: '리스트 삭제',
|
||||||
deleteList_title: '리스트 삭제',
|
deleteList_title: '리스트 삭제',
|
||||||
deleteProject: '프로젝트 삭제',
|
deleteProject: '프로젝트 삭제',
|
||||||
deleteProject_title: '프로젝트 삭제',
|
deleteProject_title: '프로젝트 삭제',
|
||||||
deleteTask: '태스트 삭제',
|
deleteTask: '태스크 삭제',
|
||||||
deleteTask_title: '태스트 삭제',
|
deleteTask_title: '태스크 삭제',
|
||||||
deleteUser: '사용자 삭제',
|
deleteUser: '사용자 삭제',
|
||||||
edit: '수정',
|
edit: '수정',
|
||||||
editDueDate_title: '완료일 수정',
|
editDueDate_title: '마감일 수정',
|
||||||
editDescription_title: '설명 수정',
|
editDescription_title: '설명 수정',
|
||||||
editEmail_title: '메일 수정',
|
editEmail_title: '이메일 수정',
|
||||||
editInformation_title: '정보 수정',
|
editInformation_title: '정보 수정',
|
||||||
editPassword_title: '패스워드 수정',
|
editPassword_title: '비밀번호 수정',
|
||||||
editStopwatch_title: '시간 수정',
|
editPermissions: '권한 수정',
|
||||||
|
editStopwatch_title: '스톱워치 수정',
|
||||||
editTitle_title: '제목 수정',
|
editTitle_title: '제목 수정',
|
||||||
editUsername_title: '이름 수정',
|
editUsername_title: '사용자 이름 수정',
|
||||||
leaveBoard: '보드에서 나가기',
|
hideDetails: '세부 사항 숨기기',
|
||||||
leaveProject: '프로젝트에서 나가기',
|
import: '가져오기',
|
||||||
|
leaveBoard: '보드 나가기',
|
||||||
|
leaveProject: '프로젝트 나가기',
|
||||||
logOut_title: '로그아웃',
|
logOut_title: '로그아웃',
|
||||||
makeCover_title: '커버 만들기',
|
makeCover_title: '커버 만들기',
|
||||||
move: '이동',
|
move: '이동',
|
||||||
moveCard_title: '항목카드 이동',
|
moveCard_title: '카드 이동',
|
||||||
remove: '제거',
|
remove: '제거',
|
||||||
removeBackground: '백그라운드 제거',
|
removeBackground: '배경 제거',
|
||||||
removeCover_title: '커버 제거',
|
removeCover_title: '커버 제거',
|
||||||
removeFromBoard: '보드에서 제거',
|
removeFromBoard: '보드 제거',
|
||||||
removeFromProject: '프로젝트에서 제거',
|
removeFromProject: '프로젝트 제거',
|
||||||
removeManager: '관리자 삭제',
|
removeManager: '매니저 제거',
|
||||||
removeMember: '멤버 삭제',
|
removeMember: '멤버 제거',
|
||||||
save: '저장',
|
save: '저장',
|
||||||
showAllAttachments: '모든 첨부파일 표시 ({{hidden}} hidden)',
|
showAllAttachments: '모든 첨부 파일 표시 ({{hidden}} 숨겨짐)',
|
||||||
|
showDetails: '세부 사항 보기',
|
||||||
showFewerAttachments: '일부 첨부 파일만 표시',
|
showFewerAttachments: '일부 첨부 파일만 표시',
|
||||||
start: '시작',
|
start: '시작',
|
||||||
stop: '끝',
|
stop: '종료',
|
||||||
subscribe: '구독하기',
|
subscribe: '구독하기',
|
||||||
unsubscribe: '구독취소',
|
unsubscribe: '구독취소',
|
||||||
uploadNewAvatar: '새로운 프로필 사진 업로드',
|
uploadNewAvatar: '새로운 아바타 업로드',
|
||||||
uploadNewImage: '새로운 이미지 업로드',
|
uploadNewImage: '새로운 이미지 업로드',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
/* Translated by Park Seok Ho */
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
translation: {
|
translation: {
|
||||||
common: {
|
common: {
|
||||||
emailOrUsername: '이메일 혹은 아이디',
|
emailOrUsername: '이메일 혹은 사용자 이름',
|
||||||
invalidEmailOrUsername: '이메일이나 사용자 이름이 유효하지 않습니다.',
|
invalidEmailOrUsername: '이메일 혹은 사용자 이름이 유효하지 않습니다.',
|
||||||
invalidPassword: '잘못된 비밀번호',
|
invalidPassword: '유효하지 않은 비밀번호',
|
||||||
logInToPlanka: 'Planka에 로그인',
|
logInToPlanka: 'Planka에 로그인',
|
||||||
noInternetConnection: '인터넷에 연결되지 않음',
|
noInternetConnection: '인터넷 연결이 없음',
|
||||||
pageNotFound_title: '페이지를 찾을 수 없습니다.',
|
pageNotFound_title: '페이지를 찾을 수 없습니다.',
|
||||||
password: '패스워드',
|
password: '비밀번호',
|
||||||
projectManagement: '프로젝트 관리',
|
projectManagement: '프로젝트 관리',
|
||||||
serverConnectionFailed: '서버 연결 실패',
|
serverConnectionFailed: '서버 연결에 실패함',
|
||||||
unknownError: '알 수 없는 오류가 발생하였습니다, 나중에 다시 시도하세요.',
|
unknownError: '알 수 없는 오류, 나중에 다시 시도하십시오.',
|
||||||
|
useSingleSignOn: 'single sign-on(SSO) 사용',
|
||||||
},
|
},
|
||||||
|
|
||||||
action: {
|
action: {
|
||||||
logIn: '로그인',
|
logIn: '로그인',
|
||||||
|
logInWithSSO: 'single sign-on(SSO)으로 로그인',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import login from './login';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
language: 'ro',
|
||||||
|
country: 'ro',
|
||||||
|
name: 'Română',
|
||||||
|
embeddedLocale: login,
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -43,16 +43,14 @@ export default class extends BaseModel {
|
|||||||
organization: attr(),
|
organization: attr(),
|
||||||
language: attr(),
|
language: attr(),
|
||||||
subscribeToOwnCards: attr(),
|
subscribeToOwnCards: attr(),
|
||||||
|
isAdmin: attr(),
|
||||||
|
isLocked: attr(),
|
||||||
|
isRoleLocked: attr(),
|
||||||
|
isDeletionLocked: attr(),
|
||||||
|
deletedAt: attr(),
|
||||||
createdAt: attr({
|
createdAt: attr({
|
||||||
getDefault: () => new Date(),
|
getDefault: () => new Date(),
|
||||||
}),
|
}),
|
||||||
deletedAt: attr(),
|
|
||||||
isAdmin: attr({
|
|
||||||
getDefault: () => false,
|
|
||||||
}),
|
|
||||||
isLocked: attr({
|
|
||||||
getDefault: () => false,
|
|
||||||
}),
|
|
||||||
isAvatarUpdating: attr({
|
isAvatarUpdating: attr({
|
||||||
getDefault: () => false,
|
getDefault: () => false,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const initialState = {
|
|||||||
export default (state = initialState, { type, payload }) => {
|
export default (state = initialState, { type, payload }) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ActionTypes.AUTHENTICATE__SUCCESS:
|
case ActionTypes.AUTHENTICATE__SUCCESS:
|
||||||
|
case ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
accessToken: payload.accessToken,
|
accessToken: payload.accessToken,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import ActionTypes from '../constants/ActionTypes';
|
|||||||
import ModalTypes from '../constants/ModalTypes';
|
import ModalTypes from '../constants/ModalTypes';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
isInitializing: true,
|
|
||||||
isLogouting: false,
|
isLogouting: false,
|
||||||
currentModal: null,
|
currentModal: null,
|
||||||
};
|
};
|
||||||
@@ -18,11 +17,6 @@ export default (state = initialState, { type, payload }) => {
|
|||||||
...state,
|
...state,
|
||||||
currentModal: null,
|
currentModal: null,
|
||||||
};
|
};
|
||||||
case ActionTypes.CORE_INITIALIZE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isInitializing: false,
|
|
||||||
};
|
|
||||||
case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
|
case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { combineReducers } from 'redux';
|
|||||||
import router from './router';
|
import router from './router';
|
||||||
import socket from './socket';
|
import socket from './socket';
|
||||||
import orm from './orm';
|
import orm from './orm';
|
||||||
|
import root from './root';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
@@ -11,6 +12,7 @@ export default combineReducers({
|
|||||||
router,
|
router,
|
||||||
socket,
|
socket,
|
||||||
orm,
|
orm,
|
||||||
|
root,
|
||||||
auth,
|
auth,
|
||||||
core,
|
core,
|
||||||
ui,
|
ui,
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
isInitializing: true,
|
||||||
|
config: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line default-param-last
|
||||||
|
export default (state = initialState, { type, payload }) => {
|
||||||
|
switch (type) {
|
||||||
|
case ActionTypes.LOGIN_INITIALIZE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isInitializing: false,
|
||||||
|
config: payload.config,
|
||||||
|
};
|
||||||
|
case ActionTypes.AUTHENTICATE__SUCCESS:
|
||||||
|
case ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isInitializing: true,
|
||||||
|
};
|
||||||
|
case ActionTypes.CORE_INITIALIZE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isInitializing: false,
|
||||||
|
};
|
||||||
|
case ActionTypes.CORE_INITIALIZE__CONFIG_FETCH:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
config: payload.config,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { LOCATION_CHANGE_HANDLE } from '../../lib/redux-router';
|
||||||
|
|
||||||
import ActionTypes from '../../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
|
import Paths from '../../constants/Paths';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
data: {
|
data: {
|
||||||
@@ -6,12 +9,22 @@ const initialState = {
|
|||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
|
isSubmittingUsingOidc: false,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line default-param-last
|
// eslint-disable-next-line default-param-last
|
||||||
export default (state = initialState, { type, payload }) => {
|
export default (state = initialState, { type, payload }) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case LOCATION_CHANGE_HANDLE:
|
||||||
|
if (payload.location.pathname === Paths.OIDC_CALLBACK) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isSubmittingUsingOidc: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
case ActionTypes.AUTHENTICATE:
|
case ActionTypes.AUTHENTICATE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -22,6 +35,7 @@ export default (state = initialState, { type, payload }) => {
|
|||||||
isSubmitting: true,
|
isSubmitting: true,
|
||||||
};
|
};
|
||||||
case ActionTypes.AUTHENTICATE__SUCCESS:
|
case ActionTypes.AUTHENTICATE__SUCCESS:
|
||||||
|
case ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS:
|
||||||
return initialState;
|
return initialState;
|
||||||
case ActionTypes.AUTHENTICATE__FAILURE:
|
case ActionTypes.AUTHENTICATE__FAILURE:
|
||||||
return {
|
return {
|
||||||
@@ -29,6 +43,12 @@ export default (state = initialState, { type, payload }) => {
|
|||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
error: payload.error,
|
error: payload.error,
|
||||||
};
|
};
|
||||||
|
case ActionTypes.USING_OIDC_AUTHENTICATE__FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isSubmittingUsingOidc: false,
|
||||||
|
error: payload.error,
|
||||||
|
};
|
||||||
case ActionTypes.AUTHENTICATE_ERROR_CLEAR:
|
case ActionTypes.AUTHENTICATE_ERROR_CLEAR:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -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 watchers from './watchers';
|
||||||
import services from './services';
|
import services from './services';
|
||||||
|
import selectors from '../../selectors';
|
||||||
import { socket } from '../../api';
|
import { socket } from '../../api';
|
||||||
import ActionTypes from '../../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
import Paths from '../../constants/Paths';
|
import Paths from '../../constants/Paths';
|
||||||
@@ -14,5 +15,12 @@ export default function* coreSaga() {
|
|||||||
|
|
||||||
yield take(ActionTypes.LOGOUT);
|
yield take(ActionTypes.LOGOUT);
|
||||||
|
|
||||||
|
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;
|
window.location.href = Paths.LOGIN;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { call, put, take } from 'redux-saga/effects';
|
import { call, put, select, take } from 'redux-saga/effects';
|
||||||
|
|
||||||
import request from '../request';
|
import request from '../request';
|
||||||
import requests from '../requests';
|
import requests from '../requests';
|
||||||
|
import selectors from '../../../selectors';
|
||||||
import actions from '../../../actions';
|
import actions from '../../../actions';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import { removeAccessToken } from '../../../utils/access-token-storage';
|
import { removeAccessToken } from '../../../utils/access-token-storage';
|
||||||
|
|
||||||
export function* initializeCore() {
|
export function* initializeCore() {
|
||||||
|
const currentConfig = yield select(selectors.selectConfig); // TODO: add boolean selector?
|
||||||
|
|
||||||
|
let config;
|
||||||
|
if (!currentConfig) {
|
||||||
|
({ item: config } = yield call(api.getConfig)); // TODO: handle error
|
||||||
|
|
||||||
|
yield put(actions.initializeCore.fetchConfig(config));
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
board,
|
board,
|
||||||
|
|||||||
@@ -33,15 +33,16 @@ export function* handleLocationChange() {
|
|||||||
|
|
||||||
switch (pathsMatch.pattern.path) {
|
switch (pathsMatch.pattern.path) {
|
||||||
case Paths.LOGIN:
|
case Paths.LOGIN:
|
||||||
|
case Paths.OIDC_CALLBACK:
|
||||||
yield call(goToRoot);
|
yield call(goToRoot);
|
||||||
|
|
||||||
break;
|
return;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCoreInitializing = yield select(selectors.selectIsCoreInitializing);
|
const isInitializing = yield select(selectors.selectIsInitializing);
|
||||||
|
|
||||||
if (isCoreInitializing) {
|
if (isInitializing) {
|
||||||
yield take(ActionTypes.CORE_INITIALIZE);
|
yield take(ActionTypes.CORE_INITIALIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,25 @@ export function* handleSocketReconnect() {
|
|||||||
|
|
||||||
yield put(actions.handleSocketReconnect.fetchCore(currentUserId, boardId));
|
yield put(actions.handleSocketReconnect.fetchCore(currentUserId, boardId));
|
||||||
|
|
||||||
const {
|
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,
|
user,
|
||||||
board,
|
board,
|
||||||
users,
|
users,
|
||||||
@@ -31,7 +49,10 @@ export function* handleSocketReconnect() {
|
|||||||
attachments,
|
attachments,
|
||||||
activities,
|
activities,
|
||||||
notifications,
|
notifications,
|
||||||
} = yield call(requests.fetchCore); // TODO: handle error
|
} = yield call(requests.fetchCore));
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
yield put(
|
yield put(
|
||||||
actions.handleSocketReconnect(
|
actions.handleSocketReconnect(
|
||||||
|
|||||||
@@ -4,8 +4,5 @@ import services from '../services';
|
|||||||
import EntryActionTypes from '../../../constants/EntryActionTypes';
|
import EntryActionTypes from '../../../constants/EntryActionTypes';
|
||||||
|
|
||||||
export default function* coreWatchers() {
|
export default function* coreWatchers() {
|
||||||
yield all([
|
yield all([takeEvery(EntryActionTypes.LOGOUT, () => services.logout())]);
|
||||||
takeEvery(EntryActionTypes.CORE_INITIALIZE, () => services.initializeCore()),
|
|
||||||
takeEvery(EntryActionTypes.LOGOUT, () => services.logout()),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import ActionTypes from '../../constants/ActionTypes';
|
|||||||
export default function* loginSaga() {
|
export default function* loginSaga() {
|
||||||
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
|
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
|
||||||
|
|
||||||
yield take(ActionTypes.AUTHENTICATE__SUCCESS);
|
yield fork(services.initializeLogin);
|
||||||
|
|
||||||
|
yield take([ActionTypes.AUTHENTICATE__SUCCESS, ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS]);
|
||||||
|
|
||||||
yield cancel(watcherTasks);
|
yield cancel(watcherTasks);
|
||||||
yield call(services.goToRoot);
|
yield call(services.goToRoot);
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import { call, put } 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 actions from '../../../actions';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import { setAccessToken } from '../../../utils/access-token-storage';
|
import { setAccessToken } from '../../../utils/access-token-storage';
|
||||||
|
import Paths from '../../../constants/Paths';
|
||||||
|
|
||||||
|
export function* initializeLogin() {
|
||||||
|
const { item: config } = yield call(api.getConfig); // TODO: handle error
|
||||||
|
|
||||||
|
yield put(actions.initializeLogin(config));
|
||||||
|
}
|
||||||
|
|
||||||
export function* authenticate(data) {
|
export function* authenticate(data) {
|
||||||
yield put(actions.authenticate(data));
|
yield put(actions.authenticate(data));
|
||||||
|
|
||||||
let accessToken = data.access_token;
|
let accessToken;
|
||||||
try {
|
try {
|
||||||
if (accessToken) {
|
|
||||||
({ item: accessToken } = yield call(api.exchangeOidcToken, accessToken));
|
|
||||||
} else {
|
|
||||||
({ item: accessToken } = yield call(api.createAccessToken, data));
|
({ item: accessToken } = yield call(api.createAccessToken, data));
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
yield put(actions.authenticate.failure(error));
|
yield put(actions.authenticate.failure(error));
|
||||||
return;
|
return;
|
||||||
@@ -23,11 +29,94 @@ export function* authenticate(data) {
|
|||||||
yield put(actions.authenticate.success(accessToken));
|
yield put(actions.authenticate.success(accessToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* authenticateUsingOidc() {
|
||||||
|
const oidcConfig = yield select(selectors.selectOidcConfig);
|
||||||
|
|
||||||
|
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* authenticateUsingOidcCallback() {
|
||||||
|
// https://github.com/plankanban/planka/issues/511#issuecomment-1771385639
|
||||||
|
const params = new URLSearchParams(window.location.hash.substring(1) || window.location.search);
|
||||||
|
|
||||||
|
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 (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() {
|
export function* clearAuthenticateError() {
|
||||||
yield put(actions.clearAuthenticateError());
|
yield put(actions.clearAuthenticateError());
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
initializeLogin,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
authenticateUsingOidc,
|
||||||
|
authenticateUsingOidcCallback,
|
||||||
clearAuthenticateError,
|
clearAuthenticateError,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { call, put, select } from 'redux-saga/effects';
|
import { call, put, select, take } from 'redux-saga/effects';
|
||||||
import { push } from '../../../lib/redux-router';
|
import { push } from '../../../lib/redux-router';
|
||||||
|
|
||||||
|
import { authenticateUsingOidcCallback } from './login';
|
||||||
import selectors from '../../../selectors';
|
import selectors from '../../../selectors';
|
||||||
|
import ActionTypes from '../../../constants/ActionTypes';
|
||||||
import Paths from '../../../constants/Paths';
|
import Paths from '../../../constants/Paths';
|
||||||
|
|
||||||
export function* goToLogin() {
|
export function* goToLogin() {
|
||||||
@@ -27,6 +29,17 @@ export function* handleLocationChange() {
|
|||||||
yield call(goToLogin);
|
yield call(goToLogin);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case Paths.OIDC_CALLBACK: {
|
||||||
|
const isInitializing = yield select(selectors.selectIsInitializing);
|
||||||
|
|
||||||
|
if (isInitializing) {
|
||||||
|
yield take(ActionTypes.LOGIN_INITIALIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield call(authenticateUsingOidcCallback);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default function* loginWatchers() {
|
|||||||
takeEvery(EntryActionTypes.AUTHENTICATE, ({ payload: { data } }) =>
|
takeEvery(EntryActionTypes.AUTHENTICATE, ({ payload: { data } }) =>
|
||||||
services.authenticate(data),
|
services.authenticate(data),
|
||||||
),
|
),
|
||||||
|
takeEvery(EntryActionTypes.USING_OIDC_AUTHENTICATE, () => services.authenticateUsingOidc()),
|
||||||
takeEvery(EntryActionTypes.AUTHENTICATE_ERROR_CLEAR, () => services.clearAuthenticateError()),
|
takeEvery(EntryActionTypes.AUTHENTICATE_ERROR_CLEAR, () => services.clearAuthenticateError()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import Config from '../constants/Config';
|
|||||||
|
|
||||||
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
|
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
|
||||||
|
|
||||||
export const selectIsCoreInitializing = ({ core: { isInitializing } }) => isInitializing;
|
|
||||||
|
|
||||||
export const selectIsLogouting = ({ core: { isLogouting } }) => isLogouting;
|
export const selectIsLogouting = ({ core: { isLogouting } }) => isLogouting;
|
||||||
|
|
||||||
const nextPosition = (items, index, excludedId) => {
|
const nextPosition = (items, index, excludedId) => {
|
||||||
@@ -115,7 +113,6 @@ export const selectNextTaskPosition = createSelector(
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
selectAccessToken,
|
selectAccessToken,
|
||||||
selectIsCoreInitializing,
|
|
||||||
selectIsLogouting,
|
selectIsLogouting,
|
||||||
selectNextBoardPosition,
|
selectNextBoardPosition,
|
||||||
selectNextLabelPosition,
|
selectNextLabelPosition,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import router from './router';
|
import router from './router';
|
||||||
import socket from './socket';
|
import socket from './socket';
|
||||||
|
import root from './root';
|
||||||
import core from './core';
|
import core from './core';
|
||||||
import modals from './modals';
|
import modals from './modals';
|
||||||
import users from './users';
|
import users from './users';
|
||||||
@@ -16,6 +17,7 @@ import attachments from './attachments';
|
|||||||
export default {
|
export default {
|
||||||
...router,
|
...router,
|
||||||
...socket,
|
...socket,
|
||||||
|
...root,
|
||||||
...core,
|
...core,
|
||||||
...modals,
|
...modals,
|
||||||
...users,
|
...users,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const selectIsInitializing = ({ root: { isInitializing } }) => isInitializing;
|
||||||
|
|
||||||
|
export const selectConfig = ({ root: { config } }) => config;
|
||||||
|
|
||||||
|
export const selectOidcConfig = (state) => selectConfig(state).oidc;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
selectIsInitializing,
|
||||||
|
selectConfig,
|
||||||
|
selectOidcConfig,
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import jwtDecode from 'jwt-decode';
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
|
||||||
import Config from '../constants/Config';
|
import Config from '../constants/Config';
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -1,16 +1,65 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
planka:
|
||||||
|
image: ghcr.io/plankanban/planka:dev
|
||||||
|
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:
|
postgres:
|
||||||
image: postgres:alpine
|
image: postgres:14-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- db-data:/var/lib/postgresql/data
|
- db-data:/var/lib/postgresql/data
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=planka
|
- POSTGRES_DB=planka
|
||||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
user-avatars:
|
||||||
|
project-background-images:
|
||||||
|
attachments:
|
||||||
db-data:
|
db-data:
|
||||||
|
|||||||
+15
-6
@@ -20,15 +20,11 @@ services:
|
|||||||
- 3000:1337
|
- 3000:1337
|
||||||
environment:
|
environment:
|
||||||
- BASE_URL=http://localhost:3000
|
- BASE_URL=http://localhost:3000
|
||||||
- TRUST_PROXY=0
|
|
||||||
- DATABASE_URL=postgresql://postgres@postgres/planka
|
- DATABASE_URL=postgresql://postgres@postgres/planka
|
||||||
- SECRET_KEY=notsecretkey
|
- SECRET_KEY=notsecretkey
|
||||||
|
|
||||||
# Can be removed after installation
|
# - TRUST_PROXY=0
|
||||||
- DEFAULT_ADMIN_EMAIL=demo@demo.demo # Do not remove if you want to prevent this user from being edited/deleted
|
# - TOKEN_EXPIRES_IN=365 # In days
|
||||||
- DEFAULT_ADMIN_PASSWORD=demo
|
|
||||||
- DEFAULT_ADMIN_NAME=Demo Demo
|
|
||||||
- DEFAULT_ADMIN_USERNAME=demo
|
|
||||||
|
|
||||||
# related: https://github.com/knex/knex/issues/2354
|
# related: https://github.com/knex/knex/issues/2354
|
||||||
# As knex does not pass query parameters from the connection string we
|
# As knex does not pass query parameters from the connection string we
|
||||||
@@ -37,6 +33,19 @@ services:
|
|||||||
|
|
||||||
# Configure knex to accept SSL certificates
|
# Configure knex to accept SSL certificates
|
||||||
# - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
|
# - 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:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
|
|
||||||
|
|||||||
Generated
+1383
-826
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.12.0",
|
"version": "1.15.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://plankanban.github.io/planka",
|
"homepage": "https://plankanban.github.io/planka",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -56,14 +56,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"concurrently": "^7.6.0",
|
"concurrently": "^8.2.2",
|
||||||
"husky": "^8.0.2",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.0.3"
|
"lint-staged": "^15.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.53.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"prettier": "^2.7.1"
|
"prettier": "^3.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-7
@@ -4,13 +4,6 @@ BASE_URL=http://localhost:1337
|
|||||||
DATABASE_URL=postgresql://postgres@localhost/planka
|
DATABASE_URL=postgresql://postgres@localhost/planka
|
||||||
SECRET_KEY=notsecretkey
|
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
|
## Optional
|
||||||
|
|
||||||
# TRUST_PROXY=0
|
# TRUST_PROXY=0
|
||||||
@@ -24,6 +17,19 @@ DEFAULT_ADMIN_USERNAME=demo
|
|||||||
# Configure knex to accept SSL certificates
|
# Configure knex to accept SSL certificates
|
||||||
# KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
|
# 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
|
||||||
|
|
||||||
## Do not edit this
|
## Do not edit this
|
||||||
|
|
||||||
TZ=UTC
|
TZ=UTC
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ const Errors = {
|
|||||||
INVALID_PASSWORD: {
|
INVALID_PASSWORD: {
|
||||||
invalidPassword: 'Invalid password',
|
invalidPassword: 'Invalid password',
|
||||||
},
|
},
|
||||||
|
USE_SINGLE_SIGN_ON: {
|
||||||
|
useSingleSignOn: 'Use single sign-on',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const emailOrUsernameValidator = (value) =>
|
const emailOrUsernameValidator = (value) =>
|
||||||
@@ -37,6 +40,9 @@ module.exports = {
|
|||||||
invalidPassword: {
|
invalidPassword: {
|
||||||
responseType: 'unauthorized',
|
responseType: 'unauthorized',
|
||||||
},
|
},
|
||||||
|
useSingleSignOn: {
|
||||||
|
responseType: 'forbidden',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
@@ -51,6 +57,10 @@ module.exports = {
|
|||||||
throw Errors.INVALID_EMAIL_OR_USERNAME;
|
throw Errors.INVALID_EMAIL_OR_USERNAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.isSso) {
|
||||||
|
throw Errors.USE_SINGLE_SIGN_ON;
|
||||||
|
}
|
||||||
|
|
||||||
if (!bcrypt.compareSync(inputs.password, user.password)) {
|
if (!bcrypt.compareSync(inputs.password, user.password)) {
|
||||||
sails.log.warn(`Invalid password! (IP: ${remoteAddress})`);
|
sails.log.warn(`Invalid password! (IP: ${remoteAddress})`);
|
||||||
throw Errors.INVALID_PASSWORD;
|
throw Errors.INVALID_PASSWORD;
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||||||
|
|
||||||
|
const Errors = {
|
||||||
|
INVALID_CODE_OR_NONCE: {
|
||||||
|
invalidCodeOrNonce: 'Invalid code or nonce',
|
||||||
|
},
|
||||||
|
EMAIL_ALREADY_IN_USE: {
|
||||||
|
emailAlreadyInUse: 'Email already in use',
|
||||||
|
},
|
||||||
|
USERNAME_ALREADY_IN_USE: {
|
||||||
|
usernameAlreadyInUse: 'Username already in use',
|
||||||
|
},
|
||||||
|
MISSING_VALUES: {
|
||||||
|
missingValues: 'Unable to retrieve required values (email, name)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
code: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
nonce: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
invalidCodeOrNonce: {
|
||||||
|
responseType: 'unauthorized',
|
||||||
|
},
|
||||||
|
emailAlreadyInUse: {
|
||||||
|
responseType: 'conflict',
|
||||||
|
},
|
||||||
|
usernameAlreadyInUse: {
|
||||||
|
responseType: 'conflict',
|
||||||
|
},
|
||||||
|
missingValues: {
|
||||||
|
responseType: 'unprocessableEntity',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fn(inputs) {
|
||||||
|
const remoteAddress = getRemoteAddress(this.req);
|
||||||
|
|
||||||
|
const user = await sails.helpers.users
|
||||||
|
.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)
|
||||||
|
.intercept('missingValues', () => Errors.MISSING_VALUES);
|
||||||
|
|
||||||
|
const accessToken = sails.helpers.utils.createToken(user.id);
|
||||||
|
|
||||||
|
await Session.create({
|
||||||
|
accessToken,
|
||||||
|
remoteAddress,
|
||||||
|
userId: user.id,
|
||||||
|
userAgent: this.req.headers['user-agent'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
item: accessToken,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const jwksClient = require('jwks-rsa');
|
|
||||||
const openidClient = require('openid-client');
|
|
||||||
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
|
||||||
|
|
||||||
const Errors = {
|
|
||||||
INVALID_TOKEN: {
|
|
||||||
invalidToken: 'Access Token is invalid',
|
|
||||||
},
|
|
||||||
MISSING_VALUES: {
|
|
||||||
missingValues:
|
|
||||||
'Unable to retrieve required values. Verify the access token or UserInfo endpoint has email, username and name claims',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const jwks = jwksClient({
|
|
||||||
jwksUri: sails.config.custom.oidcJwksUri,
|
|
||||||
requestHeaders: {}, // Optional
|
|
||||||
timeout: 30000, // Defaults to 30s
|
|
||||||
});
|
|
||||||
|
|
||||||
const getJwtVerificationOptions = () => {
|
|
||||||
const options = {};
|
|
||||||
if (sails.config.custom.oidcIssuer) {
|
|
||||||
options.issuer = sails.config.custom.oidcIssuer;
|
|
||||||
}
|
|
||||||
if (sails.config.custom.oidcAudience) {
|
|
||||||
options.audience = sails.config.custom.oidcAudience;
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateAndDecodeToken = async (accessToken, options) => {
|
|
||||||
const keys = await jwks.getSigningKeys();
|
|
||||||
let validToken = {};
|
|
||||||
|
|
||||||
const isTokenValid = keys.some((signingKey) => {
|
|
||||||
try {
|
|
||||||
const key = signingKey.getPublicKey();
|
|
||||||
validToken = jwt.verify(accessToken, key, options);
|
|
||||||
return 'true';
|
|
||||||
} catch (error) {
|
|
||||||
sails.log.error(error);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isTokenValid) {
|
|
||||||
const tokenForLogging = jwt.decode(accessToken);
|
|
||||||
const remoteAddress = getRemoteAddress(this.req);
|
|
||||||
|
|
||||||
sails.log.warn(
|
|
||||||
`invalid token: sub: "${tokenForLogging.sub}" issuer: "${tokenForLogging.iss}" audience: "${tokenForLogging.aud}" exp: ${tokenForLogging.exp} (IP: ${remoteAddress})`,
|
|
||||||
);
|
|
||||||
throw Errors.INVALID_TOKEN;
|
|
||||||
}
|
|
||||||
return validToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserInfo = async (accessToken, options) => {
|
|
||||||
if (sails.config.custom.oidcSkipUserInfo) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const issuer = await openidClient.Issuer.discover(options.issuer);
|
|
||||||
const oidcClient = new issuer.Client({
|
|
||||||
client_id: 'irrelevant',
|
|
||||||
});
|
|
||||||
const userInfo = await oidcClient.userinfo(accessToken);
|
|
||||||
return userInfo;
|
|
||||||
};
|
|
||||||
const mergeUserData = (validToken, userInfo) => {
|
|
||||||
const oidcUser = { ...validToken, ...userInfo };
|
|
||||||
return oidcUser;
|
|
||||||
};
|
|
||||||
const getOrCreateUser = async (newUser) => {
|
|
||||||
const user = await User.findOne({
|
|
||||||
where: {
|
|
||||||
username: newUser.username,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (user) {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
return User.create(newUser).fetch();
|
|
||||||
};
|
|
||||||
module.exports = {
|
|
||||||
inputs: {
|
|
||||||
token: {
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
exits: {
|
|
||||||
invalidToken: {
|
|
||||||
responseType: 'unauthorized',
|
|
||||||
},
|
|
||||||
missingValues: {
|
|
||||||
responseType: 'unauthorized',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fn(inputs) {
|
|
||||||
const options = getJwtVerificationOptions();
|
|
||||||
const validToken = await validateAndDecodeToken(inputs.token, options);
|
|
||||||
const userInfo = await getUserInfo(inputs.token, options);
|
|
||||||
const oidcUser = mergeUserData(validToken, userInfo);
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
let isAdmin = false;
|
|
||||||
if (sails.config.custom.oidcAdminRoles.includes('*')) isAdmin = true;
|
|
||||||
else if (Array.isArray(oidcUser[sails.config.custom.oidcRolesAttribute])) {
|
|
||||||
const userRoles = new Set(oidcUser[sails.config.custom.oidcRolesAttribute]);
|
|
||||||
isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = {
|
|
||||||
email: oidcUser.email,
|
|
||||||
isAdmin,
|
|
||||||
name: oidcUser.name,
|
|
||||||
username: oidcUser.preferred_username,
|
|
||||||
subscribeToOwnCards: false,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
locked: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!newUser.email || !newUser.username || !newUser.name) {
|
|
||||||
sails.log.error(Errors.MISSING_VALUES.missingValues);
|
|
||||||
throw Errors.MISSING_VALUES;
|
|
||||||
}
|
|
||||||
|
|
||||||
const identityProviderUser = await IdentityProviderUser.findOne({
|
|
||||||
where: {
|
|
||||||
issuer: oidcUser.iss,
|
|
||||||
sub: oidcUser.sub,
|
|
||||||
},
|
|
||||||
}).populate('userId');
|
|
||||||
|
|
||||||
let user = identityProviderUser ? identityProviderUser.userId : {};
|
|
||||||
if (!identityProviderUser) {
|
|
||||||
user = await getOrCreateUser(newUser);
|
|
||||||
await IdentityProviderUser.create({
|
|
||||||
issuer: oidcUser.iss,
|
|
||||||
sub: oidcUser.sub,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const controlledFields = ['email', 'password', 'isAdmin', 'name', 'username'];
|
|
||||||
const updateFields = {};
|
|
||||||
controlledFields.forEach((field) => {
|
|
||||||
if (user[field] !== newUser[field]) {
|
|
||||||
updateFields[field] = newUser[field];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Object.keys(updateFields).length > 0) {
|
|
||||||
updateFields.updatedAt = now;
|
|
||||||
await User.updateOne({ id: user.id }).set(updateFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
const plankaToken = sails.helpers.utils.createToken(user.id);
|
|
||||||
|
|
||||||
const remoteAddress = getRemoteAddress(this.req);
|
|
||||||
await Session.create({
|
|
||||||
accessToken: plankaToken,
|
|
||||||
remoteAddress,
|
|
||||||
userId: user.id,
|
|
||||||
userAgent: this.req.headers['user-agent'],
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
item: plankaToken,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
async fn() {
|
|
||||||
const config = {
|
|
||||||
authority: sails.config.custom.oidcIssuer,
|
|
||||||
clientId: sails.config.custom.oidcClientId,
|
|
||||||
redirectUri: sails.config.custom.oidcredirectUri,
|
|
||||||
scopes: sails.config.custom.oidcScopes,
|
|
||||||
};
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -0,0 +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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
item: {
|
||||||
|
oidc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
const Errors = {
|
const Errors = {
|
||||||
|
NOT_ENOUGH_RIGHTS: {
|
||||||
|
notEnoughRights: 'Not enough rights',
|
||||||
|
},
|
||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
userNotFound: 'User not found',
|
userNotFound: 'User not found',
|
||||||
},
|
},
|
||||||
@@ -14,6 +17,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
exits: {
|
exits: {
|
||||||
|
notEnoughRights: {
|
||||||
|
responseType: 'forbidden',
|
||||||
|
},
|
||||||
userNotFound: {
|
userNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
@@ -27,7 +33,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (user.email === sails.config.custom.defaultAdminEmail) {
|
if (user.email === sails.config.custom.defaultAdminEmail) {
|
||||||
throw Errors.USER_NOT_FOUND; // Forbidden
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
user = await sails.helpers.users.deleteOne.with({
|
user = await sails.helpers.users.deleteOne.with({
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
|
NOT_ENOUGH_RIGHTS: {
|
||||||
|
notEnoughRights: 'Not enough rights',
|
||||||
|
},
|
||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
userNotFound: 'User not found',
|
userNotFound: 'User not found',
|
||||||
},
|
},
|
||||||
@@ -31,6 +34,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
exits: {
|
exits: {
|
||||||
|
notEnoughRights: {
|
||||||
|
responseType: 'forbidden',
|
||||||
|
},
|
||||||
userNotFound: {
|
userNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
@@ -59,8 +65,8 @@ module.exports = {
|
|||||||
throw Errors.USER_NOT_FOUND;
|
throw Errors.USER_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.email === sails.config.custom.defaultAdminEmail) {
|
if (user.email === sails.config.custom.defaultAdminEmail || user.isSso) {
|
||||||
throw Errors.USER_NOT_FOUND; // Forbidden
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const zxcvbn = require('zxcvbn');
|
|||||||
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
|
NOT_ENOUGH_RIGHTS: {
|
||||||
|
notEnoughRights: 'Not enough rights',
|
||||||
|
},
|
||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
userNotFound: 'User not found',
|
userNotFound: 'User not found',
|
||||||
},
|
},
|
||||||
@@ -33,6 +36,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
exits: {
|
exits: {
|
||||||
|
notEnoughRights: {
|
||||||
|
responseType: 'forbidden',
|
||||||
|
},
|
||||||
userNotFound: {
|
userNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
@@ -58,8 +64,8 @@ module.exports = {
|
|||||||
throw Errors.USER_NOT_FOUND;
|
throw Errors.USER_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.email === sails.config.custom.defaultAdminEmail) {
|
if (user.email === sails.config.custom.defaultAdminEmail || user.isSso) {
|
||||||
throw Errors.USER_NOT_FOUND; // Forbidden
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
|
NOT_ENOUGH_RIGHTS: {
|
||||||
|
notEnoughRights: 'Not enough rights',
|
||||||
|
},
|
||||||
USER_NOT_FOUND: {
|
USER_NOT_FOUND: {
|
||||||
userNotFound: 'User not found',
|
userNotFound: 'User not found',
|
||||||
},
|
},
|
||||||
@@ -33,6 +36,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
exits: {
|
exits: {
|
||||||
|
notEnoughRights: {
|
||||||
|
responseType: 'forbidden',
|
||||||
|
},
|
||||||
userNotFound: {
|
userNotFound: {
|
||||||
responseType: 'notFound',
|
responseType: 'notFound',
|
||||||
},
|
},
|
||||||
@@ -61,8 +67,8 @@ module.exports = {
|
|||||||
throw Errors.USER_NOT_FOUND;
|
throw Errors.USER_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.email === sails.config.custom.defaultAdminEmail) {
|
if (user.email === sails.config.custom.defaultAdminEmail || user.isSso) {
|
||||||
throw Errors.USER_NOT_FOUND; // Forbidden
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ module.exports = {
|
|||||||
delete inputs.isAdmin;
|
delete inputs.isAdmin;
|
||||||
delete inputs.name;
|
delete inputs.name;
|
||||||
/* eslint-enable no-param-reassign */
|
/* 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
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = {
|
const values = {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const valuesValidator = (value) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isString(value.password)) {
|
if (!_.isNil(value.password) && !_.isString(value.password)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +40,10 @@ module.exports = {
|
|||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
const { values } = inputs;
|
const { values } = inputs;
|
||||||
|
|
||||||
|
if (values.password) {
|
||||||
|
values.password = bcrypt.hashSync(values.password, 10);
|
||||||
|
}
|
||||||
|
|
||||||
if (values.username) {
|
if (values.username) {
|
||||||
values.username = values.username.toLowerCase();
|
values.username = values.username.toLowerCase();
|
||||||
}
|
}
|
||||||
@@ -47,7 +51,6 @@ module.exports = {
|
|||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
...values,
|
...values,
|
||||||
email: values.email.toLowerCase(),
|
email: values.email.toLowerCase(),
|
||||||
password: bcrypt.hashSync(values.password, 10),
|
|
||||||
})
|
})
|
||||||
.intercept(
|
.intercept(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
|
await IdentityProviderUser.destroy({
|
||||||
|
userId: inputs.record.id,
|
||||||
|
});
|
||||||
|
|
||||||
await ProjectManager.destroy({
|
await ProjectManager.destroy({
|
||||||
userId: inputs.record.id,
|
userId: inputs.record.id,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
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.email || !userInfo.name) {
|
||||||
|
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.email,
|
||||||
|
isSso: true,
|
||||||
|
name: userInfo.name,
|
||||||
|
username: userInfo.preferred_username,
|
||||||
|
subscribeToOwnCards: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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', '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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -24,6 +24,11 @@ module.exports = {
|
|||||||
defaultsTo: false,
|
defaultsTo: false,
|
||||||
columnName: 'is_admin',
|
columnName: 'is_admin',
|
||||||
},
|
},
|
||||||
|
isSso: {
|
||||||
|
type: 'boolean',
|
||||||
|
defaultsTo: false,
|
||||||
|
columnName: 'is_sso',
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
@@ -67,10 +72,6 @@ module.exports = {
|
|||||||
type: 'ref',
|
type: 'ref',
|
||||||
columnName: 'password_changed_at',
|
columnName: 'password_changed_at',
|
||||||
},
|
},
|
||||||
locked: {
|
|
||||||
type: 'boolean',
|
|
||||||
columnName: 'locked',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||||
@@ -109,12 +110,16 @@ module.exports = {
|
|||||||
tableName: 'user_account',
|
tableName: 'user_account',
|
||||||
|
|
||||||
customToJSON() {
|
customToJSON() {
|
||||||
|
const isDefaultAdmin = this.email === sails.config.custom.defaultAdminEmail;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
..._.omit(this, ['password', 'avatar', 'passwordChangedAt']),
|
..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']),
|
||||||
|
isLocked: this.isSso || isDefaultAdmin,
|
||||||
|
isRoleLocked: (this.isSso && !sails.config.custom.oidcIgnoreRoles) || isDefaultAdmin,
|
||||||
|
isDeletionLocked: isDefaultAdmin,
|
||||||
avatarUrl:
|
avatarUrl:
|
||||||
this.avatar &&
|
this.avatar &&
|
||||||
`${sails.config.custom.userAvatarsUrl}/${this.avatar.dirname}/square-100.${this.avatar.extension}`,
|
`${sails.config.custom.userAvatarsUrl}/${this.avatar.dirname}/square-100.${this.avatar.extension}`,
|
||||||
isLocked: this.email === sails.config.custom.defaultAdminEmail,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-10
@@ -31,15 +31,18 @@ module.exports.custom = {
|
|||||||
attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'),
|
attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'),
|
||||||
attachmentsUrl: `${process.env.BASE_URL}/attachments`,
|
attachmentsUrl: `${process.env.BASE_URL}/attachments`,
|
||||||
|
|
||||||
oidcIssuer: process.env.OIDC_ISSUER,
|
|
||||||
oidcAudience: process.env.OIDC_AUDIENCE,
|
|
||||||
oidcClientId: process.env.OIDC_CLIENT_ID,
|
|
||||||
oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
|
|
||||||
oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
|
|
||||||
oidcredirectUri: process.env.OIDC_REDIRECT_URI,
|
|
||||||
oidcJwksUri: process.env.OIDC_JWKS_URI,
|
|
||||||
oidcScopes: process.env.OIDC_SCOPES || 'openid profile email',
|
|
||||||
oidcSkipUserInfo: process.env.OIDC_SKIP_USER_INFO === 'true',
|
|
||||||
|
|
||||||
defaultAdminEmail: process.env.DEFAULT_ADMIN_EMAIL,
|
defaultAdminEmail: process.env.DEFAULT_ADMIN_EMAIL,
|
||||||
|
|
||||||
|
oidcIssuer: process.env.OIDC_ISSUER,
|
||||||
|
oidcClientId: process.env.OIDC_CLIENT_ID,
|
||||||
|
oidcClientSecret: process.env.OIDC_CLIENT_SECRET,
|
||||||
|
oidcScopes: process.env.OIDC_SCOPES || 'openid email profile',
|
||||||
|
oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
|
||||||
|
oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
|
||||||
|
oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true',
|
||||||
|
|
||||||
|
// TODO: move client base url to environment variable?
|
||||||
|
oidcRedirectUri: `${
|
||||||
|
sails.config.environment === 'production' ? process.env.BASE_URL : 'http://localhost:3000'
|
||||||
|
}/oidc-callback`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ module.exports.policies = {
|
|||||||
|
|
||||||
'projects/create': ['is-authenticated', 'is-admin'],
|
'projects/create': ['is-authenticated', 'is-admin'],
|
||||||
|
|
||||||
|
'show-config': true,
|
||||||
'access-tokens/create': true,
|
'access-tokens/create': true,
|
||||||
'access-tokens/exchange': true,
|
'access-tokens/exchange-using-oidc': true,
|
||||||
'appconfig/index': true,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports.routes = {
|
module.exports.routes = {
|
||||||
'GET /api/appconfig': 'appconfig/index',
|
'GET /api/config': 'show-config',
|
||||||
|
|
||||||
'POST /api/access-tokens': 'access-tokens/create',
|
'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',
|
'DELETE /api/access-tokens/me': 'access-tokens/delete',
|
||||||
|
|
||||||
'GET /api/users': 'users/index',
|
'GET /api/users': 'users/index',
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
module.exports.up = (knex) =>
|
|
||||||
knex.schema.createTable('identity_provider_user', (table) => {
|
|
||||||
/* Columns */
|
|
||||||
|
|
||||||
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
|
|
||||||
table.timestamp('created_at', true);
|
|
||||||
table.timestamp('updated_at', true);
|
|
||||||
|
|
||||||
table
|
|
||||||
.bigInteger('user_id')
|
|
||||||
.notNullable()
|
|
||||||
.references('id')
|
|
||||||
.inTable('user_account')
|
|
||||||
.onDelete('CASCADE');
|
|
||||||
|
|
||||||
table.text('issuer').notNullable();
|
|
||||||
table.text('sub').notNullable();
|
|
||||||
|
|
||||||
/* Indexes */
|
|
||||||
|
|
||||||
table.index('user_id');
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports.down = (knex) => knex.schema.dropTable('identity_provider_user');
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports.up = async (knex) => {
|
||||||
|
await knex.schema.createTable('identity_provider_user', (table) => {
|
||||||
|
/* Columns */
|
||||||
|
|
||||||
|
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
|
||||||
|
|
||||||
|
table.bigInteger('user_id').notNullable();
|
||||||
|
|
||||||
|
table.text('issuer').notNullable();
|
||||||
|
table.text('sub').notNullable();
|
||||||
|
|
||||||
|
table.timestamp('created_at', true);
|
||||||
|
table.timestamp('updated_at', true);
|
||||||
|
|
||||||
|
/* Indexes */
|
||||||
|
|
||||||
|
table.unique(['issuer', 'sub']);
|
||||||
|
table.index('user_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.table('user_account', (table) => {
|
||||||
|
/* Columns */
|
||||||
|
|
||||||
|
table.boolean('is_sso').notNullable().default(false);
|
||||||
|
|
||||||
|
/* Modifications */
|
||||||
|
|
||||||
|
table.setNullable('password');
|
||||||
|
});
|
||||||
|
|
||||||
|
return knex.schema.alterTable('user_account', (table) => {
|
||||||
|
table.boolean('is_sso').notNullable().alter();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.down = async (knex) => {
|
||||||
|
await knex.schema.dropTable('identity_provider_user');
|
||||||
|
|
||||||
|
return knex.schema.table('user_account', (table) => {
|
||||||
|
table.dropColumn('is_sso');
|
||||||
|
|
||||||
|
table.dropNullable('password');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports.up = async (knex) => {
|
|
||||||
return knex.schema.table('user_account', (table) => {
|
|
||||||
table.setNullable('password');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.down = async (knex) => {
|
|
||||||
return knex.schema.table('user_account', (table) => {
|
|
||||||
table.dropNullable('password');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports.up = async (knex) => {
|
|
||||||
return knex.schema.table('user_account', (table) => {
|
|
||||||
table.boolean('locked').default(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.down = async (knex) => {
|
|
||||||
return knex.schema.table('user_account', (table) => {
|
|
||||||
table.dropColumn('locked');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -3,6 +3,7 @@ const bcrypt = require('bcrypt');
|
|||||||
const buildData = () => {
|
const buildData = () => {
|
||||||
const data = {
|
const data = {
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
|
isSso: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.DEFAULT_ADMIN_PASSWORD) {
|
if (process.env.DEFAULT_ADMIN_PASSWORD) {
|
||||||
@@ -33,10 +34,6 @@ exports.seed = async (knex) => {
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (Object.keys(data).length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await knex('user_account').update(data).where('email', process.env.DEFAULT_ADMIN_EMAIL);
|
await knex('user_account').update(data).where('email', process.env.DEFAULT_ADMIN_EMAIL);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+3110
-1729
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user