Skip to content

Commit 12f242f

Browse files
committed
feat: implement real database authentication with password hashing (Phase 3)
Authentication System: - Install bcrypt for secure password hashing (12 salt rounds) - Update User model with password hashing methods - Add setPassword() and verifyPassword() methods to User model - Implement database-backed authentication in auth.ts - Maintain environment-based admin fallback for initial setup - Check for banned users during authentication User Registration: - Create /api/auth/register endpoint with validation - Password requirements: min 8 chars, letters + numbers - Email format validation - Check for duplicate accounts - Create /register page with full registration UI - Redirect to login after successful registration Security Improvements: - Password hash never returned in JSON responses - passwordHash field excluded from queries by default (select: false) - Add emailVerified field for future email verification - Support both password-based and OAuth users (passwordHash optional) Data Privacy: - Remove hardcoded personal info from lib/variables.ts - Remove hardcoded personal info from lib/db/services/variableService.ts - All default variables now have empty values - Users must set their own personal information Database Schema: - Add passwordHash field to User model - Add emailVerified field to User model - Password hash stored with bcrypt (12 rounds) This completes Phase 3 of the authentication refactor.
1 parent 2cdc9f5 commit 12f242f

File tree

9 files changed

+456
-84
lines changed

9 files changed

+456
-84
lines changed

app/api/auth/register/route.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* User Registration API - App Router
3+
* Allows new users to create accounts
4+
*/
5+
6+
import { NextRequest, NextResponse } from 'next/server';
7+
import { connectDB } from '@/lib/db/connection';
8+
import User from '@/lib/db/models/User';
9+
10+
/**
11+
* POST /api/auth/register
12+
* Register a new user account
13+
*/
14+
export async function POST(req: NextRequest) {
15+
try {
16+
const body = await req.json();
17+
const { email, password, name } = body;
18+
19+
// Validation
20+
if (!email || !password) {
21+
return NextResponse.json(
22+
{ error: 'Email and password are required' },
23+
{ status: 400 }
24+
);
25+
}
26+
27+
// Validate email format
28+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
29+
if (!emailRegex.test(email)) {
30+
return NextResponse.json(
31+
{ error: 'Invalid email format' },
32+
{ status: 400 }
33+
);
34+
}
35+
36+
// Validate password strength
37+
if (password.length < 8) {
38+
return NextResponse.json(
39+
{ error: 'Password must be at least 8 characters long' },
40+
{ status: 400 }
41+
);
42+
}
43+
44+
// Check password complexity (at least one number and one letter)
45+
const hasNumber = /\d/.test(password);
46+
const hasLetter = /[a-zA-Z]/.test(password);
47+
48+
if (!hasNumber || !hasLetter) {
49+
return NextResponse.json(
50+
{ error: 'Password must contain both letters and numbers' },
51+
{ status: 400 }
52+
);
53+
}
54+
55+
await connectDB();
56+
57+
// Check if user already exists
58+
const existingUser = await User.findOne({ email: email.toLowerCase() });
59+
60+
if (existingUser) {
61+
return NextResponse.json(
62+
{ error: 'An account with this email already exists' },
63+
{ status: 400 }
64+
);
65+
}
66+
67+
// Create new user
68+
const user = new User({
69+
email: email.toLowerCase(),
70+
name: name || undefined,
71+
isAdmin: false,
72+
role: 'user',
73+
banned: false,
74+
});
75+
76+
// Hash and set password
77+
await user.setPassword(password);
78+
79+
// Save user to database
80+
await user.save();
81+
82+
return NextResponse.json(
83+
{
84+
message: 'Account created successfully',
85+
user: {
86+
id: user._id,
87+
email: user.email,
88+
name: user.name,
89+
},
90+
},
91+
{ status: 201 }
92+
);
93+
} catch (error: any) {
94+
console.error('Registration error:', error);
95+
96+
// Handle duplicate key error (email already exists)
97+
if (error.code === 11000) {
98+
return NextResponse.json(
99+
{ error: 'An account with this email already exists' },
100+
{ status: 400 }
101+
);
102+
}
103+
104+
return NextResponse.json(
105+
{ error: 'Failed to create account. Please try again.' },
106+
{ status: 500 }
107+
);
108+
}
109+
}

