4 Commits

Author SHA1 Message Date
Christopher Mayor
66a2d647bb feat: add Dockerfile and docker-compose.yml for containerized deployment 2026-04-24 14:34:35 -07:00
Christopher Mayor
2c2fd3547c feat: add auth middleware protecting /compare and /profile routes 2026-04-24 14:34:13 -07:00
Christopher Mayor
3568e2f008 feat: update Better Auth config with schema and session expiry 2026-04-24 14:33:54 -07:00
Christopher Mayor
d8ff5f4bb1 feat: add users and sessions tables for Better Auth 2026-04-24 14:33:37 -07:00
5 changed files with 90 additions and 7 deletions

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]

30
docker-compose.yml Normal file
View File

@@ -0,0 +1,30 @@
version: "3.8"
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/comparaison
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: comparaison
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
pgdata:

View File

@@ -1,8 +1,10 @@
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./db"; import { db } from "./db";
import * as schema from "./db/schema";
export const auth = betterAuth({ export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg" }), database: drizzleAdapter(db, { provider: "pg", schema }),
emailAndPassword: { enabled: true }, emailAndPassword: { enabled: true },
session: { expiresIn: 60 * 60 * 24 * 7 },
}); });

View File

@@ -9,6 +9,27 @@ import {
index, index,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: text("id").primaryKey(),
name: text("name"),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false),
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const sessions = pgTable("sessions", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
token: text("token").notNull().unique(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const comparisons = pgTable( export const comparisons = pgTable(
"comparisons", "comparisons",
{ {

View File

@@ -1,20 +1,29 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
const publicPaths = ["/sign-in", "/sign-up", "/api/auth"]; const publicPaths = ["/", "/explore", "/sign-in", "/sign-up", "/api/auth"];
const protectedPaths = ["/compare", "/profile"];
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl; const { pathname } = request.nextUrl;
if (
pathname.startsWith("/_next") ||
pathname.startsWith("/favicon") ||
pathname.includes(".")
) {
return NextResponse.next();
}
const isPublic = publicPaths.some( const isPublic = publicPaths.some(
(path) => pathname === path || pathname.startsWith(path + "/"), (path) => pathname === path || pathname.startsWith(path + "/"),
); );
if (isPublic) { const isProtected = protectedPaths.some(
return NextResponse.next(); (path) => pathname === path || pathname.startsWith(path + "/"),
} );
if (pathname.startsWith("/_next") || pathname.startsWith("/favicon")) { if (isPublic && !isProtected) {
return NextResponse.next(); return NextResponse.next();
} }
@@ -22,7 +31,7 @@ export async function middleware(request: NextRequest) {
headers: request.headers, headers: request.headers,
}); });
if (!session) { if (!session && isProtected) {
const signInUrl = new URL("/sign-in", request.url); const signInUrl = new URL("/sign-in", request.url);
signInUrl.searchParams.set("callbackUrl", pathname); signInUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(signInUrl); return NextResponse.redirect(signInUrl);