1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

Merge branch 'plankanban:master' into master

This commit is contained in:
gorrilla10101 2023-09-16 22:15:54 -05:00 committed by GitHub
commit dd91d2b9d4
26 changed files with 467 additions and 333 deletions

View file

@ -13,26 +13,26 @@ env:
jobs: jobs:
build-and-push-docker-base-image: build-and-push-docker-base-image:
runs-on: ubuntu-latest runs-on: self-hosted
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7

View file

@ -0,0 +1,34 @@
name: Build and push Docker DEV image
on:
push:
branches: [master]
jobs:
build-and-push-docker-image-dev:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
ghcr.io/plankanban/planka:dev

View file

@ -6,19 +6,19 @@ on:
jobs: jobs:
build-and-push-docker-image: build-and-push-docker-image:
runs-on: ubuntu-latest runs-on: self-hosted
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -32,7 +32,7 @@ jobs:
script: return context.payload.release.tag_name.replace('v', '') script: return context.payload.release.tag_name.replace('v', '')
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7

View file

@ -13,10 +13,10 @@ jobs:
# see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token # see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
permissions: permissions:
contents: write contents: write
runs-on: ubuntu-latest runs-on: self-hosted
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0

View file

@ -5,7 +5,8 @@ WORKDIR /app
COPY server/package.json server/package-lock.json . COPY server/package.json server/package-lock.json .
RUN npm install npm@latest --global \ RUN npm install npm@latest --global \
&& npm clean-install --omit=dev && npm install pnpm --global \
&& pnpm install --prod
FROM node:lts AS client FROM node:lts AS client
@ -14,7 +15,8 @@ WORKDIR /app
COPY client/package.json client/package-lock.json . COPY client/package.json client/package-lock.json .
RUN npm install npm@latest --global \ RUN npm install npm@latest --global \
&& npm clean-install --omit=dev && npm install pnpm --global \
&& pnpm install --prod
COPY client . COPY client .
RUN DISABLE_ESLINT_PLUGIN=true npm run build RUN DISABLE_ESLINT_PLUGIN=true npm run build

View file

