2024-03-06 20:41:04 +08:00

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,
});