...
Signed-off-by: szdytom <szdytom@qq.com>
This commit is contained in:
parent
936aa71b85
commit
f2434a5953
0
server/apps/README.md
Normal file
0
server/apps/README.md
Normal file
32
server/apps/auth/middlewares.mjs
Normal file
32
server/apps/auth/middlewares.mjs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { ACCOUNT_TYPE, Account } from './model.mjs';
|
||||||
|
|
||||||
|
/** @typedef {import('koa').Context} Context */
|
||||||
|
/** @typedef {import('koa').Next} Next */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware function to load user account information if user is logged in and account data is not yet loaded.
|
||||||
|
* @param {Context} ctx - Koa context object.
|
||||||
|
* @param {Next} next - Next middleware function.
|
||||||
|
*/
|
||||||
|
export async function loadAccount(ctx, next) {
|
||||||
|
if (ctx.state.user != null && ctx.state.account == null) {
|
||||||
|
ctx.state.account = await Account.load(ctx.user.uid);
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware function to check if the user has admin privileges.
|
||||||
|
* @param {Context} ctx - Koa context object.
|
||||||
|
* @param {Next} next - Next middleware function.
|
||||||
|
*/
|
||||||
|
export async function adminRequired(ctx, next) {
|
||||||
|
/** @type {?Account} */
|
||||||
|
const account = ctx.state.account;
|
||||||
|
|
||||||
|
if (account != null && (account.type == ACCOUNT_TYPE.admin || account.type == ACCOUNT_TYPE.superuser)) {
|
||||||
|
await next();
|
||||||
|
} else {
|
||||||
|
ctx.status = 403; // 403 (Forbidden)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { BASIC_TYPES } from '@og/binary-struct';
|
import { BASIC_TYPES } from '@og/binary-struct';
|
||||||
import { UUID } from '@og/uuid';
|
import { UUID } from '@og/uuid';
|
||||||
import { Model } from '../../kv/index.mjs';
|
import { Model, BaseModel } from '../../kv/index.mjs';
|
||||||
import * as config from '../../config.mjs';
|
import * as config from '../../config.mjs';
|
||||||
import { hashPassword, isWeakPassword } from './password.mjs';
|
import { hashPassword, isWeakPassword } from './password.mjs';
|
||||||
import { timingSafeEqual } from 'node:crypto';
|
import { timingSafeEqual } from 'node:crypto';
|
||||||
@ -25,6 +25,7 @@ function calcAccountUUID(handle) {
|
|||||||
/**
|
/**
|
||||||
* A Model repersenting an user account.
|
* A Model repersenting an user account.
|
||||||
* @class
|
* @class
|
||||||
|
* @extends {BaseModel}
|
||||||
*/
|
*/
|
||||||
export class Account extends Model('Account', config.kv_instance) {
|
export class Account extends Model('Account', config.kv_instance) {
|
||||||
/** @type {UUID} */
|
/** @type {UUID} */
|
||||||
@ -106,6 +107,24 @@ export class Account extends Model('Account', config.kv_instance) {
|
|||||||
return timingSafeEqual(ihash, this.passwd);
|
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.
|
* Creates an new account.
|
||||||
* @param {{handle: string, name: string, type: number, plaintext_password: string | null}}
|
* @param {{handle: string, name: string, type: number, plaintext_password: string | null}}
|
||||||
|
0
server/apps/auth/routes.mjs
Normal file
0
server/apps/auth/routes.mjs
Normal file
56
server/apps/auth/views.mjs
Normal file
56
server/apps/auth/views.mjs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { sign } from 'jsonwebtoken';
|
||||||
|
import { syllableRequired } from '../../middlewares/index.mjs.mjs';
|
||||||
|
import { ACCOUNT_TYPE, Account } from './model.mjs';
|
||||||
|
import * as config from '../../config.mjs';
|
||||||
|
|
||||||
|
/** @typedef {import('koa').Context} Context */
|
||||||
|
/** @typedef {import('koa').Next} Next */
|
||||||
|
|
||||||
|
export const login_view = [
|
||||||
|
syllableRequired('handle', 'string'),
|
||||||
|
syllableRequired('passwd', 'string'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Context} ctx
|
||||||
|
* @param {Next} next
|
||||||
|
*/
|
||||||
|
async function(ctx, next) {
|
||||||
|
/** @type {{handle: string, passwd: string}} */
|
||||||
|
const {handle, passwd} = ctx.request.body;
|
||||||
|
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.' };
|
||||||
|
} else if (!account.canLogin()) {
|
||||||
|
ctx.status = 400; // Bad Request.
|
||||||
|
ctx.body = { error: 'Your account is banned or restricted.' };
|
||||||
|
} else {
|
||||||
|
const token = sign({
|
||||||
|
uid: account.uid.toString(),
|
||||||
|
handle: account.handle,
|
||||||
|
auth_step: 'done',
|
||||||
|
}, config, { expiresIn: config.jwt_expire });
|
||||||
|
ctx.status = 200;
|
||||||
|
ctx.body = { uid: account.uid.toString(), token: token, auth_step: 'done' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const register_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;
|
||||||
|
Account.create({
|
||||||
|
handle, name,
|
||||||
|
type: ACCOUNT_TYPE.normal,
|
||||||
|
plaintext_password: passwd,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
];
|
@ -15,3 +15,6 @@ export const secret = Buffer.from('m5GDNW92K/c+YdDTlai3lG0wwL8h63LcLD9XZOSz8Lsqo
|
|||||||
// PRODUCTION: change the folder to a suitable place
|
// PRODUCTION: change the folder to a suitable place
|
||||||
// key-value storage instance to use
|
// key-value storage instance to use
|
||||||
export const kv_instance = new FileSystemKeyValueStorage('./kv-data');
|
export const kv_instance = new FileSystemKeyValueStorage('./kv-data');
|
||||||
|
|
||||||
|
// Time for a JWT to expire.
|
||||||
|
export const jwt_expire = '48h';
|
||||||
|
16
server/index.mjs
Normal file
16
server/index.mjs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import Koa from 'koa';
|
||||||
|
import * as config from './config.mjs';
|
||||||
|
import Router from 'koa-router';
|
||||||
|
import cors from '@koa/cors';
|
||||||
|
import jwt from 'koa-jwt';
|
||||||
|
import bodyParser from 'koa-bodyparser';
|
||||||
|
|
||||||
|
const app = new Koa();
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(bodyParser());
|
||||||
|
app.use(jwt({ secret: config.secret, passthrough: true }));
|
||||||
|
app.use(router.routes());
|
||||||
|
app.use(router.allowedMethods());
|
||||||
|
app.listen(config.port);
|
@ -4,22 +4,6 @@ import { pluralize } from 'inflection';
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract base class for all models.
|
|
||||||
* @abstract
|
|
||||||
* @class
|
|
||||||
*/
|
|
||||||
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 VirtualMethodNotImplementedError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @abstract
|
* @abstract
|
||||||
* @class
|
* @class
|
||||||
@ -182,6 +166,62 @@ export class FileSystemKeyValueStorage extends BaseKeyValueStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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.
|
* A mixin function that creates a model class with basic CRUD operations and storage methods.
|
||||||
*
|
*
|
||||||
|
15
server/middlewares/auth-required.mjs
Normal file
15
server/middlewares/auth-required.mjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/** @typedef {import('koa').Context} Context */
|
||||||
|
/** @typedef {import('koa').Next} Next */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware function to check if user is logged in.
|
||||||
|
* @param {Context} ctx - Koa context object.
|
||||||
|
* @param {Next} next - Next middleware function.
|
||||||
|
*/
|
||||||
|
export function loginRequired(ctx, next) {
|
||||||
|
if (ctx.state.user == null) {
|
||||||
|
ctx.status = 401; // 401 (Unauthorized)
|
||||||
|
} else {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
30
server/middlewares/data-required.mjs
Normal file
30
server/middlewares/data-required.mjs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/** @typedef {import('koa').Context} Context */
|
||||||
|
/** @typedef {import('koa').Next} Next */
|
||||||
|
/** @typedef {import('koa').Middleware} Middleware */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Koa middleware generator for checking the presence and type of a field in the request body.
|
||||||
|
*
|
||||||
|
* @param {string} field_name - The name of the field to be checked.
|
||||||
|
* @param {string} field_type - The expected type of the field.
|
||||||
|
* @returns {Middleware} Koa middleware function.
|
||||||
|
*/
|
||||||
|
export function syllableRequired(field_name, field_type) {
|
||||||
|
/**
|
||||||
|
* @param {Context} ctx - Koa context object.
|
||||||
|
* @param {Next} next - Next middleware function.
|
||||||
|
*/
|
||||||
|
return async (ctx, next) => {
|
||||||
|
const fieldValue = ctx.request.body[field_name];
|
||||||
|
|
||||||
|
if (fieldValue === undefined || typeof fieldValue !== field_type) {
|
||||||
|
ctx.status = 400; // 400 Bad Request
|
||||||
|
ctx.body = {
|
||||||
|
error: `Field '${field_name}' is required and must be of type '${field_type}'.`,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
2
server/middlewares/index.mjs
Normal file
2
server/middlewares/index.mjs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { loginRequired } from './auth-required.mjs';
|
||||||
|
export { syllableRequired } from './data-required.mjs';
|
@ -16,11 +16,15 @@
|
|||||||
"author": "szdytom <szdytom@qq.com>",
|
"author": "szdytom <szdytom@qq.com>",
|
||||||
"license": "GPL-2.0-only",
|
"license": "GPL-2.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@koa/cors": "^5.0.0",
|
||||||
"@og/binary-struct": "file:../shared/bstruct",
|
"@og/binary-struct": "file:../shared/bstruct",
|
||||||
"@og/error-utils": "file:../shared/error-utils",
|
"@og/error-utils": "file:../shared/error-utils",
|
||||||
"@og/uuid": "file:../shared/uuid",
|
"@og/uuid": "file:../shared/uuid",
|
||||||
"inflection": "^3.0.0",
|
"inflection": "^3.0.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"koa": "^2.15.0",
|
"koa": "^2.15.0",
|
||||||
|
"koa-bodyparser": "^4.4.1",
|
||||||
|
"koa-jwt": "^4.0.4",
|
||||||
"koa-router": "^12.0.1"
|
"koa-router": "^12.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -2,3 +2,4 @@ export { areArrayBuffersEqual } from './buffer.mjs';
|
|||||||
export { Queue, QueueEmptyError } from './queue.mjs';
|
export { Queue, QueueEmptyError } from './queue.mjs';
|
||||||
export { AsyncLock } from './async-lock.mjs';
|
export { AsyncLock } from './async-lock.mjs';
|
||||||
export { AsyncByteQueue } from './async-queue.mjs';
|
export { AsyncByteQueue } from './async-queue.mjs';
|
||||||
|
export { applyDecorators } from './decorator.mjs';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user