mirror of
https://github.com/lifegpc/eh-downloader.git
synced 2026-06-06 05:38:44 +08:00
Add Create Root User page
This commit is contained in:
@@ -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
8
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<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();
|
||||
}
|
||||
|
||||
@@ -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]/"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
24
server/auth.ts
Normal 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
8
server/user.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { UserPermission } from "../db.ts";
|
||||
|
||||
export type BUser = {
|
||||
id: number;
|
||||
username: string;
|
||||
is_admin: boolean;
|
||||
permissions: UserPermission;
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"login": "Login"
|
||||
"login": "Login",
|
||||
"create_root_user": "Create root user",
|
||||
"skip": "Skip"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"login": "登录"
|
||||
"login": "登录",
|
||||
"create_root_user": "创建根用户",
|
||||
"skip": "跳过"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user