From e0d78f87a882cfeb6a9b07450495e16c571de8ff Mon Sep 17 00:00:00 2001 From: patrick Date: Sun, 7 Jun 2026 21:04:19 +0800 Subject: [PATCH] [feat] Authenticate with default U&P --- deno.json | 1 + deno.lock | 10 +++- drizzle/meta/0000_snapshot.json | 78 +++++++++++++++---------------- drizzle/meta/_journal.json | 24 +++++----- src/main.ts | 82 ++++++++++++++++++++++++++++++++- src/utils/crypto.ts | 60 ++++++++++++++++++++++++ src/utils/response.ts | 2 +- 7 files changed, 202 insertions(+), 55 deletions(-) create mode 100644 src/utils/crypto.ts diff --git a/deno.json b/deno.json index 70e93e8..48323bc 100644 --- a/deno.json +++ b/deno.json @@ -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", diff --git a/deno.lock b/deno.lock index 7e6ac66..04c7963 100644 --- a/deno.lock +++ b/deno.lock @@ -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" ] } } diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index ede1e62..7b5a7ce 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -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": {} - } -} \ No newline at end of file +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index dcaf6b1..323baf0 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -1,13 +1,13 @@ { - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1780835325557, - "tag": "0000_talented_young_avengers", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1780835325557, + "tag": "0000_talented_young_avengers", + "breakpoints": true + } + ] +} diff --git a/src/main.ts b/src/main.ts index 33a59a0..96c2bc3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 = () => { diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts new file mode 100644 index 0000000..3730dc7 --- /dev/null +++ b/src/utils/crypto.ts @@ -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 { + 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, +): Promise { + 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 { + const salt = crypto.getRandomValues( + new Uint8Array(SALT_LENGTH) as Uint8Array, + ); + const hash = await deriveKey(password, salt); + return `${toHex(salt.buffer)}:${toHex(hash)}`; +} + +export async function verifyPassword( + password: string, + stored: string, +): Promise { + const [saltHex, hashHex] = stored.split(":"); + if (!saltHex || !hashHex) return false; + const salt = fromHex(saltHex); + const derived = await deriveKey(password, salt); + return toHex(derived) === hashHex; +} diff --git a/src/utils/response.ts b/src/utils/response.ts index aafdc0e..96853a3 100644 --- a/src/utils/response.ts +++ b/src/utils/response.ts @@ -6,7 +6,7 @@ export function commonResponse( ok: boolean, status: ContentfulStatusCode, data: object, - message = null, + message: string | null = null, ) { return c.json({ ok,