2024-03-05 23:18:37 +08:00

185 lines
4.4 KiB
JavaScript

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 representing 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();
}
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,
replays: this.replays.map(r => r.toString()),
};
}
/**
* 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);
}
/**
* 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<?Account>} - 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 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 (type != null && !Object.values(ACCOUNT_TYPE).includes(type)) {
throw new 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) },
];
};