import { BASIC_TYPES } from '@og/binary-struct'; import { UUID } from '@og/uuid'; import { Model, BaseModel } from '../../kv/index.mjs'; 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 repersenting an user account. * @class * @extends {BaseModel} */ export class Account extends Model('Account', config.kv_instance) { /** @type {UUID} */ uid; /** @type {number} */ type; /** @type {string} */ handle; /** @type {string} */ name; /** @type {Buffer} */ passwd; /** @type {Buffer} */ totp_key = Buffer.alloc(20, 0); /** @type {Date} */ created_at = new Date(); /** @type {number} */ rating = 0; /** @type {Map} */ preference = new Map(); /** @type {UUID[]} */ replays = []; /** * Getter of the primary key of Account. * @returns {string|null} The primary key value or null if not applicable. */ get pk() { return this.uid?.toString(); } /** * Set a new handle for the account. * @param {string} handle - new handle. */ setHandle(handle) { if (!isValidHandle(handle)) { throw 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 InvalidArgumentError('passwd', 'password is too weak.'); } this.passwd = hashPassword(passwd); } /** * Clears the password of the account. */ clearPassword() { this.type = ACCOUNT_TYPE.temporary; this.passwd = hashPassword(''); } /** * 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; } /** * Asynchronously loads a Account instance based on its handle. * @param {string} handle - the handle of the account to load. * @returns {Promise} - The loaded account instance or null if not found. * @async */ static loadByHandle(handle) { return this.load(calcAccountUUID(handle).toString()); } /** * Creates an new account. * @param {{handle: string, name: string, type: number, plaintext_password: string | null}} * @returns {Account} Created account. */ static create({ handle, name, type, plaintext_password }) { if (handle == null) { throw InvalidArgumentError('handle', 'a handle is required'); } name = name?.toString() ?? handle; if (name.length < 3 || name.length > 32) { throw InvalidArgumentError('name', 'name is too long or too short'); } if (type != null && Object.values(ACCOUNT_TYPE).includes(type)) { throw InvalidArgumentError('type', 'unknown type'); } const res = new this(); res.setHandle(handle); res.name = name; res.type = type ?? ACCOUNT_TYPE.normal; if (plaintext_password != null) { res.setPassword(plaintext_password); } else { res.clearPassword(); } return res; } static typedef = [ { field: 'uid', type: BASIC_TYPES.uuid }, { field: 'type', type: BASIC_TYPES.u8 }, { field: 'handle', type: BASIC_TYPES.str }, { field: 'name', type: BASIC_TYPES.str }, { field: 'passwd', type: BASIC_TYPES.raw(32) }, { field: 'totp_key', type: BASIC_TYPES.raw(20) }, { field: 'created_at', type: BASIC_TYPES.DateTime }, { field: 'rating', type: BASIC_TYPES.i32 }, { field: 'preference', type: BASIC_TYPES.StringMap }, { field: 'replays', type: BASIC_TYPES.array(BASIC_TYPES.uuid) }, ]; };