mirror of
https://github.com/kennethnym/aris.git
synced 2026-02-02 05:01:17 +00:00
Merge pull request #17 from kennethnym/feat/auth
feat(backend): add BetterAuth email/password authentication
This commit is contained in:
8
apps/aris-backend/.env.example
Normal file
8
apps/aris-backend/.env.example
Normal file
@@ -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
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
"name": "@aris/backend",
|
"name": "@aris/backend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/server.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --watch src/index.ts",
|
"dev": "bun run --watch src/server.ts",
|
||||||
"start": "bun run src/index.ts",
|
"start": "bun run src/server.ts",
|
||||||
"test": "bun test src/"
|
"test": "bun test src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
"@aris/source-weatherkit": "workspace:*",
|
"@aris/source-weatherkit": "workspace:*",
|
||||||
"better-auth": "^1",
|
"better-auth": "^1",
|
||||||
"hono": "^4",
|
"hono": "^4",
|
||||||
"postgres": "^3"
|
"pg": "^8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/pg": "^8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
apps/aris-backend/src/auth/http.ts
Normal file
7
apps/aris-backend/src/auth/http.ts
Normal file
@@ -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))
|
||||||
|
}
|
||||||
10
apps/aris-backend/src/auth/index.ts
Normal file
10
apps/aris-backend/src/auth/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { betterAuth } from "better-auth"
|
||||||
|
|
||||||
|
import { pool } from "../db.ts"
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: pool,
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
54
apps/aris-backend/src/auth/session-middleware.ts
Normal file
54
apps/aris-backend/src/auth/session-middleware.ts
Normal file
@@ -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<void> {
|
||||||
|
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<Response | void> {
|
||||||
|
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
|
||||||
|
}
|
||||||
5
apps/aris-backend/src/db.ts
Normal file
5
apps/aris-backend/src/db.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Pool } from "pg"
|
||||||
|
|
||||||
|
export const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
})
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
|
|
||||||
|
import { registerAuthHandlers } from "./auth/http.ts"
|
||||||
|
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
|
|
||||||
app.get("/health", (c) => c.json({ status: "ok" }))
|
app.get("/health", (c) => c.json({ status: "ok" }))
|
||||||
|
|
||||||
|
registerAuthHandlers(app)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
|
||||||
"types": ["bun-types"]
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
35
bun.lock
35
bun.lock
@@ -22,7 +22,10 @@
|
|||||||
"@aris/source-weatherkit": "workspace:*",
|
"@aris/source-weatherkit": "workspace:*",
|
||||||
"better-auth": "^1",
|
"better-auth": "^1",
|
||||||
"hono": "^4",
|
"hono": "^4",
|
||||||
"postgres": "^3",
|
"pg": "^8",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/pg": "^8",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/aris-core": {
|
"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/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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||||
|
|
||||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
"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=="],
|
"tinypool": ["tinypool@2.0.0", "", {}, "sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"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=="],
|
"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=="],
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user