@ -1,4 +1,5 @@
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import omit from 'lodash/omit';
import pickBy from 'lodash/pickBy'; import pickBy from 'lodash/pickBy';
import React, { useCallback, useMemo, useRef } from 'react'; import React, { useCallback, useMemo, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -9,7 +10,7 @@ import { useForm } from '../../hooks';
import styles from './UserInformationEdit.module.scss'; import styles from './UserInformationEdit.module.scss';
const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => { const UserInformationEdit = React.memo(({ defaultData, isNameEditable, onUpdate }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({ const [data, handleFieldChange] = useForm(() => ({
@ -32,13 +33,17 @@ const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
const nameField = useRef(null); const nameField = useRef(null);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
if (!cleanData.name) { if (isNameEditable) {
nameField.current.select(); if (!cleanData.name) {
return; nameField.current.select();
} return;
}
onUpdate(cleanData); onUpdate(cleanData);
}, [onUpdate, cleanData]); } else {
onUpdate(omit(cleanData, 'name'));
}
}, [isNameEditable, onUpdate, cleanData]);
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
@ -48,6 +53,7 @@ const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
ref={nameField} ref={nameField}
name="name" name="name"
value={data.name} value={data.name}
disabled={!isNameEditable}
className={styles.field} className={styles.field}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
@ -74,6 +80,7 @@ const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
UserInformationEdit.propTypes = { UserInformationEdit.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isNameEditable: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
}; };

View file

@ -5,33 +5,40 @@ import { Popup } from '../lib/custom-ui';
import UserInformationEdit from './UserInformationEdit'; import UserInformationEdit from './UserInformationEdit';
const UserInformationEditStep = React.memo(({ defaultData, onUpdate, onBack, onClose }) => { const UserInformationEditStep = React.memo(
const [t] = useTranslation(); ({ defaultData, isNameEditable, onUpdate, onBack, onClose }) => {
const [t] = useTranslation();
const handleUpdate = useCallback( const handleUpdate = useCallback(
(data) => { (data) => {
onUpdate(data); onUpdate(data);
onClose(); onClose();
}, },
[onUpdate, onClose], [onUpdate, onClose],
); );
return ( return (
<> <>
<Popup.Header onBack={onBack}> <Popup.Header onBack={onBack}>
{t('common.editInformation', { {t('common.editInformation', {
context: 'title', context: 'title',
})} })}
</Popup.Header> </Popup.Header>
<Popup.Content> <Popup.Content>
<UserInformationEdit defaultData={defaultData} onUpdate={handleUpdate} /> <UserInformationEdit
</Popup.Content> defaultData={defaultData}
</> isNameEditable={isNameEditable}
); onUpdate={handleUpdate}
}); />
</Popup.Content>
</>
);
},
);
UserInformationEditStep.propTypes = { UserInformationEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isNameEditable: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onBack: PropTypes.func, onBack: PropTypes.func,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,

View file

@ -23,6 +23,7 @@ const AccountPane = React.memo(
phone, phone,
organization, organization,
language, language,
isLocked,
isAvatarUpdating, isAvatarUpdating,
usernameUpdateForm, usernameUpdateForm,
emailUpdateForm, emailUpdateForm,
@ -74,6 +75,7 @@ const AccountPane = React.memo(
phone, phone,
organization, organization,
}} }}
isNameEditable={!isLocked}
onUpdate={onUpdate} onUpdate={onUpdate}
/> />
<Divider horizontal section> <Divider horizontal section>
@ -102,63 +104,67 @@ const AccountPane = React.memo(
value={language || 'auto'} value={language || 'auto'}
onChange={handleLanguageChange} onChange={handleLanguageChange}
/> />
<Divider horizontal section> {!isLocked && (
<Header as="h4"> <>
{t('common.authentication', { <Divider horizontal section>
context: 'title', <Header as="h4">
})} {t('common.authentication', {
</Header> context: 'title',
</Divider> })}
<div className={styles.action}> </Header>
<UserUsernameEditPopup </Divider>
usePasswordConfirmation <div className={styles.action}>
defaultData={usernameUpdateForm.data} <UserUsernameEditPopup
username={username} usePasswordConfirmation
isSubmitting={usernameUpdateForm.isSubmitting} defaultData={usernameUpdateForm.data}
error={usernameUpdateForm.error} username={username}
onUpdate={onUsernameUpdate} isSubmitting={usernameUpdateForm.isSubmitting}
onMessageDismiss={onUsernameUpdateMessageDismiss} error={usernameUpdateForm.error}
> onUpdate={onUsernameUpdate}
<Button className={styles.actionButton}> onMessageDismiss={onUsernameUpdateMessageDismiss}
{t('action.editUsername', { >
context: 'title', <Button className={styles.actionButton}>
})} {t('action.editUsername', {
</Button> context: 'title',
</UserUsernameEditPopup> })}
</div> </Button>
<div className={styles.action}> </UserUsernameEditPopup>
<UserEmailEditPopup </div>
usePasswordConfirmation <div className={styles.action}>
defaultData={emailUpdateForm.data} <UserEmailEditPopup
email={email} usePasswordConfirmation
isSubmitting={emailUpdateForm.isSubmitting} defaultData={emailUpdateForm.data}
error={emailUpdateForm.error} email={email}
onUpdate={onEmailUpdate} isSubmitting={emailUpdateForm.isSubmitting}
onMessageDismiss={onEmailUpdateMessageDismiss} error={emailUpdateForm.error}
> onUpdate={onEmailUpdate}
<Button className={styles.actionButton}> onMessageDismiss={onEmailUpdateMessageDismiss}
{t('action.editEmail', { >
context: 'title', <Button className={styles.actionButton}>
})} {t('action.editEmail', {
</Button> context: 'title',
</UserEmailEditPopup> })}
</div> </Button>
<div className={styles.action}> </UserEmailEditPopup>
<UserPasswordEditPopup </div>
usePasswordConfirmation <div className={styles.action}>
defaultData={passwordUpdateForm.data} <UserPasswordEditPopup
isSubmitting={passwordUpdateForm.isSubmitting} usePasswordConfirmation
error={passwordUpdateForm.error} defaultData={passwordUpdateForm.data}
onUpdate={onPasswordUpdate} isSubmitting={passwordUpdateForm.isSubmitting}
onMessageDismiss={onPasswordUpdateMessageDismiss} error={passwordUpdateForm.error}
> onUpdate={onPasswordUpdate}
<Button className={styles.actionButton}> onMessageDismiss={onPasswordUpdateMessageDismiss}
{t('action.editPassword', { >
context: 'title', <Button className={styles.actionButton}>
})} {t('action.editPassword', {
</Button> context: 'title',
</UserPasswordEditPopup> })}
</div> </Button>
</UserPasswordEditPopup>
</div>
</>
)}
</Tab.Pane> </Tab.Pane>
); );
}, },
@ -172,6 +178,7 @@ AccountPane.propTypes = {
phone: PropTypes.string, phone: PropTypes.string,
organization: PropTypes.string, organization: PropTypes.string,
language: PropTypes.string, language: PropTypes.string,
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,

View file

@ -16,6 +16,7 @@ const UserSettingsModal = React.memo(
phone, phone,
organization, organization,
language, language,
isLocked,
subscribeToOwnCards, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
usernameUpdateForm, usernameUpdateForm,
@ -48,6 +49,7 @@ const UserSettingsModal = React.memo(
phone={phone} phone={phone}
organization={organization} organization={organization}
language={language} language={language}
isLocked={isLocked}
isAvatarUpdating={isAvatarUpdating} isAvatarUpdating={isAvatarUpdating}
usernameUpdateForm={usernameUpdateForm} usernameUpdateForm={usernameUpdateForm}
emailUpdateForm={emailUpdateForm} emailUpdateForm={emailUpdateForm}
@ -104,6 +106,7 @@ 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,
isAvatarUpdating: PropTypes.bool.isRequired, isAvatarUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */

View file

@ -64,6 +64,7 @@ const ActionsStep = React.memo(
return ( return (
<UserInformationEditStep <UserInformationEditStep
defaultData={pick(user, ['name', 'phone', 'organization'])} defaultData={pick(user, ['name', 'phone', 'organization'])}
isNameEditable={!user.isLocked}
onUpdate={onUpdate} onUpdate={onUpdate}
onBack={handleBack} onBack={handleBack}
onClose={onClose} onClose={onClose}
@ -135,26 +136,30 @@ const ActionsStep = React.memo(
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}> {!user.isLocked && (
{t('action.editUsername', { <>
context: 'title', <Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
})} {t('action.editUsername', {
</Menu.Item> context: 'title',
<Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}> })}
{t('action.editEmail', { </Menu.Item>
context: 'title', <Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}>
})} {t('action.editEmail', {
</Menu.Item> context: 'title',
<Menu.Item className={styles.menuItem} onClick={handleEditPasswordClick}> })}
{t('action.editPassword', { </Menu.Item>
context: 'title', <Menu.Item className={styles.menuItem} onClick={handleEditPasswordClick}>
})} {t('action.editPassword', {
</Menu.Item> context: 'title',
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}> })}
{t('action.deleteUser', { </Menu.Item>
context: 'title', <Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
})} {t('action.deleteUser', {
</Menu.Item> context: 'title',
})}
</Menu.Item>
</>
)}
</Menu> </Menu>
</Popup.Content> </Popup.Content>
</> </>

View file

@ -17,6 +17,7 @@ const Item = React.memo(
organization, organization,
phone, phone,
isAdmin, isAdmin,
isLocked,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
usernameUpdateForm, usernameUpdateForm,
@ -46,7 +47,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} onChange={handleIsAdminChange} /> <Radio toggle checked={isAdmin} disabled={isLocked} onChange={handleIsAdminChange} />
</Table.Cell> </Table.Cell>
<Table.Cell textAlign="right"> <Table.Cell textAlign="right">
<ActionsPopup <ActionsPopup
@ -57,6 +58,7 @@ const Item = React.memo(
organization, organization,
phone, phone,
isAdmin, isAdmin,
isLocked,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
usernameUpdateForm, usernameUpdateForm,
@ -88,6 +90,7 @@ Item.propTypes = {
organization: PropTypes.string, organization: PropTypes.string,
phone: PropTypes.string, phone: PropTypes.string,
isAdmin: PropTypes.bool.isRequired, isAdmin: PropTypes.bool.isRequired,
isLocked: 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,

View file

@ -110,6 +110,7 @@ const UsersModal = React.memo(
organization={item.organization} organization={item.organization}
phone={item.phone} phone={item.phone}
isAdmin={item.isAdmin} isAdmin={item.isAdmin}
isLocked={item.isLocked}
emailUpdateForm={item.emailUpdateForm} emailUpdateForm={item.emailUpdateForm}
passwordUpdateForm={item.passwordUpdateForm} passwordUpdateForm={item.passwordUpdateForm}
usernameUpdateForm={item.usernameUpdateForm} usernameUpdateForm={item.usernameUpdateForm}

View file

@ -14,6 +14,7 @@ const mapStateToProps = (state) => {
phone, phone,
organization, organization,
language, language,
isLocked,
subscribeToOwnCards, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
emailUpdateForm, emailUpdateForm,
@ -29,6 +30,7 @@ const mapStateToProps = (state) => {
phone, phone,
organization, organization,
language, language,
isLocked,
subscribeToOwnCards, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
emailUpdateForm, emailUpdateForm,

View file

@ -24,201 +24,196 @@ export default {
all: 'Tümü', all: 'Tümü',
allChangesWillBeAutomaticallySavedAfterConnectionRestored: allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Bağlantı yeniden kurulduğunda tüm değişiklikler kaydedilecektir.', 'Bağlantı yeniden kurulduğunda tüm değişiklikler kaydedilecektir.',
areYouSureYouWantToDeleteThisAttachment: areYouSureYouWantToDeleteThisAttachment: 'Bu eki silmek istediğinize emin misiniz?',
'Bu eki silmek istediğinize emin misiniz?', areYouSureYouWantToDeleteThisBoard: 'Bu panoyu silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisBoard: 'Bu panoyu silmek istediğinizden emin misiniz?', areYouSureYouWantToDeleteThisCard: 'Bu kartı silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisCard: 'Bu kartı silmek istediğinizden emin misiniz?', areYouSureYouWantToDeleteThisComment: 'Bu yorumu silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisComment: areYouSureYouWantToDeleteThisLabel: 'Bu etiketi silmek istediğinizden emin misiniz?',
'Bu yorumu silmek istediğinizden emin misiniz?', areYouSureYouWantToDeleteThisList: 'Bu listeyi silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisLabel: 'Bu etiketi silmek istediğinizden emin misiniz?', areYouSureYouWantToDeleteThisProject: 'Bu projeyi silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisList: 'Bu listeyi silmek istediğinizden emin misiniz?', areYouSureYouWantToDeleteThisTask: 'Bu görevi silmek istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisProject: areYouSureYouWantToDeleteThisUser: 'Bu kullanıcıyı silmek istediğinizden emin misiniz?',
'Bu projeyi silmek istediğinizden emin misiniz?', areYouSureYouWantToLeaveBoard: 'Panodan ayrılmak istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisTask: 'Bu görevi silmek istediğinizden emin misiniz?', areYouSureYouWantToLeaveProject: 'Projeden ayrılmak istediğinizden emin misiniz?',
areYouSureYouWantToDeleteThisUser: areYouSureYouWantToRemoveThisManagerFromProject:
'Bu kullanıcıyı silmek istediğinizden emin misiniz?', 'Bu yöneticiyi projeden çıkarmak istediğinizden emin misiniz?',
areYouSureYouWantToLeaveBoard: 'Panodan ayrılmak istediğinizden emin misiniz?', areYouSureYouWantToRemoveThisMemberFromBoard:
areYouSureYouWantToLeaveProject: 'Projeden ayrılmak istediğinizden emin misiniz?', 'Bu üyeyi panodan çıkarmak istediğinizden emin misiniz?',
areYouSureYouWantToRemoveThisManagerFromProject: attachment: 'ek',
'Bu yöneticiyi projeden çıkarmak istediğinizden emin misiniz?', attachments: 'ekler',
areYouSureYouWantToRemoveThisMemberFromBoard: authentication: 'kimlik doğrulama',
'Bu üyeyi panodan çıkarmak istediğinizden emin misiniz?', background: 'arka plan',
attachment: 'ek', board: 'pano',
attachments: 'ekler', boardNotFound_title: 'Pano bulunamadı',
authentication: 'kimlik doğrulama', cardActions_title: 'Kart İşlemleri',
background: 'arka plan', cardNotFound_title: 'Kart bulunamadı',
board: 'pano', cardOrActionAreDeleted: 'Kart veya işlem silindi',
boardNotFound_title: 'Pano bulunamadı', color: 'renk',
cardActions_title: 'Kart İşlemleri', createBoard_title: 'Pano Oluştur',
cardNotFound_title: 'Kart bulunamadı', createLabel_title: 'Etiket Oluştur',
cardOrActionAreDeleted: 'Kart veya işlem silindi', createNewOneOrSelectExistingOne: 'Yeni bir tane oluşturun veya mevcut bir tanesini seçin.',
color: 'renk', createProject_title: 'Proje Oluştur',
createBoard_title: 'Pano Oluştur', createTextFile_title: 'Metin Dosyası Oluştur',
createLabel_title: 'Etiket Oluştur', currentPassword: 'Geçerli şifre',
createNewOneOrSelectExistingOne: dangerZone_title: 'Tehlikeli Bölge',
'Yeni bir tane oluşturun veya mevcut bir tanesini seçin.', date: 'tarih',
createProject_title: 'Proje Oluştur', dueDate_title: 'Termin Tarihi',
createTextFile_title: 'Metin Dosyası Oluştur', deleteAttachment_title: 'Eki Sil',
currentPassword: 'Geçerli şifre', deleteBoard_title: 'Panoyu Sil',
dangerZone_title: 'Tehlikeli Bölge', deleteCard_title: 'Kartı Sil',
date: 'tarih', deleteComment_title: 'Yorumu Sil',
dueDate_title: 'Termin Tarihi', deleteLabel_title: 'Etiketi Sil',
deleteAttachment_title: 'Eki Sil', deleteList_title: 'Listeyi Sil',
deleteBoard_title: 'Panoyu Sil', deleteProject_title: 'Projeyi Sil',
deleteCard_title: 'Kartı Sil', deleteTask_title: 'Görevi Sil',
deleteComment_title: 'Yorumu Sil', deleteUser_title: 'Kullanıcıyı Sil',
deleteLabel_title: 'Etiketi Sil', description: 'açıklama',
deleteList_title: 'Listeyi Sil', detectAutomaically: 'Otomatik olarak algıla',
deleteProject_title: 'Projeyi Sil', dropFileToUpload: 'Yüklenecek dosyayı buraya bırakın',
deleteTask_title: 'Görevi Sil', editAttachment_title: 'Eki Düzenle',
deleteUser_title: 'Kullanıcıyı Sil', editAvatar_title: 'Avatarı Düzenle',
description: 'açıklama', editBoard_title: 'Panoyu Düzenle',
detectAutomaically: 'Otomatik olarak algıla', editDueDate_title: 'Son Tarihi Düzenle',
dropFileToUpload: 'Yüklenecek dosyayı buraya bırakın', editEmail_title: 'E-posta Adresini Düzenle',
editAttachment_title: 'Eki Düzenle', editLabel_title: 'Etiketi Düzenle',
editAvatar_title: 'Avatarı Düzenle', editPassword_title: 'Şifreyi Değiştir',
editBoard_title: 'Panoyu Düzenle', editStopwatch_title: 'Kronometreyi Düzenle',
editDueDate_title: 'Son Tarihi Düzenle', editUsername_title: 'Kullanıcı Adını Düzenle',
editEmail_title: 'E-posta Adresini Düzenle', email: 'e-posta',
editLabel_title: 'Etiketi Düzenle', emailAlreadyInUse: 'E-posta adresi zaten kullanımda',
editPassword_title: 'Şifreyi Değiştir', enterCardTitle: 'Kart başlığını girin',
editStopwatch_title: 'Kronometreyi Düzenle', enterDescription: 'Açıklamayı girin',
editUsername_title: 'Kullanıcı Adını Düzenle', enterFilename: 'Dosya adını girin',
email: 'e-posta', enterListTitle: 'Liste başlığını girin',
emailAlreadyInUse: 'E-posta adresi zaten kullanımda', enterProjectTitle: 'Proje başlığını girin',
enterCardTitle: 'Kart başlığını girin', enterTaskDescription: 'Görev açıklamasını girin',
enterDescription: 'Açıklamayı girin', filterByLabels_title: 'Etikete Göre Filtrele',
enterFilename: 'Dosya adını girin', filterByMembers_title: 'Üyelere göre filtrele',
enterListTitle: 'Liste başlığını girin', fromComputer_title: 'Bilgisayardan',
enterProjectTitle: 'Proje başlığını girin', general: 'Genel',
enterTaskDescription: 'Görev açıklamasını girin', hours: 'saat',
filterByLabels_title: 'Etikete Göre Filtrele', invalidCurrentPassword: 'Mevcut şifre yanlış',
filterByMembers_title: 'Üyelere göre filtrele', labels: 'etiketler',
fromComputer_title: 'Bilgisayardan', language: 'dil',
general: 'Genel', leaveBoard_title: 'Panodan Ayrıl',
hours: 'saat', leaveProject_title: 'Projeden Ayrıl',
invalidCurrentPassword: 'Mevcut şifre yanlış', list: 'Listeler',
labels: 'etiketler', listActions_title: 'Görevleri Listele',
language: 'dil', managers: 'Yöneticiler',
leaveBoard_title: 'Panodan Ayrıl', members: 'Üyeler',
leaveProject_title: 'Projeden Ayrıl', minutes: 'dakika',
list: 'Listeler', moveCard_title: 'Kartı Taşı',
listActions_title: 'Görevleri Listele', name: 'isim',
managers: 'Yöneticiler', newEmail: 'Yeni e-posta adresi',
members: 'Üyeler', newPassword: 'Yeni şifre',
minutes: 'dakika', newUsername: 'Yeni kullanıcı adı',
moveCard_title: 'Kartı Taşı', noConnectionToServer: 'Sunucuya bağlantı yok',
name: 'isim', noBoards: 'Pano yok',
newEmail: 'Yeni e-posta adresi', noLists: 'Liste yok',
newPassword: 'Yeni şifre', noProjects: 'Proje yok',
newUsername: 'Yeni kullanıcı adı', notifications: 'Bildirimler',
noConnectionToServer: 'Sunucuya bağlantı yok', noUnreadNotifications: 'Okunmamış bildirim yok',
noBoards: 'Pano yok', openBoard_title: 'Açık Pano',
noLists: 'Liste yok', optional_inline: 'İsteğe bağlı',
noProjects: 'Proje yok', organization: 'Organizasyon',
notifications: 'Bildirimler', phone: 'telefon',
noUnreadNotifications: 'Okunmamış bildirim yok', preferences: 'Tercihler',
openBoard_title: 'Açık Pano', pressPasteShortcutToAddAttachmentFromClipboard:
optional_inline: 'İsteğe bağlı', 'İpucu: Panodan bir ek eklemek için CTRL-V ye (Macte Cmd-V) basın',
organization: 'Organizasyon', project: 'Proje',
phone: 'telefon', projectNotFound_title: 'Proje bulunamadı',
preferences: 'Tercihler', removeManager_title: 'Yöneticiyi Kaldır',
pressPasteShortcutToAddAttachmentFromClipboard: removeMember_title: 'Üyeyi Kaldır',
'İpucu: Panodan bir ek eklemek için CTRL-V ye (Macte Cmd-V) basın', seconds: 'saniye',
project: 'Proje', selectBoard: 'Pano Seç',
projectNotFound_title: 'Proje bulunamadı', selectList: 'Liste seç',
removeManager_title: 'Yöneticiyi Kaldır', selectProject: 'Proje seç',
removeMember_title: 'Üyeyi Kaldır', settings: 'Ayarlar',
seconds: 'saniye', stopwatch: 'kronometre',
selectBoard: 'Pano Seç', subscribeToMyOwnCardsByDefault: 'Varsayılan olarak kendi kartlarıma abone ol',
selectList: 'Liste seç', taskActions_title: 'Görev Eylemleri',
selectProject: 'Proje seç', tasks: 'Görevler',
settings: 'Ayarlar', thereIsNoPreviewAvailableForThisAttachment: 'Bu ek için önizleme mevcut değil',
stopwatch: 'kronometre', time: 'zaman',
subscribeToMyOwnCardsByDefault: 'Varsayılan olarak kendi kartlarıma abone ol', title: 'başlık',
taskActions_title: 'Görev Eylemleri', userActions_title: 'Kullanıcı İşlemleri',
tasks: 'Görevler', userAddedThisCardToList: '<0>{{user}}</0><1> bu kartı {{list}</1> listesine ekledi',
thereIsNoPreviewAvailableForThisAttachment: 'Bu ek için önizleme mevcut değil', userLeftNewCommentToCard:
time: 'zaman', '{{user}} yeni bir yorum yazdı: <2>{{card}</2> kartına «{{comment}}»',
title: 'başlık', userMovedCardFromListToList:
userActions_title: 'Kullanıcı İşlemleri', '{{user}}, <2>{{card}></2> kartını {{fromList}} listesinden {{toList}} listesine taşıdı',
userAddedThisCardToList: '<0>{{user}}</0><1> bu kartı {{list}</1> listesine ekledi', userMovedThisCardFromListToList:
userLeftNewCommentToCard: '<0>{{user}}</0><1> bu kartı {{fromList}} konumundan {{toList}}</1> konumuna taşıdı',
'{{user}} yeni bir yorum yazdı: <2>{{card}</2> kartına «{{comment}}»', username: 'kullanıcı adı',
userMovedCardFromListToList: usernameAlreadyInUse: 'Kullanıcı adı zaten kullanımda',
'{{user}}, <2>{{card}></2> kartını {{fromList}} listesinden {{toList}} listesine taşıdı', users: 'kullanıcı',
userMovedThisCardFromListToList: writeComment: 'Yorum yazın',
'<0>{{user}}</0><1> bu kartı {{fromList}} konumundan {{toList}}</1> konumuna taşıdı', },
username: 'kullanıcı adı', action: {
usernameAlreadyInUse: 'Kullanıcı adı zaten kullanımda', addAnotherCard: 'Başka bir kart ekle',
users: 'kullanıcı', addAnotherList: 'Başka bir liste ekle',
writeComment: 'Yorum yazın', addAnotherTask: 'Başka bir görev ekle',
}, addCard: 'Kart ekle',
action: { addCard_title: 'Kart Ekle',
addAnotherCard: 'Başka bir kart ekle', addComment: 'Yorum ekle',
addAnotherList: 'Başka bir liste ekle', addList: 'Liste ekle',
addAnotherTask: 'Başka bir görev ekle', addMoreDetailedDescription: 'Ayrıntılı bir açıklama ekleyin',
addCard: 'Kart ekle', addTask: 'Görev ekle',
addCard_title: 'Kart Ekle', addToCard: 'Karta ekle',
addComment: 'Yorum ekle', addUser: 'Kullanıcı ekle',
addList: 'Liste ekle', createBoard: 'Pano oluştur',
addMoreDetailedDescription: 'Ayrıntılı bir açıklama ekleyin', createFile: 'Dosya oluştur',
addTask: 'Görev ekle', createLabel: 'Etiket Oluştur',
addToCard: 'Karta ekle', createNewLabel: 'Yeni etiket oluştur',
addUser: 'Kullanıcı ekle', createProject: 'Proje oluştur',
createBoard: 'Pano oluştur', delete: 'Sil',
createFile: 'Dosya oluştur', deleteAttachment: 'Eki sil',
createLabel: 'Etiket Oluştur', deleteAvatar: 'Avatarı sil',
createNewLabel: 'Yeni etiket oluştur', deleteBoard: 'Panoyu Sil',
createProject: 'Proje oluştur', deleteCard: 'Kartı sil',
delete: 'Sil', deleteCard_title: 'Kartı Sil',
deleteAttachment: 'Eki sil', deleteComment: 'Yorumu sil',
deleteAvatar: 'Avatarı sil', deleteImage: 'Resmi sil',
deleteBoard: 'Panoyu Sil', deleteLabel: 'Etiketi sil',
deleteCard: 'Kartı sil', deleteList: 'Listeyi sil',
deleteCard_title: 'Kartı Sil', deleteList_title: 'Listeyi Sil',
deleteComment: 'Yorumu sil', deleteProject: 'Projeyi sil',
deleteImage: 'Resmi sil', deleteProject_title: 'Projeyi Sil',
deleteLabel: 'Etiketi sil', deleteTask: 'Görevi sil',
deleteList: 'Listeyi sil', deleteTask_title: 'Görevi Sil',
deleteList_title: 'Listeyi Sil', deleteUser: 'Kullanıcıyı sil',
deleteProject: 'Projeyi sil', edit: 'Düzenle',
deleteProject_title: 'Projeyi Sil', editDueDate_title: 'Son Tarihi Düzenle',
deleteTask: 'Görevi sil', editDescription_title: 'Açıklamayı Düzenle',
deleteTask_title: 'Görevi Sil', editEmail_title: 'E-posta Adresini Düzenle',
deleteUser: 'Kullanıcıyı sil', editPassword_title: 'Şifreyi Değiştir',
edit: 'Düzenle', editStopwatch_title: 'Kronometreyi Düzenle',
editDueDate_title: 'Son Tarihi Düzenle', editTitle_title: 'Başlığı düzenle',
editDescription_title: 'Açıklamayı Düzenle', editUsername_title: 'Kullanıcı Adını Düzenle',
editEmail_title: 'E-posta Adresini Düzenle', hideDetails: 'Ayrıntıları gizle',
editPassword_title: 'Şifreyi Değiştir', leaveBoard: 'Panodan Ayrıl',
editStopwatch_title: 'Kronometreyi Düzenle', leaveProject: 'Projeden ayrıl',
editTitle_title: 'Başlığı düzenle', logOut_title: ıkış',
editUsername_title: 'Kullanıcı Adını Düzenle', makeCover_title: 'Önizleme olarak ayarla',
hideDetails: 'Ayrıntıları gizle', move: 'Taşı',
leaveBoard: 'Panodan Ayrıl', moveCard_title: 'Kartı Taşı',
leaveProject: 'Projeden ayrıl', remove: 'Sil',
logOut_title: ıkış', removeBackground: 'Arka planı kaldır',
makeCover_title: 'Önizleme olarak ayarla', removeCover_title: 'Önizlemeyi Kaldır',
move: 'Taşı', removeFromBoard: 'Panodan kaldır',
moveCard_title: 'Kartı Taşı', removeFromProject: 'Projeden kaldır',
remove: 'Sil', removeManager: 'Yöneticiyi kaldır',
removeBackground: 'Arka planı kaldır', removeMember: 'Üyeyi kaldır',
removeCover_title: 'Önizlemeyi Kaldır', save: 'Kaydet',
removeFromBoard: 'Panodan kaldır', showAllAttachments: 'Tüm ekleri göster ({{hidden}} gizli)',
removeFromProject: 'Projeden kaldır', showDetails: 'Ayrıntıları göster',
removeManager: 'Yöneticiyi kaldır', showFewerAttachments: 'Daha az ek göster',
removeMember: 'Üyeyi kaldır', start: 'başlat',
save: 'Kaydet', stop: 'dur',
showAllAttachments: 'Tüm ekleri göster ({{hidden}} gizli)', subscribe: 'Abone ol',
showDetails: 'Ayrıntıları göster', unsubscribe: 'Abonelikten çık',
showFewerAttachments: 'Daha az ek göster', uploadNewAvatar: 'Yeni avatar yükle',
start: 'başlat', uploadNewImage: 'Yeni resim yükle',
stop: 'dur',
subscribe: 'Abone ol',
unsubscribe: 'Abonelikten çık',
uploadNewAvatar: 'Yeni avatar yükle',
uploadNewImage: 'Yeni resim yükle',
}, },
}, },
}; };

View file

@ -50,6 +50,9 @@ export default class extends BaseModel {
isAdmin: attr({ isAdmin: attr({
getDefault: () => false, getDefault: () => false,
}), }),
isLocked: attr({
getDefault: () => false,
}),
isAvatarUpdating: attr({ isAvatarUpdating: attr({
getDefault: () => false, getDefault: () => false,
}), }),

View file

@ -24,6 +24,12 @@ services:
- DATABASE_URL=postgresql://postgres@postgres/planka - DATABASE_URL=postgresql://postgres@postgres/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
# 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
# have to use environment variables in order to pass the desired values, e.g. # have to use environment variables in order to pass the desired values, e.g.

View file

@ -4,6 +4,13 @@ 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

View file

@ -26,6 +26,10 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail) {
throw Errors.USER_NOT_FOUND; // Forbidden
}
user = await sails.helpers.users.deleteOne.with({ user = await sails.helpers.users.deleteOne.with({
record: user, record: user,
request: this.req, request: this.req,

View file

@ -59,6 +59,10 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail) {
throw Errors.USER_NOT_FOUND; // Forbidden
}
if ( if (
inputs.id === currentUser.id && inputs.id === currentUser.id &&
!bcrypt.compareSync(inputs.currentPassword, user.password) !bcrypt.compareSync(inputs.currentPassword, user.password)

View file

@ -58,6 +58,10 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail) {
throw Errors.USER_NOT_FOUND; // Forbidden
}
if ( if (
inputs.id === currentUser.id && inputs.id === currentUser.id &&
!bcrypt.compareSync(inputs.currentPassword, user.password) !bcrypt.compareSync(inputs.currentPassword, user.password)

View file

@ -61,6 +61,10 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail) {
throw Errors.USER_NOT_FOUND; // Forbidden
}
if ( if (
inputs.id === currentUser.id && inputs.id === currentUser.id &&
!bcrypt.compareSync(inputs.currentPassword, user.password) !bcrypt.compareSync(inputs.currentPassword, user.password)

View file

@ -67,6 +67,13 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail) {
/* eslint-disable no-param-reassign */
delete inputs.isAdmin;
delete inputs.name;
/* eslint-enable no-param-reassign */
}
const values = { const values = {
..._.pick(inputs, [ ..._.pick(inputs, [
'isAdmin', 'isAdmin',

View file

@ -114,6 +114,7 @@ module.exports = {
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,
}; };
}, },
}; };

View file

@ -35,9 +35,11 @@ module.exports.custom = {
oidcAudience: process.env.OIDC_AUDIENCE, oidcAudience: process.env.OIDC_AUDIENCE,
oidcClientId: process.env.OIDC_CLIENT_ID, oidcClientId: process.env.OIDC_CLIENT_ID,
oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups', oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
oidcAdminRoles: process.env.OIDC_ADMIN_ROLES.split(',') || [], oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
oidcredirectUri: process.env.OIDC_REDIRECT_URI, oidcredirectUri: process.env.OIDC_REDIRECT_URI,
oidcJwksUri: process.env.OIDC_JWKS_URI, oidcJwksUri: process.env.OIDC_JWKS_URI,
oidcScopes: process.env.OIDC_SCOPES || 'openid profile email', oidcScopes: process.env.OIDC_SCOPES || 'openid profile email',
oidcSkipUserInfo: process.env.OIDC_SKIP_USER_INFO === 'true', oidcSkipUserInfo: process.env.OIDC_SKIP_USER_INFO === 'true',
defaultAdminEmail: process.env.DEFAULT_ADMIN_EMAIL,
}; };

View file

@ -6,12 +6,8 @@ const knex = initKnex(knexfile);
(async () => { (async () => {
try { try {
const isExists = await knex.schema.hasTable(knexfile.migrations.tableName);
await knex.migrate.latest(); await knex.migrate.latest();
if (!isExists) { await knex.seed.run();
await knex.seed.run();
}
} catch (error) { } catch (error) {
process.exitCode = 1; process.exitCode = 1;

View file

@ -1,12 +1,42 @@
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
exports.seed = (knex) => const buildData = () => {
knex('user_account').insert({ const data = {
email: 'demo@demo.demo',
password: bcrypt.hashSync('demo', 10),
isAdmin: true, isAdmin: true,
name: 'Demo Demo', };
username: 'demo',
subscribeToOwnCards: false, if (process.env.DEFAULT_ADMIN_PASSWORD) {
createdAt: new Date().toISOString(), data.password = bcrypt.hashSync(process.env.DEFAULT_ADMIN_PASSWORD, 10);
}); }
if (process.env.DEFAULT_ADMIN_NAME) {
data.name = process.env.DEFAULT_ADMIN_NAME;
}
if (process.env.DEFAULT_ADMIN_USERNAME) {
data.username = process.env.DEFAULT_ADMIN_USERNAME;
}
return data;
};
exports.seed = async (knex) => {
if (!process.env.DEFAULT_ADMIN_EMAIL) {
return;
}
const data = buildData();
try {
await knex('user_account').insert({
...data,
email: process.env.DEFAULT_ADMIN_EMAIL,
subscribeToOwnCards: false,
createdAt: new Date().toISOString(),
});
} catch (error) {
if (Object.keys(data).length === 0) {
return;
}
await knex('user_account').update(data).where('email', process.env.DEFAULT_ADMIN_EMAIL);
}
};