[feat] Authenticate with default U&P
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -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
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export function commonResponse(
|
||||
ok: boolean,
|
||||
status: ContentfulStatusCode,
|
||||
data: object,
|
||||
message = null,
|
||||
message: string | null = null,
|
||||
) {
|
||||
return c.json({
|
||||
ok,
|
||||
|
||||
Reference in New Issue
Block a user