Skip to content

Commit

Permalink
feat: 🔥 migrating to sessions, using file routes, adding auth provider
Browse files Browse the repository at this point in the history
  • Loading branch information
aacevski committed Jan 5, 2025
1 parent 73ddd6d commit d6f8ecc
Show file tree
Hide file tree
Showing 49 changed files with 768 additions and 267 deletions.
21 changes: 21 additions & 0 deletions apps/api/drizzle/0002_flowery_secret_warriors.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`expires_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_user` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`password` text NOT NULL,
`email` text NOT NULL,
`created_at` integer DEFAULT '"2025-01-04T22:24:29.828Z"' NOT NULL
);
--> statement-breakpoint
INSERT INTO `__new_user`("id", "name", "password", "email", "created_at") SELECT "id", "name", "password", "email", "created_at" FROM `user`;--> statement-breakpoint
DROP TABLE `user`;--> statement-breakpoint
ALTER TABLE `__new_user` RENAME TO `user`;--> statement-breakpoint
PRAGMA foreign_keys=ON;--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);
111 changes: 111 additions & 0 deletions apps/api/drizzle/meta/0002_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{
"version": "6",
"dialect": "sqlite",
"id": "1c74d9e8-3e5e-4199-9175-7514767a6912",
"prevId": "e1a9d8d8-e2dd-4fa3-864a-b396b6f6034e",
"tables": {
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2025-01-04T22:24:29.828Z\"'"
}
},
"indexes": {
"user_email_unique": {
"name": "user_email_unique",
"columns": ["email"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
7 changes: 7 additions & 0 deletions apps/api/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
"when": 1735996197501,
"tag": "0001_melted_whizzer",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1736029469835,
"tag": "0002_flowery_secret_warriors",
"breakpoints": true
}
]
}
6 changes: 4 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
"@elysiajs/cors": "^1.2.0",
"@elysiajs/jwt": "^1.2.0",
"@elysiajs/websocket": "^0.2.8",
"@kaneo/typescript-config": "workspace:*",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@paralleldrive/cuid2": "^2.2.2",
"better-sqlite3": "^11.7.0",
"drizzle-kit": "^0.30.1",
"drizzle-orm": "^0.38.3",
"drizzle-typebox": "^0.2.1",
"elysia": "latest",
"@kaneo/typescript-config": "workspace:*"
"elysia": "latest"
},
"devDependencies": {
"bun-types": "latest"
Expand Down
14 changes: 14 additions & 0 deletions apps/api/src/database/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createId } from "@paralleldrive/cuid2";
import type { InferSelectModel } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";

export const userTable = sqliteTable("user", {
Expand All @@ -12,3 +13,16 @@ export const userTable = sqliteTable("user", {
.default(new Date())
.notNull(),
});

export const sessionTable = sqliteTable("session", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => userTable.id),
expiresAt: integer("expires_at", {
mode: "timestamp",
}).notNull(),
});

export type User = InferSelectModel<typeof userTable>;
export type Session = InferSelectModel<typeof sessionTable>;
37 changes: 13 additions & 24 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,34 @@
import { cors } from "@elysiajs/cors";
import { Elysia } from "elysia";
import user from "./user";
import { REFRESH_TOKEN_EXPIRY } from "./user/constants";
import createToken from "./user/utils/create-token";
import { validateSessionToken } from "./user/controllers/validate-session-token";

const app = new Elysia()
.use(cors())
.use(user)
.guard({
async beforeHandle({
set,
accessJwt,
refreshJwt,
cookie: { accessToken, refreshToken },
}) {
const decodedAccessToken = await accessJwt.verify(accessToken.value);
const decodedRefreshToken = await refreshJwt.verify(refreshToken.value);

if (!decodedAccessToken) {
async beforeHandle({ set, cookie: { session } }) {
if (!session?.value) {
set.status = "Unauthorized";

return set.status;
}

if (!decodedRefreshToken) {
const refreshToken = await createToken({
jwt: refreshJwt,
expires: REFRESH_TOKEN_EXPIRY,
payload: {
id: String(decodedAccessToken.id),
},
});
const { user, session: validatedSession } = await validateSessionToken(
session.value,
);

if (set.cookie) set.cookie.refreshToken = refreshToken;
if (!user || !validatedSession) {
set.status = "Unauthorized";

return set.status;
}
},
})
.get("/me", async ({ refreshJwt, cookie: { refreshToken } }) => {
const profile = await refreshJwt.verify(refreshToken.value);
.get("/me", async ({ cookie: { session } }) => {
const { user } = await validateSessionToken(session.value ?? "");

return profile;
return user;
})
.onError(({ code, error }) => {
switch (code) {
Expand Down
7 changes: 0 additions & 7 deletions apps/api/src/user/constants.ts

This file was deleted.

18 changes: 18 additions & 0 deletions apps/api/src/user/controllers/create-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { sha256 } from "@oslojs/crypto/sha2";
import { encodeHexLowerCase } from "@oslojs/encoding";
import db from "../../database";
import type { Session } from "../../database/schema";
import { sessionTable } from "../../database/schema";

async function createSession(token: string, userId: string): Promise<Session> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: Session = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
};
await db.insert(sessionTable).values(session);
return session;
}

export default createSession;
9 changes: 9 additions & 0 deletions apps/api/src/user/controllers/invalidate-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { eq } from "drizzle-orm";
import db from "../../database";
import { sessionTable } from "../../database/schema";

async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(sessionTable).where(eq(sessionTable.id, sessionId));
}

export default invalidateSession;
45 changes: 45 additions & 0 deletions apps/api/src/user/controllers/validate-session-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { sha256 } from "@oslojs/crypto/sha2";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { eq } from "drizzle-orm";
import db from "../../database";
import { sessionTable, userTable } from "../../database/schema";
import type { SessionValidationResult } from "../types";

export async function validateSessionToken(
token: string,
): Promise<SessionValidationResult> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const result = await db
.select({ user: userTable, session: sessionTable })
.from(sessionTable)
.innerJoin(userTable, eq(sessionTable.userId, userTable.id))
.where(eq(sessionTable.id, sessionId));

if (result.length < 1) {
return { session: null, user: null };
}

const { user, session } = result[0];

const isSessionExpired = Date.now() >= session.expiresAt.getTime();

if (isSessionExpired) {
await db.delete(sessionTable).where(eq(sessionTable.id, session.id));
return { session: null, user: null };
}

const isSessionHalfWayExpired =
Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15;

if (isSessionHalfWayExpired) {
session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
await db
.update(sessionTable)
.set({
expiresAt: session.expiresAt,
})
.where(eq(sessionTable.id, session.id));
}

return { session, user };
}
Loading

0 comments on commit d6f8ecc

Please sign in to comment.