diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 409513d5..910ab3be 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -6,6 +6,15 @@ import { isRateLimited, registerFailedAttempt, resetAttempts } from "@/src/lib/r export const { GET } = handlers; function getClientIp(request: NextRequest): string { + // Use Next.js request.ip which provides the actual client IP + // This is more secure than trusting X-Forwarded-For header + const ip = request.ip; + if (ip) { + return ip; + } + + // Fallback to headers only if request.ip is not available + // This may happen in development environments const forwarded = request.headers.get("x-forwarded-for"); if (forwarded) { return forwarded.split(",")[0]?.trim() || "unknown"; diff --git a/docker-compose.yml b/docker-compose.yml index 3223f1ee..d8a81025 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,7 +57,8 @@ services: ports: - "80:80" - "443:443" - - "2019:2019" + # Admin API only exposed on internal network for security + # Web UI accesses via http://caddy:2019 internally environment: # Primary domain for Caddy configuration PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:-caddyproxymanager.com} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index b763de54..742ec32b 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,6 +1,7 @@ import NextAuth, { type DefaultSession } from "next-auth"; import Credentials from "next-auth/providers/credentials"; -import { config } from "./config"; +import bcrypt from "bcryptjs"; +import prisma from "./db"; declare module "next-auth" { interface Session { @@ -15,7 +16,7 @@ declare module "next-auth" { } } -// Simple credentials provider that checks against environment variables +// Credentials provider that checks against hashed passwords in the database function createCredentialsProvider() { return Credentials({ id: "credentials", @@ -32,17 +33,28 @@ function createCredentialsProvider() { return null; } - // Check against environment variables - if (username === config.adminUsername && password === config.adminPassword) { - return { - id: "1", - name: config.adminUsername, - email: `${config.adminUsername}@localhost`, - role: "admin" - }; + // Look up user in database by email (constructed from username) + const email = `${username}@localhost`; + const user = await prisma.user.findUnique({ + where: { email } + }); + + if (!user || user.status !== "active" || !user.passwordHash) { + return null; } - return null; + // Verify password against hashed password in database + const isValidPassword = bcrypt.compareSync(password, user.passwordHash); + if (!isValidPassword) { + return null; + } + + return { + id: user.id.toString(), + name: user.name ?? username, + email: user.email, + role: user.role + }; } }); } diff --git a/src/lib/init-db.ts b/src/lib/init-db.ts index c48a47ba..89f39928 100644 --- a/src/lib/init-db.ts +++ b/src/lib/init-db.ts @@ -1,9 +1,11 @@ +import bcrypt from "bcryptjs"; import prisma, { nowIso } from "./db"; import { config } from "./config"; /** * Ensures the admin user from environment variables exists in the database. * This is called during application startup. + * The password from environment variables is hashed and stored securely. */ export async function ensureAdminUser(): Promise { const adminId = 1; // Must match the hardcoded ID in auth.ts @@ -11,36 +13,39 @@ export async function ensureAdminUser(): Promise { const provider = "credentials"; const subject = config.adminUsername; + // Hash the admin password for secure storage + const passwordHash = bcrypt.hashSync(config.adminPassword, 12); + // Check if admin user already exists const existingUser = await prisma.user.findUnique({ where: { id: adminId } }); if (existingUser) { - // Admin user exists, update if needed - if (existingUser.email !== adminEmail || existingUser.subject !== subject) { - const now = new Date(nowIso()); - await prisma.user.update({ - where: { id: adminId }, - data: { - email: adminEmail, - subject, - updatedAt: now - } - }); - console.log(`Updated admin user: ${config.adminUsername}`); - } + // Admin user exists, update credentials if needed + // Always update password hash to handle password changes in env vars + const now = new Date(nowIso()); + await prisma.user.update({ + where: { id: adminId }, + data: { + email: adminEmail, + subject, + passwordHash, + updatedAt: now + } + }); + console.log(`Updated admin user: ${config.adminUsername}`); return; } - // Create admin user + // Create admin user with hashed password const now = new Date(nowIso()); await prisma.user.create({ data: { id: adminId, email: adminEmail, name: config.adminUsername, - passwordHash: null, // Using environment variable auth, not password hash + passwordHash, // Store hashed password instead of plaintext role: "admin", provider, subject,