diff --git a/.env.example b/.env.example deleted file mode 100644 index 1125f0f..0000000 --- a/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -# Database -DATABASE_URL=postgresql://user:password@localhost:5432/loam - -# Auth (required for user accounts) -JWT_SECRET=change-this-to-a-long-random-string-in-production - -# AI recommendations (optional, defaults shown) -OLLAMA_URL=http://localhost:11434 -OLLAMA_MODEL=llama3.1:8b diff --git a/Dockerfile b/Dockerfile index 2a03026..05e53ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,8 @@ COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma COPY --from=builder /app/node_modules/prisma ./node_modules/prisma +RUN mkdir -p /app/public/images/user && chown -R nextjs:nodejs /app/public + USER nextjs EXPOSE 3000 diff --git a/docker-compose.yml b/docker-compose.yml index ceadaf1..fdaef71 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,15 +4,27 @@ services: context: . dockerfile: Dockerfile ports: - - '3333:3000' + - "3333:3000" environment: DATABASE_URL: postgresql://bikeapp:bikeapp@db:5432/bikeapp JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} + OLLAMA_URL: http://ollama:11434 + COOKIE_SECURE: "false" depends_on: db: condition: service_healthy + # ollama: + # condition: service_started restart: unless-stopped + # ollama: + # image: ollama/ollama:latest + # ports: + # - "11434:11434" + # volumes: + # - ollama_data:/root/.ollama + # restart: unless-stopped + db: image: postgres:16-alpine environment: @@ -22,12 +34,13 @@ services: volumes: - postgres_data:/var/lib/postgresql/data ports: - - '5432:5432' + - "5432:5432" healthcheck: - test: ['CMD-SHELL', 'pg_isready -U bikeapp -d bikeapp'] + test: ["CMD-SHELL", "pg_isready -U bikeapp -d bikeapp"] interval: 5s timeout: 5s retries: 5 volumes: postgres_data: +# ollama_data: diff --git a/src/app/api/ai/recommend/route.ts b/src/app/api/ai/recommend/route.ts index d4076ef..38cc039 100644 --- a/src/app/api/ai/recommend/route.ts +++ b/src/app/api/ai/recommend/route.ts @@ -47,22 +47,30 @@ export async function POST(request: Request) { const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434"; const model = process.env.OLLAMA_MODEL ?? "llama3.1:8b"; - const response = await fetch(`${ollamaUrl}/api/chat`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model, - stream: false, - messages: [ - { - role: "system", - content: - "Use metric units plus PSI/clicks. Structure output as Fork, Shock, Tires, Rationale, Next Ride.", - }, - { role: "user", content: prompt }, - ], - }), - }); + let response: Response; + try { + response = await fetch(`${ollamaUrl}/api/chat`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model, + stream: false, + messages: [ + { + role: "system", + content: + "Use metric units plus PSI/clicks. Structure output as Fork, Shock, Tires, Rationale, Next Ride.", + }, + { role: "user", content: prompt }, + ], + }), + }); + } catch (err) { + return NextResponse.json( + { error: `Unable to reach Ollama at ${ollamaUrl}. Is the Ollama service running?` }, + { status: 503 }, + ); + } if (!response.ok) { return NextResponse.json( diff --git a/src/lib/auth.ts b/src/lib/auth.ts index edcbf9f..d709f0a 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -7,74 +7,74 @@ const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET ?? "dev-secre const COOKIE_NAME = "loam-session"; export type AuthUser = { - id: string; - email: string; - name: string | null; + id: string; + email: string; + name: string | null; }; export async function hashPassword(password: string) { - return bcrypt.hash(password, 12); + return bcrypt.hash(password, 12); } export async function verifyPassword(password: string, hash: string) { - return bcrypt.compare(password, hash); + return bcrypt.compare(password, hash); } export async function createToken(user: AuthUser) { - return new SignJWT({ id: user.id, email: user.email, name: user.name }) - .setProtectedHeader({ alg: "HS256" }) - .setIssuedAt() - .setExpirationTime("7d") - .sign(JWT_SECRET); + return new SignJWT({ id: user.id, email: user.email, name: user.name }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("7d") + .sign(JWT_SECRET); } export async function verifyToken(token: string): Promise { - try { - const { payload } = await jwtVerify(token, JWT_SECRET, { clockTolerance: 60 }); - if (typeof payload.id !== "string" || typeof payload.email !== "string") return null; - return { id: payload.id, email: payload.email, name: (payload.name as string | null) ?? null }; - } catch { - return null; - } + try { + const { payload } = await jwtVerify(token, JWT_SECRET, { clockTolerance: 60 }); + if (typeof payload.id !== "string" || typeof payload.email !== "string") return null; + return { id: payload.id, email: payload.email, name: (payload.name as string | null) ?? null }; + } catch { + return null; + } } export async function getCurrentUser(): Promise { - const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; - if (!token) return null; - return verifyToken(token); + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; + if (!token) return null; + return verifyToken(token); } export async function requireAuth(): Promise { - const user = await getCurrentUser(); - if (!user) throw new Error("Unauthorized"); - return user; + const user = await getCurrentUser(); + if (!user) throw new Error("Unauthorized"); + return user; } export async function setSessionCookie(token: string) { - const cookieStore = await cookies(); - cookieStore.set(COOKIE_NAME, token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - maxAge: 60 * 60 * 24 * 7, - path: "/", - }); + const cookieStore = await cookies(); + cookieStore.set(COOKIE_NAME, token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production" && process.env.COOKIE_SECURE !== "false", + sameSite: "lax", + maxAge: 60 * 60 * 24 * 7, + path: "/", + }); } export async function clearSessionCookie() { - const cookieStore = await cookies(); - cookieStore.delete(COOKIE_NAME); + const cookieStore = await cookies(); + cookieStore.delete(COOKIE_NAME); } export async function findUserByEmail(email: string) { - return prisma.user.findUnique({ where: { email: email.toLowerCase().trim() } }); + return prisma.user.findUnique({ where: { email: email.toLowerCase().trim() } }); } export async function createUser(email: string, password: string, name?: string) { - const passwordHash = await hashPassword(password); - const user = await prisma.user.create({ - data: { email: email.toLowerCase().trim(), passwordHash, name: name || null }, - }); - return { id: user.id, email: user.email, name: user.name }; + const passwordHash = await hashPassword(password); + const user = await prisma.user.create({ + data: { email: email.toLowerCase().trim(), passwordHash, name: name || null }, + }); + return { id: user.id, email: user.email, name: user.name }; }