Add Create Root User page

This commit is contained in:
2023-07-15 14:36:31 +08:00
parent 0c6c49a041
commit 0519334686
12 changed files with 145 additions and 24 deletions

View File

@@ -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<Props> {
render() {
if (!this.props.show) return null;
if (!MdTonalButton.value) return null;
if (!MdTextButton.value) return null;
const [username, set_username] = useState<string>();
const [password, set_password] = useState<string>();
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 (
<div>
<BMd3TextField
@@ -32,12 +73,28 @@ export default class CreateRootUser extends Component<Props> {
value={password}
set_value={set_password}
/>
<Button
disabled={!username && !password}
<TextButton
onClick={() => {
localStorage.setItem("skip_create_root_user", "1");
set_state("#/");
}}
>
{t("user.login")}
{t("user.skip")}
</TextButton>
<Button
disabled={disabled || !username || !password}
onClick={() => {
if (!username || !password) return;
create_user(username, password).then(() => {
set_disabled(false);
set_state("#/");
}).catch((e) => {
console.error(e);
set_disabled(false);
});
}}
>
{t("user.create_root_user")}
</Button>
</div>
);

8
db.ts
View File

@@ -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<string, number> | 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();
}

View File

@@ -23,6 +23,7 @@
"mime": "https://esm.sh/[email protected]",
"ua-parser-js": "https://esm.sh/[email protected]",
"pbkdf2-hmac": "https://esm.sh/[email protected]",
"pbkdf2-hmac/": "https://esm.sh/[email protected]/",
"randomstring": "https://esm.sh/[email protected]",
"@material/web/": "https://unpkg.lifegpc.workers.dev/@material/[email protected]/",
"@lit-labs/react/": "https://esm.sh/@lit-labs/[email protected]/"

View File

@@ -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<ContainerProps> {
}
};
useEffect(() => {
initState(set_state1);
const dm = parse_int(
localStorage.getItem("darkmode"),
DarkMode.Auto,
@@ -92,6 +92,10 @@ export default class Container extends Component<ContainerProps> {
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) {

View File

@@ -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;

View File

@@ -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);
},
};

View File

@@ -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<BUser>({
id: us.id,
username: us.username,
is_admin: us.is_admin,

24
server/auth.ts Normal file
View File

@@ -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<BUser> = 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<StatusData> = 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");
}
}
}

8
server/user.ts Normal file
View File

@@ -0,0 +1,8 @@
import type { UserPermission } from "../db.ts";
export type BUser = {
id: number;
username: string;
is_admin: boolean;
permissions: UserPermission;
};

View File

@@ -13,14 +13,17 @@ export type JSONResult<T> = {
error: string;
};
function gen_response<T>(d: JSONResult<T>, status = 200) {
function gen_response<T>(
d: JSONResult<T>,
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<T = unknown>(
@@ -30,8 +33,12 @@ export function return_error<T = unknown>(
return gen_response<T>({ ok: false, status, error });
}
export function return_data<T = unknown>(data: T, status = 200) {
return gen_response<T>({ ok: true, status: 0, data }, status);
export function return_data<T = unknown>(
data: T,
status = 200,
headers: HeadersInit = {},
) {
return gen_response<T>({ ok: true, status: 0, data }, status, headers);
}
export function return_json<T = unknown>(data: T, status = 200) {

View File

@@ -1,5 +1,7 @@
{
"username": "Username",
"password": "Password",
"login": "Login"
"login": "Login",
"create_root_user": "Create root user",
"skip": "Skip"
}

View File

@@ -1,5 +1,7 @@
{
"username": "用户名",
"password": "密码",
"login": "登录"
"login": "登录",
"create_root_user": "创建根用户",
"skip": "跳过"
}