Remove .env.example and fix auth cookie secure flag
Delete the environment variable example file. Add error handling to the Ollama AI route. Update Docker configuration to support Ollama services and create required image directories. Fix indentation in auth library and allow overriding secure cookie setting in non-production environments.
This commit is contained in:
@@ -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
|
|
||||||
@@ -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
|
||||||
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
|
USER nextjs
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
+16
-3
@@ -4,15 +4,27 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- '3333:3000'
|
- "3333:3000"
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://bikeapp:bikeapp@db:5432/bikeapp
|
DATABASE_URL: postgresql://bikeapp:bikeapp@db:5432/bikeapp
|
||||||
JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production}
|
JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production}
|
||||||
|
OLLAMA_URL: http://ollama:11434
|
||||||
|
COOKIE_SECURE: "false"
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
# ollama:
|
||||||
|
# condition: service_started
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ollama:
|
||||||
|
# image: ollama/ollama:latest
|
||||||
|
# ports:
|
||||||
|
# - "11434:11434"
|
||||||
|
# volumes:
|
||||||
|
# - ollama_data:/root/.ollama
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
environment:
|
environment:
|
||||||
@@ -22,12 +34,13 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- '5432:5432'
|
- "5432:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD-SHELL', 'pg_isready -U bikeapp -d bikeapp']
|
test: ["CMD-SHELL", "pg_isready -U bikeapp -d bikeapp"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
# ollama_data:
|
||||||
|
|||||||
@@ -47,22 +47,30 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
||||||
const model = process.env.OLLAMA_MODEL ?? "llama3.1:8b";
|
const model = process.env.OLLAMA_MODEL ?? "llama3.1:8b";
|
||||||
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
let response: Response;
|
||||||
method: "POST",
|
try {
|
||||||
headers: { "Content-Type": "application/json" },
|
response = await fetch(`${ollamaUrl}/api/chat`, {
|
||||||
body: JSON.stringify({
|
method: "POST",
|
||||||
model,
|
headers: { "Content-Type": "application/json" },
|
||||||
stream: false,
|
body: JSON.stringify({
|
||||||
messages: [
|
model,
|
||||||
{
|
stream: false,
|
||||||
role: "system",
|
messages: [
|
||||||
content:
|
{
|
||||||
"Use metric units plus PSI/clicks. Structure output as Fork, Shock, Tires, Rationale, Next Ride.",
|
role: "system",
|
||||||
},
|
content:
|
||||||
{ role: "user", content: prompt },
|
"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) {
|
if (!response.ok) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
+40
-40
@@ -7,74 +7,74 @@ const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET ?? "dev-secre
|
|||||||
const COOKIE_NAME = "loam-session";
|
const COOKIE_NAME = "loam-session";
|
||||||
|
|
||||||
export type AuthUser = {
|
export type AuthUser = {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function hashPassword(password: string) {
|
export async function hashPassword(password: string) {
|
||||||
return bcrypt.hash(password, 12);
|
return bcrypt.hash(password, 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyPassword(password: string, hash: string) {
|
export async function verifyPassword(password: string, hash: string) {
|
||||||
return bcrypt.compare(password, hash);
|
return bcrypt.compare(password, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createToken(user: AuthUser) {
|
export async function createToken(user: AuthUser) {
|
||||||
return new SignJWT({ id: user.id, email: user.email, name: user.name })
|
return new SignJWT({ id: user.id, email: user.email, name: user.name })
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
.setIssuedAt()
|
.setIssuedAt()
|
||||||
.setExpirationTime("7d")
|
.setExpirationTime("7d")
|
||||||
.sign(JWT_SECRET);
|
.sign(JWT_SECRET);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyToken(token: string): Promise<AuthUser | null> {
|
export async function verifyToken(token: string): Promise<AuthUser | null> {
|
||||||
try {
|
try {
|
||||||
const { payload } = await jwtVerify(token, JWT_SECRET, { clockTolerance: 60 });
|
const { payload } = await jwtVerify(token, JWT_SECRET, { clockTolerance: 60 });
|
||||||
if (typeof payload.id !== "string" || typeof payload.email !== "string") return null;
|
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 };
|
return { id: payload.id, email: payload.email, name: (payload.name as string | null) ?? null };
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCurrentUser(): Promise<AuthUser | null> {
|
export async function getCurrentUser(): Promise<AuthUser | null> {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const token = cookieStore.get(COOKIE_NAME)?.value;
|
const token = cookieStore.get(COOKIE_NAME)?.value;
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
return verifyToken(token);
|
return verifyToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requireAuth(): Promise<AuthUser> {
|
export async function requireAuth(): Promise<AuthUser> {
|
||||||
const user = await getCurrentUser();
|
const user = await getCurrentUser();
|
||||||
if (!user) throw new Error("Unauthorized");
|
if (!user) throw new Error("Unauthorized");
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setSessionCookie(token: string) {
|
export async function setSessionCookie(token: string) {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
cookieStore.set(COOKIE_NAME, token, {
|
cookieStore.set(COOKIE_NAME, token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: process.env.NODE_ENV === "production" && process.env.COOKIE_SECURE !== "false",
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
maxAge: 60 * 60 * 24 * 7,
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
path: "/",
|
path: "/",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearSessionCookie() {
|
export async function clearSessionCookie() {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
cookieStore.delete(COOKIE_NAME);
|
cookieStore.delete(COOKIE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findUserByEmail(email: string) {
|
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) {
|
export async function createUser(email: string, password: string, name?: string) {
|
||||||
const passwordHash = await hashPassword(password);
|
const passwordHash = await hashPassword(password);
|
||||||
const user = await prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
data: { email: email.toLowerCase().trim(), passwordHash, name: name || null },
|
data: { email: email.toLowerCase().trim(), passwordHash, name: name || null },
|
||||||
});
|
});
|
||||||
return { id: user.id, email: user.email, name: user.name };
|
return { id: user.id, email: user.email, name: user.name };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user