forked from cartsnitch/auth
feat: migrate authentication to Better-Auth (Phase 1)
Replace hand-rolled JWT auth with Better-Auth session-based authentication. - Scaffold auth/ Node.js service with Better-Auth, bcrypt password compat, Postgres adapter mapped to existing users table - Add Alembic migration (002) creating sessions, accounts, verifications tables and migrating password hashes to accounts table - Update FastAPI auth dependency to validate sessions via shared DB (supports both cookie and Bearer token) - Remove registration/login/refresh endpoints from API gateway (now handled by Better-Auth service) - Update frontend to use better-auth/react client with httpOnly cookies (no tokens in localStorage or memory) - Rewrite auth store, Login, Register, Dashboard, Settings, ProtectedRoute to use session-based auth - Update all tests to create sessions directly in DB instead of JWT tokens Resolves CAR-27 See plan: CAR-26#document-plan Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Required: Generate with `openssl rand -base64 32`
|
||||
BETTER_AUTH_SECRET=change-me-in-production-min-32-chars!!
|
||||
|
||||
# Base URL of the auth service
|
||||
BETTER_AUTH_URL=http://localhost:3001
|
||||
|
||||
# Shared PostgreSQL database
|
||||
DATABASE_URL=postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch
|
||||
|
||||
# Port the auth service listens on
|
||||
PORT=3001
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
COPY tsconfig.json ./
|
||||
COPY src/ src/
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY --from=builder /app/dist/ dist/
|
||||
USER 101
|
||||
EXPOSE 3001
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@cartsnitch/auth",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"generate": "npx @better-auth/cli generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-auth": "^1.2.0",
|
||||
"pg": "^8.13.0",
|
||||
"bcrypt": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import bcrypt from "bcrypt";
|
||||
import pg from "pg";
|
||||
|
||||
const { Pool } = pg;
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString:
|
||||
process.env.DATABASE_URL ??
|
||||
"postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch",
|
||||
});
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: pool,
|
||||
basePath: "/auth",
|
||||
secret: process.env.BETTER_AUTH_SECRET ?? "change-me-in-production-min-32-chars!!",
|
||||
baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3001",
|
||||
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
minPasswordLength: 8,
|
||||
maxPasswordLength: 128,
|
||||
password: {
|
||||
hash: async (password: string) => {
|
||||
return bcrypt.hash(password, 10);
|
||||
},
|
||||
verify: async (data: { hash: string; password: string }) => {
|
||||
return bcrypt.compare(data.password, data.hash);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||
updateAge: 60 * 60 * 24, // refresh after 1 day
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 5 * 60, // 5-minute cookie cache
|
||||
},
|
||||
},
|
||||
|
||||
user: {
|
||||
modelName: "users",
|
||||
fields: {
|
||||
name: "display_name",
|
||||
emailVerified: "email_verified",
|
||||
image: "image",
|
||||
createdAt: "created_at",
|
||||
updatedAt: "updated_at",
|
||||
},
|
||||
},
|
||||
|
||||
account: {
|
||||
modelName: "accounts",
|
||||
fields: {
|
||||
userId: "user_id",
|
||||
accountId: "account_id",
|
||||
providerId: "provider_id",
|
||||
accessToken: "access_token",
|
||||
refreshToken: "refresh_token",
|
||||
accessTokenExpiresAt: "access_token_expires_at",
|
||||
refreshTokenExpiresAt: "refresh_token_expires_at",
|
||||
idToken: "id_token",
|
||||
createdAt: "created_at",
|
||||
updatedAt: "updated_at",
|
||||
},
|
||||
},
|
||||
|
||||
verification: {
|
||||
modelName: "verifications",
|
||||
fields: {
|
||||
expiresAt: "expires_at",
|
||||
createdAt: "created_at",
|
||||
updatedAt: "updated_at",
|
||||
},
|
||||
},
|
||||
|
||||
trustedOrigins: [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:5173",
|
||||
"https://cartsnitch.com",
|
||||
"https://cartsnitch.farh.net",
|
||||
"https://cartsnitch.dev.farh.net",
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createServer } from "node:http";
|
||||
import { toNodeHandler } from "better-auth/node";
|
||||
import { auth } from "./auth.js";
|
||||
|
||||
const port = parseInt(process.env.PORT ?? "3001", 10);
|
||||
|
||||
const handler = toNodeHandler(auth);
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
// Health check
|
||||
if (req.url === "/health" && req.method === "GET") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ status: "ok" }));
|
||||
return;
|
||||
}
|
||||
|
||||
// All /auth/* routes handled by Better-Auth
|
||||
await handler(req, res);
|
||||
});
|
||||
|
||||
server.listen(port, "0.0.0.0", () => {
|
||||
console.log(`CartSnitch auth service listening on port ${port}`);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user