db 마이그레이션 기능 #17
					 8 changed files with 114 additions and 26 deletions
				
			
		| 
						 | 
				
			
			@ -53,6 +53,12 @@ export const UserSchema = z.object({
 | 
			
		|||
 | 
			
		||||
export type User = z.infer<typeof UserSchema>;
 | 
			
		||||
 | 
			
		||||
export const UserSettingSchema = z.object({
 | 
			
		||||
	fileDeepLinkRegex: z.string().optional(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export type UserSetting = z.infer<typeof UserSettingSchema>;
 | 
			
		||||
 | 
			
		||||
export const SchemaMigrationSchema = z.object({
 | 
			
		||||
	version: z.string().nullable(),
 | 
			
		||||
	dirty: z.boolean(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,6 +45,11 @@ export interface Users {
 | 
			
		|||
  username: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserSettings {
 | 
			
		||||
  username: string;
 | 
			
		||||
  settings: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DB {
 | 
			
		||||
  doc_tag_relation: DocTagRelation;
 | 
			
		||||
  document: Document;
 | 
			
		||||
| 
						 | 
				
			
			@ -52,4 +57,5 @@ export interface DB {
 | 
			
		|||
  schema_migration: SchemaMigration;
 | 
			
		||||
  tags: Tags;
 | 
			
		||||
  users: Users;
 | 
			
		||||
  user_settings: UserSettings;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ export async function up(db: Kysely<any>) {
 | 
			
		|||
    await db.schema
 | 
			
		||||
        .createTable("user_settings")
 | 
			
		||||
        .addColumn("username", "varchar(256)", col => col.notNull().primaryKey())
 | 
			
		||||
        .addColumn("settings", "jsonb", col => col.notNull())
 | 
			
		||||
        .addColumn("settings", "json", col => col.notNull())
 | 
			
		||||
        .addForeignKeyConstraint("user_settings_username_fk", ["username"], "users", ["username"])
 | 
			
		||||
        .execute();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { getKysely } from "./kysely.ts";
 | 
			
		||||
import { type IUser, Password, type UserAccessor, type UserCreateInput } from "../model/user.ts";
 | 
			
		||||
import { type IUser, IUserSettings, Password, type UserAccessor, type UserCreateInput } from "../model/user.ts";
 | 
			
		||||
 | 
			
		||||
class SqliteUser implements IUser {
 | 
			
		||||
	readonly username: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -41,6 +41,24 @@ class SqliteUser implements IUser {
 | 
			
		|||
			.executeTakeFirst();
 | 
			
		||||
		return (result.numDeletedRows ?? 0n) > 0;
 | 
			
		||||
	}
 | 
			
		||||
	async get_settings(): Promise<IUserSettings | undefined> {
 | 
			
		||||
		const settings = await this.kysely
 | 
			
		||||
			.selectFrom("user_settings")
 | 
			
		||||
			.select("settings")
 | 
			
		||||
			.where("username", "=", this.username)
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		if (!settings) return undefined;
 | 
			
		||||
		return settings.settings ? JSON.parse(settings.settings) as IUserSettings : undefined;
 | 
			
		||||
	}
 | 
			
		||||
	async set_settings(settings: IUserSettings) {
 | 
			
		||||
		const settingsJson = JSON.stringify(settings);
 | 
			
		||||
		const result = await this.kysely
 | 
			
		||||
			.insertInto("user_settings")
 | 
			
		||||
			.values({ username: this.username, settings: settingsJson })
 | 
			
		||||
			.onConflict((oc) => oc.doUpdateSet({ settings: settingsJson }))
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		return (result.numInsertedOrUpdatedRows ?? 0n) > 0;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const createSqliteUserController = (kysely = getKysely()): UserAccessor => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,7 +76,7 @@ function setToken(ctx: Koa.Context, token_name: string, token_payload: string |
 | 
			
		|||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const createLoginMiddleware = (userController: UserAccessor) => async (ctx: Koa.Context, _next: Koa.Next) => {
 | 
			
		||||
export const createLoginHandler = (userController: UserAccessor) => async (ctx: Koa.Context, _next: Koa.Next) => {
 | 
			
		||||
	const setting = get_setting();
 | 
			
		||||
	const secretKey = setting.jwt_secretkey;
 | 
			
		||||
	const body = ctx.request.body;
 | 
			
		||||
| 
						 | 
				
			
			@ -115,7 +115,7 @@ export const createLoginMiddleware = (userController: UserAccessor) => async (ct
 | 
			
		|||
	return;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
 | 
			
		||||
export const LogoutHandler = (ctx: Koa.Context, _next: Koa.Next) => {
 | 
			
		||||
	const setting = get_setting();
 | 
			
		||||
	ctx.cookies.set(accessTokenName, null);
 | 
			
		||||
	ctx.cookies.set(refreshTokenName, null);
 | 
			
		||||
| 
						 | 
				
			
			@ -127,9 +127,9 @@ export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
 | 
			
		|||
	return;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createUserMiddleWare =
 | 
			
		||||
export const createUserHandler =
 | 
			
		||||
	(userController: UserAccessor) => async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
 | 
			
		||||
		const refreshToken = refreshTokenHandler(userController);
 | 
			
		||||
		const refreshToken = makeRefreshToken(userController);
 | 
			
		||||
		const setting = get_setting();
 | 
			
		||||
		const setGuest = async () => {
 | 
			
		||||
			setToken(ctx, accessTokenName, null, 0);
 | 
			
		||||
| 
						 | 
				
			
			@ -140,7 +140,7 @@ export const createUserMiddleWare =
 | 
			
		|||
		return await refreshToken(ctx, setGuest, next);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => {
 | 
			
		||||
const makeRefreshToken = (cntr: UserAccessor) => async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => {
 | 
			
		||||
	const accessPayload = ctx.cookies.get(accessTokenName);
 | 
			
		||||
	const setting = get_setting();
 | 
			
		||||
	const secretKey = setting.jwt_secretkey;
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +200,7 @@ const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fai
 | 
			
		|||
	}
 | 
			
		||||
};
 | 
			
		||||
export const createRefreshTokenMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => {
 | 
			
		||||
	const handler = refreshTokenHandler(cntr);
 | 
			
		||||
	const handler = makeRefreshToken(cntr);
 | 
			
		||||
	await handler(ctx, fail, success);
 | 
			
		||||
	async function fail() {
 | 
			
		||||
		const user = ctx.state.user as PayloadInfo;
 | 
			
		||||
| 
						 | 
				
			
			@ -242,12 +242,54 @@ export const resetPasswordMiddleware = (cntr: UserAccessor) => async (ctx: Koa.C
 | 
			
		|||
	ctx.type = "json";
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function getUserSettingHandler(userController: UserAccessor) {
 | 
			
		||||
	return async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
 | 
			
		||||
		const username = ctx.state.user.username;
 | 
			
		||||
		if (!username) {
 | 
			
		||||
			return sendError(403, "not authorized");
 | 
			
		||||
		}
 | 
			
		||||
		const user = await userController.findUser(username);
 | 
			
		||||
		if (user === undefined) {
 | 
			
		||||
			return sendError(403, "not authorized");
 | 
			
		||||
		}
 | 
			
		||||
		const settings = await user.get_settings();
 | 
			
		||||
		if (settings === undefined) {
 | 
			
		||||
			ctx.body = {};
 | 
			
		||||
			ctx.type = "json";
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		ctx.body = settings;
 | 
			
		||||
		ctx.type = "json";
 | 
			
		||||
		await next();
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
export function setUserSettingHandler(userController: UserAccessor) {
 | 
			
		||||
	return async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
 | 
			
		||||
		const username = ctx.state.user.username;
 | 
			
		||||
		if (!username) {
 | 
			
		||||
			return sendError(403, "not authorized");
 | 
			
		||||
		}
 | 
			
		||||
		const user = await userController.findUser(username);
 | 
			
		||||
		if (user === undefined) {
 | 
			
		||||
			return sendError(403, "not authorized");
 | 
			
		||||
		}
 | 
			
		||||
		const body = ctx.request.body;
 | 
			
		||||
		const settings = body as Record<string, unknown>;
 | 
			
		||||
		await user.set_settings(settings);
 | 
			
		||||
		ctx.body = { ok: true };
 | 
			
		||||
		ctx.type = "json";
 | 
			
		||||
		await next();
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createLoginRouter(userController: UserAccessor) {
 | 
			
		||||
	const router = new Router();
 | 
			
		||||
	router.post("/login", createLoginMiddleware(userController));
 | 
			
		||||
	router.post("/logout", LogoutMiddleware);
 | 
			
		||||
	router.post("/login", createLoginHandler(userController));
 | 
			
		||||
	router.post("/logout", LogoutHandler);
 | 
			
		||||
	router.post("/refresh", createRefreshTokenMiddleware(userController));
 | 
			
		||||
	router.post("/reset", resetPasswordMiddleware(userController));
 | 
			
		||||
	router.get("/settings", getUserSettingHandler(userController));
 | 
			
		||||
	router.post("/settings", setUserSettingHandler(userController));
 | 
			
		||||
	return router;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
import { UserSetting } from "dbtype";
 | 
			
		||||
import { createHmac, randomBytes } from "node:crypto";
 | 
			
		||||
 | 
			
		||||
function hashForPassword(salt: string, password: string) {
 | 
			
		||||
| 
						 | 
				
			
			@ -41,6 +42,8 @@ export interface UserCreateInput {
 | 
			
		|||
	password: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type IUserSettings = UserSetting;
 | 
			
		||||
 | 
			
		||||
export interface IUser {
 | 
			
		||||
	readonly username: string;
 | 
			
		||||
	readonly password: Password;
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +68,18 @@ export interface IUser {
 | 
			
		|||
	 * @param password password to set
 | 
			
		||||
	 */
 | 
			
		||||
	reset_password(password: string): Promise<void>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * get user settings
 | 
			
		||||
	 * @returns user settings, or undefined if not set
 | 
			
		||||
	 */
 | 
			
		||||
	get_settings(): Promise<IUserSettings | undefined>;
 | 
			
		||||
	/**
 | 
			
		||||
	 * set user settings
 | 
			
		||||
	 * @param settings user settings to set
 | 
			
		||||
	 * @returns if settings updated, return true
 | 
			
		||||
	 */
 | 
			
		||||
	set_settings(settings: IUserSettings): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserAccessor {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,10 +46,11 @@ export const createPermissionCheckMiddleware =
 | 
			
		|||
			if (!permissions.map((p) => user_permission.includes(p)).every((x) => x)) {
 | 
			
		||||
				if (user.username === "") {
 | 
			
		||||
					return sendError(401, "you are guest. login needed.");
 | 
			
		||||
			}return sendError(403, "do not have permission");
 | 
			
		||||
				} return sendError(403, "do not have permission");
 | 
			
		||||
			}
 | 
			
		||||
			await next();
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
 | 
			
		||||
	const user = ctx.state.user;
 | 
			
		||||
	if (user.username !== "admin") {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ import { get_setting, SettingConfig } from "./SettingConfig.ts";
 | 
			
		|||
import { createReadStream, readFileSync } from "node:fs";
 | 
			
		||||
import bodyparser from "koa-bodyparser";
 | 
			
		||||
import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod.ts";
 | 
			
		||||
import { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login.ts";
 | 
			
		||||
import { createLoginRouter, createUserHandler, getAdmin, isAdminFirst } from "./login.ts";
 | 
			
		||||
import getContentRouter from "./route/contents.ts";
 | 
			
		||||
import { error_handler } from "./route/error_handler.ts";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +63,7 @@ class ServerApplication {
 | 
			
		|||
		}
 | 
			
		||||
		app.use(bodyparser());
 | 
			
		||||
		app.use(error_handler);
 | 
			
		||||
		app.use(createUserMiddleWare(this.userController));
 | 
			
		||||
		app.use(createUserHandler(this.userController));
 | 
			
		||||
 | 
			
		||||
		const diff_router = createDiffRouter(this.diffManger);
 | 
			
		||||
		this.diffManger.register("comic", createComicWatcher());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue