183 lines
4.2 KiB
JavaScript
183 lines
4.2 KiB
JavaScript
import { UUID } from '@og/uuid';
|
|
import { DataTypes, Model } from 'sequelize';
|
|
import * as config from '../../config.mjs';
|
|
import { hashPassword, isWeakPassword } from './password.mjs';
|
|
import { timingSafeEqual } from 'node:crypto';
|
|
import { InvalidArgumentError } from '@og/error-utils';
|
|
import { isValidHandle } from './handle.mjs';
|
|
|
|
export const ACCOUNT_TYPE = {
|
|
banned: 0x00,
|
|
normal: 0x01,
|
|
admin: 0x02,
|
|
robot: 0x03,
|
|
temporary: 0x04,
|
|
superuser: 0xff,
|
|
};
|
|
|
|
export const ACCOUNT_UUID_NS = 'aff7791d-4b71-4187-9788-13fa0c7fb51e';
|
|
|
|
function calcAccountUUID(handle) {
|
|
return UUID.v5('account:' + handle, ACCOUNT_UUID_NS);
|
|
}
|
|
|
|
/**
|
|
* A Model representing an user account.
|
|
* @class
|
|
* @property {UUID} uid
|
|
* @property {number} type
|
|
* @property {string} handle
|
|
* @property {string} name
|
|
* @property {Buffer} passwd
|
|
* @property {Buffer?} totp_key
|
|
* @property {Date} created_at
|
|
* @property {number} rating
|
|
* @property {Map} preference
|
|
*/
|
|
export class Account extends Model {
|
|
/**
|
|
* Asynchronously loads a Account instance based on its handle.
|
|
* @param {string} handle - the handle of the account to load.
|
|
* @returns {Promise<?Account>} - The loaded account instance or null if not found.
|
|
* @async
|
|
*/
|
|
static loadByHandle(handle) {
|
|
return this.findByPk(calcAccountUUID(handle).toString());
|
|
}
|
|
|
|
asInfo() {
|
|
return {
|
|
uid: this.uid.toString(),
|
|
type: this.type,
|
|
handle: this.handle,
|
|
name: this.name,
|
|
created_at: Math.floor(this.created_at.getTime() / 1000),
|
|
rating: this.rating,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Set a new handle for the account.
|
|
* @param {string} handle - new handle.
|
|
*/
|
|
setHandle(handle) {
|
|
if (!isValidHandle(handle)) {
|
|
throw new InvalidArgumentError('handle', 'handle is unacceptable');
|
|
}
|
|
this.handle = handle;
|
|
this.uid = calcAccountUUID(handle);
|
|
}
|
|
|
|
/**
|
|
* Set a new password for the account.
|
|
* @param {string} passwd - new password.
|
|
*/
|
|
setPassword(passwd) {
|
|
if (isWeakPassword(passwd)) {
|
|
throw new InvalidArgumentError('passwd', 'password is too weak');
|
|
}
|
|
this.passwd = hashPassword(passwd);
|
|
}
|
|
|
|
/**
|
|
* Checks if the given password is correct.
|
|
* @param {string} passwd - password.
|
|
* @returns {boolean} - true if the given value is the password of the account.
|
|
*/
|
|
checkPassword(input) {
|
|
const ihash = hashPassword(input);
|
|
return timingSafeEqual(ihash, this.passwd);
|
|
}
|
|
|
|
/**
|
|
* Checks if the account can be logged in.
|
|
* @returns {boolean} - true if the account can be logged in.
|
|
*/
|
|
canLogin() {
|
|
return this.type != ACCOUNT_TYPE.banned;
|
|
}
|
|
|
|
/**
|
|
* Creates an new account.
|
|
* @param {{handle: string, name: string, type: number, plaintext_password: string | null}}
|
|
* @returns {Account} Created account.
|
|
*/
|
|
static createInstance({ handle, name, type, plaintext_password }) {
|
|
if (handle == null) {
|
|
throw new InvalidArgumentError('handle', 'a handle is required');
|
|
}
|
|
|
|
name = name?.toString() ?? handle;
|
|
|
|
if (name.length < 3 || name.length > 32) {
|
|
throw new InvalidArgumentError('name', 'name is too long or too short');
|
|
}
|
|
|
|
if (!Object.values(ACCOUNT_TYPE).includes(type)) {
|
|
throw new InvalidArgumentError('type', 'unknown type');
|
|
}
|
|
|
|
if (typeof plaintext_password != 'string') {
|
|
throw new InvalidArgumentError('passwd', 'incorrect type');
|
|
}
|
|
|
|
const res = this.build();
|
|
res.setHandle(handle);
|
|
res.name = name;
|
|
res.type = type ?? ACCOUNT_TYPE.normal;
|
|
res.setPassword(plaintext_password);
|
|
return res;
|
|
}
|
|
};
|
|
|
|
Account.init({
|
|
uid: {
|
|
type: DataTypes.UUID,
|
|
allowNull: false,
|
|
primaryKey: true,
|
|
unique: true,
|
|
get() {
|
|
return new UUID(this.getDataValue('uid'));
|
|
},
|
|
set(uuid) {
|
|
this.setDataValue('uid', uuid.toString());
|
|
}
|
|
},
|
|
type: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false,
|
|
defaultValue: ACCOUNT_TYPE.banned,
|
|
validate: {
|
|
isIn: [Object.values(ACCOUNT_TYPE)],
|
|
},
|
|
},
|
|
handle: {
|
|
type: DataTypes.STRING(32),
|
|
allowNull: false,
|
|
unique: true,
|
|
},
|
|
name: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false,
|
|
},
|
|
passwd: {
|
|
type: DataTypes.STRING(32, true),
|
|
allowNull: false,
|
|
},
|
|
totp_key: {
|
|
type: DataTypes.STRING(20, true),
|
|
allowNull: true,
|
|
},
|
|
rating: {
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false,
|
|
defaultValue: 1500,
|
|
}
|
|
}, {
|
|
sequelize: config.db_instance,
|
|
modelName: 'Account',
|
|
timestamps: true,
|
|
createdAt: 'created_at',
|
|
updatedAt: false,
|
|
});
|