[server] use database
This commit is contained in:
parent
ee4dbc0428
commit
501e09cc89
2
.gitignore
vendored
2
.gitignore
vendored
@ -24,4 +24,4 @@ node_modules
|
||||
|
||||
# Config
|
||||
config.mjs
|
||||
kv-data
|
||||
*.sqlite3
|
||||
|
@ -10,7 +10,7 @@ import { ACCOUNT_TYPE, Account } from './model.mjs';
|
||||
*/
|
||||
export async function loadAccount(ctx, next) {
|
||||
if (ctx.state.user != null && ctx.state.user.auth_step == 'done' && ctx.state.account == null) {
|
||||
ctx.state.account = await Account.load(ctx.state.user.uid);
|
||||
ctx.state.account = await Account.findByPk(ctx.state.user.uid);
|
||||
}
|
||||
await next();
|
||||
}
|
||||
@ -28,6 +28,6 @@ export async function adminRequired(ctx, next) {
|
||||
await next();
|
||||
} else {
|
||||
ctx.status = 403; // 403 (Forbidden)
|
||||
ctx.body = { error: 'You must be an administrator to access this resource.' };
|
||||
ctx.body = { error: 'You must be an administrator to access this resource.', errno: 'EPERM' };
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { BASIC_TYPES } from '@og/binary-struct';
|
||||
import { UUID } from '@og/uuid';
|
||||
import { Model, BaseModel } from '../../kv/index.mjs';
|
||||
import { DataTypes, Model } from 'sequelize';
|
||||
import * as config from '../../config.mjs';
|
||||
import { hashPassword, isWeakPassword } from './password.mjs';
|
||||
import { timingSafeEqual } from 'node:crypto';
|
||||
@ -25,45 +24,25 @@ function calcAccountUUID(handle) {
|
||||
/**
|
||||
* A Model representing an user account.
|
||||
* @class
|
||||
* @extends {BaseModel}
|
||||
* @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('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 = [];
|
||||
|
||||
export class Account extends Model {
|
||||
/**
|
||||
* Getter of the primary key of Account.
|
||||
* @returns {string|null} The primary key value or null if not applicable.
|
||||
* 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
|
||||
*/
|
||||
get pk() {
|
||||
return this.uid?.toString();
|
||||
static loadByHandle(handle) {
|
||||
return this.findByPk(calcAccountUUID(handle).toString());
|
||||
}
|
||||
|
||||
asInfo() {
|
||||
@ -74,7 +53,6 @@ export class Account extends Model('Account', config.kv_instance) {
|
||||
name: this.name,
|
||||
created_at: Math.floor(this.created_at.getTime() / 1000),
|
||||
rating: this.rating,
|
||||
replays: this.replays.map(r => r.toString()),
|
||||
};
|
||||
}
|
||||
|
||||
@ -101,14 +79,6 @@ export class Account extends Model('Account', config.kv_instance) {
|
||||
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.
|
||||
@ -127,22 +97,12 @@ export class Account extends Model('Account', config.kv_instance) {
|
||||
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 }) {
|
||||
static createInstance({ handle, name, type, plaintext_password }) {
|
||||
if (handle == null) {
|
||||
throw new InvalidArgumentError('handle', 'a handle is required');
|
||||
}
|
||||
@ -153,32 +113,70 @@ export class Account extends Model('Account', config.kv_instance) {
|
||||
throw new InvalidArgumentError('name', 'name is too long or too short');
|
||||
}
|
||||
|
||||
if (type != null && !Object.values(ACCOUNT_TYPE).includes(type)) {
|
||||
if (!Object.values(ACCOUNT_TYPE).includes(type)) {
|
||||
throw new InvalidArgumentError('type', 'unknown type');
|
||||
}
|
||||
|
||||
const res = new this();
|
||||
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;
|
||||
if (plaintext_password != null) {
|
||||
res.setPassword(plaintext_password);
|
||||
} else {
|
||||
res.clearPassword();
|
||||
}
|
||||
res.setPassword(plaintext_password);
|
||||
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) },
|
||||
];
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ import { loadAccount } from './middlewares.mjs';
|
||||
import { ACCOUNT_TYPE, Account } from './model.mjs';
|
||||
import { isValidUUID } from '@og/uuid';
|
||||
import * as config from '../../config.mjs';
|
||||
import { ValidationErrorItem } from 'sequelize';
|
||||
|
||||
/** @typedef {import('koa').Context} Context */
|
||||
/** @typedef {import('koa').Next} Next */
|
||||
@ -22,10 +23,10 @@ export const login_view = [
|
||||
const account = await Account.loadByHandle(handle);
|
||||
if (account == null || !account.checkPassword(passwd)) {
|
||||
ctx.status = 400; // Bad Request.
|
||||
ctx.body = { error: 'Authentication failed: handle or password is incorrect.' };
|
||||
ctx.body = { error: 'Authentication failed: handle or password is incorrect.', errno: 'EPASSWD' };
|
||||
} else if (!account.canLogin()) {
|
||||
ctx.status = 400; // Bad Request.
|
||||
ctx.body = { error: 'Your account is banned or restricted.' };
|
||||
ctx.body = { error: 'Your account is banned or restricted.', errno: 'EBANNED' };
|
||||
} else {
|
||||
const token = jwt.sign({
|
||||
uid: account.uid.toString(),
|
||||
@ -75,27 +76,26 @@ export const refresh_token_view = [
|
||||
|
||||
export const signup_view = [
|
||||
syllableRequired('handle', 'string'),
|
||||
syllableRequired('name', 'string'),
|
||||
syllableRequired('passwd', 'string'),
|
||||
/**
|
||||
* @param {Context} ctx
|
||||
* @param {Next} next
|
||||
*/
|
||||
async function(ctx, next) {
|
||||
/** @type {{handle: string, name: string, passwd: string}} */
|
||||
const {handle, name, passwd} = ctx.request.body;
|
||||
let account = Account.create({
|
||||
handle, name,
|
||||
/** @type {{handle: string, passwd: string}} */
|
||||
const {handle, passwd} = ctx.request.body;
|
||||
|
||||
if (Account.loadByHandle(handle) != null) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: 'handle is taken', errno: 'EUNIQUE' };
|
||||
}
|
||||
|
||||
let account = Account.createInstance({
|
||||
handle,
|
||||
type: ACCOUNT_TYPE.normal,
|
||||
plaintext_password: passwd,
|
||||
});
|
||||
|
||||
if (!await account.canCreate()) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: 'Handle is taken or cannot be used.' };
|
||||
return;
|
||||
}
|
||||
|
||||
await account.save();
|
||||
ctx.status = 204;
|
||||
}
|
||||
@ -109,10 +109,10 @@ export const account_info_view = [
|
||||
*/
|
||||
async function(ctx, next) {
|
||||
/** @type {Account?} */
|
||||
let account = await Account.load(ctx.params.uid);
|
||||
let account = await Account.findByPk(ctx.params.uid);
|
||||
if (account == null) {
|
||||
ctx.status = 404; // 404 Not Found
|
||||
ctx.body = { error: 'requested user is not found' };
|
||||
ctx.body = { error: 'requested user is not found', errno: 'E404' };
|
||||
} else {
|
||||
ctx.status = 200;
|
||||
ctx.body = account.asInfo();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FileSystemKeyValueStorage } from './kv/index.mjs';
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
// PRODUCTION: set this to false in production!
|
||||
export const debug = true;
|
||||
@ -14,7 +14,7 @@ export const secret = Buffer.from('m5GDNW92K/c+YdDTlai3lG0wwL8h63LcLD9XZOSz8Lsqo
|
||||
|
||||
// PRODUCTION: change the folder to a suitable place
|
||||
// key-value storage instance to use
|
||||
export const kv_instance = new FileSystemKeyValueStorage('./kv-data');
|
||||
export const db_instance = new Sequelize('sqlite:server-db.sqlite3', { logging: false });
|
||||
|
||||
// Time for a JWT to expire.
|
||||
export const jwt_expire = '48h';
|
||||
|
@ -1,318 +0,0 @@
|
||||
import { deserializeFromBinary, serializeToBinary } from '@og/binary-struct';
|
||||
import { VirtualMethodNotImplementedError } from '@og/error-utils';
|
||||
import { pluralize } from 'inflection';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @class
|
||||
* @classdesc Represents a key-value storage interface.
|
||||
*/
|
||||
export class BaseKeyValueStorage {
|
||||
/**
|
||||
* Sets a value for the given class and primary key.
|
||||
* @param {string} cls - The class identifier.
|
||||
* @param {string} pk - The primary key.
|
||||
* @param {ArrayBuffer} val - The value to set.
|
||||
* @abstruct
|
||||
* @async
|
||||
*/
|
||||
async set(cls, pk, val) {
|
||||
throw new VirtualMethodNotImplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value associated with the given class and primary key.
|
||||
* @param {string} cls - The class identifier.
|
||||
* @param {string} pk - The primary key.
|
||||
* @abstruct
|
||||
* @async
|
||||
* @returns {Promise<ArrayBuffer|null>} - The value associated with the class and primary key, or null if not found.
|
||||
*/
|
||||
async get(cls, pk) {
|
||||
throw new VirtualMethodNotImplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a value exists for the given class and primary key.
|
||||
* @param {string} cls - The class identifier.
|
||||
* @param {string} pk - The primary key.
|
||||
* @abstruct
|
||||
* @async
|
||||
* @returns {Promise<boolean>} - True if the value exists, otherwise false.
|
||||
*/
|
||||
async has(cls, pk) {
|
||||
throw new VirtualMethodNotImplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the value associated with the given class and primary key.
|
||||
* @param {string} cls - The class identifier.
|
||||
* @param {string} pk - The primary key.
|
||||
* @abstruct
|
||||
* @async
|
||||
*/
|
||||
async destroy(cls, pk) {
|
||||
throw new VirtualMethodNotImplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes or updates the class.
|
||||
* @param {string} cls - The class identifier.
|
||||
* @abstruct
|
||||
* @async
|
||||
*/
|
||||
async touchClass(cls) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a key-value storage implementation using the file system.
|
||||
* @class
|
||||
* @extends {BaseKeyValueStorage}
|
||||
*/
|
||||
export class FileSystemKeyValueStorage extends BaseKeyValueStorage {
|
||||
/**
|
||||
* Creates an instance of FileSystemKeyValueStorage.
|
||||
* @param {string} root - The root directory where key-value data will be stored.
|
||||
*/
|
||||
constructor(root) {
|
||||
super();
|
||||
/** @private @type {string} */
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value for the given class and primary key.
|
||||
* @param {string} cls - The class identifier.
|
||||
* @param {string} pk - The primary key.
|
||||
* @param {ArrayBuffer} val - The value to set.
|
||||
* @abstruct
|
||||
*/
|
||||
async set(cls, pk, val) {
|
||||
const filePath = this._getFilePath(cls, pk);
|
||||
await fs.writeFile(filePath, Buffer.from(val));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value associated with the given class and primary key.
|
||||
* @param {string} cls - The class identifier.
|
||||
* @param {string} pk - The primary key.
|
||||
* @abstruct
|
||||
* @returns {Promise<ArrayBuffer|null>} - The value associated with the class and primary key, or null if not found.
|
||||
*/
|
||||
async get(cls, pk) {
|
||||
const filepath = this._getFilePath(cls, pk);
|
||||
try {
|
||||
const data = await fs.readFile(filepath);
|
||||
return data.buffer;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') { // File not found
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a value exists for the given class and primary key.
|
||||
* @param {string} cls - The class identifier.
|
||||
* @param {string} pk - The primary key.
|
||||
* @abstruct
|
||||
* @returns {Promise<boolean>} - True if the value exists, otherwise false.
|
||||
*/
|
||||
async has(cls, pk) {
|
||||
const filepath = this._getFilePath(cls, pk);
|
||||
try {
|
||||
await fs.access(filepath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the value associated with the given class and primary key.
|
||||
* @param {string} cls - The class identifier.
|
||||
* @param {string} pk - The primary key.
|
||||
* @abstruct
|
||||
*/
|
||||
async destroy(cls, pk) {
|
||||
const filepath = this._getFilePath(cls, pk);
|
||||
await fs.unlink(filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes or updates the class by creating a folder for it if it doesn't exist.
|
||||
* @param {string} cls - The class identifier.
|
||||
* @abstruct
|
||||
*/
|
||||
async touchClass(cls) {
|
||||
const cls_dir = path.join(this.root, cls);
|
||||
await fs.mkdir(cls_dir, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the file path for a given class and primary key.
|
||||
* @private
|
||||
* @param {string} cls - The class identifier.
|
||||
* @param {string} pk - The primary key.
|
||||
* @returns {string} - The file path.
|
||||
*/
|
||||
_getFilePath(cls, pk) {
|
||||
const cls_dir = path.join(this.root, cls);
|
||||
return path.join(cls_dir, pk);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Abstract base class for all models.
|
||||
* @abstract
|
||||
* @class
|
||||
*/
|
||||
export class BaseModel {
|
||||
/**
|
||||
* Gets the primary key of the model.
|
||||
* @abstract
|
||||
* @returns {string|null} The primary key value or null if not applicable.
|
||||
*/
|
||||
get pk() {
|
||||
throw new VirtualMethodNotImplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously saves the current model instance to the key-value storage.
|
||||
* @throws {Error} - Throws an error if the primary key is not set.
|
||||
* @returns {Promise<void>}
|
||||
* @abstract
|
||||
*/
|
||||
async save() {
|
||||
throw new VirtualMethodNotImplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously destroys the current model instance in the key-value storage.
|
||||
* @throws {Error} - Throws an error if the primary key is not set.
|
||||
* @returns {Promise<void>}
|
||||
* @abstract
|
||||
*/
|
||||
async destroy() {
|
||||
throw new VirtualMethodNotImplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously checks if the current model instance can be created in the key-value storage.
|
||||
* @returns {Promise<boolean>} - True if the model can be created, false otherwise.
|
||||
* @abstract
|
||||
*/
|
||||
async canCreate() {
|
||||
throw new VirtualMethodNotImplementedError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously loads a model instance from the key-value storage based on its primary key.
|
||||
* @param {string} pk - The primary key of the model to load.
|
||||
* @returns {Promise<*>} The loaded model instance or null if not found.
|
||||
* @abstract
|
||||
*/
|
||||
static async load(pk) {
|
||||
throw new VirtualMethodNotImplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A mixin function that creates a model class with basic CRUD operations and storage methods.
|
||||
*
|
||||
* @param {string} name - The name of the model.
|
||||
* @param {BaseKeyValueStorage} kv - The key-value storage for data persistence.
|
||||
* @returns {Function} - The generated model class.
|
||||
*/
|
||||
export function Model(name, kv) {
|
||||
const cls = pluralize(name);
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @extends BaseModel
|
||||
*/
|
||||
return class extends BaseModel {
|
||||
/**
|
||||
* Asynchronously saves the current model instance to the key-value storage.
|
||||
* @throws {Error} - Throws an error if the primary key is not set.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async save() {
|
||||
const pk = this.pk;
|
||||
if (pk == null) {
|
||||
throw new PrimaryKeyNotSetError();
|
||||
}
|
||||
|
||||
const buf = serializeToBinary(this, this.constructor);
|
||||
await kv.set(cls, pk, buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously destroys the current model instance in the key-value storage.
|
||||
* @throws {Error} - Throws an error if the primary key is not set.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async destroy() {
|
||||
const pk = this.pk;
|
||||
if (pk == null) {
|
||||
throw new PrimaryKeyNotSetError();
|
||||
}
|
||||
|
||||
await kv.destroy(cls, pk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously checks if the current model instance can be created in the key-value storage.
|
||||
* @returns {Promise<boolean>} - True if the model can be created, false otherwise.
|
||||
*/
|
||||
async canCreate() {
|
||||
const pk = this.pk;
|
||||
if (pk == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(await kv.has(cls, pk));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously loads a model instance from the key-value storage based on its primary key.
|
||||
* @param {string} pk - The primary key of the model to load.
|
||||
* @returns {Promise<*>} - The loaded model instance or null if not found.
|
||||
*/
|
||||
static async load(pk) {
|
||||
const buf = await kv.get(cls, pk);
|
||||
if (buf == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return deserializeFromBinary(new DataView(buf), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the key-value storage for the current class.
|
||||
*/
|
||||
static {
|
||||
kv.touchClass(cls);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an error that is thrown when the primary key of an object is not set.
|
||||
* @class
|
||||
* @extends {Error}
|
||||
*/
|
||||
class PrimaryKeyNotSetError extends Error {
|
||||
/**
|
||||
* Constructs a new PrimaryKeyNotSetError instance with a default error message.
|
||||
* @constructor
|
||||
*/
|
||||
constructor() {
|
||||
super('pk() returns null');
|
||||
}
|
||||
};
|
@ -9,7 +9,7 @@
|
||||
export function loginRequired(ctx, next) {
|
||||
if (ctx.state.user == null || ctx.state.user.auth_step != 'done') {
|
||||
ctx.status = 401; // 401 (Unauthorized)
|
||||
ctx.body = { error: 'You are not identified or your authentication process is incomplete.' };
|
||||
ctx.body = { error: 'You are not identified or your authentication process is incomplete.', errno: 'EAUTH' };
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export async function errorAsResponse(ctx, next) {
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidArgumentError) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: err.toString() };
|
||||
ctx.body = { error: err.toString(), errno: 'EIA' };
|
||||
} else {
|
||||
ctx.status = 500;
|
||||
throw err;
|
||||
|
@ -25,9 +25,13 @@
|
||||
"koa": "^2.15.0",
|
||||
"koa-bodyparser": "^4.4.1",
|
||||
"koa-jwt": "^4.0.4",
|
||||
"koa-router": "^12.0.1"
|
||||
"koa-router": "^12.0.1",
|
||||
"mariadb": "^3.2.3",
|
||||
"sequelize": "^6.37.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/koa__router": "^12.0.4"
|
||||
"@types/koa__router": "^12.0.4",
|
||||
"sequelize-cli": "^6.6.2"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user