Compare commits

..
13 Commits
Author SHA1 Message Date
Maksim Eltyshev 73a641b935 chore: Update version 2024-03-12 20:44:27 +01:00
Maksim Eltyshev 6802a0dc69 fix: Fix case sensitivity of default admin environment variables 2024-03-12 20:40:46 +01:00
Daniel Hiller 81d6c43cd9 fix: Container build windows 2024-02-14 03:15:34 +01:00
Maksim Eltyshev ca6a1a14f5 fix: Fix nullable boolean inputs 2024-02-08 16:42:10 +01:00
Daniel Hiller c096444acb Update build-and-push-docker-image-dev.yml 2024-02-08 15:34:43 +01:00
Maksim Eltyshev 51d05f3cd3 fix: Include due date when importing from Trello
Closes #598
2024-02-08 13:30:07 +01:00
Daniel Hiller 221da3c455 ci: update workflow 2024-02-06 02:07:27 +01:00
Daniel Hiller 92098e136c feat: Add healthcheck for docker deployments
fix: Missing healthcheck.js

fix: missing healthcheck.js

fix: HEALTHCHECK command
2024-02-06 01:51:00 +01:00
Maksim Eltyshev fd5c33dd5d fix: Remove overflow of attachment dropzone 2024-02-05 13:24:50 +01:00
Maksim Eltyshev 0c1c7e2192 chore: Update version 2024-02-01 00:34:31 +01:00
Maksim Eltyshev b8d262f745 feat: Add ability to enforce SSO
Closes #543, closes #545
2024-02-01 00:31:15 +01:00
Maksim Eltyshev a1fd694248 chore: Update version 2024-01-31 15:58:57 +01:00
Maksim Eltyshev 634d6ceab1 feat: Add ability to map OIDC attributes and ignore username
Closes #554
2024-01-25 23:01:59 +01:00
33 changed files with 263 additions and 161 deletions
+8 -3
View File
@@ -6,7 +6,7 @@ RUN apk -U upgrade \
WORKDIR /app 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 install pnpm --global \ && npm install pnpm --global \
@@ -17,7 +17,7 @@ FROM node:lts AS client
WORKDIR /app 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 install pnpm --global \ && npm install pnpm --global \
@@ -38,6 +38,7 @@ WORKDIR /app
COPY --chown=node:node start.sh . COPY --chown=node:node start.sh .
COPY --chown=node:node server . COPY --chown=node:node server .
COPY --chown=node:node healthcheck.js .
RUN mv .env.sample .env RUN mv .env.sample .env
@@ -52,4 +53,8 @@ VOLUME /app/private/attachments
EXPOSE 1337 EXPOSE 1337
CMD ["./start.sh"] HEALTHCHECK --interval=10s --timeout=2s --start-period=15s \
CMD node ./healthcheck.js
CMD [ "bash", "start.sh" ]
+2 -2
View File
@@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # 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.15 version: 0.1.18
# 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.15.4" appVersion: "1.15.7"
dependencies: dependencies:
- alias: postgresql - alias: postgresql
+1 -1
View File
@@ -1 +1 @@
REACT_APP_VERSION=1.15.4 REACT_APP_VERSION=1.15.7
@@ -94,7 +94,7 @@ const AttachmentAddZone = React.memo(({ children, onCreate }) => {
return ( return (
<> <>
{/* eslint-disable-next-line react/jsx-props-no-spreading */} {/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div {...getRootProps()} className={styles.wrapper}> <div {...getRootProps()}>
{isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>} {isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>}
{children} {children}
{/* eslint-disable-next-line react/jsx-props-no-spreading */} {/* eslint-disable-next-line react/jsx-props-no-spreading */}
@@ -12,8 +12,4 @@
width: 100%; width: 100%;
z-index: 2001; z-index: 2001;
} }
.wrapper {
overflow: hidden;
}
} }
+50 -40
View File
@@ -68,6 +68,7 @@ const Login = React.memo(
isSubmittingUsingOidc, isSubmittingUsingOidc,
error, error,
withOidc, withOidc,
isOidcEnforced,
onAuthenticate, onAuthenticate,
onAuthenticateUsingOidc, onAuthenticateUsingOidc,
onMessageDismiss, onMessageDismiss,
@@ -107,8 +108,10 @@ const Login = React.memo(
}, [onAuthenticate, data]); }, [onAuthenticate, data]);
useEffect(() => { useEffect(() => {
emailOrUsernameField.current.focus(); if (!isOidcEnforced) {
}, []); emailOrUsernameField.current.focus();
}
}, [isOidcEnforced]);
useEffect(() => { useEffect(() => {
if (wasSubmitting && !isSubmitting && error) { if (wasSubmitting && !isSubmitting && error) {
@@ -159,51 +162,57 @@ const Login = React.memo(
onDismiss={onMessageDismiss} onDismiss={onMessageDismiss}
/> />
)} )}
<Form size="large" onSubmit={handleSubmit}> {!isOidcEnforced && (
<div className={styles.inputWrapper}> <Form size="large" onSubmit={handleSubmit}>
<div className={styles.inputLabel}>{t('common.emailOrUsername')}</div> <div className={styles.inputWrapper}>
<Input <div className={styles.inputLabel}>{t('common.emailOrUsername')}</div>
fluid <Input
ref={emailOrUsernameField} fluid
name="emailOrUsername" ref={emailOrUsernameField}
value={data.emailOrUsername} name="emailOrUsername"
readOnly={isSubmitting} value={data.emailOrUsername}
className={styles.input} readOnly={isSubmitting}
onChange={handleFieldChange} className={styles.input}
onChange={handleFieldChange}
/>
</div>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.password')}</div>
<Input.Password
fluid
ref={passwordField}
name="password"
value={data.password}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
/>
</div>
<Form.Button
primary
size="large"
icon="right arrow"
labelPosition="right"
content={t('action.logIn')}
floated="right"
loading={isSubmitting}
disabled={isSubmitting || isSubmittingUsingOidc}
/> />
</div> </Form>
<div className={styles.inputWrapper}> )}
<div className={styles.inputLabel}>{t('common.password')}</div>
<Input.Password
fluid
ref={passwordField}
name="password"
value={data.password}
readOnly={isSubmitting}
className={styles.input}
onChange={handleFieldChange}
/>
</div>
<Form.Button
primary
size="large"
icon="right arrow"
labelPosition="right"
content={t('action.logIn')}
floated="right"
loading={isSubmitting}
disabled={isSubmitting || isSubmittingUsingOidc}
/>
</Form>
{withOidc && ( {withOidc && (
<Button <Button
type="button" type="button"
fluid={isOidcEnforced}
primary={isOidcEnforced}
size={isOidcEnforced ? 'large' : undefined}
icon={isOidcEnforced ? 'right arrow' : undefined}
labelPosition={isOidcEnforced ? 'right' : undefined}
content={t('action.logInWithSSO')}
loading={isSubmittingUsingOidc} loading={isSubmittingUsingOidc}
disabled={isSubmitting || isSubmittingUsingOidc} disabled={isSubmitting || isSubmittingUsingOidc}
onClick={onAuthenticateUsingOidc} onClick={onAuthenticateUsingOidc}
> />
{t('action.logInWithSSO')}
</Button>
)} )}
</div> </div>
</div> </div>
@@ -242,6 +251,7 @@ Login.propTypes = {
isSubmittingUsingOidc: 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, withOidc: PropTypes.bool.isRequired,
isOidcEnforced: PropTypes.bool.isRequired,
onAuthenticate: PropTypes.func.isRequired, onAuthenticate: PropTypes.func.isRequired,
onAuthenticateUsingOidc: PropTypes.func.isRequired, onAuthenticateUsingOidc: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired, onMessageDismiss: PropTypes.func.isRequired,
@@ -24,6 +24,7 @@ const AccountPane = React.memo(
organization, organization,
language, language,
isLocked, isLocked,
isUsernameLocked,
isAvatarUpdating, isAvatarUpdating,
usernameUpdateForm, usernameUpdateForm,
emailUpdateForm, emailUpdateForm,
@@ -104,7 +105,7 @@ const AccountPane = React.memo(
value={language || 'auto'} value={language || 'auto'}
onChange={handleLanguageChange} onChange={handleLanguageChange}
/> />
{!isLocked && ( {(!isLocked || !isUsernameLocked) && (
<> <>
<Divider horizontal section> <Divider horizontal section>
<Header as="h4"> <Header as="h4">
@@ -113,56 +114,62 @@ const AccountPane = React.memo(
})} })}
</Header> </Header>
</Divider> </Divider>
<div className={styles.action}> {!isUsernameLocked && (
<UserUsernameEditPopup <div className={styles.action}>
usePasswordConfirmation <UserUsernameEditPopup
defaultData={usernameUpdateForm.data} defaultData={usernameUpdateForm.data}
username={username} username={username}
isSubmitting={usernameUpdateForm.isSubmitting} isSubmitting={usernameUpdateForm.isSubmitting}
error={usernameUpdateForm.error} error={usernameUpdateForm.error}
onUpdate={onUsernameUpdate} usePasswordConfirmation={!isLocked} // FIXME: hack
onMessageDismiss={onUsernameUpdateMessageDismiss} onUpdate={onUsernameUpdate}
> onMessageDismiss={onUsernameUpdateMessageDismiss}
<Button className={styles.actionButton}> >
{t('action.editUsername', { <Button className={styles.actionButton}>
context: 'title', {t('action.editUsername', {
})} context: 'title',
</Button> })}
</UserUsernameEditPopup> </Button>
</div> </UserUsernameEditPopup>
<div className={styles.action}> </div>
<UserEmailEditPopup )}
usePasswordConfirmation {!isLocked && (
defaultData={emailUpdateForm.data} <>
email={email} <div className={styles.action}>
isSubmitting={emailUpdateForm.isSubmitting} <UserEmailEditPopup
error={emailUpdateForm.error} usePasswordConfirmation
onUpdate={onEmailUpdate} defaultData={emailUpdateForm.data}
onMessageDismiss={onEmailUpdateMessageDismiss} email={email}
> isSubmitting={emailUpdateForm.isSubmitting}
<Button className={styles.actionButton}> error={emailUpdateForm.error}
{t('action.editEmail', { onUpdate={onEmailUpdate}
context: 'title', onMessageDismiss={onEmailUpdateMessageDismiss}
})} >
</Button> <Button className={styles.actionButton}>
</UserEmailEditPopup> {t('action.editEmail', {
</div> context: 'title',
<div className={styles.action}> })}
<UserPasswordEditPopup </Button>
usePasswordConfirmation </UserEmailEditPopup>
defaultData={passwordUpdateForm.data} </div>
isSubmitting={passwordUpdateForm.isSubmitting} <div className={styles.action}>
error={passwordUpdateForm.error} <UserPasswordEditPopup
onUpdate={onPasswordUpdate} usePasswordConfirmation
onMessageDismiss={onPasswordUpdateMessageDismiss} defaultData={passwordUpdateForm.data}
> isSubmitting={passwordUpdateForm.isSubmitting}
<Button className={styles.actionButton}> error={passwordUpdateForm.error}
{t('action.editPassword', { onUpdate={onPasswordUpdate}
context: 'title', onMessageDismiss={onPasswordUpdateMessageDismiss}
})} >
</Button> <Button className={styles.actionButton}>
</UserPasswordEditPopup> {t('action.editPassword', {
</div> context: 'title',
})}
</Button>
</UserPasswordEditPopup>
</div>
</>
)}
</> </>
)} )}
</Tab.Pane> </Tab.Pane>
@@ -179,6 +186,7 @@ AccountPane.propTypes = {
organization: PropTypes.string, organization: PropTypes.string,
language: PropTypes.string, language: PropTypes.string,
isLocked: PropTypes.bool.isRequired, isLocked: PropTypes.bool.isRequired,
isUsernameLocked: 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,
@@ -18,6 +18,7 @@ const UserSettingsModal = React.memo(
language, language,
subscribeToOwnCards, subscribeToOwnCards,
isLocked, isLocked,
isUsernameLocked,
isAvatarUpdating, isAvatarUpdating,
usernameUpdateForm, usernameUpdateForm,
emailUpdateForm, emailUpdateForm,
@@ -50,6 +51,7 @@ const UserSettingsModal = React.memo(
organization={organization} organization={organization}
language={language} language={language}
isLocked={isLocked} isLocked={isLocked}
isUsernameLocked={isUsernameLocked}
isAvatarUpdating={isAvatarUpdating} isAvatarUpdating={isAvatarUpdating}
usernameUpdateForm={usernameUpdateForm} usernameUpdateForm={usernameUpdateForm}
emailUpdateForm={emailUpdateForm} emailUpdateForm={emailUpdateForm}
@@ -108,6 +110,7 @@ UserSettingsModal.propTypes = {
language: PropTypes.string, language: PropTypes.string,
subscribeToOwnCards: PropTypes.bool.isRequired, subscribeToOwnCards: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired, isLocked: PropTypes.bool.isRequired,
isUsernameLocked: 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,
@@ -136,13 +136,15 @@ const ActionsStep = React.memo(
context: 'title', context: 'title',
})} })}
</Menu.Item> </Menu.Item>
{!user.isUsernameLocked && (
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
{t('action.editUsername', {
context: 'title',
})}
</Menu.Item>
)}
{!user.isLocked && ( {!user.isLocked && (
<> <>
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
{t('action.editUsername', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}> <Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}>
{t('action.editEmail', { {t('action.editEmail', {
context: 'title', context: 'title',
@@ -19,6 +19,7 @@ const Item = React.memo(
isAdmin, isAdmin,
isLocked, isLocked,
isRoleLocked, isRoleLocked,
isUsernameLocked,
isDeletionLocked, isDeletionLocked,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
@@ -61,6 +62,7 @@ const Item = React.memo(
phone, phone,
isAdmin, isAdmin,
isLocked, isLocked,
isUsernameLocked,
isDeletionLocked, isDeletionLocked,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
@@ -95,6 +97,7 @@ Item.propTypes = {
isAdmin: PropTypes.bool.isRequired, isAdmin: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired, isLocked: PropTypes.bool.isRequired,
isRoleLocked: PropTypes.bool.isRequired, isRoleLocked: PropTypes.bool.isRequired,
isUsernameLocked: PropTypes.bool.isRequired,
isDeletionLocked: 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,
@@ -10,6 +10,7 @@ import Item from './Item';
const UsersModal = React.memo( const UsersModal = React.memo(
({ ({
items, items,
canAdd,
onUpdate, onUpdate,
onUsernameUpdate, onUsernameUpdate,
onUsernameUpdateMessageDismiss, onUsernameUpdateMessageDismiss,
@@ -112,6 +113,7 @@ const UsersModal = React.memo(
isAdmin={item.isAdmin} isAdmin={item.isAdmin}
isLocked={item.isLocked} isLocked={item.isLocked}
isRoleLocked={item.isRoleLocked} isRoleLocked={item.isRoleLocked}
isUsernameLocked={item.isUsernameLocked}
isDeletionLocked={item.isDeletionLocked} isDeletionLocked={item.isDeletionLocked}
emailUpdateForm={item.emailUpdateForm} emailUpdateForm={item.emailUpdateForm}
passwordUpdateForm={item.passwordUpdateForm} passwordUpdateForm={item.passwordUpdateForm}
@@ -129,11 +131,13 @@ const UsersModal = React.memo(
</Table.Body> </Table.Body>
</Table> </Table>
</Modal.Content> </Modal.Content>
<Modal.Actions> {canAdd && (
<UserAddPopupContainer> <Modal.Actions>
<Button positive content={t('action.addUser')} /> <UserAddPopupContainer>
</UserAddPopupContainer> <Button positive content={t('action.addUser')} />
</Modal.Actions> </UserAddPopupContainer>
</Modal.Actions>
)}
</Modal> </Modal>
); );
}, },
@@ -141,6 +145,7 @@ const UsersModal = React.memo(
UsersModal.propTypes = { UsersModal.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canAdd: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired, onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired, onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
+1
View File
@@ -20,6 +20,7 @@ const mapStateToProps = (state) => {
isSubmittingUsingOidc, isSubmittingUsingOidc,
error, error,
withOidc: !!oidcConfig, withOidc: !!oidcConfig,
isOidcEnforced: oidcConfig && oidcConfig.isEnforced,
}; };
}; };
@@ -16,6 +16,7 @@ const mapStateToProps = (state) => {
language, language,
subscribeToOwnCards, subscribeToOwnCards,
isLocked, isLocked,
isUsernameLocked,
isAvatarUpdating, isAvatarUpdating,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
@@ -32,6 +33,7 @@ const mapStateToProps = (state) => {
language, language,
subscribeToOwnCards, subscribeToOwnCards,
isLocked, isLocked,
isUsernameLocked,
isAvatarUpdating, isAvatarUpdating,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
@@ -6,10 +6,12 @@ import entryActions from '../entry-actions';
import UsersModal from '../components/UsersModal'; import UsersModal from '../components/UsersModal';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const oidcConfig = selectors.selectOidcConfig(state);
const users = selectors.selectUsersExceptCurrent(state); const users = selectors.selectUsersExceptCurrent(state);
return { return {
items: users, items: users,
canAdd: !oidcConfig || !oidcConfig.isEnforced,
}; };
}; };
+1
View File
@@ -46,6 +46,7 @@ export default class extends BaseModel {
isAdmin: attr(), isAdmin: attr(),
isLocked: attr(), isLocked: attr(),
isRoleLocked: attr(), isRoleLocked: attr(),
isUsernameLocked: attr(),
isDeletionLocked: attr(), isDeletionLocked: attr(),
deletedAt: attr(), deletedAt: attr(),
createdAt: attr({ createdAt: attr({
+14 -11
View File
@@ -3,15 +3,7 @@ version: '3'
services: services:
planka: planka:
image: ghcr.io/plankanban/planka:master image: ghcr.io/plankanban/planka:master
command: > restart: on-failure
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: volumes:
- user-avatars:/app/public/user-avatars - user-avatars:/app/public/user-avatars
- project-background-images:/app/public/project-background-images - project-background-images:/app/public/project-background-images
@@ -44,19 +36,30 @@ services:
# - OIDC_CLIENT_SECRET= # - OIDC_CLIENT_SECRET=
# - OIDC_SCOPES=openid email profile # - OIDC_SCOPES=openid email profile
# - OIDC_ADMIN_ROLES=admin # - OIDC_ADMIN_ROLES=admin
# - OIDC_EMAIL_ATTRIBUTE=email
# - OIDC_NAME_ATTRIBUTE=name
# - OIDC_USERNAME_ATTRIBUTE=preferred_username
# - OIDC_ROLES_ATTRIBUTE=groups # - OIDC_ROLES_ATTRIBUTE=groups
# - OIDC_IGNORE_USERNAME=true
# - OIDC_IGNORE_ROLES=true # - OIDC_IGNORE_ROLES=true
# - OIDC_ENFORCED=true
depends_on: depends_on:
- postgres postgres:
condition: service_healthy
postgres: postgres:
image: postgres:14-alpine image: postgres:14-alpine
restart: unless-stopped restart: on-failure
volumes: volumes:
- db-data:/var/lib/postgresql/data - db-data:/var/lib/postgresql/data
environment: environment:
- POSTGRES_DB=planka - POSTGRES_DB=planka
- POSTGRES_HOST_AUTH_METHOD=trust - POSTGRES_HOST_AUTH_METHOD=trust
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d planka"]
interval: 10s
timeout: 5s
retries: 5
volumes: volumes:
user-avatars: user-avatars:
+14 -11
View File
@@ -3,15 +3,7 @@ version: '3'
services: services:
planka: planka:
image: ghcr.io/plankanban/planka:latest image: ghcr.io/plankanban/planka:latest
command: > restart: on-failure
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: volumes:
- user-avatars:/app/public/user-avatars - user-avatars:/app/public/user-avatars
- project-background-images:/app/public/project-background-images - project-background-images:/app/public/project-background-images
@@ -44,19 +36,30 @@ services:
# - OIDC_CLIENT_SECRET= # - OIDC_CLIENT_SECRET=
# - OIDC_SCOPES=openid email profile # - OIDC_SCOPES=openid email profile
# - OIDC_ADMIN_ROLES=admin # - OIDC_ADMIN_ROLES=admin
# - OIDC_EMAIL_ATTRIBUTE=email
# - OIDC_NAME_ATTRIBUTE=name
# - OIDC_USERNAME_ATTRIBUTE=preferred_username
# - OIDC_ROLES_ATTRIBUTE=groups # - OIDC_ROLES_ATTRIBUTE=groups
# - OIDC_IGNORE_USERNAME=true
# - OIDC_IGNORE_ROLES=true # - OIDC_IGNORE_ROLES=true
# - OIDC_ENFORCED=true
depends_on: depends_on:
- postgres postgres:
condition: service_healthy
postgres: postgres:
image: postgres:14-alpine image: postgres:14-alpine
restart: unless-stopped restart: on-failure
volumes: volumes:
- db-data:/var/lib/postgresql/data - db-data:/var/lib/postgresql/data
environment: environment:
- POSTGRES_DB=planka - POSTGRES_DB=planka
- POSTGRES_HOST_AUTH_METHOD=trust - POSTGRES_HOST_AUTH_METHOD=trust
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d planka"]
interval: 10s
timeout: 5s
retries: 5
volumes: volumes:
user-avatars: user-avatars:
+23
View File
@@ -0,0 +1,23 @@
const http = require('http');
const options = {
host: 'localhost',
port: 1337,
timeout: 2000
};
const healthCheck = http.request(options, (res) => {
console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
process.exit(0);
}
else {
process.exit(1);
}
});
healthCheck.on('error', function (err) {
console.error('ERROR');
process.exit(1);
});
healthCheck.end();
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "planka", "name": "planka",
"version": "1.15.4", "version": "1.15.7",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "planka", "name": "planka",
"version": "1.15.4", "version": "1.15.7",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "planka", "name": "planka",
"version": "1.15.4", "version": "1.15.7",
"private": true, "private": true,
"homepage": "https://plankanban.github.io/planka", "homepage": "https://plankanban.github.io/planka",
"repository": { "repository": {
+5
View File
@@ -27,8 +27,13 @@ SECRET_KEY=notsecretkey
# OIDC_CLIENT_SECRET= # OIDC_CLIENT_SECRET=
# OIDC_SCOPES=openid email profile # OIDC_SCOPES=openid email profile
# OIDC_ADMIN_ROLES=admin # OIDC_ADMIN_ROLES=admin
# OIDC_EMAIL_ATTRIBUTE=email
# OIDC_NAME_ATTRIBUTE=name
# OIDC_USERNAME_ATTRIBUTE=preferred_username
# OIDC_ROLES_ATTRIBUTE=groups # OIDC_ROLES_ATTRIBUTE=groups
# OIDC_IGNORE_USERNAME=true
# OIDC_IGNORE_ROLES=true # OIDC_IGNORE_ROLES=true
# OIDC_ENFORCED=true
## Do not edit this ## Do not edit this
@@ -46,8 +46,11 @@ module.exports = {
}, },
async fn(inputs) { async fn(inputs) {
const remoteAddress = getRemoteAddress(this.req); if (sails.config.custom.oidcEnforced) {
throw Errors.USE_SINGLE_SIGN_ON;
}
const remoteAddress = getRemoteAddress(this.req);
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername); const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
if (!user) { if (!user) {
@@ -29,6 +29,7 @@ module.exports = {
}, },
canComment: { canComment: {
type: 'boolean', type: 'boolean',
allowNull: true,
}, },
}, },
@@ -17,6 +17,7 @@ module.exports = {
}, },
canComment: { canComment: {
type: 'boolean', type: 'boolean',
allowNull: true,
}, },
}, },
+1
View File
@@ -10,6 +10,7 @@ module.exports = {
response_mode: 'fragment', response_mode: 'fragment',
}), }),
endSessionUrl: oidcClient.issuer.end_session_endpoint ? oidcClient.endSessionUrl({}) : null, endSessionUrl: oidcClient.issuer.end_session_endpoint ? oidcClient.endSessionUrl({}) : null,
isEnforced: sails.config.custom.oidcEnforced,
}; };
} }
+10
View File
@@ -1,6 +1,9 @@
const zxcvbn = require('zxcvbn'); const zxcvbn = require('zxcvbn');
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
EMAIL_ALREADY_IN_USE: { EMAIL_ALREADY_IN_USE: {
emailAlreadyInUse: 'Email already in use', emailAlreadyInUse: 'Email already in use',
}, },
@@ -56,6 +59,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
emailAlreadyInUse: { emailAlreadyInUse: {
responseType: 'conflict', responseType: 'conflict',
}, },
@@ -65,6 +71,10 @@ module.exports = {
}, },
async fn(inputs) { async fn(inputs) {
if (sails.config.custom.oidcEnforced) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, [ const values = _.pick(inputs, [
'email', 'email',
'password', 'password',
+10 -11
View File
@@ -53,11 +53,7 @@ module.exports = {
async fn(inputs) { async fn(inputs) {
const { currentUser } = this.req; const { currentUser } = this.req;
if (inputs.id === currentUser.id) { if (inputs.id !== currentUser.id && !currentUser.isAdmin) {
if (!inputs.currentPassword) {
throw Errors.INVALID_CURRENT_PASSWORD;
}
} else if (!currentUser.isAdmin) {
throw Errors.USER_NOT_FOUND; // Forbidden throw Errors.USER_NOT_FOUND; // Forbidden
} }
@@ -67,15 +63,18 @@ module.exports = {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
} }
if (user.email === sails.config.custom.defaultAdminEmail || user.isSso) { if (user.email === sails.config.custom.defaultAdminEmail) {
throw Errors.NOT_ENOUGH_RIGHTS; throw Errors.NOT_ENOUGH_RIGHTS;
} }
if ( if (user.isSso) {
inputs.id === currentUser.id && if (!sails.config.custom.oidcIgnoreUsername) {
!bcrypt.compareSync(inputs.currentPassword, user.password) throw Errors.NOT_ENOUGH_RIGHTS;
) { }
throw Errors.INVALID_CURRENT_PASSWORD; } else if (inputs.id === currentUser.id) {
if (!inputs.currentPassword || !bcrypt.compareSync(inputs.currentPassword, user.password)) {
throw Errors.INVALID_CURRENT_PASSWORD;
}
} }
const values = _.pick(inputs, ['username']); const values = _.pick(inputs, ['username']);
@@ -107,6 +107,7 @@ module.exports = {
position: trelloCard.pos, position: trelloCard.pos,
name: trelloCard.name, name: trelloCard.name,
description: trelloCard.desc || null, description: trelloCard.desc || null,
dueDate: trelloCard.due,
}).fetch(); }).fetch();
await importCardLabels(plankaCard, trelloCard); await importCardLabels(plankaCard, trelloCard);
@@ -38,7 +38,10 @@ module.exports = {
throw 'invalidCodeOrNonce'; throw 'invalidCodeOrNonce';
} }
if (!userInfo.email || !userInfo.name) { if (
!userInfo[sails.config.custom.oidcEmailAttribute] ||
!userInfo[sails.config.custom.oidcNameAttribute]
) {
throw 'missingValues'; throw 'missingValues';
} }
@@ -56,12 +59,14 @@ module.exports = {
const values = { const values = {
isAdmin, isAdmin,
email: userInfo.email, email: userInfo[sails.config.custom.oidcEmailAttribute],
isSso: true, isSso: true,
name: userInfo.name, name: userInfo[sails.config.custom.oidcNameAttribute],
username: userInfo.preferred_username,
subscribeToOwnCards: false, subscribeToOwnCards: false,
}; };
if (!sails.config.custom.oidcIgnoreUsername) {
values.username = userInfo[sails.config.custom.oidcUsernameAttribute];
}
let user; let user;
// This whole block technically needs to be executed in a transaction // This whole block technically needs to be executed in a transaction
@@ -95,7 +100,10 @@ module.exports = {
}); });
} }
const updateFieldKeys = ['email', 'isSso', 'name', 'username']; const updateFieldKeys = ['email', 'isSso', 'name'];
if (!sails.config.custom.oidcIgnoreUsername) {
updateFieldKeys.push('username');
}
if (!sails.config.custom.oidcIgnoreRoles) { if (!sails.config.custom.oidcIgnoreRoles) {
updateFieldKeys.push('isAdmin'); updateFieldKeys.push('isAdmin');
} }
+1
View File
@@ -116,6 +116,7 @@ module.exports = {
..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']), ..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']),
isLocked: this.isSso || isDefaultAdmin, isLocked: this.isSso || isDefaultAdmin,
isRoleLocked: (this.isSso && !sails.config.custom.oidcIgnoreRoles) || isDefaultAdmin, isRoleLocked: (this.isSso && !sails.config.custom.oidcIgnoreRoles) || isDefaultAdmin,
isUsernameLocked: (this.isSso && !sails.config.custom.oidcIgnoreUsername) || isDefaultAdmin,
isDeletionLocked: isDefaultAdmin, isDeletionLocked: isDefaultAdmin,
avatarUrl: avatarUrl:
this.avatar && this.avatar &&
+7 -1
View File
@@ -31,15 +31,21 @@ 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`,
defaultAdminEmail: process.env.DEFAULT_ADMIN_EMAIL, defaultAdminEmail:
process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(),
oidcIssuer: process.env.OIDC_ISSUER, oidcIssuer: process.env.OIDC_ISSUER,
oidcClientId: process.env.OIDC_CLIENT_ID, oidcClientId: process.env.OIDC_CLIENT_ID,
oidcClientSecret: process.env.OIDC_CLIENT_SECRET, oidcClientSecret: process.env.OIDC_CLIENT_SECRET,
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(',') : [],
oidcEmailAttribute: process.env.OIDC_EMAIL_ATTRIBUTE || 'email',
oidcNameAttribute: process.env.OIDC_NAME_ATTRIBUTE || 'name',
oidcUsernameAttribute: process.env.OIDC_USERNAME_ATTRIBUTE || 'preferred_username',
oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups', oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
oidcIgnoreUsername: process.env.OIDC_IGNORE_USERNAME === 'true',
oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true', oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true',
oidcEnforced: process.env.OIDC_ENFORCED === 'true',
// TODO: move client base url to environment variable? // TODO: move client base url to environment variable?
oidcRedirectUri: `${ oidcRedirectUri: `${
+4 -3
View File
@@ -13,7 +13,7 @@ const buildData = () => {
data.name = process.env.DEFAULT_ADMIN_NAME; data.name = process.env.DEFAULT_ADMIN_NAME;
} }
if (process.env.DEFAULT_ADMIN_USERNAME) { if (process.env.DEFAULT_ADMIN_USERNAME) {
data.username = process.env.DEFAULT_ADMIN_USERNAME; data.username = process.env.DEFAULT_ADMIN_USERNAME.toLowerCase();
} }
return data; return data;
@@ -24,16 +24,17 @@ exports.seed = async (knex) => {
return; return;
} }
const email = process.env.DEFAULT_ADMIN_EMAIL.toLowerCase();
const data = buildData(); const data = buildData();
try { try {
await knex('user_account').insert({ await knex('user_account').insert({
...data, ...data,
email: process.env.DEFAULT_ADMIN_EMAIL, email,
subscribeToOwnCards: false, subscribeToOwnCards: false,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}); });
} catch (error) { } catch (error) {
await knex('user_account').update(data).where('email', process.env.DEFAULT_ADMIN_EMAIL); await knex('user_account').update(data).where('email', email);
} }
}; };
+1 -3
View File
@@ -1,4 +1,2 @@
#!/bin/bash #!/bin/bash
set -e export NODE_ENV=production && set -e && node db/init.js && node app.js --prod
node db/init.js
exec node app.js --prod $@