Adds TOTP setup with QR code, login challenge, recovery codes and trusted devices that let a browser skip the second factor for 30 days. Admins can reset another user's second factor by confirming with their own password.
278 lines
10 KiB
JavaScript
278 lines
10 KiB
JavaScript
/**
|
|
* Route Mappings
|
|
* (sails.config.routes)
|
|
*
|
|
* Your routes tell Sails what to do each time it receives a request.
|
|
*
|
|
* For more information on configuring custom routes, check out:
|
|
* https://sailsjs.com/anatomy/config/routes-js
|
|
*/
|
|
|
|
const path = require('path');
|
|
const sails = require('sails');
|
|
|
|
// Remove prefix from urlPath, assuming completely matches a subpath of
|
|
// urlPath. The result preserves query params and fragment if present
|
|
//
|
|
// Examples:
|
|
// '/foo', '/foo/bar' -> '/bar'
|
|
// '/foo', '/foo' -> '/'
|
|
// '/foo', '/foo?baz=bux' -> '/?baz=bux'
|
|
// '/foo', '/foobar' -> '/foobar'
|
|
const removeRoutePrefix = (prefix, urlPath) => {
|
|
if (urlPath.startsWith(prefix)) {
|
|
const subpath = urlPath.substring(prefix.length);
|
|
|
|
if (subpath.startsWith('/')) {
|
|
// Prefix matched a complete set of path segments, with a valid path
|
|
// remaining.
|
|
return subpath;
|
|
}
|
|
|
|
if (subpath.length === 0 || subpath.startsWith('?') || subpath.startsWith('#')) {
|
|
// Prefix matched a complete set of path segments, but there is no path
|
|
// remaining. Add '/'.
|
|
return `/${subpath}`;
|
|
}
|
|
}
|
|
|
|
// Either the prefix didn't match at all, or it wasn't a complete path match
|
|
// (e.g. we don't want to treat '/foo' as a prefix of '/foobar'). Leave the
|
|
// path as-is.
|
|
return urlPath;
|
|
};
|
|
|
|
const serveStatic = async (prefix, getPathSegment, req, res) => {
|
|
// Custom config properties are not available when the routes config is
|
|
// loaded, so resolve the target value just before serving the request.
|
|
const pathSegment = getPathSegment();
|
|
// Remove the leading route prefix, since it's already included in path
|
|
// segment.
|
|
const normalizedUrlPath = removeRoutePrefix(prefix, req.url);
|
|
|
|
const fileManager = sails.hooks['file-manager'].getInstance();
|
|
const filePathSegment = path.join(pathSegment, normalizedUrlPath);
|
|
|
|
let readStream;
|
|
let headers;
|
|
|
|
try {
|
|
[readStream, headers] = await fileManager.read(filePathSegment, {
|
|
withHeaders: true,
|
|
});
|
|
} catch (err) {
|
|
return res.sendStatus(404);
|
|
}
|
|
|
|
res.set({
|
|
...headers,
|
|
'Cache-Control': `private, max-age=${sails.config.http.cache}, immutable`,
|
|
});
|
|
|
|
readStream.on('error', () => {
|
|
if (res.headersSent) {
|
|
res.destroy();
|
|
} else {
|
|
res.sendStatus(404);
|
|
}
|
|
});
|
|
|
|
return readStream.pipe(res);
|
|
};
|
|
|
|
/* const publicStaticDirServer = (prefix, getPathSegment) => (req, res, next) => {
|
|
if (!req.url.startsWith(prefix)) {
|
|
return next();
|
|
}
|
|
|
|
return serveStatic(prefix, getPathSegment, req, res);
|
|
}; */
|
|
|
|
const protectedStaticDirServer = (prefix, getPathSegment) => (req, res, next) => {
|
|
if (!req.url.startsWith(prefix)) {
|
|
return next();
|
|
}
|
|
|
|
try {
|
|
sails.helpers.utils.verifyJwtToken(req.cookies.accessToken);
|
|
} catch (error) {
|
|
return res.sendStatus(401);
|
|
}
|
|
|
|
return serveStatic(prefix, getPathSegment, req, res);
|
|
};
|
|
|
|
module.exports.routes = {
|
|
'GET /api/bootstrap': 'bootstrap/show',
|
|
|
|
'GET /api/terms': 'terms/show',
|
|
|
|
'GET /api/config': 'config/show',
|
|
'PATCH /api/config': 'config/update',
|
|
'POST /api/config/test-smtp': 'config/test-smtp',
|
|
|
|
'GET /api/webhooks': 'webhooks/index',
|
|
'POST /api/webhooks': 'webhooks/create',
|
|
'PATCH /api/webhooks/:id': 'webhooks/update',
|
|
'DELETE /api/webhooks/:id': 'webhooks/delete',
|
|
|
|
'POST /api/access-tokens': 'access-tokens/create',
|
|
'POST /api/access-tokens/verify-totp': 'access-tokens/verify-totp',
|
|
'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',
|
|
'POST /api/users': 'users/create',
|
|
'GET /api/users/:id': 'users/show',
|
|
'PATCH /api/users/:id': 'users/update',
|
|
'PATCH /api/users/:id/email': 'users/update-email',
|
|
'PATCH /api/users/:id/password': 'users/update-password',
|
|
'PATCH /api/users/:id/username': 'users/update-username',
|
|
'POST /api/users/:id/avatar': 'users/update-avatar',
|
|
'POST /api/users/:id/api-key': 'users/create-api-key',
|
|
'POST /api/users/:id/totp/setup': 'users/setup-totp',
|
|
'POST /api/users/:id/totp/enable': 'users/enable-totp',
|
|
'DELETE /api/users/:id/totp': 'users/disable-totp',
|
|
'POST /api/users/:id/totp/recovery-codes': 'users/regenerate-totp-recovery-codes',
|
|
'GET /api/users/:id/trusted-devices': 'users/index-trusted-devices',
|
|
'DELETE /api/users/:id/trusted-devices/:deviceId': 'users/delete-trusted-device',
|
|
'DELETE /api/users/:id': 'users/delete',
|
|
|
|
'GET /api/projects': 'projects/index',
|
|
'POST /api/projects': 'projects/create',
|
|
'GET /api/projects/:id': 'projects/show',
|
|
'PATCH /api/projects/:id': 'projects/update',
|
|
'DELETE /api/projects/:id': 'projects/delete',
|
|
|
|
'POST /api/projects/:projectId/project-managers': 'project-managers/create',
|
|
'DELETE /api/project-managers/:id': 'project-managers/delete',
|
|
|
|
'POST /api/projects/:projectId/background-images': 'background-images/create',
|
|
'DELETE /api/background-images/:id': 'background-images/delete',
|
|
|
|
'POST /api/projects/:projectId/base-custom-field-groups': 'base-custom-field-groups/create',
|
|
'PATCH /api/base-custom-field-groups/:id': 'base-custom-field-groups/update',
|
|
'DELETE /api/base-custom-field-groups/:id': 'base-custom-field-groups/delete',
|
|
|
|
'POST /api/projects/:projectId/boards': 'boards/create',
|
|
'GET /api/boards/:id': 'boards/show',
|
|
'PATCH /api/boards/:id': 'boards/update',
|
|
'DELETE /api/boards/:id': 'boards/delete',
|
|
|
|
'POST /api/boards/:boardId/board-memberships': 'board-memberships/create',
|
|
'PATCH /api/board-memberships/:id': 'board-memberships/update',
|
|
'DELETE /api/board-memberships/:id': 'board-memberships/delete',
|
|
|
|
'POST /api/boards/:boardId/labels': 'labels/create',
|
|
'PATCH /api/labels/:id': 'labels/update',
|
|
'DELETE /api/labels/:id': 'labels/delete',
|
|
|
|
'POST /api/boards/:boardId/lists': 'lists/create',
|
|
'GET /api/lists/:id': 'lists/show',
|
|
'PATCH /api/lists/:id': 'lists/update',
|
|
'POST /api/lists/:id/sort': 'lists/sort',
|
|
'POST /api/lists/:id/move-cards': 'lists/move-cards',
|
|
'POST /api/lists/:id/clear': 'lists/clear',
|
|
'DELETE /api/lists/:id': 'lists/delete',
|
|
|
|
'GET /api/lists/:listId/cards': 'cards/index',
|
|
'POST /api/lists/:listId/cards': 'cards/create',
|
|
'GET /api/cards/:id': 'cards/show',
|
|
'PATCH /api/cards/:id': 'cards/update',
|
|
'POST /api/cards/:id/duplicate': 'cards/duplicate',
|
|
'POST /api/cards/:id/read-notifications': 'cards/read-notifications',
|
|
'DELETE /api/cards/:id': 'cards/delete',
|
|
'POST /api/cards/:cardId/card-memberships': 'card-memberships/create',
|
|
'DELETE /api/cards/:cardId/card-memberships/userId::userId': 'card-memberships/delete',
|
|
'POST /api/cards/:cardId/card-labels': 'card-labels/create',
|
|
'DELETE /api/cards/:cardId/card-labels/labelId::labelId': 'card-labels/delete',
|
|
|
|
'POST /api/cards/:cardId/task-lists': 'task-lists/create',
|
|
'GET /api/task-lists/:id': 'task-lists/show',
|
|
'PATCH /api/task-lists/:id': 'task-lists/update',
|
|
'DELETE /api/task-lists/:id': 'task-lists/delete',
|
|
|
|
'POST /api/task-lists/:taskListId/tasks': 'tasks/create',
|
|
'PATCH /api/tasks/:id': 'tasks/update',
|
|
'DELETE /api/tasks/:id': 'tasks/delete',
|
|
|
|
'POST /api/cards/:cardId/attachments': 'attachments/create',
|
|
'PATCH /api/attachments/:id': 'attachments/update',
|
|
'DELETE /api/attachments/:id': 'attachments/delete',
|
|
|
|
'POST /api/boards/:boardId/custom-field-groups': 'custom-field-groups/create-in-board',
|
|
'POST /api/cards/:cardId/custom-field-groups': 'custom-field-groups/create-in-card',
|
|
'GET /api/custom-field-groups/:id': 'custom-field-groups/show',
|
|
'PATCH /api/custom-field-groups/:id': 'custom-field-groups/update',
|
|
'DELETE /api/custom-field-groups/:id': 'custom-field-groups/delete',
|
|
|
|
'POST /api/base-custom-field-groups/:baseCustomFieldGroupId/custom-fields':
|
|
'custom-fields/create-in-base-custom-field-group',
|
|
'POST /api/custom-field-groups/:customFieldGroupId/custom-fields':
|
|
'custom-fields/create-in-custom-field-group',
|
|
'PATCH /api/custom-fields/:id': 'custom-fields/update',
|
|
'DELETE /api/custom-fields/:id': 'custom-fields/delete',
|
|
|
|
'PATCH /api/cards/:cardId/custom-field-values/customFieldGroupId::customFieldGroupId[:]customFieldId::customFieldId':
|
|
'custom-field-values/create-or-update',
|
|
'DELETE /api/cards/:cardId/custom-field-values/customFieldGroupId::customFieldGroupId[:]customFieldId::customFieldId':
|
|
'custom-field-values/delete',
|
|
|
|
'GET /api/cards/:cardId/comments': 'comments/index',
|
|
'POST /api/cards/:cardId/comments': 'comments/create',
|
|
'PATCH /api/comments/:id': 'comments/update',
|
|
'DELETE /api/comments/:id': 'comments/delete',
|
|
|
|
'GET /api/boards/:boardId/actions': 'actions/index-in-board',
|
|
'GET /api/cards/:cardId/actions': 'actions/index-in-card',
|
|
|
|
'GET /api/notifications': 'notifications/index',
|
|
'GET /api/notifications/:id': 'notifications/show',
|
|
'PATCH /api/notifications/:id': 'notifications/update',
|
|
'POST /api/notifications/read-all': 'notifications/read-all',
|
|
|
|
'POST /api/users/:userId/notification-services': 'notification-services/create-in-user',
|
|
'POST /api/boards/:boardId/notification-services': 'notification-services/create-in-board',
|
|
'PATCH /api/notification-services/:id': 'notification-services/update',
|
|
'POST /api/notification-services/:id/test': 'notification-services/test',
|
|
'DELETE /api/notification-services/:id': 'notification-services/delete',
|
|
|
|
'PATCH /api/_internal/config': '_internal/update-config',
|
|
|
|
'GET /swagger.json': 'swagger/show',
|
|
|
|
'GET /favicons/*': {
|
|
fn: protectedStaticDirServer('/favicons', () => sails.config.custom.faviconsPathSegment),
|
|
skipAssets: false,
|
|
},
|
|
|
|
'GET /user-avatars/*': {
|
|
fn: protectedStaticDirServer('/user-avatars', () => sails.config.custom.userAvatarsPathSegment),
|
|
skipAssets: false,
|
|
},
|
|
|
|
'GET /background-images/*': {
|
|
fn: protectedStaticDirServer(
|
|
'/background-images',
|
|
() => sails.config.custom.backgroundImagesPathSegment,
|
|
),
|
|
skipAssets: false,
|
|
},
|
|
|
|
'GET /attachments/:id/download/:filename': {
|
|
action: 'file-attachments/download',
|
|
skipAssets: false,
|
|
},
|
|
|
|
'GET r|^/attachments/(\\w+)/download/thumbnails/([\\w-]+).(\\w+)$|id,fileName,fileExtension': {
|
|
action: 'file-attachments/download-thumbnail',
|
|
skipAssets: false,
|
|
},
|
|
|
|
'GET /*': {
|
|
action: 'index',
|
|
skipAssets: true,
|
|
},
|
|
};
|