@coko/server
Version:
Reusable server for use by Coko's projects
274 lines • 10.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const bcrypt_1 = __importDefault(require("bcrypt"));
const errors_1 = require("../../errors");
const logger_1 = __importDefault(require("../../logger"));
const base_model_1 = __importDefault(require("../base.model"));
const useTransaction_1 = __importDefault(require("../useTransaction"));
const identity_model_1 = __importDefault(require("../identity/identity.model"));
const team_model_1 = __importDefault(require("../team/team.model"));
const teamMember_model_1 = __importDefault(require("../teamMember/teamMember.model"));
const types_1 = require("../_helpers/types");
const BCRYPT_COST = process.env.NODE_ENV === 'test' ? 1 : 12;
class User extends base_model_1.default {
agreedTc;
givenNames;
invitationToken;
invitationTokenTimestamp;
isActive;
password;
passwordHash;
passwordResetTimestamp;
passwordResetToken;
surname;
titlePost;
titlePre;
username;
defaultIdentity;
identities;
teams;
constructor() {
super();
this.type = 'user';
}
static get tableName() {
return 'users';
}
// Username & password are not required to allow for scenarios where a user
// has been created (eg. reviewer invitation), but they have not signed up yet.
static get schema() {
return {
type: 'object',
required: [],
properties: {
username: types_1.alphaNumericStringNotNullable,
passwordHash: types_1.stringNotEmpty,
passwordResetToken: types_1.stringNullable,
passwordResetTimestamp: types_1.dateNullable,
agreedTc: types_1.booleanDefaultFalse,
isActive: types_1.booleanDefaultFalse,
invitationToken: types_1.stringNotEmpty,
invitationTokenTimestamp: types_1.dateNullable,
password: types_1.password,
givenNames: types_1.string,
surname: types_1.string,
titlePre: types_1.string,
titlePost: types_1.string,
},
};
}
static get relationMappings() {
return {
identities: {
relation: base_model_1.default.HasManyRelation,
modelClass: identity_model_1.default,
join: {
from: 'users.id',
to: 'identities.userId',
},
},
defaultIdentity: {
relation: base_model_1.default.HasOneRelation,
modelClass: identity_model_1.default,
join: {
from: 'users.id',
to: 'identities.userId',
},
filter: (builder) => {
builder.where('isDefault', true);
},
},
teams: {
relation: base_model_1.default.ManyToManyRelation,
modelClass: team_model_1.default,
join: {
from: 'users.id',
through: {
modelClass: teamMember_model_1.default,
from: 'team_members.user_id',
to: 'team_members.team_id',
},
to: 'teams.id',
},
},
};
}
async patch(data, options = {}) {
const { password: providedPassword, passwordHash } = data;
if (!providedPassword && !passwordHash) {
return super.patch(data, options);
}
throw new Error('if you want to change user password you should use updatePassword method');
}
static async patchAndFetchById(id, data, options = {}) {
const { password: providedPassword, passwordHash } = data;
if (!providedPassword && !passwordHash) {
return super.patchAndFetchById(id, data, options);
}
throw new Error('if you want to change user password you should use updatePassword method');
}
async update(data, options = {}) {
const { password: providedPassword, passwordHash } = data;
if (!providedPassword && !passwordHash) {
return super.update(data, options);
}
throw new Error('if you want to change user password you should use updatePassword method');
}
static async updateAndFetchById(id, data, options = {}) {
const { password: providedPassword, passwordHash } = data;
if (!providedPassword && !passwordHash) {
return super.updateAndFetchById(id, data, options);
}
throw new Error('if you want to change user password you should use updatePassword method');
}
// From https://gitlab.coko.foundation/ncbi/ncbi/-/blob/develop/server/models/user/user.js#L61-101
static async hasGlobalRole(userId, role, options = {}) {
try {
return (0, useTransaction_1.default)(async (tr) => {
const isMember = await teamMember_model_1.default.query(tr)
.leftJoin('teams', 'team_members.teamId', 'teams.id')
.findOne({
global: true,
role,
userId,
});
return !!isMember;
}, { trx: options.trx, passedTrxOnly: true });
}
catch (e) {
logger_1.default.error('User model: hasGlobalRole failed', e);
throw e;
}
}
async hasGlobalRole(role, options = {}) {
return User.hasGlobalRole(this.id, role, options);
}
static async hasRoleOnObject(userId, role, objectId, options = {}) {
try {
return (0, useTransaction_1.default)(async (tr) => {
const isMember = await teamMember_model_1.default.query(tr)
.leftJoin('teams', 'team_members.teamId', 'teams.id')
.findOne({
role,
userId,
objectId,
});
return !!isMember;
}, { trx: options.trx, passedTrxOnly: true });
}
catch (e) {
logger_1.default.error('User model: hasRoleOnObject failed', e);
throw e;
}
}
async hasRoleOnObject(role, objectId, options = {}) {
return User.hasRoleOnObject(this.id, role, objectId, options);
}
static async getTeams(userId, options = {}) {
try {
const userWithTeams = await User.query(options.trx)
.findById(userId)
.withGraphFetched('teams')
.throwIfNotFound();
return userWithTeams.teams;
}
catch (e) {
logger_1.default.error(`User model: getTeams: ${e.message}`);
throw e;
}
}
async getTeams() {
return User.getTeams(this.id);
}
static async updatePassword(userId, currentPassword, newPassword, passwordResetToken, options = {}) {
try {
return (0, useTransaction_1.default)(async (tr) => {
const user = await User.findById(userId, {
trx: tr,
related: 'defaultIdentity',
});
if (currentPassword && !passwordResetToken) {
if (!(await user.isPasswordValid(currentPassword))) {
throw new errors_1.ValidationError('Update password: Current password is not valid');
}
}
else if (user.passwordResetToken !== passwordResetToken) {
throw new errors_1.ValidationError('Update password: passwordResetToken is not valid');
}
if (await user.isPasswordValid(newPassword)) {
throw new errors_1.ValidationError('Update password: New password must be different from current password');
}
return user.$query(tr).patchAndFetch({
password: newPassword,
});
}, { trx: options.trx, passedTrxOnly: true });
}
catch (e) {
logger_1.default.error('User model: updatePassword failed', e);
throw new Error('User model: Cannot update password');
}
}
async updatePassword(currentPassword, newPassword, passwordResetToken, options = {}) {
return User.updatePassword(this.id, currentPassword, newPassword, passwordResetToken, options);
}
$formatJson(json) {
json = super.$formatJson(json);
delete json.passwordHash;
return json;
}
static async hashPassword(plaintext) {
return await bcrypt_1.default.hash(plaintext, BCRYPT_COST);
}
async hashPassword(plaintext) {
this.passwordHash = await bcrypt_1.default.hash(plaintext, BCRYPT_COST);
delete this.password;
}
async $beforeInsert() {
if (this.password)
await this.hashPassword(this.password);
super.$beforeInsert();
}
async $beforeUpdate() {
if (this.password)
await this.hashPassword(this.password);
super.$beforeUpdate();
}
async isPasswordValid(plaintext) {
if (!plaintext || !this.passwordHash)
return false;
return await bcrypt_1.default.compare(plaintext, this.passwordHash);
}
static async activateUsers(ids, options = {}) {
try {
return (0, useTransaction_1.default)(async (tr) => {
return User.query(tr)
.patch({ isActive: true })
.whereIn('id', ids)
.returning('*');
}, { trx: options.trx, passedTrxOnly: true });
}
catch (e) {
logger_1.default.error('User model: activateUsers failed', e);
throw new Error('User model: Cannot update isActive');
}
}
static async deactivateUsers(ids, options = {}) {
try {
return (0, useTransaction_1.default)(async (tr) => {
return User.query(tr)
.patch({ isActive: false })
.whereIn('id', ids)
.returning('*');
}, { trx: options.trx, passedTrxOnly: true });
}
catch (e) {
logger_1.default.error('User model: deactivateUsers failed', e);
throw new Error('User model: Cannot update isActive');
}
}
}
exports.default = User;
//# sourceMappingURL=user.model.js.map