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.
468 lines
13 KiB
JavaScript
Executable File
468 lines
13 KiB
JavaScript
Executable File
/*!
|
|
* Copyright (c) 2024 PLANKA Software GmbH
|
|
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
|
*/
|
|
|
|
/**
|
|
* User.js
|
|
*
|
|
* @description :: A model definition represents a database table/collection.
|
|
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
|
*/
|
|
|
|
/**
|
|
* @swagger
|
|
* components:
|
|
* schemas:
|
|
* User:
|
|
* type: object
|
|
* required:
|
|
* - id
|
|
* - role
|
|
* - name
|
|
* - username
|
|
* - avatar
|
|
* - phone
|
|
* - organization
|
|
* - isDeactivated
|
|
* - createdAt
|
|
* - updatedAt
|
|
* properties:
|
|
* id:
|
|
* type: string
|
|
* description: Unique identifier for the user
|
|
* example: "1357158568008091264"
|
|
* email:
|
|
* type: string
|
|
* format: email
|
|
* description: Email address for login and notifications (private field)
|
|
* example: john.doe@example.com
|
|
* role:
|
|
* type: string
|
|
* enum: [admin, projectOwner, boardUser]
|
|
* description: User role defining access permissions
|
|
* example: admin
|
|
* name:
|
|
* type: string
|
|
* description: Full display name of the user
|
|
* example: John Doe
|
|
* username:
|
|
* type: string
|
|
* minLength: 3
|
|
* maxLength: 64
|
|
* pattern: "^[a-z0-9._-]*$"
|
|
* nullable: true
|
|
* description: Unique username for user identification
|
|
* example: john_doe
|
|
* avatar:
|
|
* type: object
|
|
* required:
|
|
* - url
|
|
* - thumbnailUrls
|
|
* nullable: true
|
|
* description: Avatar information for the user with generated URLs
|
|
* properties:
|
|
* url:
|
|
* type: string
|
|
* format: uri
|
|
* description: URL to the full-size avatar image
|
|
* example: https://storage.example.com/user-avatars/1357158568008091264/original.jpg
|
|
* thumbnailUrls:
|
|
* type: object
|
|
* required:
|
|
* - cover180
|
|
* description: URLs for different thumbnail sizes
|
|
* properties:
|
|
* cover180:
|
|
* type: string
|
|
* format: uri
|
|
* description: URL for 180px thumbnail version
|
|
* example: https://storage.example.com/user-avatars/1357158568008091264/cover-180.jpg
|
|
* gravatarUrl:
|
|
* type: string
|
|
* format: uri
|
|
* description: Gravatar URL for the user (conditionally added if configured)
|
|
* example: https://www.gravatar.com/avatar/abc123
|
|
* phone:
|
|
* type: string
|
|
* nullable: true
|
|
* description: Contact phone number
|
|
* example: "+1234567890"
|
|
* organization:
|
|
* type: string
|
|
* nullable: true
|
|
* description: Organization or company name
|
|
* example: Acme Corporation
|
|
* language:
|
|
* type: string
|
|
* enum: [ar-YE, bg-BG, ca-ES, cs-CZ, da-DK, de-DE, el-GR, en-GB, en-US, es-ES, et-EE, fa-IR, fi-FI, fr-FR, hu-HU, id-ID, it-IT, ja-JP, ko-KR, nl-NL, pl-PL, pt-BR, pt-PT, ro-RO, ru-RU, sk-SK, sr-Cyrl-RS, sr-Latn-RS, sv-SE, tr-TR, uk-UA, uz-UZ, vi-VN, zh-CN, zh-TW]
|
|
* nullable: true
|
|
* description: Preferred language for user interface and notifications (personal field)
|
|
* example: en-US
|
|
* apiKeyPrefix:
|
|
* type: string
|
|
* nullable: true
|
|
* description: Prefix of the API key for display purposes (private field)
|
|
* example: D89VszVs
|
|
* subscribeToOwnCards:
|
|
* type: boolean
|
|
* default: false
|
|
* description: Whether the user subscribes to their own cards (personal field)
|
|
* example: false
|
|
* subscribeToCardWhenCommenting:
|
|
* type: boolean
|
|
* default: true
|
|
* description: Whether the user subscribes to cards when commenting (personal field)
|
|
* example: true
|
|
* turnOffRecentCardHighlighting:
|
|
* type: boolean
|
|
* default: false
|
|
* description: Whether recent card highlighting is disabled (personal field)
|
|
* example: false
|
|
* enableFavoritesByDefault:
|
|
* type: boolean
|
|
* default: true
|
|
* description: Whether favorites are enabled by default (personal field)
|
|
* example: true
|
|
* defaultEditorMode:
|
|
* type: string
|
|
* enum: [wysiwyg, markup]
|
|
* default: wysiwyg
|
|
* description: Default markdown editor mode (personal field)
|
|
* example: wysiwyg
|
|
* defaultHomeView:
|
|
* type: string
|
|
* enum: [gridProjects, groupedProjects]
|
|
* default: groupedProjects
|
|
* description: Default view mode for the home page (personal field)
|
|
* example: groupedProjects
|
|
* defaultProjectsOrder:
|
|
* type: string
|
|
* enum: [byDefault, alphabetically, byCreationTime]
|
|
* default: byDefault
|
|
* description: Default sort order for projects display (personal field)
|
|
* example: byDefault
|
|
* isTotpEnabled:
|
|
* type: boolean
|
|
* default: false
|
|
* description: Whether TOTP-based two-factor authentication is enabled (visible only to current user or admin)
|
|
* example: false
|
|
* totpEnabledAt:
|
|
* type: string
|
|
* format: date-time
|
|
* nullable: true
|
|
* description: When TOTP was enabled (visible only to current user or admin)
|
|
* example: 2026-05-14T10:00:00.000Z
|
|
* totpRecoveryCodesRemaining:
|
|
* type: integer
|
|
* description: Number of unused recovery codes (visible only to current user or admin)
|
|
* example: 10
|
|
* isDeactivated:
|
|
* type: boolean
|
|
* default: false
|
|
* description: Whether the user account is deactivated and cannot log in
|
|
* example: false
|
|
* isDefaultAdmin:
|
|
* type: boolean
|
|
* description: Whether the user is the default admin (visible only to current user or admin)
|
|
* example: false
|
|
* lockedFieldNames:
|
|
* type: array
|
|
* description: List of fields locked from editing (visible only to current user or admin)
|
|
* items:
|
|
* type: string
|
|
* example: [email, password, name]
|
|
* createdAt:
|
|
* type: string
|
|
* format: date-time
|
|
* nullable: true
|
|
* description: When the user was created
|
|
* example: 2024-01-01T00:00:00.000Z
|
|
* updatedAt:
|
|
* type: string
|
|
* format: date-time
|
|
* nullable: true
|
|
* description: When the user was last updated
|
|
* example: 2024-01-01T00:00:00.000Z
|
|
*/
|
|
|
|
const Roles = {
|
|
ADMIN: 'admin',
|
|
PROJECT_OWNER: 'projectOwner',
|
|
BOARD_USER: 'boardUser',
|
|
};
|
|
|
|
// TODO: should not be here
|
|
const EditorModes = {
|
|
WYSIWYG: 'wysiwyg',
|
|
MARKUP: 'markup',
|
|
};
|
|
|
|
const HomeViews = {
|
|
GRID_PROJECTS: 'gridProjects',
|
|
GROUPED_PROJECTS: 'groupedProjects',
|
|
};
|
|
|
|
const ProjectOrders = {
|
|
BY_DEFAULT: 'byDefault',
|
|
ALPHABETICALLY: 'alphabetically',
|
|
BY_CREATION_TIME: 'byCreationTime',
|
|
};
|
|
|
|
const LANGUAGES = [
|
|
'ar-YE',
|
|
'bg-BG',
|
|
'ca-ES',
|
|
'cs-CZ',
|
|
'da-DK',
|
|
'de-DE',
|
|
'el-GR',
|
|
'en-GB',
|
|
'en-US',
|
|
'es-ES',
|
|
'et-EE',
|
|
'fa-IR',
|
|
'fi-FI',
|
|
'fr-FR',
|
|
'hu-HU',
|
|
'id-ID',
|
|
'it-IT',
|
|
'ja-JP',
|
|
'ko-KR',
|
|
'nl-NL',
|
|
'pl-PL',
|
|
'pt-BR',
|
|
'pt-PT',
|
|
'ro-RO',
|
|
'ru-RU',
|
|
'sk-SK',
|
|
'sr-Cyrl-RS',
|
|
'sr-Latn-RS',
|
|
'sv-SE',
|
|
'tr-TR',
|
|
'uk-UA',
|
|
'uz-UZ',
|
|
'vi-VN',
|
|
'zh-CN',
|
|
'zh-TW',
|
|
];
|
|
|
|
// TODO: find better way to handle apiKeyHash and apiKeyCreatedAt
|
|
const PRIVATE_FIELD_NAMES = [
|
|
'email',
|
|
'apiKeyPrefix',
|
|
'apiKeyHash',
|
|
'apiKeyCreatedAt',
|
|
'totpSecret',
|
|
'totpRecoveryCodes',
|
|
];
|
|
|
|
const TWO_FACTOR_VISIBLE_FIELD_NAMES = [
|
|
'isTotpEnabled',
|
|
'totpEnabledAt',
|
|
'totpRecoveryCodesRemaining',
|
|
];
|
|
|
|
const PERSONAL_FIELD_NAMES = [
|
|
'language',
|
|
'subscribeToOwnCards',
|
|
'subscribeToCardWhenCommenting',
|
|
'turnOffRecentCardHighlighting',
|
|
'enableFavoritesByDefault',
|
|
'defaultEditorMode',
|
|
'defaultHomeView',
|
|
'defaultProjectsOrder',
|
|
];
|
|
|
|
const INTERNAL = {
|
|
id: '_internal',
|
|
role: Roles.ADMIN,
|
|
};
|
|
|
|
module.exports = {
|
|
Roles,
|
|
EditorModes,
|
|
HomeViews,
|
|
ProjectOrders,
|
|
LANGUAGES,
|
|
PRIVATE_FIELD_NAMES,
|
|
PERSONAL_FIELD_NAMES,
|
|
TWO_FACTOR_VISIBLE_FIELD_NAMES,
|
|
INTERNAL,
|
|
|
|
attributes: {
|
|
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
|
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
|
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
|
|
|
email: {
|
|
type: 'string',
|
|
isEmail: true,
|
|
required: true,
|
|
},
|
|
password: {
|
|
type: 'string',
|
|
isNotEmptyString: true,
|
|
allowNull: true,
|
|
},
|
|
role: {
|
|
type: 'string',
|
|
isIn: Object.values(Roles),
|
|
required: true,
|
|
},
|
|
name: {
|
|
type: 'string',
|
|
required: true,
|
|
},
|
|
username: {
|
|
type: 'string',
|
|
isNotEmptyString: true,
|
|
minLength: 3,
|
|
maxLength: 64,
|
|
regex: /^[a-z0-9._-]*$/,
|
|
allowNull: true,
|
|
},
|
|
avatar: {
|
|
type: 'json',
|
|
},
|
|
phone: {
|
|
type: 'string',
|
|
isNotEmptyString: true,
|
|
allowNull: true,
|
|
},
|
|
organization: {
|
|
type: 'string',
|
|
isNotEmptyString: true,
|
|
allowNull: true,
|
|
},
|
|
language: {
|
|
type: 'string',
|
|
isIn: LANGUAGES,
|
|
allowNull: true,
|
|
},
|
|
apiKeyPrefix: {
|
|
type: 'string',
|
|
isNotEmptyString: true,
|
|
allowNull: true,
|
|
columnName: 'api_key_prefix',
|
|
},
|
|
apiKeyHash: {
|
|
type: 'string',
|
|
isNotEmptyString: true,
|
|
allowNull: true,
|
|
columnName: 'api_key_hash',
|
|
},
|
|
subscribeToOwnCards: {
|
|
type: 'boolean',
|
|
defaultsTo: false,
|
|
columnName: 'subscribe_to_own_cards',
|
|
},
|
|
subscribeToCardWhenCommenting: {
|
|
type: 'boolean',
|
|
defaultsTo: true,
|
|
columnName: 'subscribe_to_card_when_commenting',
|
|
},
|
|
turnOffRecentCardHighlighting: {
|
|
type: 'boolean',
|
|
defaultsTo: false,
|
|
columnName: 'turn_off_recent_card_highlighting',
|
|
},
|
|
enableFavoritesByDefault: {
|
|
type: 'boolean',
|
|
defaultsTo: true,
|
|
columnName: 'enable_favorites_by_default',
|
|
},
|
|
defaultEditorMode: {
|
|
type: 'string',
|
|
isIn: Object.values(EditorModes),
|
|
defaultsTo: EditorModes.WYSIWYG,
|
|
columnName: 'default_editor_mode',
|
|
},
|
|
defaultHomeView: {
|
|
type: 'string',
|
|
isIn: Object.values(HomeViews),
|
|
defaultsTo: HomeViews.GROUPED_PROJECTS,
|
|
columnName: 'default_home_view',
|
|
},
|
|
defaultProjectsOrder: {
|
|
type: 'string',
|
|
isIn: Object.values(ProjectOrders),
|
|
defaultsTo: ProjectOrders.BY_DEFAULT,
|
|
columnName: 'default_projects_order',
|
|
},
|
|
termsSignature: {
|
|
type: 'string',
|
|
isNotEmptyString: true,
|
|
allowNull: true,
|
|
columnName: 'terms_signature',
|
|
},
|
|
isDeactivated: {
|
|
type: 'boolean',
|
|
defaultsTo: false,
|
|
columnName: 'is_deactivated',
|
|
},
|
|
passwordChangedAt: {
|
|
type: 'ref',
|
|
columnName: 'password_changed_at',
|
|
},
|
|
apiKeyCreatedAt: {
|
|
type: 'ref',
|
|
columnName: 'api_key_created_at',
|
|
},
|
|
termsAcceptedAt: {
|
|
type: 'ref',
|
|
columnName: 'terms_accepted_at',
|
|
},
|
|
totpSecret: {
|
|
type: 'string',
|
|
isNotEmptyString: true,
|
|
allowNull: true,
|
|
columnName: 'totp_secret',
|
|
},
|
|
isTotpEnabled: {
|
|
type: 'boolean',
|
|
defaultsTo: false,
|
|
columnName: 'is_totp_enabled',
|
|
},
|
|
totpEnabledAt: {
|
|
type: 'ref',
|
|
columnName: 'totp_enabled_at',
|
|
},
|
|
totpRecoveryCodes: {
|
|
type: 'json',
|
|
columnName: 'totp_recovery_codes',
|
|
},
|
|
|
|
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
|
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
|
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
|
|
|
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
|
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
|
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
|
|
|
managerProjects: {
|
|
collection: 'Project',
|
|
via: 'userId',
|
|
through: 'ProjectManager',
|
|
},
|
|
membershipBoards: {
|
|
collection: 'Board',
|
|
via: 'userId',
|
|
through: 'BoardMembership',
|
|
},
|
|
subscriptionCards: {
|
|
collection: 'Card',
|
|
via: 'userId',
|
|
through: 'CardSubscription',
|
|
},
|
|
membershipCards: {
|
|
collection: 'Card',
|
|
via: 'userId',
|
|
through: 'CardMembership',
|
|
},
|
|
},
|
|
|
|
tableName: 'user_account',
|
|
};
|