[feat] Authenticate with default U&P
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@hono/hono": "jsr:@hono/hono@^4.12.23",
|
"@hono/hono": "jsr:@hono/hono@^4.12.23",
|
||||||
|
"jose": "npm:jose@^6.2.2",
|
||||||
"@libsql/client": "npm:@libsql/client@^0.17.3",
|
"@libsql/client": "npm:@libsql/client@^0.17.3",
|
||||||
"dotenv": "npm:dotenv@^17.4.2",
|
"dotenv": "npm:dotenv@^17.4.2",
|
||||||
"drizzle-kit": "npm:drizzle-kit@^0.31.10",
|
"drizzle-kit": "npm:drizzle-kit@^0.31.10",
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"npm:dotenv@^17.4.2": "17.4.2",
|
"npm:dotenv@^17.4.2": "17.4.2",
|
||||||
"npm:drizzle-kit@*": "0.31.10",
|
"npm:drizzle-kit@*": "0.31.10",
|
||||||
"npm:drizzle-kit@~0.31.10": "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": {
|
"jsr": {
|
||||||
"@hono/hono@4.12.23": {
|
"@hono/hono@4.12.23": {
|
||||||
@@ -736,6 +737,10 @@
|
|||||||
],
|
],
|
||||||
"tarball": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz"
|
"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": {
|
"js-base64@3.7.8": {
|
||||||
"integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
|
"integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
|
||||||
"tarball": "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.8.tgz"
|
"tarball": "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.8.tgz"
|
||||||
@@ -807,7 +812,8 @@
|
|||||||
"npm:@libsql/client@~0.17.3",
|
"npm:@libsql/client@~0.17.3",
|
||||||
"npm:dotenv@^17.4.2",
|
"npm:dotenv@^17.4.2",
|
||||||
"npm:drizzle-kit@~0.31.10",
|
"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",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "345f884f-f61d-47b9-89cd-8aa53c2ed828",
|
"id": "345f884f-f61d-47b9-89cd-8aa53c2ed828",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"tables": {
|
"tables": {
|
||||||
"metas": {
|
"metas": {
|
||||||
"name": "metas",
|
"name": "metas",
|
||||||
"columns": {
|
"columns": {
|
||||||
"key": {
|
"key": {
|
||||||
"name": "key",
|
"name": "key",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"name": "value",
|
"name": "value",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"views": {},
|
||||||
"foreignKeys": {},
|
"enums": {},
|
||||||
"compositePrimaryKeys": {},
|
"_meta": {
|
||||||
"uniqueConstraints": {},
|
"schemas": {},
|
||||||
"checkConstraints": {}
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"views": {},
|
|
||||||
"enums": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+12
-12
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1780835325557,
|
"when": 1780835325557,
|
||||||
"tag": "0000_talented_young_avengers",
|
"tag": "0000_talented_young_avengers",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-1
@@ -1,17 +1,45 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { drizzle } from "drizzle-orm/libsql";
|
import { drizzle } from "drizzle-orm/libsql";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { createClient } from "@libsql/client";
|
import { createClient } from "@libsql/client";
|
||||||
|
import { SignJWT } from "jose";
|
||||||
|
|
||||||
import { Hono } from "@hono/hono";
|
import { Hono } from "@hono/hono";
|
||||||
import { commonResponse } from "./utils/response.ts";
|
import { commonResponse } from "./utils/response.ts";
|
||||||
|
import { hashPassword, verifyPassword } from "./utils/crypto.ts";
|
||||||
|
import { metasTable } from "./db/schema.ts";
|
||||||
|
|
||||||
const client = createClient({
|
const client = createClient({
|
||||||
url: `file:${process.env.DATABASE_PATH!}`,
|
url: `file:${process.env.DATABASE_PATH!}`,
|
||||||
});
|
});
|
||||||
const db = drizzle(client);
|
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) => {
|
app.get("/", (c) => {
|
||||||
return commonResponse(c, true, 200, { "hint": "Hello! But nothing here." });
|
return commonResponse(c, true, 200, { "hint": "Hello! But nothing here." });
|
||||||
@@ -20,6 +48,58 @@ app.get("/ping", (c) => {
|
|||||||
return commonResponse(c, true, 200, {});
|
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);
|
Deno.serve(app.fetch);
|
||||||
|
|
||||||
const handleExit = () => {
|
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,
|
ok: boolean,
|
||||||
status: ContentfulStatusCode,
|
status: ContentfulStatusCode,
|
||||||
data: object,
|
data: object,
|
||||||
message = null,
|
message: string | null = null,
|
||||||
) {
|
) {
|
||||||
return c.json({
|
return c.json({
|
||||||
ok,
|
ok,
|
||||||
|
|||||||
Reference in New Issue
Block a user