From b777b4758c5be2c44e6d6bf250a680da7a56d7e4 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sun, 2 Jul 2023 17:29:37 +0800 Subject: [PATCH] Update --- db.ts | 105 +++++++++++++++++++++++++++++++++++++++++++ fresh.gen.ts | 18 ++++---- import_map.json | 4 +- routes/api/login.ts | 45 +++++++++++++++++++ routes/api/status.ts | 2 + server/status.ts | 1 + 6 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 routes/api/login.ts diff --git a/db.ts b/db.ts index fc792bc..9219b08 100644 --- a/db.ts +++ b/db.ts @@ -10,6 +10,7 @@ import { SqliteError } from "sqlite/mod.ts"; import { Status } from "sqlite/src/constants.ts"; import { sleep, sure_dir_sync, try_remove_sync } from "./utils.ts"; import { Task, TaskType } from "./task.ts"; +import { generate as randomstring } from "randomstring"; type SqliteMaster = { type: string; @@ -126,6 +127,38 @@ export type EhFileMetaRaw = { is_nsfw: number; is_ad: number; }; +export enum UserPermisson { + None = 0, + ReadGallery = 1 << 0, + EditGallery = 1 << 1, + All = ~(~0 << 2), +} +export type User = { + id: number; + username: string; + password: Uint8Array; + is_admin: boolean; + permissions: UserPermisson; +}; +type UserRaw = { + id: number; + username: string; + password: Uint8Array; + is_admin: number; + permissions: UserPermisson; +}; +export type Token = { + id: number; + uid: number; + token: string; + expired: Date; +}; +type TokenRaw = { + id: number; + uid: number; + token: string; + expired: string; +}; const ALL_TABLES = [ "version", "task", @@ -135,6 +168,8 @@ const ALL_TABLES = [ "gtag", "file", "filemeta", + "user", + "token", ]; const VERSION_TABLE = `CREATE TABLE version ( id TEXT, @@ -201,6 +236,19 @@ const FILEMETA_TABLE = `CREATE TABLE filemeta ( is_ad BOOLEAN, PRIMARY KEY (token) );`; +const USER_TABLE = `CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT, + password BLOB, + is_admin BOOLEAN, + permissions INT +);`; +const TOKEN_TABLE = `CREATE TABLE token ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT, + token TEXT, + expired TEXT +);`; export class EhDb { db; @@ -368,6 +416,12 @@ export class EhDb { if (!this.#exist_table.has("filemeta")) { this.db.execute(FILEMETA_TABLE); } + if (!this.#exist_table.has("user")) { + this.db.execute(USER_TABLE); + } + if (!this.#exist_table.has("token")) { + this.db.execute(TOKEN_TABLE); + } this.#updateExistsTable(); } #read_version() { @@ -518,6 +572,19 @@ export class EhDb { )[0]; }); } + add_token(uid: number, added: number): Token { + let token = randomstring(); + while (this.get_token(token)) { + token = randomstring(); + } + this.db.query( + "INSERT INTO token (uid, token, expired) VALUES (?, ?, ?);", + [uid, token, new Date(added + 2592000000)], + ); + const t = this.get_token(token); + if (!t) throw Error("Failed to add token."); + return t; + } begin(type: SqliteTransactionType) { try { this.db.execute(`BEGIN ${type} TRANSACTION;`); @@ -666,6 +733,23 @@ export class EhDb { return t; }); } + convert_token(m: TokenRaw[]) { + return m.map((m) => { + const e = new Date(m.expired); + const t = m; + t.expired = e; + return t; + }); + } + convert_user(m: UserRaw[]) { + return m.map((m) => { + const a = m.is_admin !== 0; + const t = m; + t.is_admin = a; + if (t.is_admin) t.permissions = UserPermisson.All; + return t; + }); + } delete_file(f: EhFile) { this.db.query("DELETE FROM file WHERE id = ?;", [f.id]); } @@ -876,6 +960,27 @@ export class EhDb { ]) ); } + get_token(token: string) { + const s = this.convert_token( + this.db.queryEntries( + "SELECT * FROM token WHERE token = ?;", + [token], + ), + ); + return s.length ? s[0] : undefined; + } + get_user_by_name(name: string) { + const s = this.convert_user( + this.db.queryEntries( + "SELECT * FROM user WHERE username = ?;", + [name], + ), + ); + return s.length ? s[0] : undefined; + } + get_user_count() { + return this.db.query<[number]>("SELECT COUNT(*) FROM user;")[0][0]; + } optimize() { this.db.execute("VACUUM;"); } diff --git a/fresh.gen.ts b/fresh.gen.ts index f3e7675..6ec56f8 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -15,10 +15,11 @@ import * as $9 from "./routes/api/filemeta/[token].ts"; import * as $10 from "./routes/api/files/[token].ts"; import * as $11 from "./routes/api/gallery/[gid].ts"; import * as $12 from "./routes/api/gallery/list.ts"; -import * as $13 from "./routes/api/status.ts"; -import * as $14 from "./routes/api/task.ts"; -import * as $15 from "./routes/api/thumbnail/[id].ts"; -import * as $16 from "./routes/index.tsx"; +import * as $13 from "./routes/api/login.ts"; +import * as $14 from "./routes/api/status.ts"; +import * as $15 from "./routes/api/task.ts"; +import * as $16 from "./routes/api/thumbnail/[id].ts"; +import * as $17 from "./routes/index.tsx"; import * as $$0 from "./islands/Container.tsx"; import * as $$1 from "./islands/Settings.tsx"; import * as $$2 from "./islands/TaskManager.tsx"; @@ -38,10 +39,11 @@ const manifest = { "./routes/api/files/[token].ts": $10, "./routes/api/gallery/[gid].ts": $11, "./routes/api/gallery/list.ts": $12, - "./routes/api/status.ts": $13, - "./routes/api/task.ts": $14, - "./routes/api/thumbnail/[id].ts": $15, - "./routes/index.tsx": $16, + "./routes/api/login.ts": $13, + "./routes/api/status.ts": $14, + "./routes/api/task.ts": $15, + "./routes/api/thumbnail/[id].ts": $16, + "./routes/index.tsx": $17, }, islands: { "./islands/Container.tsx": $$0, diff --git a/import_map.json b/import_map.json index ab7dbee..ee2da5d 100644 --- a/import_map.json +++ b/import_map.json @@ -21,6 +21,8 @@ "meilisearch": "https://esm.sh/meilisearch@0.33.0", "lodash/": "https://esm.sh/lodash@4.17.21/", "mime": "https://esm.sh/mime@3.0.0", - "ua-parser-js": "https://esm.sh/ua-parser-js@1.0.35" + "ua-parser-js": "https://esm.sh/ua-parser-js@1.0.35", + "pbkdf2-hmac": "https://esm.sh/pbkdf2-hmac@1.2.1", + "randomstring": "https://esm.sh/randomstring@1.3.0" } } diff --git a/routes/api/login.ts b/routes/api/login.ts new file mode 100644 index 0000000..8a3e6c9 --- /dev/null +++ b/routes/api/login.ts @@ -0,0 +1,45 @@ +import { Handlers } from "$fresh/server.ts"; +import { decode } from "std/encoding/base64.ts"; +import { get_string, parse_int } from "../../server/parse_form.ts"; +import { return_data, return_error } from "../../server/utils.ts"; +import { get_task_manager } from "../../server.ts"; +import pbkdf2Hmac from "pbkdf2-hmac"; +import isEqual from "lodash/isEqual"; + +const USER_PASSWORD_ERROR = "Incorrect username or password."; + +export const handler: Handlers = { + async POST(req, _ctx) { + const data = await req.formData(); + const username = await get_string(data.get("username")); + if (!username) return return_error(1, "username not specified."); + const p = await get_string(data.get("password")); + if (!p) return return_error(1, "password not specified."); + let password = null; + try { + password = decode(p); + } catch (_) { + return return_error(2, "Failed to decode password with base64."); + } + if (password.length !== 64) { + return return_error(2, "Password need 64 bytes."); + } + const t = await parse_int(data.get("t"), null); + if (t === null) return return_error(1, "t not specified."); + const now = (new Date()).getTime(); + if (t > now + 60000 || t < now - 60000) { + return return_error(3, "Time is not corrected."); + } + const m = get_task_manager(); + const u = m.db.get_user_by_name(username); + if (!u) return return_error(4, USER_PASSWORD_ERROR); + const pa = new Uint8Array( + await pbkdf2Hmac(u.password, t.toString(), 1000, 64), + ); + if (!isEqual(pa, password)) { + return return_error(4, USER_PASSWORD_ERROR); + } + const token = m.db.add_token(u.id, now); + return return_data(token); + }, +}; diff --git a/routes/api/status.ts b/routes/api/status.ts index 56d2815..9f90350 100644 --- a/routes/api/status.ts +++ b/routes/api/status.ts @@ -19,10 +19,12 @@ export const handler: Handlers = { m.cfg.meili_update_api_key, } : undefined; + const no_user = m.db.get_user_count() === 0; return return_data({ ffmpeg_binary_enabled, meilisearch_enabled, meilisearch, + no_user, }); }, }; diff --git a/server/status.ts b/server/status.ts index 5a88fca..83c3fd7 100644 --- a/server/status.ts +++ b/server/status.ts @@ -5,4 +5,5 @@ export type StatusData = { host: string; key: string; }; + no_user: boolean; };