feat: Add legal requirements (#1306)
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const { getRemoteAddress } = require('../../../utils/remote-address');
|
||||
|
||||
const { AccessTokenSteps } = require('../../../constants');
|
||||
|
||||
const Errors = {
|
||||
INVALID_PENDING_TOKEN: {
|
||||
invalidPendingToken: 'Invalid pending token',
|
||||
},
|
||||
INVALID_SIGNATURE: {
|
||||
invalidSignature: 'Invalid signature',
|
||||
},
|
||||
ADMIN_LOGIN_REQUIRED_TO_INITIALIZE_INSTANCE: {
|
||||
adminLoginRequiredToInitializeInstance: 'Admin login required to initialize instance',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
pendingToken: {
|
||||
type: 'string',
|
||||
maxLength: 1024,
|
||||
required: true,
|
||||
},
|
||||
signature: {
|
||||
type: 'string',
|
||||
minLength: 64,
|
||||
maxLength: 64,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
exits: {
|
||||
invalidPendingToken: {
|
||||
responseType: 'unauthorized',
|
||||
},
|
||||
invalidSignature: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
adminLoginRequiredToInitializeInstance: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const remoteAddress = getRemoteAddress(this.req);
|
||||
const { httpOnlyToken } = this.req.cookies;
|
||||
|
||||
try {
|
||||
payload = sails.helpers.utils.verifyJwtToken(inputs.pendingToken);
|
||||
} catch (error) {
|
||||
if (error.raw.name === 'TokenExpiredError') {
|
||||
throw Errors.INVALID_PENDING_TOKEN;
|
||||
}
|
||||
|
||||
sails.log.warn(`Invalid pending token! (IP: ${remoteAddress})`);
|
||||
throw Errors.INVALID_PENDING_TOKEN;
|
||||
}
|
||||
|
||||
if (payload.subject !== AccessTokenSteps.ACCEPT_TERMS) {
|
||||
throw Errors.INVALID_PENDING_TOKEN;
|
||||
}
|
||||
|
||||
let session = await Session.qm.getOneUndeletedByPendingToken(inputs.pendingToken);
|
||||
|
||||
if (!session) {
|
||||
sails.log.warn(`Invalid pending token! (IP: ${remoteAddress})`);
|
||||
throw Errors.INVALID_PENDING_TOKEN;
|
||||
}
|
||||
|
||||
if (session.httpOnlyToken && httpOnlyToken !== session.httpOnlyToken) {
|
||||
throw Errors.INVALID_PENDING_TOKEN;
|
||||
}
|
||||
|
||||
let user = await User.qm.getOneById(session.userId, {
|
||||
withDeactivated: false,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw Errors.INVALID_PENDING_TOKEN; // TODO: introduce separate error?
|
||||
}
|
||||
|
||||
if (!user.termsSignature) {
|
||||
const termsSignature = sails.hooks.terms.getSignatureByUserRole(user.role);
|
||||
|
||||
if (inputs.signature !== termsSignature) {
|
||||
throw Errors.INVALID_SIGNATURE;
|
||||
}
|
||||
|
||||
user = await User.qm.updateOne(user.id, {
|
||||
termsSignature,
|
||||
termsAcceptedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const config = await Config.qm.getOneMain();
|
||||
|
||||
if (!config.isInitialized) {
|
||||
if (user.role === User.Roles.ADMIN) {
|
||||
await Config.qm.updateOneMain({
|
||||
isInitialized: true,
|
||||
});
|
||||
} else {
|
||||
throw Errors.ADMIN_LOGIN_REQUIRED_TO_INITIALIZE_INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
const { token: accessToken, payload: accessTokenPayload } = sails.helpers.utils.createJwtToken(
|
||||
user.id,
|
||||
);
|
||||
|
||||
session = await Session.qm.updateOne(session.id, {
|
||||
accessToken,
|
||||
pendingToken: null,
|
||||
});
|
||||
|
||||
if (session.httpOnlyToken && !this.req.isSocket) {
|
||||
sails.helpers.utils.setHttpOnlyTokenCookie(
|
||||
session.httpOnlyToken,
|
||||
accessTokenPayload,
|
||||
this.res,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
item: accessToken,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const { isEmailOrUsername } = require('../../../utils/validators');
|
||||
const { getRemoteAddress } = require('../../../utils/remote-address');
|
||||
@@ -22,6 +21,9 @@ const Errors = {
|
||||
USE_SINGLE_SIGN_ON: {
|
||||
useSingleSignOn: 'Use single sign-on',
|
||||
},
|
||||
TERMS_ACCEPTANCE_REQUIRED: {
|
||||
termsAcceptanceRequired: 'Terms acceptance required',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
@@ -39,7 +41,6 @@ module.exports = {
|
||||
},
|
||||
withHttpOnlyToken: {
|
||||
type: 'boolean',
|
||||
defaultsTo: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -56,6 +57,12 @@ module.exports = {
|
||||
useSingleSignOn: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
termsAcceptanceRequired: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
adminLoginRequiredToInitializeInstance: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
@@ -90,26 +97,19 @@ module.exports = {
|
||||
: Errors.INVALID_CREDENTIALS;
|
||||
}
|
||||
|
||||
const { token: accessToken, payload: accessTokenPayload } = sails.helpers.utils.createJwtToken(
|
||||
user.id,
|
||||
);
|
||||
|
||||
const httpOnlyToken = inputs.withHttpOnlyToken ? uuid() : null;
|
||||
|
||||
await Session.qm.createOne({
|
||||
accessToken,
|
||||
httpOnlyToken,
|
||||
remoteAddress,
|
||||
userId: user.id,
|
||||
userAgent: this.req.headers['user-agent'],
|
||||
});
|
||||
|
||||
if (httpOnlyToken && !this.req.isSocket) {
|
||||
sails.helpers.utils.setHttpOnlyTokenCookie(httpOnlyToken, accessTokenPayload, this.res);
|
||||
}
|
||||
|
||||
return {
|
||||
item: accessToken,
|
||||
};
|
||||
return sails.helpers.accessTokens.handleSteps
|
||||
.with({
|
||||
user,
|
||||
remoteAddress,
|
||||
request: this.req,
|
||||
response: this.res,
|
||||
withHttpOnlyToken: inputs.withHttpOnlyToken,
|
||||
})
|
||||
.intercept('adminLoginRequiredToInitializeInstance', (error) => ({
|
||||
adminLoginRequiredToInitializeInstance: error.raw,
|
||||
}))
|
||||
.intercept('termsAcceptanceRequired', (error) => ({
|
||||
termsAcceptanceRequired: error.raw,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const { getRemoteAddress } = require('../../../utils/remote-address');
|
||||
|
||||
const Errors = {
|
||||
@@ -17,6 +15,9 @@ const Errors = {
|
||||
INVALID_USERINFO_CONFIGURATION: {
|
||||
invalidUserinfoConfiguration: 'Invalid userinfo configuration',
|
||||
},
|
||||
TERMS_ACCEPTANCE_REQUIRED: {
|
||||
termsAcceptanceRequired: 'Terms acceptance required',
|
||||
},
|
||||
EMAIL_ALREADY_IN_USE: {
|
||||
emailAlreadyInUse: 'Email already in use',
|
||||
},
|
||||
@@ -45,7 +46,6 @@ module.exports = {
|
||||
},
|
||||
withHttpOnlyToken: {
|
||||
type: 'boolean',
|
||||
defaultsTo: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -59,6 +59,12 @@ module.exports = {
|
||||
invalidUserinfoConfiguration: {
|
||||
responseType: 'unauthorized',
|
||||
},
|
||||
termsAcceptanceRequired: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
adminLoginRequiredToInitializeInstance: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
emailAlreadyInUse: {
|
||||
responseType: 'conflict',
|
||||
},
|
||||
@@ -89,26 +95,19 @@ module.exports = {
|
||||
.intercept('activeLimitReached', () => Errors.ACTIVE_USERS_LIMIT_REACHED)
|
||||
.intercept('missingValues', () => Errors.MISSING_VALUES);
|
||||
|
||||
const { token: accessToken, payload: accessTokenPayload } = sails.helpers.utils.createJwtToken(
|
||||
user.id,
|
||||
);
|
||||
|
||||
const httpOnlyToken = inputs.withHttpOnlyToken ? uuid() : null;
|
||||
|
||||
await Session.qm.createOne({
|
||||
accessToken,
|
||||
httpOnlyToken,
|
||||
remoteAddress,
|
||||
userId: user.id,
|
||||
userAgent: this.req.headers['user-agent'],
|
||||
});
|
||||
|
||||
if (httpOnlyToken && !this.req.isSocket) {
|
||||
sails.helpers.utils.setHttpOnlyTokenCookie(httpOnlyToken, accessTokenPayload, this.res);
|
||||
}
|
||||
|
||||
return {
|
||||
item: accessToken,
|
||||
};
|
||||
return sails.helpers.accessTokens.handleSteps
|
||||
.with({
|
||||
user,
|
||||
remoteAddress,
|
||||
request: this.req,
|
||||
response: this.res,
|
||||
withHttpOnlyToken: inputs.withHttpOnlyToken,
|
||||
})
|
||||
.intercept('adminLoginRequiredToInitializeInstance', (error) => ({
|
||||
adminLoginRequiredToInitializeInstance: error.raw,
|
||||
}))
|
||||
.intercept('termsAcceptanceRequired', (error) => ({
|
||||
termsAcceptanceRequired: error.raw,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const Errors = {
|
||||
PENDING_TOKEN_NOT_FOUND: {
|
||||
pendingTokenNotFound: 'Pending token not found',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
pendingToken: {
|
||||
type: 'string',
|
||||
maxLength: 1024,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
exits: {
|
||||
pendingTokenNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { httpOnlyToken } = this.req.cookies;
|
||||
let session = await Session.qm.getOneUndeletedByPendingToken(inputs.pendingToken);
|
||||
|
||||
if (!session) {
|
||||
throw Errors.PENDING_TOKEN_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (session.httpOnlyToken && httpOnlyToken !== session.httpOnlyToken) {
|
||||
throw Errors.PENDING_TOKEN_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
session = await Session.qm.deleteOneById(session.id);
|
||||
|
||||
if (session.httpOnlyToken && !this.req.isSocket) {
|
||||
sails.helpers.utils.clearHttpOnlyTokenCookie(this.res);
|
||||
}
|
||||
|
||||
return {
|
||||
item: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
type: {
|
||||
type: 'string',
|
||||
isIn: Object.values(sails.hooks.terms.Types),
|
||||
required: true,
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
isIn: User.LANGUAGES,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const terms = await sails.hooks.terms.getPayload(inputs.type, inputs.language);
|
||||
|
||||
return {
|
||||
item: terms,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const { AccessTokenSteps } = require('../../../constants');
|
||||
|
||||
const Errors = {
|
||||
ADMIN_LOGIN_REQUIRED_TO_INITIALIZE_INSTANCE: {
|
||||
adminLoginRequiredToInitializeInstance: 'Admin login required to initialize instance',
|
||||
},
|
||||
};
|
||||
|
||||
const PENDING_TOKEN_EXPIRES_IN = 10 * 60;
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
user: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
request: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
response: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
remoteAddress: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
withHttpOnlyToken: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
|
||||
exits: {
|
||||
adminLoginRequiredToInitializeInstance: {},
|
||||
termsAcceptanceRequired: {},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const config = await Config.qm.getOneMain();
|
||||
|
||||
if (!config.isInitialized) {
|
||||
if (inputs.user.role === User.Roles.ADMIN) {
|
||||
if (inputs.user.termsSignature) {
|
||||
await Config.qm.updateOneMain({
|
||||
isInitialized: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw Errors.ADMIN_LOGIN_REQUIRED_TO_INITIALIZE_INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sails.hooks.terms.hasSignature(inputs.user.termsSignature)) {
|
||||
const { token: pendingToken, payload: pendingTokenPayload } =
|
||||
sails.helpers.utils.createJwtToken(
|
||||
AccessTokenSteps.ACCEPT_TERMS,
|
||||
undefined,
|
||||
PENDING_TOKEN_EXPIRES_IN,
|
||||
);
|
||||
|
||||
const session = await sails.helpers.sessions.createOne.with({
|
||||
values: {
|
||||
pendingToken,
|
||||
userId: inputs.user.id,
|
||||
remoteAddress: inputs.remoteAddress,
|
||||
userAgent: inputs.request.headers['user-agent'],
|
||||
},
|
||||
withHttpOnlyToken: inputs.withHttpOnlyToken,
|
||||
});
|
||||
|
||||
if (session.httpOnlyToken && !inputs.request.isSocket) {
|
||||
sails.helpers.utils.setHttpOnlyTokenCookie(
|
||||
session.httpOnlyToken,
|
||||
pendingTokenPayload,
|
||||
inputs.response,
|
||||
);
|
||||
}
|
||||
|
||||
const termsType = sails.hooks.terms.getTypeByUserRole(inputs.user.role);
|
||||
|
||||
throw {
|
||||
termsAcceptanceRequired: {
|
||||
pendingToken,
|
||||
termsType,
|
||||
message: 'Terms acceptance required',
|
||||
step: AccessTokenSteps.ACCEPT_TERMS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { token: accessToken, payload: accessTokenPayload } = sails.helpers.utils.createJwtToken(
|
||||
inputs.user.id,
|
||||
);
|
||||
|
||||
const session = await sails.helpers.sessions.createOne.with({
|
||||
values: {
|
||||
accessToken,
|
||||
userId: inputs.user.id,
|
||||
remoteAddress: inputs.remoteAddress,
|
||||
userAgent: inputs.request.headers['user-agent'],
|
||||
},
|
||||
withHttpOnlyToken: inputs.withHttpOnlyToken,
|
||||
});
|
||||
|
||||
if (session.httpOnlyToken && !inputs.request.isSocket) {
|
||||
sails.helpers.utils.setHttpOnlyTokenCookie(
|
||||
session.httpOnlyToken,
|
||||
accessTokenPayload,
|
||||
inputs.response,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
item: accessToken,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
values: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
},
|
||||
withHttpOnlyToken: {
|
||||
type: 'boolean',
|
||||
defaultsTo: false,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { values } = inputs;
|
||||
|
||||
return Session.qm.createOne({
|
||||
...values,
|
||||
httpOnlyToken: inputs.withHttpOnlyToken ? uuid() : null,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -20,13 +20,20 @@ module.exports = {
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
const data = {
|
||||
..._.omit(inputs.record, ['password', 'avatar', 'passwordChangedAt']),
|
||||
..._.omit(inputs.record, [
|
||||
'password',
|
||||
'avatar',
|
||||
'termsSignature',
|
||||
'passwordChangedAt',
|
||||
'termsAcceptedAt',
|
||||
]),
|
||||
avatar: inputs.record.avatar && {
|
||||
url: `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}/original.${inputs.record.avatar.extension}`)}`,
|
||||
thumbnailUrls: {
|
||||
cover180: `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}/cover-180.${inputs.record.avatar.extension}`)}`,
|
||||
},
|
||||
},
|
||||
termsType: sails.hooks.terms.getTypeByUserRole(inputs.record.role),
|
||||
};
|
||||
|
||||
if (inputs.user) {
|
||||
|
||||
@@ -17,13 +17,16 @@ module.exports = {
|
||||
issuedAt: {
|
||||
type: 'ref',
|
||||
},
|
||||
expiresIn: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
const { issuedAt = new Date() } = inputs;
|
||||
const { issuedAt = new Date(), expiresIn = sails.config.custom.tokenExpiresIn } = inputs;
|
||||
|
||||
const iat = Math.floor(issuedAt / 1000);
|
||||
const exp = iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60;
|
||||
const exp = iat + expiresIn;
|
||||
|
||||
const payload = {
|
||||
iat,
|
||||
|
||||
@@ -24,7 +24,7 @@ module.exports = {
|
||||
try {
|
||||
payload = jwt.verify(inputs.token, sails.config.session.secret);
|
||||
} catch (error) {
|
||||
throw 'invalidToken';
|
||||
throw { invalidToken: error };
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/* Query methods */
|
||||
|
||||
const getOneMain = () => Config.findOne(Config.MAIN_ID);
|
||||
|
||||
const updateOneMain = (values) => Config.updateOne(Config.MAIN_ID).set({ ...values });
|
||||
|
||||
module.exports = {
|
||||
getOneMain,
|
||||
updateOneMain,
|
||||
};
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/* Query methods */
|
||||
|
||||
const createOne = (values) => IdentityProviderUser.create({ ...values }).fetch();
|
||||
|
||||
const getOneByIssuerAndSub = (issuer, sub) =>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/* Query methods */
|
||||
|
||||
const createOne = (values) => Session.create({ ...values }).fetch();
|
||||
|
||||
const getOneUndeletedByAccessToken = (accessToken) =>
|
||||
@@ -11,6 +13,14 @@ const getOneUndeletedByAccessToken = (accessToken) =>
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const getOneUndeletedByPendingToken = (pendingToken) =>
|
||||
Session.findOne({
|
||||
pendingToken,
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const updateOne = (criteria, values) => Session.updateOne(criteria).set({ ...values });
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const delete_ = (criteria) => Session.destroy(criteria).fetch();
|
||||
|
||||
@@ -25,6 +35,8 @@ const deleteOneById = (id) =>
|
||||
module.exports = {
|
||||
createOne,
|
||||
getOneUndeletedByAccessToken,
|
||||
getOneUndeletedByPendingToken,
|
||||
updateOne,
|
||||
deleteOneById,
|
||||
delete: delete_,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/**
|
||||
* terms hook
|
||||
*
|
||||
* @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions,
|
||||
* and/or initialization logic.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
|
||||
*/
|
||||
|
||||
const fsPromises = require('fs').promises;
|
||||
const crypto = require('crypto');
|
||||
|
||||
const Types = {
|
||||
GENERAL: 'general',
|
||||
EXTENDED: 'extended',
|
||||
};
|
||||
|
||||
const LANGUAGES = ['de-DE', 'en-US'];
|
||||
const DEFAULT_LANGUAGE = 'en-US';
|
||||
|
||||
const hashContent = (content) => crypto.createHash('sha256').update(content).digest('hex');
|
||||
|
||||
module.exports = function defineTermsHook(sails) {
|
||||
let signatureByType;
|
||||
let signaturesSet;
|
||||
|
||||
return {
|
||||
Types,
|
||||
LANGUAGES,
|
||||
|
||||
/**
|
||||
* Runs when this Sails app loads/lifts.
|
||||
*/
|
||||
|
||||
async initialize() {
|
||||
sails.log.info('Initializing custom hook (`terms`)');
|
||||
|
||||
signatureByType = {
|
||||
[Types.GENERAL]: hashContent(await this.getContent(Types.GENERAL)),
|
||||
[Types.EXTENDED]: hashContent(await this.getContent(Types.EXTENDED)),
|
||||
};
|
||||
|
||||
signaturesSet = new Set(Object.values(signatureByType));
|
||||
},
|
||||
|
||||
async getPayload(type, language = DEFAULT_LANGUAGE) {
|
||||
if (!Object.values(Types).includes(type)) {
|
||||
throw new Error(`Unknown type: ${type}`);
|
||||
}
|
||||
|
||||
if (!LANGUAGES.includes(language)) {
|
||||
language = DEFAULT_LANGUAGE; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
language,
|
||||
content: await this.getContent(type, language),
|
||||
signature: this.getSignatureByType(type),
|
||||
};
|
||||
},
|
||||
|
||||
getTypeByUserRole(userRole) {
|
||||
return userRole === User.Roles.ADMIN ? Types.EXTENDED : Types.GENERAL;
|
||||
},
|
||||
|
||||
getContent(type, language = DEFAULT_LANGUAGE) {
|
||||
return fsPromises.readFile(`${sails.config.appPath}/terms/${language}/${type}.md`, 'utf8');
|
||||
},
|
||||
|
||||
getSignatureByType(type) {
|
||||
return signatureByType[type];
|
||||
},
|
||||
|
||||
getSignatureByUserRole(userRole) {
|
||||
return signatureByType[this.getTypeByUserRole(userRole)];
|
||||
},
|
||||
|
||||
hasSignature(signature) {
|
||||
return signaturesSet.has(signature);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/**
|
||||
* Config.js
|
||||
*
|
||||
* @description :: A model definition represents a database table/collection.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||
*/
|
||||
|
||||
const MAIN_ID = '1';
|
||||
|
||||
module.exports = {
|
||||
MAIN_ID,
|
||||
|
||||
attributes: {
|
||||
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||
|
||||
isInitialized: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
columnName: 'is_initialized',
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
||||
|
||||
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
},
|
||||
};
|
||||
@@ -18,9 +18,16 @@ module.exports = {
|
||||
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
isNotEmptyString: true,
|
||||
allowNull: true,
|
||||
columnName: 'access_token',
|
||||
},
|
||||
pendingToken: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
allowNull: true,
|
||||
columnName: 'pending_token',
|
||||
},
|
||||
httpOnlyToken: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
|
||||
@@ -191,6 +191,12 @@ module.exports = {
|
||||
defaultsTo: ProjectOrders.BY_DEFAULT,
|
||||
columnName: 'default_projects_order',
|
||||
},
|
||||
termsSignature: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
allowNull: true,
|
||||
columnName: 'terms_signature',
|
||||
},
|
||||
isSsoUser: {
|
||||
type: 'boolean',
|
||||
defaultsTo: false,
|
||||
@@ -205,6 +211,10 @@ module.exports = {
|
||||
type: 'ref',
|
||||
columnName: 'password_changed_at',
|
||||
},
|
||||
termsAcceptedAt: {
|
||||
type: 'ref',
|
||||
columnName: 'terms_accepted_at',
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
|
||||
@@ -34,8 +34,15 @@
|
||||
module.exports = function forbidden(message) {
|
||||
const { res } = this;
|
||||
|
||||
return res.status(403).json({
|
||||
const data = {
|
||||
code: 'E_FORBIDDEN',
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
if (_.isPlainObject(message)) {
|
||||
Object.assign(data, message);
|
||||
} else {
|
||||
data.message = message;
|
||||
}
|
||||
|
||||
return res.status(403).json(data);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ module.exports.custom = {
|
||||
baseUrlPath: parsedBasedUrl.pathname,
|
||||
baseUrlSecure: parsedBasedUrl.protocol === 'https:',
|
||||
|
||||
tokenExpiresIn: parseInt(process.env.TOKEN_EXPIRES_IN, 10) || 365,
|
||||
tokenExpiresIn: (parseInt(process.env.TOKEN_EXPIRES_IN, 10) || 365) * 24 * 60 * 60,
|
||||
|
||||
// Location to receive uploaded files in. Default (non-string value) is a Sails-specific location.
|
||||
uploadsTempPath: null,
|
||||
|
||||
@@ -36,6 +36,9 @@ module.exports.policies = {
|
||||
'projects/create': ['is-authenticated', 'is-external', 'is-admin-or-project-owner'],
|
||||
|
||||
'config/show': true,
|
||||
'terms/show': true,
|
||||
'access-tokens/create': true,
|
||||
'access-tokens/exchange-with-oidc': true,
|
||||
'access-tokens/accept-terms': true,
|
||||
'access-tokens/revoke-pending-token': true,
|
||||
};
|
||||
|
||||
@@ -64,6 +64,8 @@ function staticDirServer(prefix, dirFn) {
|
||||
module.exports.routes = {
|
||||
'GET /api/config': 'config/show',
|
||||
|
||||
'GET /api/terms/:type': 'terms/show',
|
||||
|
||||
'GET /api/webhooks': 'webhooks/index',
|
||||
'POST /api/webhooks': 'webhooks/create',
|
||||
'PATCH /api/webhooks/:id': 'webhooks/update',
|
||||
@@ -71,6 +73,8 @@ module.exports.routes = {
|
||||
|
||||
'POST /api/access-tokens': 'access-tokens/create',
|
||||
'POST /api/access-tokens/exchange-with-oidc': 'access-tokens/exchange-with-oidc',
|
||||
'POST /api/access-tokens/accept-terms': 'access-tokens/accept-terms',
|
||||
'POST /api/access-tokens/revoke-pending-token': 'access-tokens/revoke-pending-token',
|
||||
'DELETE /api/access-tokens/me': 'access-tokens/delete',
|
||||
|
||||
'GET /api/users': 'users/index',
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
const AccessTokenSteps = {
|
||||
ACCEPT_TERMS: 'accept-terms',
|
||||
};
|
||||
|
||||
const POSITION_GAP = 65536;
|
||||
|
||||
const MAX_SIZE_IN_BYTES_TO_GET_ENCODING = 8 * 1024 * 1024;
|
||||
|
||||
module.exports = {
|
||||
AccessTokenSteps,
|
||||
POSITION_GAP,
|
||||
MAX_SIZE_IN_BYTES_TO_GET_ENCODING,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
exports.up = async (knex) => {
|
||||
await knex.schema.createTable('config', (table) => {
|
||||
/* Columns */
|
||||
|
||||
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
|
||||
|
||||
table.boolean('is_initialized').notNullable();
|
||||
|
||||
table.timestamp('created_at', true);
|
||||
table.timestamp('updated_at', true);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('session', (table) => {
|
||||
/* Columns */
|
||||
|
||||
table.text('pending_token');
|
||||
|
||||
/* Modifications */
|
||||
|
||||
table.setNullable('access_token');
|
||||
|
||||
/* Indexes */
|
||||
|
||||
table.unique('pending_token');
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('user_account', (table) => {
|
||||
/* Columns */
|
||||
|
||||
table.text('terms_signature');
|
||||
|
||||
table.timestamp('terms_accepted_at', true);
|
||||
});
|
||||
|
||||
const isInitialized = !!(await knex('user_account').first());
|
||||
|
||||
await knex('config').insert({
|
||||
isInitialized,
|
||||
id: 1,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await knex('session')
|
||||
.update({
|
||||
deletedAt: new Date().toISOString(),
|
||||
})
|
||||
.whereNull('deletedAt');
|
||||
};
|
||||
|
||||
exports.down = async (knex) => {
|
||||
await knex.schema.dropTable('config');
|
||||
|
||||
await knex('session').del().whereNull('access_token');
|
||||
|
||||
await knex.schema.alterTable('session', (table) => {
|
||||
table.dropColumn('pending_token');
|
||||
|
||||
table.dropNullable('access_token');
|
||||
});
|
||||
|
||||
return knex.schema.table('user_account', (table) => {
|
||||
table.dropColumn('terms_signature');
|
||||
table.dropColumn('terms_accepted_at');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
# Beispiel-Nutzungsbedingungen - Administratoren
|
||||
_Nicht rechtsverbindlich. Dies ist ein Platzhaltertext nur zu Testzwecken._
|
||||
|
||||
_Letzte Aktualisierung: 14. August 2025_
|
||||
|
||||
Willkommen, Administrator! Diese Beispiel-Nutzungsbedingungen ("Bedingungen") dienen **ausschließlich der Demonstration** und sind rechtlich nicht gültig. Diese erweiterte Version enthält zusätzliche Klauseln, die höhere Verantwortlichkeiten für Administratoren widerspiegeln.
|
||||
|
||||
---
|
||||
|
||||
## 1. Einführung
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed nibh id elit ultricies posuere.
|
||||
|
||||
## 2. Teilnahmeberechtigung
|
||||
Administratoren müssen mindestens 21 Jahre alt und von der Organisation autorisiert sein. Nulla rhoncus diam non dictum fermentum.
|
||||
|
||||
## 3. Administrative Pflichten
|
||||
Als Administrator stimmen Sie zu:
|
||||
- Benutzerkonten verantwortungsvoll zu verwalten.
|
||||
- Sicherheitseinstellungen korrekt zu konfigurieren.
|
||||
- Die Einhaltung von Datenschutzvorschriften zu gewährleisten.
|
||||
|
||||
## 4. Verbotene Administrative Handlungen
|
||||
Administratoren dürfen nicht:
|
||||
1. Unbefugten Zugriff gewähren.
|
||||
2. Prüfprotokolle ohne triftigen Grund ändern.
|
||||
3. Administrative Rechte zum persönlichen Vorteil nutzen.
|
||||
|
||||
## 5. Datenverwaltung
|
||||
Sie sind verantwortlich für den Schutz der Benutzerdaten und die Systemintegrität.
|
||||
|
||||
## 6. Änderungen
|
||||
Diese Beispiel-Bedingungen können jederzeit zu Testzwecken geändert werden.
|
||||
|
||||
## 7. Kontakt
|
||||
Bei Fragen zu diesen Beispiel-Bedingungen wenden Sie sich bitte an `placeholder@example.com`.
|
||||
|
||||
---
|
||||
|
||||
**Ende des Beispiels – Erweiterte (Admin) Bedingungen**
|
||||
@@ -0,0 +1,36 @@
|
||||
# Beispiel-Nutzungsbedingungen - Allgemeine Benutzer
|
||||
_Nicht rechtsverbindlich. Dies ist ein Platzhaltertext nur zu Testzwecken._
|
||||
|
||||
_Letzte Aktualisierung: 14. August 2025_
|
||||
|
||||
Willkommen bei ExampleCorp! Diese Beispiel-Nutzungsbedingungen ("Bedingungen") dienen ausschließlich der Demonstration und dem Testen in Ihrer Anwendung. Sie sind **rechtlich nicht gültig**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Einführung
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu mauris at est lacinia gravida. Nulla facilisi.
|
||||
|
||||
## 2. Teilnahmeberechtigung
|
||||
Benutzer müssen mindestens 18 Jahre alt sein, um diesen Dienst zu nutzen. Sed vulputate sapien eu varius efficitur.
|
||||
|
||||
## 3. Pflichten der Benutzer
|
||||
Durch die Nutzung der Plattform stimmen Sie zu:
|
||||
- Genaue Informationen bereitzustellen.
|
||||
- Keine illegalen Aktivitäten durchzuführen.
|
||||
- Andere Benutzer zu respektieren.
|
||||
|
||||
## 4. Verbotene Aktivitäten
|
||||
Nicht erlaubt:
|
||||
1. Andere belästigen.
|
||||
2. Schadcode hochladen.
|
||||
3. Sicherheitsmaßnahmen umgehen.
|
||||
|
||||
## 5. Änderungen
|
||||
Diese Beispiel-Bedingungen können jederzeit zu Testzwecken geändert werden.
|
||||
|
||||
## 6. Kontakt
|
||||
Bei Fragen zu diesen Beispiel-Bedingungen wenden Sie sich bitte an `placeholder@example.com`.
|
||||
|
||||
---
|
||||
|
||||
**Ende des Beispiels - Allgemeine Bedingungen**
|
||||
@@ -0,0 +1,39 @@
|
||||
# Example Terms of Service - Admin Users
|
||||
_Not legally binding. This is placeholder text for testing purposes only._
|
||||
|
||||
_Last updated: August 14, 2025_
|
||||
|
||||
Welcome, Admin! These Example Terms of Service ("Terms") are **for demonstration purposes only** and are not legally valid. This extended version contains additional clauses reflecting higher responsibilities for administrative users.
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed nibh id elit ultricies posuere.
|
||||
|
||||
## 2. Eligibility
|
||||
Admins must be at least 21 years old and authorized by the organization. Nulla rhoncus diam non dictum fermentum.
|
||||
|
||||
## 3. Administrative Responsibilities
|
||||
As an administrator, you agree to:
|
||||
- Manage user accounts responsibly.
|
||||
- Configure security settings accurately.
|
||||
- Ensure compliance with data protection rules.
|
||||
|
||||
## 4. Prohibited Administrative Actions
|
||||
Admins may not:
|
||||
1. Grant unauthorized access.
|
||||
2. Alter audit logs without proper reason.
|
||||
3. Use administrative privileges for personal gain.
|
||||
|
||||
## 5. Data Management
|
||||
You are responsible for safeguarding user data and system integrity.
|
||||
|
||||
## 6. Modifications
|
||||
These example Terms may be updated for testing purposes without notice.
|
||||
|
||||
## 7. Contact
|
||||
For questions about these example Terms, please contact `placeholder@example.com`.
|
||||
|
||||
---
|
||||
|
||||
**End of Example – Extended (Admin) Terms**
|
||||
@@ -0,0 +1,36 @@
|
||||
# Example Terms of Service - General Users
|
||||
_Not legally binding. This is placeholder text for testing purposes only._
|
||||
|
||||
_Last updated: August 14, 2025_
|
||||
|
||||
Welcome to ExampleCorp! These Example Terms of Service ("Terms") are provided solely for demonstration and testing purposes in your application. They are **not legally valid**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu mauris at est lacinia gravida. Nulla facilisi.
|
||||
|
||||
## 2. Eligibility
|
||||
Users must be at least 18 years old to use this service. Sed vulputate sapien eu varius efficitur.
|
||||
|
||||
## 3. User Responsibilities
|
||||
By using the platform, you agree to:
|
||||
- Provide accurate information.
|
||||
- Avoid illegal activities.
|
||||
- Respect other users.
|
||||
|
||||
## 4. Prohibited Activities
|
||||
Do not:
|
||||
1. Harass others.
|
||||
2. Upload harmful code.
|
||||
3. Attempt to bypass security.
|
||||
|
||||
## 5. Modifications
|
||||
These example Terms may change at any time for testing purposes.
|
||||
|
||||
## 6. Contact
|
||||
For questions about these example Terms, please contact `placeholder@example.com`.
|
||||
|
||||
---
|
||||
|
||||
**End of Example - General Terms**
|
||||
Reference in New Issue
Block a user