diff --git a/components/CreateRootUser.tsx b/components/CreateRootUser.tsx index 375a970..c77b749 100644 --- a/components/CreateRootUser.tsx +++ b/components/CreateRootUser.tsx @@ -3,7 +3,10 @@ import { GlobalCtx } from "./GlobalContext.tsx"; import BMd3TextField from "./BMd3TextField.tsx"; import t from "../server/i18n.ts"; import { useState } from "preact/hooks"; -import { MdTonalButton } from "../server/dmodule.ts"; +import { MdTextButton, MdTonalButton } from "../server/dmodule.ts"; +import { set_state } from "../server/state.ts"; +import pbkdf2Hmac from "pbkdf2-hmac/?target=es2022"; +import { encode } from "std/encoding/base64.ts"; type Props = { show: boolean; @@ -15,9 +18,47 @@ export default class CreateRootUser extends Component { render() { if (!this.props.show) return null; if (!MdTonalButton.value) return null; + if (!MdTextButton.value) return null; const [username, set_username] = useState(); const [password, set_password] = useState(); + const [disabled, set_disabled] = useState(false); + const create_user = async (username: string, password: string) => { + set_disabled(true); + const body = new URLSearchParams(); + body.append("name", username); + body.append("password", password); + const re = await fetch("/api/user", { method: "PUT", body }); + if (re.status !== 201) { + throw Error(re.statusText); + } + const b = new URLSearchParams(); + b.append("username", username); + const p = new Uint8Array( + await pbkdf2Hmac( + password, + "eh-downloader-salt", + 210000, + 64, + "SHA-512", + ), + ); + const t = (new Date()).getTime(); + const p2 = encode( + new Uint8Array( + await pbkdf2Hmac(p, t.toString(), 1000, 64, "SHA-512"), + ), + ); + b.append("password", p2); + b.append("t", t.toString()); + b.append("set_cookie", "1"); + const re2 = await fetch("/api/token", { method: "PUT", body: b }); + const token = await re2.json(); + if (!token.ok) { + throw Error(token.error); + } + }; const Button = MdTonalButton.value; + const TextButton = MdTextButton.value; return (
{ value={password} set_value={set_password} /> -
); diff --git a/db.ts b/db.ts index 2f632d8..9f9dfcd 100644 --- a/db.ts +++ b/db.ts @@ -245,7 +245,7 @@ const USER_TABLE = `CREATE TABLE user ( );`; const TOKEN_TABLE = `CREATE TABLE token ( id INTEGER PRIMARY KEY AUTOINCREMENT, - uid TEXT, + uid INT, token TEXT, expired TEXT );`; @@ -259,7 +259,7 @@ export class EhDb { #lock_file: string | undefined; #dblock_file: string | undefined; #_tags: Map | undefined; - readonly version = parse_ver("1.0.0-7"); + readonly version = parse_ver("1.0.0-8"); constructor(base_path: string) { const db_path = join(base_path, "data.db"); sure_dir_sync(base_path); @@ -381,6 +381,10 @@ export class EhDb { this.db.execute("DROP TABLE file_origin;"); need_optimize = true; } + if (compare_ver(v, parse_ver("1.0.0-8")) === -1) { + this.db.execute("DROP TABLE token;"); + this.db.execute(TOKEN_TABLE); + } this.#write_version(); if (need_optimize) this.optimize(); } diff --git a/import_map.json b/import_map.json index d141447..eeba364 100644 --- a/import_map.json +++ b/import_map.json @@ -23,6 +23,7 @@ "mime": "https://esm.sh/mime@3.0.0", "ua-parser-js": "https://esm.sh/ua-parser-js@1.0.35", "pbkdf2-hmac": "https://esm.sh/pbkdf2-hmac@1.2.1", + "pbkdf2-hmac/": "https://esm.sh/pbkdf2-hmac@1.2.1/", "randomstring": "https://esm.sh/randomstring@1.3.0", "@material/web/": "https://unpkg.lifegpc.workers.dev/@material/web@1.0.0-pre.12/", "@lit-labs/react/": "https://esm.sh/@lit-labs/react@1.2.0/" diff --git a/islands/Container.tsx b/islands/Container.tsx index dad8b96..0af29ab 100644 --- a/islands/Container.tsx +++ b/islands/Container.tsx @@ -17,6 +17,7 @@ import { registeServiceWorker } from "../server/sw.ts"; import { initCfg } from "../server/cfg.ts"; import { load_dmodule } from "../server/dmodule.ts"; import CreateRootUser from "../components/CreateRootUser.tsx"; +import { check_auth_status } from "../server/auth.ts"; export type ContainerProps = { i18n: I18NMap; @@ -60,7 +61,6 @@ export default class Container extends Component { } }; useEffect(() => { - initState(set_state1); const dm = parse_int( localStorage.getItem("darkmode"), DarkMode.Auto, @@ -92,6 +92,10 @@ export default class Container extends Component { load_dmodule().then(() => set_dmodule_loaded(true)).catch((e) => { console.error(e); }); + initState(set_state1); + check_auth_status().catch((e) => { + console.error(e); + }); }, []); let main = null; if (dmodule_loaded) { diff --git a/routes/api/_middleware.ts b/routes/api/_middleware.ts index 3a7ed35..f7b04f7 100644 --- a/routes/api/_middleware.ts +++ b/routes/api/_middleware.ts @@ -9,22 +9,23 @@ function handle_auth(req: Request, ctx: MiddlewareHandlerContext) { if (m.db.get_user_count() === 0) return true; const u = new URL(req.url); let token: string | null | undefined = req.headers.get("X-TOKEN"); - const cookies = parse_cookies(req.headers.get("Cookies")); + const cookies = parse_cookies(req.headers.get("Cookie")); if (!token) { token = cookies.get("token"); } - if (!token) { + const check = () => { if (u.pathname === "/api/token" && req.method === "PUT") return true; if (u.pathname === "/api/status" && req.method === "GET") return true; return false; - } + }; + if (!token) return check(); const t = m.db.get_token(token); const now = (new Date()).getTime(); - if (!t || t.expired.getTime() < now) return false; + if (!t || t.expired.getTime() < now) return check(); const user = m.db.get_user(t.uid); if (!user) { m.db.delete_token(token); - return false; + return check(); } ctx.state.user = user; return true; diff --git a/routes/api/token.ts b/routes/api/token.ts index 7a71e74..f67d76b 100644 --- a/routes/api/token.ts +++ b/routes/api/token.ts @@ -1,6 +1,6 @@ import { Handlers } from "$fresh/server.ts"; import { decode } from "std/encoding/base64.ts"; -import { get_string, parse_int } from "../../server/parse_form.ts"; +import { get_string, parse_bool, 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"; @@ -57,6 +57,9 @@ export const handler: Handlers = { if (t > now + 60000 || t < now - 60000) { return return_error(3, "Time is not corrected."); } + const set_cookie = await parse_bool(data.get("set_cookie"), false); + const http_only = await parse_bool(data.get("http_only"), true); + const secure = await parse_bool(data.get("secure"), false); const m = get_task_manager(); const u = m.db.get_user_by_name(username); if (!u) return return_error(4, USER_PASSWORD_ERROR); @@ -67,6 +70,13 @@ export const handler: Handlers = { return return_error(4, USER_PASSWORD_ERROR); } const token = m.db.add_token(u.id, now); - return return_data(token, 201); + const headers: HeadersInit = {}; + if (set_cookie) { + headers["Set-Cookie"] = + `token=${token.token}; Expires=${token.expired.toUTCString()}${ + http_only ? "; HttpOnly" : "" + }${secure ? "; Secure" : ""}`; + } + return return_data(token, 201, headers); }, }; diff --git a/routes/api/user.ts b/routes/api/user.ts index 72894c8..eef1f86 100644 --- a/routes/api/user.ts +++ b/routes/api/user.ts @@ -2,6 +2,7 @@ import { Handlers } from "$fresh/server.ts"; import { User, UserPermission } from "../../db.ts"; import { get_task_manager } from "../../server.ts"; import { get_string, parse_bool, parse_int } from "../../server/parse_form.ts"; +import type { BUser } from "../../server/user.ts"; import { return_data, return_error } from "../../server/utils.ts"; import pbkdf2Hmac from "pbkdf2-hmac"; @@ -24,7 +25,7 @@ export const handler: Handlers = { if (user && !user.is_admin && us.id !== user.id) { return return_error(403, "Permission denied."); } - return return_data({ + return return_data({ id: us.id, username: us.username, is_admin: us.is_admin, diff --git a/server/auth.ts b/server/auth.ts new file mode 100644 index 0000000..061602e --- /dev/null +++ b/server/auth.ts @@ -0,0 +1,24 @@ +import { parse_bool } from "./parse.ts"; +import { set_state } from "./state.ts"; +import type { StatusData } from "./status.ts"; +import type { BUser } from "./user.ts"; +import type { JSONResult } from "./utils.ts"; + +export async function check_auth_status() { + const re = await fetch("/api/user"); + const u: JSONResult = await re.json(); + if (u.ok) return true; + if (u.status !== 404 && u.status !== 1) { + throw Error(u.error); + } + const re2 = await fetch("/api/status"); + const s: JSONResult = await re2.json(); + if (!s.ok) { + throw Error(u.error); + } + if (s.data.no_user) { + if (!parse_bool(localStorage.getItem("skip_create_root_user"), false)) { + set_state("#/create_root_user"); + } + } +} diff --git a/server/user.ts b/server/user.ts new file mode 100644 index 0000000..70e47ca --- /dev/null +++ b/server/user.ts @@ -0,0 +1,8 @@ +import type { UserPermission } from "../db.ts"; + +export type BUser = { + id: number; + username: string; + is_admin: boolean; + permissions: UserPermission; +}; diff --git a/server/utils.ts b/server/utils.ts index 4c7a9f3..b5b8444 100644 --- a/server/utils.ts +++ b/server/utils.ts @@ -13,14 +13,17 @@ export type JSONResult = { error: string; }; -function gen_response(d: JSONResult, status = 200) { +function gen_response( + d: JSONResult, + status = 200, + headers: HeadersInit = {}, +) { if (d.status !== 0) { status = (d.status >= 400 && d.status < 600) ? d.status : 400; } - return new Response(JSON.stringify(d), { - status, - headers: { "Content-Type": "application/json" }, - }); + const h = new Headers(headers); + h.set("Content-Type", "application/json"); + return new Response(JSON.stringify(d), { status, headers: h }); } export function return_error( @@ -30,8 +33,12 @@ export function return_error( return gen_response({ ok: false, status, error }); } -export function return_data(data: T, status = 200) { - return gen_response({ ok: true, status: 0, data }, status); +export function return_data( + data: T, + status = 200, + headers: HeadersInit = {}, +) { + return gen_response({ ok: true, status: 0, data }, status, headers); } export function return_json(data: T, status = 200) { diff --git a/translation/en/user.jsonc b/translation/en/user.jsonc index d4b2fba..d4b923f 100644 --- a/translation/en/user.jsonc +++ b/translation/en/user.jsonc @@ -1,5 +1,7 @@ { "username": "Username", "password": "Password", - "login": "Login" + "login": "Login", + "create_root_user": "Create root user", + "skip": "Skip" } diff --git a/translation/zh-cn/user.jsonc b/translation/zh-cn/user.jsonc index a419be7..b6981c9 100644 --- a/translation/zh-cn/user.jsonc +++ b/translation/zh-cn/user.jsonc @@ -1,5 +1,7 @@ { "username": "用户名", "password": "密码", - "login": "登录" + "login": "登录", + "create_root_user": "创建根用户", + "skip": "跳过" }