app/register/page.tsx

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { useRouter } from 'next/navigation';
5+
import Link from 'next/link';
6+
import { Button } from '@/components/ui/button';
7+
import { Input } from '@/components/ui/input';
8+
import { Label } from '@/components/ui/label';
9+
import { Alert, AlertDescription } from '@/components/ui/alert';
10+
11+
export default function RegisterPage() {
12+
const router = useRouter();
13+
const [formData, setFormData] = useState({
14+
name: '',
15+
email: '',
16+
password: '',
17+
confirmPassword: '',
18+
});
19+
const [error, setError] = useState('');
20+
const [loading, setLoading] = useState(false);
21+
22+
const handleSubmit = async (e: React.FormEvent) => {
23+
e.preventDefault();
24+
setError('');
25+
26+
// Client-side validation
27+
if (!formData.email || !formData.password) {
28+
setError('Email and password are required');
29+
return;
30+
}
31+
32+
if (formData.password !== formData.confirmPassword) {
33+
setError('Passwords do not match');
34+
return;
35+
}
36+
37+
if (formData.password.length < 8) {
38+
setError('Password must be at least 8 characters long');
39+
return;
40+
}
41+
42+
setLoading(true);
43+
44+
try {
45+
const response = await fetch('/api/auth/register', {
46+
method: 'POST',
47+
headers: {
48+
'Content-Type': 'application/json',
49+
},
50+
body: JSON.stringify({
51+
email: formData.email,
52+
password: formData.password,
53+
name: formData.name,
54+
}),
55+
});
56+
57+
const data = await response.json();
58+
59+
if (!response.ok) {
60+
throw new Error(data.error || 'Registration failed');
61+
}
62+
63+
// Registration successful, redirect to login
64+
router.push('/login?registered=true');
65+
} catch (err: any) {
66+
setError(err.message || 'Failed to create account');
67+
} finally {
68+
setLoading(false);
69+
}
70+
};
71+
72+
return (
73+
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 dark:bg-gray-900 sm:px-6 lg:px-8">
74+
<div className="w-full max-w-md space-y-8">
75+
<div>
76+
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
77+
Create your account
78+
</h2>
79+
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
80+
Or{' '}
81+
<Link href="/login" className="font-medium text-primary hover:underline">
82+
sign in to your existing account
83+
</Link>
84+
</p>
85+
</div>
86+
87+
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
88+
{error && (
89+
<Alert variant="destructive">
90+
<AlertDescription>{error}</AlertDescription>
91+
</Alert>
92+
)}
93+
94+
<div className="space-y-4 rounded-md shadow-sm">
95+
<div>
96+
<Label htmlFor="name">Name (optional)</Label>
97+
<Input
98+
id="name"
99+
name="name"
100+
type="text"
101+
autoComplete="name"
102+
value={formData.name}
103+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
104+
placeholder="Your name"
105+
className="mt-1"
106+
/>
107+
</div>
108+
109+
<div>
110+
<Label htmlFor="email">Email address *</Label>
111+
<Input
112+
id="email"
113+
name="email"
114+
type="email"
115+
autoComplete="email"
116+
required
117+
value={formData.email}
118+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
119+
placeholder="[email protected]"
120+
className="mt-1"
121+
/>
122+
</div>
123+
124+
<div>
125+
<Label htmlFor="password">Password *</Label>
126+
<Input
127+
id="password"
128+
name="password"
129+
type="password"
130+
autoComplete="new-password"
131+
required
132+
value={formData.password}
133+
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
134+
placeholder="At least 8 characters"
135+
className="mt-1"
136+
/>
137+
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
138+
Must be at least 8 characters with letters and numbers
139+
</p>
140+
</div>
141+
142+
<div>
143+
<Label htmlFor="confirmPassword">Confirm Password *</Label>
144+
<Input
145+
id="confirmPassword"
146+
name="confirmPassword"
147+
type="password"
148+
autoComplete="new-password"
149+
required
150+
value={formData.confirmPassword}
151+
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
152+
placeholder="Confirm your password"
153+
className="mt-1"
154+
/>
155+
</div>
156+
</div>
157+
158+
<div>
159+
<Button type="submit" className="w-full" disabled={loading}>
160+
{loading ? 'Creating account...' : 'Create account'}
161+
</Button>
162+
</div>
163+
164+
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
165+
By creating an account, you agree to our terms of service and privacy policy.
166+
</div>
167+
</form>
168+
</div>
169+
</div>
170+
);
171+
}

