Compare commits
7
Commits
planka-0.1.7
...
v1.14.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
019766dced | ||
|
|
6dc9e4ed99 | ||
|
|
d4b64b90fc | ||
|
|
1a49826b85 | ||
|
|
815643eb2c | ||
|
|
bcc4c0d923 | ||
|
|
cf7810b067 |
@@ -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:
|
||||||
|
|||||||
@@ -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.7
|
version: 0.1.9
|
||||||
|
|
||||||
# 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.14.2"
|
appVersion: "1.14.3"
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ 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 }}
|
{{ range $k, $v := .Values.env }}
|
||||||
- name: {{ $k | quote }}
|
- name: {{ $k | quote }}
|
||||||
value: {{ $v | quote }}
|
value: {{ $v | quote }}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
REACT_APP_VERSION=1.14.2
|
REACT_APP_VERSION=1.14.3
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ const ActionsStep = React.memo(
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!user.isLockedAdmin && (
|
{!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',
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ const Item = React.memo(
|
|||||||
phone,
|
phone,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isLocked,
|
isLocked,
|
||||||
isLockedAdmin,
|
isRoleLocked,
|
||||||
|
isDeletionLocked,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
@@ -48,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={isLockedAdmin} onChange={handleIsAdminChange} />
|
<Radio toggle checked={isAdmin} disabled={isRoleLocked} onChange={handleIsAdminChange} />
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell textAlign="right">
|
<Table.Cell textAlign="right">
|
||||||
<ActionsPopup
|
<ActionsPopup
|
||||||
@@ -60,7 +61,7 @@ const Item = React.memo(
|
|||||||
phone,
|
phone,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isLocked,
|
isLocked,
|
||||||
isLockedAdmin,
|
isDeletionLocked,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
@@ -93,7 +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,
|
||||||
isLockedAdmin: 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,7 +111,8 @@ const UsersModal = React.memo(
|
|||||||
phone={item.phone}
|
phone={item.phone}
|
||||||
isAdmin={item.isAdmin}
|
isAdmin={item.isAdmin}
|
||||||
isLocked={item.isLocked}
|
isLocked={item.isLocked}
|
||||||
isLockedAdmin={item.isLockedAdmin}
|
isRoleLocked={item.isRoleLocked}
|
||||||
|
isDeletionLocked={item.isDeletionLocked}
|
||||||
emailUpdateForm={item.emailUpdateForm}
|
emailUpdateForm={item.emailUpdateForm}
|
||||||
passwordUpdateForm={item.passwordUpdateForm}
|
passwordUpdateForm={item.passwordUpdateForm}
|
||||||
usernameUpdateForm={item.usernameUpdateForm}
|
usernameUpdateForm={item.usernameUpdateForm}
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export default class extends BaseModel {
|
|||||||
subscribeToOwnCards: attr(),
|
subscribeToOwnCards: attr(),
|
||||||
isAdmin: attr(),
|
isAdmin: attr(),
|
||||||
isLocked: attr(),
|
isLocked: attr(),
|
||||||
isLockedAdmin: attr(),
|
isRoleLocked: attr(),
|
||||||
|
isDeletionLocked: attr(),
|
||||||
deletedAt: attr(),
|
deletedAt: attr(),
|
||||||
createdAt: attr({
|
createdAt: attr({
|
||||||
getDefault: () => new Date(),
|
getDefault: () => new Date(),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ services:
|
|||||||
# - OIDC_SCOPES=openid email profile
|
# - OIDC_SCOPES=openid email profile
|
||||||
# - OIDC_ADMIN_ROLES=admin
|
# - OIDC_ADMIN_ROLES=admin
|
||||||
# - OIDC_ROLES_ATTRIBUTE=groups
|
# - OIDC_ROLES_ATTRIBUTE=groups
|
||||||
|
# - OIDC_IGNORE_ROLES=true
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.14.2",
|
"version": "1.14.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.14.2",
|
"version": "1.14.3",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planka",
|
"name": "planka",
|
||||||
"version": "1.14.2",
|
"version": "1.14.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://plankanban.github.io/planka",
|
"homepage": "https://plankanban.github.io/planka",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ SECRET_KEY=notsecretkey
|
|||||||
# OIDC_SCOPES=openid email profile
|
# OIDC_SCOPES=openid email profile
|
||||||
# OIDC_ADMIN_ROLES=admin
|
# OIDC_ADMIN_ROLES=admin
|
||||||
# OIDC_ROLES_ATTRIBUTE=groups
|
# OIDC_ROLES_ATTRIBUTE=groups
|
||||||
|
# OIDC_IGNORE_ROLES=true
|
||||||
|
|
||||||
## Do not edit this
|
## Do not edit this
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ module.exports = {
|
|||||||
delete inputs.name;
|
delete inputs.name;
|
||||||
/* eslint-enable no-param-reassign */
|
/* eslint-enable no-param-reassign */
|
||||||
} else if (user.isSso) {
|
} 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
|
delete inputs.name; // eslint-disable-line no-param-reassign
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,10 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFieldKeys = ['email', 'isAdmin', 'isSso', 'name', 'username'];
|
const updateFieldKeys = ['email', 'isSso', 'name', 'username'];
|
||||||
|
if (!sails.config.custom.oidcIgnoreRoles) {
|
||||||
|
updateFieldKeys.push('isAdmin');
|
||||||
|
}
|
||||||
|
|
||||||
const updateValues = {};
|
const updateValues = {};
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
|||||||
@@ -110,12 +110,13 @@ module.exports = {
|
|||||||
tableName: 'user_account',
|
tableName: 'user_account',
|
||||||
|
|
||||||
customToJSON() {
|
customToJSON() {
|
||||||
const isLockedAdmin = this.email === sails.config.custom.defaultAdminEmail;
|
const isDefaultAdmin = this.email === sails.config.custom.defaultAdminEmail;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']),
|
..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']),
|
||||||
isLockedAdmin,
|
isLocked: this.isSso || isDefaultAdmin,
|
||||||
isLocked: this.isSso || isLockedAdmin,
|
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}`,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ module.exports.custom = {
|
|||||||
oidcScopes: process.env.OIDC_SCOPES || 'openid email profile',
|
oidcScopes: process.env.OIDC_SCOPES || 'openid email profile',
|
||||||
oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
|
oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
|
||||||
oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
|
oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
|
||||||
|
oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true',
|
||||||
|
|
||||||
// TODO: move client base url to environment variable?
|
// TODO: move client base url to environment variable?
|
||||||
oidcRedirectUri: `${
|
oidcRedirectUri: `${
|
||||||
|
|||||||
@@ -34,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user