diff options
-rw-r--r-- | migrations/0000_real_hitman.sql (renamed from migrations/0000_bitter_xorn.sql) | 3 | ||||
-rw-r--r-- | migrations/0001_funny_ronan.sql | 13 | ||||
-rw-r--r-- | migrations/0002_red_morlun.sql | 13 | ||||
-rw-r--r-- | migrations/meta/0000_snapshot.json | 9 | ||||
-rw-r--r-- | migrations/meta/0001_snapshot.json | 166 | ||||
-rw-r--r-- | migrations/meta/0002_snapshot.json | 166 | ||||
-rw-r--r-- | migrations/meta/_journal.json | 18 | ||||
-rw-r--r-- | src/auth.tsx | 26 | ||||
-rw-r--r-- | src/db/schema.ts | 3 | ||||
-rw-r--r-- | src/index.tsx | 19 |
10 files changed, 417 insertions, 19 deletions
diff --git a/migrations/0000_bitter_xorn.sql b/migrations/0000_real_hitman.sql index 4f4c445..fc43e8e 100644 --- a/migrations/0000_bitter_xorn.sql +++ b/migrations/0000_real_hitman.sql @@ -6,7 +6,8 @@ CREATE TABLE `groups` ( CREATE TABLE `sessions` ( `id` integer PRIMARY KEY NOT NULL, `uuid` text NOT NULL, - `user_id` integer NOT NULL + `user_id` integer NOT NULL, + `last_use` integer NOT NULL ); --> statement-breakpoint CREATE UNIQUE INDEX `sessions_uuid_unique` ON `sessions` (`uuid`);--> statement-breakpoint diff --git a/migrations/0001_funny_ronan.sql b/migrations/0001_funny_ronan.sql new file mode 100644 index 0000000..9928b60 --- /dev/null +++ b/migrations/0001_funny_ronan.sql @@ -0,0 +1,13 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_sessions` ( + `id` integer PRIMARY KEY NOT NULL, + `uuid` text NOT NULL, + `user_id` integer NOT NULL, + `last_use` integer DEFAULT current_timestamp NOT NULL +); +--> statement-breakpoint +INSERT INTO `__new_sessions`("id", "uuid", "user_id", "last_use") SELECT "id", "uuid", "user_id", "last_use" FROM `sessions`;--> statement-breakpoint +DROP TABLE `sessions`;--> statement-breakpoint +ALTER TABLE `__new_sessions` RENAME TO `sessions`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `sessions_uuid_unique` ON `sessions` (`uuid`);
\ No newline at end of file diff --git a/migrations/0002_red_morlun.sql b/migrations/0002_red_morlun.sql new file mode 100644 index 0000000..23de4ce --- /dev/null +++ b/migrations/0002_red_morlun.sql @@ -0,0 +1,13 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_sessions` ( + `id` integer PRIMARY KEY NOT NULL, + `uuid` text NOT NULL, + `user_id` integer NOT NULL, + `last_use` integer DEFAULT (unixepoch()) NOT NULL +); +--> statement-breakpoint +INSERT INTO `__new_sessions`("id", "uuid", "user_id", "last_use") SELECT "id", "uuid", "user_id", "last_use" FROM `sessions`;--> statement-breakpoint +DROP TABLE `sessions`;--> statement-breakpoint +ALTER TABLE `__new_sessions` RENAME TO `sessions`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `sessions_uuid_unique` ON `sessions` (`uuid`);
\ No newline at end of file diff --git a/migrations/meta/0000_snapshot.json b/migrations/meta/0000_snapshot.json index ba0da3c..ec8a4df 100644 --- a/migrations/meta/0000_snapshot.json +++ b/migrations/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "a2343f4c-71c5-4992-b49e-d30f2a4f08c0", + "id": "95d1eecd-0df6-4d23-8d18-cd455608bae7", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "groups": { @@ -51,6 +51,13 @@ "primaryKey": false, "notNull": true, "autoincrement": false + }, + "last_use": { + "name": "last_use", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false } }, "indexes": { diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..e8a216d --- /dev/null +++ b/migrations/meta/0001_snapshot.json @@ -0,0 +1,166 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b9c41e3d-8811-4705-8345-a3a8386748b8", + "prevId": "95d1eecd-0df6-4d23-8d18-cd455608bae7", + "tables": { + "groups": { + "name": "groups", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_use": { + "name": "last_use", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "current_timestamp" + } + }, + "indexes": { + "sessions_uuid_unique": { + "name": "sessions_uuid_unique", + "columns": [ + "uuid" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "passkey": { + "name": "passkey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "passkey_id": { + "name": "passkey_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_name_unique": { + "name": "users_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +}
\ No newline at end of file diff --git a/migrations/meta/0002_snapshot.json b/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..0d326c1 --- /dev/null +++ b/migrations/meta/0002_snapshot.json @@ -0,0 +1,166 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "29e91293-4ebe-4039-a781-8609bf9505d5", + "prevId": "b9c41e3d-8811-4705-8345-a3a8386748b8", + "tables": { + "groups": { + "name": "groups", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_use": { + "name": "last_use", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "sessions_uuid_unique": { + "name": "sessions_uuid_unique", + "columns": [ + "uuid" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "passkey": { + "name": "passkey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "passkey_id": { + "name": "passkey_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_name_unique": { + "name": "users_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +}
\ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 3f28ad0..a434ab9 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -5,8 +5,22 @@ { "idx": 0, "version": "6", - "when": 1755189001309, - "tag": "0000_bitter_xorn", + "when": 1755543105259, + "tag": "0000_real_hitman", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1755543598172, + "tag": "0001_funny_ronan", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1755543983317, + "tag": "0002_red_morlun", "breakpoints": true } ] diff --git a/src/auth.tsx b/src/auth.tsx index ebda74e..eef8b8f 100644 --- a/src/auth.tsx +++ b/src/auth.tsx @@ -1,19 +1,35 @@ import { Hono } from "hono"; import * as swa from "@simplewebauthn/server"; import { randomUUID } from "node:crypto"; -import { eq } from "drizzle-orm"; +import { and, eq, gt, sql } from "drizzle-orm"; import { RP_ID, ORIGIN, db } from "./index.js"; import { sessionTable, userTable, webauthnChallenges } from "./db/schema.js"; import { stringify, parse } from "superjson"; -import { setCookie } from "hono/cookie"; - -let app = new Hono(); +import { getCookie, setCookie } from "hono/cookie"; +import type { Context } from "hono"; export const LoginForm = () => <section class="register-form"> <button hx-post="/auth/register-begin" hx-target="closest .register-form" hx-swap="outerHTML">register</button> <button hx-post="/auth/login-begin" hx-target="closest .register-form" hx-swap="outerHTML">login</button> </section>; +export async function getSession(c: Context) { + let sessionId = getCookie(c, "session"); + if (!sessionId) return null; + + let [result] = await db + .select() + .from(sessionTable) + .innerJoin(userTable, eq(userTable.id, sessionTable.userId)) + .where(({ sessions: session }) => and(eq(session.uuid, sessionId), gt(session.lastUse, sql`unixepoch() - ${60 * 60 * 24 * 7}`))); + if (!result) return null; + await db.update(sessionTable).set({ lastUse: sql`unixepoch()` }).where(eq(sessionTable.id, result.sessions.id)); + return { user: result.users, lastUse: result.sessions.lastUse, uuid: result.sessions.uuid }; +} + +let app = new Hono(); +export default app; + app.post("/register-begin", async c => { const username = randomUUID(); let options = await swa.generateRegistrationOptions({ @@ -89,5 +105,3 @@ app.post("/login-finish", async c => { setCookie(c, "session", uuid); return c.html(<p>Logged in!</p>); }); - -export default app; diff --git a/src/db/schema.ts b/src/db/schema.ts index 01b7228..339dfc0 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,4 +1,4 @@ -import { relations } from "drizzle-orm"; +import { relations, sql } from "drizzle-orm"; import { text, sqliteTable, integer } from "drizzle-orm/sqlite-core"; export const groupTable = sqliteTable("groups", { @@ -17,6 +17,7 @@ export const sessionTable = sqliteTable("sessions", { id: integer().primaryKey(), uuid: text().unique().notNull(), userId: integer("user_id").notNull(), + lastUse: integer("last_use", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`), }); export const sessionRelations = relations(sessionTable, ({ one }) => ({ diff --git a/src/index.tsx b/src/index.tsx index e029577..5975284 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,7 @@ import { Hono } from "hono"; import { createClient } from "@libsql/client"; import { drizzle } from "drizzle-orm/libsql"; import { groupTable } from "./db/schema.js"; -import authRouter, { LoginForm } from "./auth.js"; +import authRouter, { getSession, LoginForm } from "./auth.js"; export const RP_ID = "localhost"; // "uneven.0m.nu"; export const ORIGIN = `http://${RP_ID}`; @@ -38,13 +38,16 @@ app.get("/", c => c.html( )); let colors = ["red", "green", "blue"]; -app.get("/button", c => c.html( - <button - hx-get="/button" - hx-swap="outerHTML" - style={{ backgroundColor: colors[Math.floor(Math.random() * colors.length)] }} - >disco button!</button> -)); +app.get("/button", async c => { + let session = await getSession(c); + return c.html( + <button + hx-get="/button" + hx-swap="outerHTML" + style={{ backgroundColor: colors[Math.floor(Math.random() * colors.length)] }} + >disco button! {session.user.name}</button> + ); +}); app.route("/auth", authRouter); |