lib/auth.ts

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { NextAuthOptions } from 'next-auth';
22
import Credentials from 'next-auth/providers/credentials';
3+
import { connectDB } from '@/lib/db/connection';
4+
import User from '@/lib/db/models/User';
35

46
export const authOptions: NextAuthOptions = {
57
session: { strategy: 'jwt' },
@@ -14,28 +16,63 @@ export const authOptions: NextAuthOptions = {
1416
const email = credentials?.email?.toLowerCase();
1517
const password = credentials?.password || '';
1618

17-
// Environment-based admin authentication
18-
const adminEmail = process.env.ADMIN_EMAIL?.toLowerCase();
19-
const adminPassword = process.env.ADMIN_PASSWORD;
20-
21-
// Validate that admin credentials are properly configured
22-
if (!adminEmail || !adminPassword) {
23-
console.error('❌ ADMIN_EMAIL and ADMIN_PASSWORD must be set in environment variables');
19+
if (!email || !password) {
2420
return null;
2521
}
2622

27-
// Enforce minimum password length for security
28-
if (adminPassword.length < 16) {
29-
console.error('❌ ADMIN_PASSWORD must be at least 16 characters long');
30-
return null;
31-
}
23+
try {
24+
// Connect to database
25+
await connectDB();
3226

33-
// Verify credentials
34-
if (email && password && email === adminEmail && password === adminPassword) {
35-
return { id: 'admin', name: 'Admin', email: adminEmail, isAdmin: true } as any;
36-
}
27+
// Try to find user in database (include passwordHash for verification)
28+
const user = await User.findOne({ email }).select('+passwordHash');
29+
30+
if (user) {
31+
// Check if user is banned
32+
if (user.banned) {
33+
console.error('❌ User is banned:', email);
34+
return null;
35+
}
36+
37+
// If user has a password hash, verify it
38+
if (user.passwordHash) {
39+
const isValid = await user.verifyPassword(password);
3740

38-
return null;
41+
if (isValid) {
42+
return {
43+
id: user._id.toString(),
44+
email: user.email,
45+
name: user.name,
46+
isAdmin: user.isAdmin,
47+
} as any;
48+
}
49+
}
50+
}
51+
52+
// Fallback: Environment-based admin authentication
53+
// This is a backup method for initial setup
54+
const adminEmail = process.env.ADMIN_EMAIL?.toLowerCase();
55+
const adminPassword = process.env.ADMIN_PASSWORD;
56+
57+
if (adminEmail && adminPassword && adminPassword.length >= 16) {
58+
if (email === adminEmail && password === adminPassword) {
59+
// Return a special admin session
60+
// Note: This won't have a real user ID in the database
61+
return {
62+
id: 'env-admin',
63+
name: 'Admin',
64+
email: adminEmail,
65+
isAdmin: true,
66+
} as any;
67+
}
68+
}
69+
70+
// Authentication failed
71+
return null;
72+
} catch (error) {
73+
console.error('Authentication error:', error);
74+
return null;
75+
}
3976
},
4077
}),
4178
],

0 commit comments

Comments
 (0)