initial commit
This commit is contained in:
120
app/api/auth/forgot-password/route.ts
Normal file
120
app/api/auth/forgot-password/route.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
const ForgotPasswordSchema = z.object({
|
||||
email: z.string().email('Please enter a valid email address'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate the request body
|
||||
const validatedData = ForgotPasswordSchema.parse(body)
|
||||
|
||||
// Simulate processing delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Dummy API - Always return an error for demonstration
|
||||
// You can change this behavior for testing different scenarios
|
||||
|
||||
const email = validatedData.email
|
||||
|
||||
// Simulate different error scenarios based on email
|
||||
if (email === 'test@example.com') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Email address not found in our system',
|
||||
code: 'EMAIL_NOT_FOUND'
|
||||
}
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
if (email.includes('blocked')) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'This email address has been temporarily blocked',
|
||||
code: 'EMAIL_BLOCKED'
|
||||
}
|
||||
},
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
|
||||
if (email.includes('invalid')) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid email format',
|
||||
code: 'INVALID_EMAIL'
|
||||
}
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Default error response (500 Internal Server Error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Unable to process password reset request at this time. Please try again later.',
|
||||
code: 'SERVER_ERROR'
|
||||
}
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
// Uncomment below for success response (when you want to test success state)
|
||||
/*
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Password reset email sent successfully',
|
||||
data: {
|
||||
email: validatedData.email,
|
||||
resetTokenExpiry: Date.now() + 3600000 // 1 hour from now
|
||||
}
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
*/
|
||||
|
||||
} catch (error) {
|
||||
console.error('Forgot password API error:', error)
|
||||
|
||||
// Handle validation errors
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid request data',
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: error.issues
|
||||
}
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Handle other errors
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'An unexpected error occurred',
|
||||
code: 'INTERNAL_ERROR'
|
||||
}
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
113
app/api/auth/google/callback/route.ts
Normal file
113
app/api/auth/google/callback/route.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getGoogleUser } from '@/lib/google-oauth'
|
||||
import { generateTokens } from '@/lib/jwt'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { appConfig } from '@/lib/env'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const code = searchParams.get('code')
|
||||
const state = searchParams.get('state')
|
||||
const error = searchParams.get('error')
|
||||
|
||||
// Handle OAuth errors
|
||||
if (error) {
|
||||
return NextResponse.redirect(
|
||||
new URL(`/auth?error=${encodeURIComponent(error)}`, appConfig.appUrl)
|
||||
)
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!code || state !== 'google_oauth') {
|
||||
return NextResponse.redirect(new URL('/auth?error=invalid_oauth_callback', appConfig.appUrl))
|
||||
}
|
||||
|
||||
// Get user info from Google
|
||||
const googleUser = await getGoogleUser(code)
|
||||
|
||||
// Connect to database
|
||||
await connectDB()
|
||||
|
||||
// Check if user exists
|
||||
let user = await User.findOne({
|
||||
$or: [
|
||||
{ email: googleUser.email.toLowerCase() },
|
||||
{ providerId: googleUser.id, provider: 'google' },
|
||||
],
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
// Create new user
|
||||
user = new User({
|
||||
email: googleUser.email.toLowerCase(),
|
||||
name: googleUser.name,
|
||||
provider: 'google',
|
||||
providerId: googleUser.id,
|
||||
avatar: googleUser.picture || undefined,
|
||||
isVerified: googleUser.verified_email,
|
||||
lastLogin: new Date(),
|
||||
})
|
||||
} else {
|
||||
// Update existing user with Google info (preserve original provider)
|
||||
// Only link Google if user doesn't already have a local account
|
||||
if (user.provider !== 'local') {
|
||||
user.provider = 'google'
|
||||
user.providerId = googleUser.id
|
||||
} else {
|
||||
// For local users, just add Google info without changing provider
|
||||
if (!user.providerId) {
|
||||
user.providerId = googleUser.id
|
||||
}
|
||||
}
|
||||
|
||||
// Update other info safely
|
||||
user.name = googleUser.name
|
||||
user.isVerified = googleUser.verified_email || user.isVerified
|
||||
user.lastLogin = new Date()
|
||||
|
||||
// Update avatar only if user doesn't have one
|
||||
if (googleUser.picture && !user.avatar) {
|
||||
user.avatar = googleUser.picture
|
||||
}
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const { accessToken, refreshToken } = generateTokens({
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
})
|
||||
|
||||
// Update user's refresh token
|
||||
user.refreshToken = refreshToken
|
||||
await user.save()
|
||||
|
||||
// Create redirect response using the public app URL
|
||||
const redirectURL = new URL('/dashboard', appConfig.appUrl)
|
||||
const response = NextResponse.redirect(redirectURL)
|
||||
|
||||
// Set HTTP-only cookies
|
||||
response.cookies.set('accessToken', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 15 * 60, // 15 minutes
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('refreshToken', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Google OAuth callback error:', error)
|
||||
return NextResponse.redirect(new URL('/auth?error=oauth_callback_failed', appConfig.appUrl))
|
||||
}
|
||||
}
|
||||
22
app/api/auth/google/route.ts
Normal file
22
app/api/auth/google/route.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getGoogleAuthURL } from '@/lib/google-oauth'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authURL = getGoogleAuthURL()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { authURL },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Google OAuth URL generation error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Failed to generate Google auth URL', code: 'OAUTH_ERROR' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
98
app/api/auth/login/route.ts
Normal file
98
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { generateTokens } from '@/lib/jwt'
|
||||
|
||||
const LoginSchema = z.object({
|
||||
emailOrId: z.string().min(1, 'Email or Silicon ID is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
rememberMe: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate input
|
||||
const validatedData = LoginSchema.parse(body)
|
||||
|
||||
// Connect to database
|
||||
await connectDB()
|
||||
|
||||
// Find user by email or Silicon ID
|
||||
const emailOrId = validatedData.emailOrId
|
||||
const user = await User.findOne({
|
||||
$or: [{ email: emailOrId.toLowerCase() }, { siliconId: emailOrId }],
|
||||
})
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Invalid credentials', code: 'INVALID_CREDENTIALS' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isPasswordValid = await user.comparePassword(validatedData.password)
|
||||
if (!isPasswordValid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Invalid credentials', code: 'INVALID_CREDENTIALS' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const { accessToken, refreshToken } = generateTokens({
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
})
|
||||
|
||||
// Update user's refresh token and last login
|
||||
user.refreshToken = refreshToken
|
||||
user.lastLogin = new Date()
|
||||
await user.save()
|
||||
|
||||
// Create response with tokens
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: user.toJSON(),
|
||||
accessToken,
|
||||
},
|
||||
})
|
||||
|
||||
// Set HTTP-only cookies
|
||||
response.cookies.set('accessToken', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 15 * 60, // 15 minutes
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('refreshToken', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: error.issues[0].message, code: 'VALIDATION_ERROR' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
62
app/api/auth/logout/route.ts
Normal file
62
app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { verifyRefreshToken } from '@/lib/jwt'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get refresh token from cookie
|
||||
const refreshToken = request.cookies.get('refreshToken')?.value
|
||||
|
||||
if (refreshToken) {
|
||||
// Verify and decode the refresh token to get user ID
|
||||
const payload = verifyRefreshToken(refreshToken)
|
||||
|
||||
if (payload) {
|
||||
// Connect to database and remove refresh token
|
||||
await connectDB()
|
||||
await User.findByIdAndUpdate(payload.userId, {
|
||||
$unset: { refreshToken: 1 },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create response
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
data: { message: 'Logged out successfully' },
|
||||
})
|
||||
|
||||
// Clear cookies
|
||||
response.cookies.set('accessToken', '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 0,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('refreshToken', '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 0,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
|
||||
// Even if there's an error, we should still clear the cookies
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
data: { message: 'Logged out successfully' },
|
||||
})
|
||||
|
||||
response.cookies.set('accessToken', '', { maxAge: 0, path: '/' })
|
||||
response.cookies.set('refreshToken', '', { maxAge: 0, path: '/' })
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
33
app/api/auth/me/route.ts
Normal file
33
app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { withAuth } from '@/lib/auth-middleware'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
|
||||
export const GET = withAuth(async (request: NextRequest & { user?: any }) => {
|
||||
try {
|
||||
// Connect to database
|
||||
await connectDB()
|
||||
|
||||
// Get user details
|
||||
const user = await User.findById(request.user.userId).select('-password -refreshToken')
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'User not found', code: 'USER_NOT_FOUND' } },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { user: user.toJSON() },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get user info error:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
99
app/api/auth/refresh/route.ts
Normal file
99
app/api/auth/refresh/route.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User } from '@/models/user'
|
||||
import { verifyRefreshToken, generateTokens } from '@/lib/jwt'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get refresh token from cookie
|
||||
const refreshToken = request.cookies.get('refreshToken')?.value
|
||||
|
||||
if (!refreshToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'No refresh token provided', code: 'NO_REFRESH_TOKEN' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify refresh token
|
||||
const payload = verifyRefreshToken(refreshToken)
|
||||
if (!payload) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { message: 'Invalid refresh token', code: 'INVALID_REFRESH_TOKEN' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Connect to database and find user
|
||||
await connectDB()
|
||||
const user = await User.findById(payload.userId)
|
||||
|
||||
// Check if user exists and refresh token matches
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'User not found', code: 'USER_NOT_FOUND' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify the stored refresh token matches (both are JWT tokens, so direct comparison is valid)
|
||||
if (user.refreshToken !== refreshToken) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Refresh token mismatch', code: 'TOKEN_MISMATCH' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
const { accessToken, refreshToken: newRefreshToken } = generateTokens({
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
})
|
||||
|
||||
// Update user's refresh token
|
||||
user.refreshToken = newRefreshToken
|
||||
await user.save()
|
||||
|
||||
// Create response with new tokens
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
accessToken,
|
||||
user: user.toJSON(),
|
||||
},
|
||||
})
|
||||
|
||||
// Set new cookies
|
||||
response.cookies.set('accessToken', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 15 * 60, // 15 minutes
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('refreshToken', newRefreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Token refresh error:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
109
app/api/auth/register/route.ts
Normal file
109
app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import connectDB from '@/lib/mongodb'
|
||||
import { User, UserSchema } from '@/models/user'
|
||||
import { generateTokens } from '@/lib/jwt'
|
||||
import { generateSiliconId } from '@/lib/siliconId'
|
||||
|
||||
const RegisterSchema = UserSchema.pick({
|
||||
email: true,
|
||||
name: true,
|
||||
password: true,
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate input
|
||||
const validatedData = RegisterSchema.parse(body)
|
||||
|
||||
// Connect to database
|
||||
await connectDB()
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await User.findOne({ email: validatedData.email.toLowerCase() })
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'User already exists', code: 'USER_EXISTS' } },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate tokens first
|
||||
const tempUser = {
|
||||
email: validatedData.email.toLowerCase(),
|
||||
name: validatedData.name,
|
||||
role: 'user' as const,
|
||||
}
|
||||
|
||||
// Generate unique Silicon ID
|
||||
const siliconId = generateSiliconId()
|
||||
console.log('Generated siliconId:', siliconId)
|
||||
|
||||
// Create new user
|
||||
const user = new User({
|
||||
email: tempUser.email,
|
||||
name: tempUser.name,
|
||||
password: validatedData.password,
|
||||
siliconId: siliconId,
|
||||
provider: 'local',
|
||||
})
|
||||
console.log('User before save:', JSON.stringify(user.toObject(), null, 2))
|
||||
await user.save()
|
||||
console.log('user after save:', user)
|
||||
|
||||
// Generate tokens with actual user ID
|
||||
const { accessToken, refreshToken } = generateTokens({
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
})
|
||||
|
||||
// Update user with refresh token (single save)
|
||||
user.refreshToken = refreshToken
|
||||
await user.save()
|
||||
|
||||
// Create response with tokens
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: user.toJSON(),
|
||||
accessToken,
|
||||
},
|
||||
})
|
||||
|
||||
// Set HTTP-only cookies
|
||||
response.cookies.set('accessToken', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 15 * 60, // 15 minutes
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('refreshToken', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error)
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: error.issues[0].message, code: 'VALIDATION_ERROR' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: { message: 'Internal server error', code: 'INTERNAL_ERROR' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user