diff --git a/apps/aris-backend/.env.example b/apps/aris-backend/.env.example new file mode 100644 index 0000000..f729274 --- /dev/null +++ b/apps/aris-backend/.env.example @@ -0,0 +1,8 @@ +# PostgreSQL connection string +DATABASE_URL=postgresql://user:password@localhost:5432/aris + +# BetterAuth secret (min 32 chars, generate with: openssl rand -base64 32) +BETTER_AUTH_SECRET= + +# Base URL of the backend +BETTER_AUTH_URL=http://localhost:3000 diff --git a/apps/aris-backend/package.json b/apps/aris-backend/package.json index 3f11cde..87e03c6 100644 --- a/apps/aris-backend/package.json +++ b/apps/aris-backend/package.json @@ -2,10 +2,10 @@ "name": "@aris/backend", "version": "0.0.0", "type": "module", - "main": "src/index.ts", + "main": "src/server.ts", "scripts": { - "dev": "bun run --watch src/index.ts", - "start": "bun run src/index.ts", + "dev": "bun run --watch src/server.ts", + "start": "bun run src/server.ts", "test": "bun test src/" }, "dependencies": { @@ -14,6 +14,9 @@ "@aris/source-weatherkit": "workspace:*", "better-auth": "^1", "hono": "^4", - "postgres": "^3" + "pg": "^8" + }, + "devDependencies": { + "@types/pg": "^8" } } diff --git a/apps/aris-backend/src/auth/http.ts b/apps/aris-backend/src/auth/http.ts new file mode 100644 index 0000000..028e2cb --- /dev/null +++ b/apps/aris-backend/src/auth/http.ts @@ -0,0 +1,7 @@ +import type { Hono } from "hono" + +import { auth } from "./index.ts" + +export function registerAuthHandlers(app: Hono): void { + app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw)) +} diff --git a/apps/aris-backend/src/auth/index.ts b/apps/aris-backend/src/auth/index.ts new file mode 100644 index 0000000..098d841 --- /dev/null +++ b/apps/aris-backend/src/auth/index.ts @@ -0,0 +1,10 @@ +import { betterAuth } from "better-auth" + +import { pool } from "../db.ts" + +export const auth = betterAuth({ + database: pool, + emailAndPassword: { + enabled: true, + }, +}) diff --git a/apps/aris-backend/src/auth/session-middleware.ts b/apps/aris-backend/src/auth/session-middleware.ts new file mode 100644 index 0000000..85c51d7 --- /dev/null +++ b/apps/aris-backend/src/auth/session-middleware.ts @@ -0,0 +1,54 @@ +import type { Context, Next } from "hono" + +import { auth } from "./index.ts" + +type SessionUser = typeof auth.$Infer.Session.user +type Session = typeof auth.$Infer.Session.session + +export interface SessionVariables { + user: SessionUser | null + session: Session | null +} + +/** + * Middleware that attaches session and user to the context. + * Does not reject unauthenticated requests - use requireSession for that. + */ +export async function sessionMiddleware(c: Context, next: Next): Promise { + const session = await auth.api.getSession({ headers: c.req.raw.headers }) + + if (session) { + c.set("user", session.user) + c.set("session", session.session) + } else { + c.set("user", null) + c.set("session", null) + } + + await next() +} + +/** + * Middleware that requires a valid session. Returns 401 if not authenticated. + */ +export async function requireSession(c: Context, next: Next): Promise { + const session = await auth.api.getSession({ headers: c.req.raw.headers }) + + if (!session) { + return c.json({ error: "Unauthorized" }, 401) + } + + c.set("user", session.user) + c.set("session", session.session) + await next() +} + +/** + * Get session from headers. Useful for WebSocket upgrade validation. + */ +export async function getSessionFromHeaders( + headers: Headers, +): Promise<{ user: SessionUser; session: Session } | null> { + const session = await auth.api.getSession({ headers }) + return session +} diff --git a/apps/aris-backend/src/db.ts b/apps/aris-backend/src/db.ts new file mode 100644 index 0000000..b6ec04a --- /dev/null +++ b/apps/aris-backend/src/db.ts @@ -0,0 +1,5 @@ +import { Pool } from "pg" + +export const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}) diff --git a/apps/aris-backend/src/index.ts b/apps/aris-backend/src/server.ts similarity index 65% rename from apps/aris-backend/src/index.ts rename to apps/aris-backend/src/server.ts index 9f30f9b..dd0ed3c 100644 --- a/apps/aris-backend/src/index.ts +++ b/apps/aris-backend/src/server.ts @@ -1,9 +1,13 @@ import { Hono } from "hono" +import { registerAuthHandlers } from "./auth/http.ts" + const app = new Hono() app.get("/health", (c) => c.json({ status: "ok" })) +registerAuthHandlers(app) + export default { port: 3000, fetch: app.fetch, diff --git a/apps/aris-backend/tsconfig.json b/apps/aris-backend/tsconfig.json index a88fedd..0c91d62 100644 --- a/apps/aris-backend/tsconfig.json +++ b/apps/aris-backend/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "../../tsconfig.json", - "compilerOptions": { - "types": ["bun-types"] - }, "include": ["src"] } diff --git a/bun.lock b/bun.lock index 10a722d..16d2d02 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,10 @@ "@aris/source-weatherkit": "workspace:*", "better-auth": "^1", "hono": "^4", - "postgres": "^3", + "pg": "^8", + }, + "devDependencies": { + "@types/pg": "^8", }, }, "packages/aris-core": { @@ -130,6 +133,8 @@ "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], + "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], + "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], "arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="], @@ -154,18 +159,44 @@ "oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="], - "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="], + "pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="], + + "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], + + "pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="], + + "pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "tinypool": ["tinypool@2.0.0", "", {}, "sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], } }