[feat] Authenticate with default U&P

This commit is contained in:
2026-06-07 21:04:19 +08:00
Unverified
parent 9e506c06fe
commit e0d78f87a8
7 changed files with 202 additions and 55 deletions
+1
View File
@@ -7,6 +7,7 @@
},
"imports": {
"@hono/hono": "jsr:@hono/hono@^4.12.23",
"jose": "npm:jose@^6.2.2",
"@libsql/client": "npm:@libsql/client@^0.17.3",
"dotenv": "npm:dotenv@^17.4.2",
"drizzle-kit": "npm:drizzle-kit@^0.31.10",
Generated
+8 -2
View File
@@ -7,7 +7,8 @@
"npm:dotenv@^17.4.2": "17.4.2",
"npm:drizzle-kit@*": "0.31.10",
"npm:drizzle-kit@~0.31.10": "0.31.10",
"npm:drizzle-orm@~0.45.2": "0.45.2_@libsql+client@0.17.3"
"npm:drizzle-orm@~0.45.2": "0.45.2_@libsql+client@0.17.3",
"npm:jose@^6.2.2": "6.2.3"
},
"jsr": {
"@hono/hono@4.12.23": {
@@ -736,6 +737,10 @@
],
"tarball": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz"
},
"jose@6.2.3": {
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
"tarball": "https://registry.npmmirror.com/jose/-/jose-6.2.3.tgz"
},
"js-base64@3.7.8": {
"integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
"tarball": "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.8.tgz"
@@ -807,7 +812,8 @@
"npm:@libsql/client@~0.17.3",
"npm:dotenv@^17.4.2",
"npm:drizzle-kit@~0.31.10",
"npm:drizzle-orm@~0.45.2"
"npm:drizzle-orm@~0.45.2",
"npm:jose@^6.2.2"
]
}
}
+38 -38
View File
@@ -1,42 +1,42 @@
{
"version": "6",
"dialect": "sqlite",
"id": "345f884f-f61d-47b9-89cd-8aa53c2ed828",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"metas": {
"name": "metas",
"columns": {
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
"version": "6",
"dialect": "sqlite",
"id": "345f884f-f61d-47b9-89cd-8aa53c2ed828",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"metas": {
"name": "metas",
"columns": {
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
+11 -11
View File
@@ -1,13 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1780835325557,
"tag": "0000_talented_young_avengers",
"breakpoints": true
}
]
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1780835325557,
"tag": "0000_talented_young_avengers",
"breakpoints": true
}
]
}
+81 -1
View File
@@ -1,17 +1,45 @@
import "dotenv/config";
import { drizzle } from "drizzle-orm/libsql";
import { eq } from "drizzle-orm";
import { createClient } from "@libsql/client";
import { SignJWT } from "jose";
import { Hono } from "@hono/hono";
import { commonResponse } from "./utils/response.ts";
import { hashPassword, verifyPassword } from "./utils/crypto.ts";
import { metasTable } from "./db/schema.ts";
const client = createClient({
url: `file:${process.env.DATABASE_PATH!}`,
});
const db = drizzle(client);
const app = new Hono();
const JWT_SECRET = crypto.getRandomValues(new Uint8Array(32));
async function initDefaultUser() {
const row = await db
.select()
.from(metasTable)
.where(eq(metasTable.key, "userinfo_username"))
.get();
if (row) return;
const usernameHash = await hashPassword("ah");
const passwordHash = await hashPassword("123456");
await db
.insert(metasTable)
.values([
{ key: "userinfo_username", value: usernameHash },
{ key: "userinfo_password", value: passwordHash },
])
.run();
console.log("Default user created (username: ah, password: 123456)");
}
await initDefaultUser();
const app = new Hono();
app.get("/", (c) => {
return commonResponse(c, true, 200, { "hint": "Hello! But nothing here." });
@@ -20,6 +48,58 @@ app.get("/ping", (c) => {
return commonResponse(c, true, 200, {});
});
app.post("/authenticate", async (c) => {
const body = await c.req.json<{ username?: string; password?: string }>();
if (!body.username || !body.password) {
return commonResponse(
c,
false,
400,
{},
"Missing username or password",
);
}
const storedUsername = await db
.select()
.from(metasTable)
.where(eq(metasTable.key, "userinfo_username"))
.get();
const storedPassword = await db
.select()
.from(metasTable)
.where(eq(metasTable.key, "userinfo_password"))
.get();
if (!storedUsername?.value || !storedPassword?.value) {
return commonResponse(c, false, 500, {}, "User not configured");
}
const usernameValid = await verifyPassword(
body.username,
storedUsername.value,
);
if (!usernameValid) {
return commonResponse(c, false, 401, {}, "Invalid credentials");
}
const passwordValid = await verifyPassword(
body.password,
storedPassword.value,
);
if (!passwordValid) {
return commonResponse(c, false, 401, {}, "Invalid credentials");
}
const jwt = await new SignJWT({ username: body.username })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("24h")
.sign(JWT_SECRET);
return commonResponse(c, true, 200, { token: jwt });
});
Deno.serve(app.fetch);
const handleExit = () => {
+60
View File
@@ -0,0 +1,60 @@
const ITERATIONS = 2000;
const SALT_LENGTH = 16;
const HASH_LENGTH = 64;
function toHex(buffer: ArrayBuffer): string {
return Array.from(new Uint8Array(buffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
function fromHex(hex: string): Uint8Array<ArrayBuffer> {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
async function deriveKey(
password: string,
salt: Uint8Array<ArrayBuffer>,
): Promise<ArrayBuffer> {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
encoder.encode(password),
{ name: "PBKDF2" },
false,
["deriveBits"],
);
return crypto.subtle.deriveBits(
{
name: "PBKDF2",
hash: "SHA-256",
salt,
iterations: ITERATIONS,
},
keyMaterial,
HASH_LENGTH * 8,
);
}
export async function hashPassword(password: string): Promise<string> {
const salt = crypto.getRandomValues(
new Uint8Array(SALT_LENGTH) as Uint8Array<ArrayBuffer>,
);
const hash = await deriveKey(password, salt);
return `${toHex(salt.buffer)}:${toHex(hash)}`;
}
export async function verifyPassword(
password: string,
stored: string,
): Promise<boolean> {
const [saltHex, hashHex] = stored.split(":");
if (!saltHex || !hashHex) return false;
const salt = fromHex(saltHex);
const derived = await deriveKey(password, salt);
return toHex(derived) === hashHex;
}
+1 -1
View File
@@ -6,7 +6,7 @@ export function commonResponse(
ok: boolean,
status: ContentfulStatusCode,
data: object,
message = null,
message: string | null = null,
) {
return c.json({
ok,