search console SEO ableitungen
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import NextAuth from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
import NextAuth from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
@@ -1,89 +1,89 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { sendPasswordResetEmail } from '@/lib/email';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Verify CSRF token
|
||||
const csrfCheck = csrfProtection(req);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error || 'Invalid CSRF token' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { email } = body;
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid email format' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Find user by email
|
||||
const user = await db.user.findUnique({
|
||||
where: { email: email.toLowerCase() },
|
||||
});
|
||||
|
||||
// For security, always return success even if email doesn't exist
|
||||
// This prevents email enumeration attacks
|
||||
if (!user) {
|
||||
console.log('Password reset requested for non-existent email:', email);
|
||||
return NextResponse.json(
|
||||
{ message: 'If an account with that email exists, a password reset link has been sent.' },
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
|
||||
// Generate secure random token
|
||||
const resetToken = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// Set token expiration to 1 hour from now
|
||||
const resetExpires = new Date(Date.now() + 3600000); // 1 hour
|
||||
|
||||
// Save token and expiration to database
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
resetPasswordToken: resetToken,
|
||||
resetPasswordExpires: resetExpires,
|
||||
},
|
||||
});
|
||||
|
||||
// Send password reset email
|
||||
try {
|
||||
await sendPasswordResetEmail(email, resetToken);
|
||||
} catch (emailError) {
|
||||
console.error('Error sending password reset email:', emailError);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to send reset email. Please try again later.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'Password reset email sent successfully' },
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in forgot-password route:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'An error occurred. Please try again.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { sendPasswordResetEmail } from '@/lib/email';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Verify CSRF token
|
||||
const csrfCheck = csrfProtection(req);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error || 'Invalid CSRF token' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { email } = body;
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid email format' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Find user by email
|
||||
const user = await db.user.findUnique({
|
||||
where: { email: email.toLowerCase() },
|
||||
});
|
||||
|
||||
// For security, always return success even if email doesn't exist
|
||||
// This prevents email enumeration attacks
|
||||
if (!user) {
|
||||
console.log('Password reset requested for non-existent email:', email);
|
||||
return NextResponse.json(
|
||||
{ message: 'If an account with that email exists, a password reset link has been sent.' },
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
|
||||
// Generate secure random token
|
||||
const resetToken = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// Set token expiration to 1 hour from now
|
||||
const resetExpires = new Date(Date.now() + 3600000); // 1 hour
|
||||
|
||||
// Save token and expiration to database
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
resetPasswordToken: resetToken,
|
||||
resetPasswordExpires: resetExpires,
|
||||
},
|
||||
});
|
||||
|
||||
// Send password reset email
|
||||
try {
|
||||
await sendPasswordResetEmail(email, resetToken);
|
||||
} catch (emailError) {
|
||||
console.error('Error sending password reset email:', emailError);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to send reset email. Please try again later.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'Password reset email sent successfully' },
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in forgot-password route:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'An error occurred. Please try again.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,165 +1,165 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { cookies } from 'next/headers';
|
||||
import { getAuthCookieOptions } from '@/lib/cookieConfig';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const code = searchParams.get('code');
|
||||
|
||||
// If no code, redirect to Google OAuth
|
||||
if (!code) {
|
||||
const googleClientId = process.env.GOOGLE_CLIENT_ID;
|
||||
|
||||
if (!googleClientId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Google Client ID not configured' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/google`;
|
||||
const scope = 'openid email profile';
|
||||
|
||||
const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleClientId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}`;
|
||||
|
||||
return NextResponse.redirect(googleAuthUrl);
|
||||
}
|
||||
|
||||
// Handle callback with code
|
||||
try {
|
||||
const googleClientId = process.env.GOOGLE_CLIENT_ID;
|
||||
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
||||
|
||||
if (!googleClientId || !googleClientSecret) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Google OAuth not configured' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/google`;
|
||||
|
||||
// Exchange code for tokens
|
||||
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
code,
|
||||
client_id: googleClientId,
|
||||
client_secret: googleClientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error('Failed to exchange code for tokens');
|
||||
}
|
||||
|
||||
const tokens = await tokenResponse.json();
|
||||
|
||||
// Get user info from Google
|
||||
const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.access_token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userInfoResponse.ok) {
|
||||
throw new Error('Failed to get user info');
|
||||
}
|
||||
|
||||
const userInfo = await userInfoResponse.json();
|
||||
|
||||
// Check if user exists in database
|
||||
let user = await db.user.findUnique({
|
||||
where: { email: userInfo.email },
|
||||
});
|
||||
|
||||
const isNewUser = !user;
|
||||
|
||||
// Create user if they don't exist
|
||||
if (!user) {
|
||||
user = await db.user.create({
|
||||
data: {
|
||||
email: userInfo.email,
|
||||
name: userInfo.name || userInfo.email.split('@')[0],
|
||||
image: userInfo.picture,
|
||||
emailVerified: new Date(), // Google already verified the email
|
||||
password: null, // OAuth users don't need a password
|
||||
},
|
||||
});
|
||||
|
||||
// Create Account entry for the OAuth provider
|
||||
await db.account.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: 'oauth',
|
||||
provider: 'google',
|
||||
providerAccountId: userInfo.sub || userInfo.id,
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null,
|
||||
token_type: tokens.token_type,
|
||||
scope: tokens.scope,
|
||||
id_token: tokens.id_token,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Update existing account tokens
|
||||
const existingAccount = await db.account.findUnique({
|
||||
where: {
|
||||
provider_providerAccountId: {
|
||||
provider: 'google',
|
||||
providerAccountId: userInfo.sub || userInfo.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingAccount) {
|
||||
await db.account.update({
|
||||
where: { id: existingAccount.id },
|
||||
data: {
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Create Account entry if it doesn't exist
|
||||
await db.account.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: 'oauth',
|
||||
provider: 'google',
|
||||
providerAccountId: userInfo.sub || userInfo.id,
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null,
|
||||
token_type: tokens.token_type,
|
||||
scope: tokens.scope,
|
||||
id_token: tokens.id_token,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Set authentication cookie
|
||||
cookies().set('userId', user.id, getAuthCookieOptions());
|
||||
|
||||
// Redirect to dashboard with tracking params
|
||||
const redirectUrl = new URL(`${process.env.NEXT_PUBLIC_APP_URL}/dashboard`);
|
||||
redirectUrl.searchParams.set('authMethod', 'google');
|
||||
redirectUrl.searchParams.set('isNewUser', isNewUser.toString());
|
||||
|
||||
return NextResponse.redirect(redirectUrl.toString());
|
||||
} catch (error) {
|
||||
console.error('Google OAuth error:', error);
|
||||
return NextResponse.redirect(
|
||||
`${process.env.NEXT_PUBLIC_APP_URL}/login?error=google-signin-failed`
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { cookies } from 'next/headers';
|
||||
import { getAuthCookieOptions } from '@/lib/cookieConfig';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const code = searchParams.get('code');
|
||||
|
||||
// If no code, redirect to Google OAuth
|
||||
if (!code) {
|
||||
const googleClientId = process.env.GOOGLE_CLIENT_ID;
|
||||
|
||||
if (!googleClientId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Google Client ID not configured' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/google`;
|
||||
const scope = 'openid email profile';
|
||||
|
||||
const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleClientId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}`;
|
||||
|
||||
return NextResponse.redirect(googleAuthUrl);
|
||||
}
|
||||
|
||||
// Handle callback with code
|
||||
try {
|
||||
const googleClientId = process.env.GOOGLE_CLIENT_ID;
|
||||
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
||||
|
||||
if (!googleClientId || !googleClientSecret) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Google OAuth not configured' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/google`;
|
||||
|
||||
// Exchange code for tokens
|
||||
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
code,
|
||||
client_id: googleClientId,
|
||||
client_secret: googleClientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error('Failed to exchange code for tokens');
|
||||
}
|
||||
|
||||
const tokens = await tokenResponse.json();
|
||||
|
||||
// Get user info from Google
|
||||
const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.access_token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userInfoResponse.ok) {
|
||||
throw new Error('Failed to get user info');
|
||||
}
|
||||
|
||||
const userInfo = await userInfoResponse.json();
|
||||
|
||||
// Check if user exists in database
|
||||
let user = await db.user.findUnique({
|
||||
where: { email: userInfo.email },
|
||||
});
|
||||
|
||||
const isNewUser = !user;
|
||||
|
||||
// Create user if they don't exist
|
||||
if (!user) {
|
||||
user = await db.user.create({
|
||||
data: {
|
||||
email: userInfo.email,
|
||||
name: userInfo.name || userInfo.email.split('@')[0],
|
||||
image: userInfo.picture,
|
||||
emailVerified: new Date(), // Google already verified the email
|
||||
password: null, // OAuth users don't need a password
|
||||
},
|
||||
});
|
||||
|
||||
// Create Account entry for the OAuth provider
|
||||
await db.account.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: 'oauth',
|
||||
provider: 'google',
|
||||
providerAccountId: userInfo.sub || userInfo.id,
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null,
|
||||
token_type: tokens.token_type,
|
||||
scope: tokens.scope,
|
||||
id_token: tokens.id_token,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Update existing account tokens
|
||||
const existingAccount = await db.account.findUnique({
|
||||
where: {
|
||||
provider_providerAccountId: {
|
||||
provider: 'google',
|
||||
providerAccountId: userInfo.sub || userInfo.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingAccount) {
|
||||
await db.account.update({
|
||||
where: { id: existingAccount.id },
|
||||
data: {
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Create Account entry if it doesn't exist
|
||||
await db.account.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: 'oauth',
|
||||
provider: 'google',
|
||||
providerAccountId: userInfo.sub || userInfo.id,
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null,
|
||||
token_type: tokens.token_type,
|
||||
scope: tokens.scope,
|
||||
id_token: tokens.id_token,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Set authentication cookie
|
||||
cookies().set('userId', user.id, getAuthCookieOptions());
|
||||
|
||||
// Redirect to dashboard with tracking params
|
||||
const redirectUrl = new URL(`${process.env.NEXT_PUBLIC_APP_URL}/dashboard`);
|
||||
redirectUrl.searchParams.set('authMethod', 'google');
|
||||
redirectUrl.searchParams.set('isNewUser', isNewUser.toString());
|
||||
|
||||
return NextResponse.redirect(redirectUrl.toString());
|
||||
} catch (error) {
|
||||
console.error('Google OAuth error:', error);
|
||||
return NextResponse.redirect(
|
||||
`${process.env.NEXT_PUBLIC_APP_URL}/login?error=google-signin-failed`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Verify CSRF token
|
||||
const csrfCheck = csrfProtection(req);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error || 'Invalid CSRF token' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { token, password } = body;
|
||||
|
||||
if (!token || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Token and password are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate password length
|
||||
if (password.length < 8) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Password must be at least 8 characters long' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Find user with this reset token
|
||||
const user = await db.user.findUnique({
|
||||
where: { resetPasswordToken: token },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid or expired reset token' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if token has expired
|
||||
if (!user.resetPasswordExpires || user.resetPasswordExpires < new Date()) {
|
||||
// Clear expired token
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
resetPasswordToken: null,
|
||||
resetPasswordExpires: null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Reset token has expired. Please request a new password reset link.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash the new password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Update user's password and clear reset token
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
resetPasswordToken: null,
|
||||
resetPasswordExpires: null,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Password successfully reset for user:', user.email);
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'Password reset successfully' },
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in reset-password route:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'An error occurred. Please try again.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Verify CSRF token
|
||||
const csrfCheck = csrfProtection(req);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error || 'Invalid CSRF token' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { token, password } = body;
|
||||
|
||||
if (!token || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Token and password are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate password length
|
||||
if (password.length < 8) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Password must be at least 8 characters long' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Find user with this reset token
|
||||
const user = await db.user.findUnique({
|
||||
where: { resetPasswordToken: token },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid or expired reset token' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if token has expired
|
||||
if (!user.resetPasswordExpires || user.resetPasswordExpires < new Date()) {
|
||||
// Clear expired token
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
resetPasswordToken: null,
|
||||
resetPasswordExpires: null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Reset token has expired. Please request a new password reset link.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash the new password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Update user's password and clear reset token
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
resetPasswordToken: null,
|
||||
resetPasswordExpires: null,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Password successfully reset for user:', user.email);
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'Password reset successfully' },
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in reset-password route:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'An error occurred. Please try again.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { cookies } from 'next/headers';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
import { getAuthCookieOptions } from '@/lib/cookieConfig';
|
||||
import { loginSchema, validateRequest } from '@/lib/validationSchemas';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Rate Limiting
|
||||
const clientId = getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.LOGIN);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many login attempts. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validate request body
|
||||
const validation = await validateRequest(loginSchema, body);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(validation.error, { status: 400 });
|
||||
}
|
||||
|
||||
const { email, password } = validation.data;
|
||||
|
||||
// Find user
|
||||
const user = await db.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid email or password' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValid = await bcrypt.compare(password, user.password || '');
|
||||
|
||||
if (!isValid) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid email or password' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Set cookie
|
||||
cookies().set('userId', user.id, getAuthCookieOptions());
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: { id: user.id, email: user.email, name: user.name, plan: user.plan || 'FREE' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { cookies } from 'next/headers';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
import { getAuthCookieOptions } from '@/lib/cookieConfig';
|
||||
import { loginSchema, validateRequest } from '@/lib/validationSchemas';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Rate Limiting
|
||||
const clientId = getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.LOGIN);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many login attempts. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validate request body
|
||||
const validation = await validateRequest(loginSchema, body);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(validation.error, { status: 400 });
|
||||
}
|
||||
|
||||
const { email, password } = validation.data;
|
||||
|
||||
// Find user
|
||||
const user = await db.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid email or password' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValid = await bcrypt.compare(password, user.password || '');
|
||||
|
||||
if (!isValid) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid email or password' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Set cookie
|
||||
cookies().set('userId', user.id, getAuthCookieOptions());
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: { id: user.id, email: user.email, name: user.name, plan: user.plan || 'FREE' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getOrCreateCsrfToken } from '@/lib/csrf';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* GET /api/csrf
|
||||
* Returns a CSRF token for the current session
|
||||
*/
|
||||
export async function GET() {
|
||||
const token = getOrCreateCsrfToken();
|
||||
|
||||
return NextResponse.json({ csrfToken: token });
|
||||
}
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getOrCreateCsrfToken } from '@/lib/csrf';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* GET /api/csrf
|
||||
* Returns a CSRF token for the current session
|
||||
*/
|
||||
export async function GET() {
|
||||
const token = getOrCreateCsrfToken();
|
||||
|
||||
return NextResponse.json({ csrfToken: token });
|
||||
}
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { db } from '@/lib/db';
|
||||
import { getAuthCookieOptions } from '@/lib/cookieConfig';
|
||||
|
||||
/**
|
||||
* POST /api/newsletter/admin-login
|
||||
* Simple admin login for newsletter management (no CSRF required)
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email, password } = body;
|
||||
|
||||
// Validate input
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email and password are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// SECURITY: Only allow support@qrmaster.net to access newsletter admin
|
||||
const ALLOWED_ADMIN_EMAIL = 'support@qrmaster.net';
|
||||
const ALLOWED_ADMIN_PASSWORD = 'Timo.16092005';
|
||||
|
||||
if (email.toLowerCase() !== ALLOWED_ADMIN_EMAIL) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Access denied. Only authorized accounts can access this area.' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify password with hardcoded value
|
||||
if (password !== ALLOWED_ADMIN_PASSWORD) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid credentials' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Set auth cookie with a simple session identifier
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
});
|
||||
|
||||
response.cookies.set('newsletter-admin', 'authenticated', getAuthCookieOptions());
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Newsletter admin login error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Login failed. Please try again.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { db } from '@/lib/db';
|
||||
import { getAuthCookieOptions } from '@/lib/cookieConfig';
|
||||
|
||||
/**
|
||||
* POST /api/newsletter/admin-login
|
||||
* Simple admin login for newsletter management (no CSRF required)
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email, password } = body;
|
||||
|
||||
// Validate input
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email and password are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// SECURITY: Only allow support@qrmaster.net to access newsletter admin
|
||||
const ALLOWED_ADMIN_EMAIL = 'support@qrmaster.net';
|
||||
const ALLOWED_ADMIN_PASSWORD = 'Timo.16092005';
|
||||
|
||||
if (email.toLowerCase() !== ALLOWED_ADMIN_EMAIL) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Access denied. Only authorized accounts can access this area.' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify password with hardcoded value
|
||||
if (password !== ALLOWED_ADMIN_PASSWORD) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid credentials' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Set auth cookie with a simple session identifier
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
});
|
||||
|
||||
response.cookies.set('newsletter-admin', 'authenticated', getAuthCookieOptions());
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Newsletter admin login error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Login failed. Please try again.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,163 +1,163 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { sendAIFeatureLaunchEmail } from '@/lib/email';
|
||||
import { rateLimit, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
/**
|
||||
* POST /api/newsletter/broadcast
|
||||
* Send AI feature launch email to all subscribed users
|
||||
* PROTECTED: Only authenticated users can access (you may want to add admin check)
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication using newsletter-admin cookie
|
||||
const adminCookie = cookies().get('newsletter-admin')?.value;
|
||||
|
||||
if (adminCookie !== 'authenticated') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized. Please log in.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Optional: Add admin check here
|
||||
// const user = await db.user.findUnique({ where: { id: userId } });
|
||||
// if (user?.role !== 'ADMIN') {
|
||||
// return NextResponse.json({ error: 'Forbidden. Admin access required.' }, { status: 403 });
|
||||
// }
|
||||
|
||||
// Rate limiting (prevent accidental spam)
|
||||
const rateLimitResult = rateLimit('newsletter-admin', {
|
||||
name: 'newsletter-broadcast',
|
||||
maxRequests: 2, // Only 2 broadcasts per hour
|
||||
windowSeconds: 60 * 60,
|
||||
});
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many broadcast attempts. Please wait before trying again.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000),
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get all subscribed users
|
||||
const subscribers = await db.newsletterSubscription.findMany({
|
||||
where: {
|
||||
status: 'subscribed',
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (subscribers.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'No subscribers found',
|
||||
sent: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Send emails in batches to avoid overwhelming Resend
|
||||
const batchSize = 10;
|
||||
const results = {
|
||||
sent: 0,
|
||||
failed: 0,
|
||||
errors: [] as string[],
|
||||
};
|
||||
|
||||
for (let i = 0; i < subscribers.length; i += batchSize) {
|
||||
const batch = subscribers.slice(i, i + batchSize);
|
||||
|
||||
// Send emails in parallel within batch
|
||||
const promises = batch.map(async (subscriber) => {
|
||||
try {
|
||||
await sendAIFeatureLaunchEmail(subscriber.email);
|
||||
results.sent++;
|
||||
} catch (error) {
|
||||
results.failed++;
|
||||
results.errors.push(`Failed to send to ${subscriber.email}`);
|
||||
console.error(`Failed to send to ${subscriber.email}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
// Small delay between batches to be nice to the email service
|
||||
if (i + batchSize < subscribers.length) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Broadcast completed. Sent to ${results.sent} subscribers.`,
|
||||
sent: results.sent,
|
||||
failed: results.failed,
|
||||
total: subscribers.length,
|
||||
errors: results.errors.length > 0 ? results.errors : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Newsletter broadcast error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to send broadcast emails. Please try again.',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/newsletter/broadcast
|
||||
* Get subscriber count and preview
|
||||
* PROTECTED: Only authenticated users
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication using newsletter-admin cookie
|
||||
const adminCookie = cookies().get('newsletter-admin')?.value;
|
||||
|
||||
if (adminCookie !== 'authenticated') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized. Please log in.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const subscriberCount = await db.newsletterSubscription.count({
|
||||
where: {
|
||||
status: 'subscribed',
|
||||
},
|
||||
});
|
||||
|
||||
const recentSubscribers = await db.newsletterSubscription.findMany({
|
||||
where: {
|
||||
status: 'subscribed',
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 5,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
total: subscriberCount,
|
||||
recent: recentSubscribers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching subscriber info:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch subscriber information' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { sendAIFeatureLaunchEmail } from '@/lib/email';
|
||||
import { rateLimit, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
/**
|
||||
* POST /api/newsletter/broadcast
|
||||
* Send AI feature launch email to all subscribed users
|
||||
* PROTECTED: Only authenticated users can access (you may want to add admin check)
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication using newsletter-admin cookie
|
||||
const adminCookie = cookies().get('newsletter-admin')?.value;
|
||||
|
||||
if (adminCookie !== 'authenticated') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized. Please log in.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Optional: Add admin check here
|
||||
// const user = await db.user.findUnique({ where: { id: userId } });
|
||||
// if (user?.role !== 'ADMIN') {
|
||||
// return NextResponse.json({ error: 'Forbidden. Admin access required.' }, { status: 403 });
|
||||
// }
|
||||
|
||||
// Rate limiting (prevent accidental spam)
|
||||
const rateLimitResult = rateLimit('newsletter-admin', {
|
||||
name: 'newsletter-broadcast',
|
||||
maxRequests: 2, // Only 2 broadcasts per hour
|
||||
windowSeconds: 60 * 60,
|
||||
});
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many broadcast attempts. Please wait before trying again.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000),
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get all subscribed users
|
||||
const subscribers = await db.newsletterSubscription.findMany({
|
||||
where: {
|
||||
status: 'subscribed',
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (subscribers.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'No subscribers found',
|
||||
sent: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Send emails in batches to avoid overwhelming Resend
|
||||
const batchSize = 10;
|
||||
const results = {
|
||||
sent: 0,
|
||||
failed: 0,
|
||||
errors: [] as string[],
|
||||
};
|
||||
|
||||
for (let i = 0; i < subscribers.length; i += batchSize) {
|
||||
const batch = subscribers.slice(i, i + batchSize);
|
||||
|
||||
// Send emails in parallel within batch
|
||||
const promises = batch.map(async (subscriber) => {
|
||||
try {
|
||||
await sendAIFeatureLaunchEmail(subscriber.email);
|
||||
results.sent++;
|
||||
} catch (error) {
|
||||
results.failed++;
|
||||
results.errors.push(`Failed to send to ${subscriber.email}`);
|
||||
console.error(`Failed to send to ${subscriber.email}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
// Small delay between batches to be nice to the email service
|
||||
if (i + batchSize < subscribers.length) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Broadcast completed. Sent to ${results.sent} subscribers.`,
|
||||
sent: results.sent,
|
||||
failed: results.failed,
|
||||
total: subscribers.length,
|
||||
errors: results.errors.length > 0 ? results.errors : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Newsletter broadcast error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to send broadcast emails. Please try again.',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/newsletter/broadcast
|
||||
* Get subscriber count and preview
|
||||
* PROTECTED: Only authenticated users
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Check authentication using newsletter-admin cookie
|
||||
const adminCookie = cookies().get('newsletter-admin')?.value;
|
||||
|
||||
if (adminCookie !== 'authenticated') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized. Please log in.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const subscriberCount = await db.newsletterSubscription.count({
|
||||
where: {
|
||||
status: 'subscribed',
|
||||
},
|
||||
});
|
||||
|
||||
const recentSubscribers = await db.newsletterSubscription.findMany({
|
||||
where: {
|
||||
status: 'subscribed',
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 5,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
total: subscriberCount,
|
||||
recent: recentSubscribers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching subscriber info:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch subscriber information' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +1,91 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { newsletterSubscribeSchema, validateRequest } from '@/lib/validationSchemas';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
import { sendNewsletterWelcomeEmail } from '@/lib/email';
|
||||
|
||||
/**
|
||||
* POST /api/newsletter/subscribe
|
||||
* Subscribe to AI features newsletter
|
||||
* Public endpoint - no authentication required
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get client identifier for rate limiting
|
||||
const clientId = getClientIdentifier(request);
|
||||
|
||||
// Apply rate limiting (5 per hour)
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.NEWSLETTER_SUBSCRIBE);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many subscription attempts. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000),
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
'Retry-After': Math.ceil((rateLimitResult.reset - Date.now()) / 1000).toString(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Parse and validate request body
|
||||
const body = await request.json();
|
||||
const validation = await validateRequest(newsletterSubscribeSchema, body);
|
||||
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(validation.error, { status: 400 });
|
||||
}
|
||||
|
||||
const { email } = validation.data;
|
||||
|
||||
// Check if email already subscribed
|
||||
const existing = await db.newsletterSubscription.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// If already subscribed, return success (idempotent)
|
||||
// Don't reveal if email exists for privacy
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Successfully subscribed to AI features newsletter!',
|
||||
alreadySubscribed: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Create new subscription
|
||||
await db.newsletterSubscription.create({
|
||||
data: {
|
||||
email,
|
||||
source: 'ai-coming-soon',
|
||||
status: 'subscribed',
|
||||
},
|
||||
});
|
||||
|
||||
// Send welcome email (don't block response)
|
||||
sendNewsletterWelcomeEmail(email).catch((error) => {
|
||||
console.error('Failed to send welcome email (non-blocking):', error);
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Successfully subscribed to AI features newsletter!',
|
||||
alreadySubscribed: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Newsletter subscription error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to subscribe to newsletter. Please try again.',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { newsletterSubscribeSchema, validateRequest } from '@/lib/validationSchemas';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
import { sendNewsletterWelcomeEmail } from '@/lib/email';
|
||||
|
||||
/**
|
||||
* POST /api/newsletter/subscribe
|
||||
* Subscribe to AI features newsletter
|
||||
* Public endpoint - no authentication required
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get client identifier for rate limiting
|
||||
const clientId = getClientIdentifier(request);
|
||||
|
||||
// Apply rate limiting (5 per hour)
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.NEWSLETTER_SUBSCRIBE);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many subscription attempts. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000),
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
'Retry-After': Math.ceil((rateLimitResult.reset - Date.now()) / 1000).toString(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Parse and validate request body
|
||||
const body = await request.json();
|
||||
const validation = await validateRequest(newsletterSubscribeSchema, body);
|
||||
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(validation.error, { status: 400 });
|
||||
}
|
||||
|
||||
const { email } = validation.data;
|
||||
|
||||
// Check if email already subscribed
|
||||
const existing = await db.newsletterSubscription.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// If already subscribed, return success (idempotent)
|
||||
// Don't reveal if email exists for privacy
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Successfully subscribed to AI features newsletter!',
|
||||
alreadySubscribed: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Create new subscription
|
||||
await db.newsletterSubscription.create({
|
||||
data: {
|
||||
email,
|
||||
source: 'ai-coming-soon',
|
||||
status: 'subscribed',
|
||||
},
|
||||
});
|
||||
|
||||
// Send welcome email (don't block response)
|
||||
sendNewsletterWelcomeEmail(email).catch((error) => {
|
||||
console.error('Failed to send welcome email (non-blocking):', error);
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Successfully subscribed to AI features newsletter!',
|
||||
alreadySubscribed: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Newsletter subscription error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to subscribe to newsletter. Please try again.',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,205 +1,205 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { z } from 'zod';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
const updateQRSchema = z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
content: z.any().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
style: z.any().optional(),
|
||||
});
|
||||
|
||||
// GET /api/qrs/[id] - Get a single QR code
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const qrCode = await db.qRCode.findFirst({
|
||||
where: {
|
||||
id: params.id,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
scans: {
|
||||
orderBy: { ts: 'desc' },
|
||||
take: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!qrCode) {
|
||||
return NextResponse.json({ error: 'QR code not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(qrCode);
|
||||
} catch (error) {
|
||||
console.error('Error fetching QR code:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/qrs/[id] - Update a QR code
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.QR_MODIFY);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const data = updateQRSchema.parse(body);
|
||||
|
||||
// Check ownership
|
||||
const existing = await db.qRCode.findFirst({
|
||||
where: {
|
||||
id: params.id,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'QR code not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Static QR codes cannot be edited
|
||||
if (existing.type === 'STATIC' && data.content) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Static QR codes cannot be edited' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Update QR code
|
||||
const updated = await db.qRCode.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
...(data.title && { title: data.title }),
|
||||
...(data.content && { content: data.content }),
|
||||
...(data.tags && { tags: data.tags }),
|
||||
...(data.style && { style: data.style }),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input', details: error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.error('Error updating QR code:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/qrs/[id] - Delete a QR code
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.QR_MODIFY);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
const existing = await db.qRCode.findFirst({
|
||||
where: {
|
||||
id: params.id,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'QR code not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete QR code (cascades to scans)
|
||||
await db.qRCode.delete({
|
||||
where: { id: params.id },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting QR code:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { z } from 'zod';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
const updateQRSchema = z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
content: z.any().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
style: z.any().optional(),
|
||||
});
|
||||
|
||||
// GET /api/qrs/[id] - Get a single QR code
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const qrCode = await db.qRCode.findFirst({
|
||||
where: {
|
||||
id: params.id,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
scans: {
|
||||
orderBy: { ts: 'desc' },
|
||||
take: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!qrCode) {
|
||||
return NextResponse.json({ error: 'QR code not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(qrCode);
|
||||
} catch (error) {
|
||||
console.error('Error fetching QR code:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/qrs/[id] - Update a QR code
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.QR_MODIFY);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const data = updateQRSchema.parse(body);
|
||||
|
||||
// Check ownership
|
||||
const existing = await db.qRCode.findFirst({
|
||||
where: {
|
||||
id: params.id,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'QR code not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Static QR codes cannot be edited
|
||||
if (existing.type === 'STATIC' && data.content) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Static QR codes cannot be edited' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Update QR code
|
||||
const updated = await db.qRCode.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
...(data.title && { title: data.title }),
|
||||
...(data.content && { content: data.content }),
|
||||
...(data.tags && { tags: data.tags }),
|
||||
...(data.style && { style: data.style }),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid input', details: error.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.error('Error updating QR code:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/qrs/[id] - Delete a QR code
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.QR_MODIFY);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
const existing = await db.qRCode.findFirst({
|
||||
where: {
|
||||
id: params.id,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'QR code not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete QR code (cascades to scans)
|
||||
await db.qRCode.delete({
|
||||
where: { id: params.id },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting QR code:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,60 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.QR_DELETE_ALL);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Delete all QR codes for this user
|
||||
const result = await db.qRCode.deleteMany({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
deletedCount: result.count,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting all QR codes:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.QR_DELETE_ALL);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Delete all QR codes for this user
|
||||
const result = await db.qRCode.deleteMany({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
deletedCount: result.count,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting all QR codes:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +1,93 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { generateSlug } from '@/lib/hash';
|
||||
|
||||
// POST /api/qrs/static - Create a STATIC QR code that contains the direct URL
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { title, contentType, content, tags, style } = body;
|
||||
|
||||
// Generate the actual QR content based on type
|
||||
let qrContent = '';
|
||||
switch (contentType) {
|
||||
case 'URL':
|
||||
qrContent = content.url;
|
||||
break;
|
||||
case 'PHONE':
|
||||
qrContent = `tel:${content.phone}`;
|
||||
break;
|
||||
case 'SMS':
|
||||
qrContent = `sms:${content.phone}${content.message ? `?body=${encodeURIComponent(content.message)}` : ''}`;
|
||||
break;
|
||||
case 'VCARD':
|
||||
qrContent = `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:${content.firstName || ''} ${content.lastName || ''}
|
||||
N:${content.lastName || ''};${content.firstName || ''};;;
|
||||
${content.organization ? `ORG:${content.organization}` : ''}
|
||||
${content.title ? `TITLE:${content.title}` : ''}
|
||||
${content.email ? `EMAIL:${content.email}` : ''}
|
||||
${content.phone ? `TEL:${content.phone}` : ''}
|
||||
END:VCARD`;
|
||||
break;
|
||||
case 'GEO':
|
||||
const lat = content.latitude || 0;
|
||||
const lon = content.longitude || 0;
|
||||
const label = content.label ? `?q=${encodeURIComponent(content.label)}` : '';
|
||||
qrContent = `geo:${lat},${lon}${label}`;
|
||||
break;
|
||||
case 'TEXT':
|
||||
qrContent = content.text;
|
||||
break;
|
||||
case 'WHATSAPP':
|
||||
qrContent = `https://wa.me/${content.phone}${content.message ? `?text=${encodeURIComponent(content.message)}` : ''}`;
|
||||
break;
|
||||
default:
|
||||
qrContent = content.url || 'https://example.com';
|
||||
}
|
||||
|
||||
// Store the QR content in a special field
|
||||
const enrichedContent = {
|
||||
...content,
|
||||
qrContent // This is what the QR code should actually contain
|
||||
};
|
||||
|
||||
// Generate slug
|
||||
const slug = generateSlug(title);
|
||||
|
||||
// Create QR code
|
||||
const qrCode = await db.qRCode.create({
|
||||
data: {
|
||||
userId,
|
||||
title,
|
||||
type: 'STATIC',
|
||||
contentType,
|
||||
content: enrichedContent,
|
||||
tags: tags || [],
|
||||
style: style || {
|
||||
foregroundColor: '#000000',
|
||||
backgroundColor: '#FFFFFF',
|
||||
cornerStyle: 'square',
|
||||
size: 200,
|
||||
},
|
||||
slug,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(qrCode);
|
||||
} catch (error) {
|
||||
console.error('Error creating static QR code:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { generateSlug } from '@/lib/hash';
|
||||
|
||||
// POST /api/qrs/static - Create a STATIC QR code that contains the direct URL
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { title, contentType, content, tags, style } = body;
|
||||
|
||||
// Generate the actual QR content based on type
|
||||
let qrContent = '';
|
||||
switch (contentType) {
|
||||
case 'URL':
|
||||
qrContent = content.url;
|
||||
break;
|
||||
case 'PHONE':
|
||||
qrContent = `tel:${content.phone}`;
|
||||
break;
|
||||
case 'SMS':
|
||||
qrContent = `sms:${content.phone}${content.message ? `?body=${encodeURIComponent(content.message)}` : ''}`;
|
||||
break;
|
||||
case 'VCARD':
|
||||
qrContent = `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:${content.firstName || ''} ${content.lastName || ''}
|
||||
N:${content.lastName || ''};${content.firstName || ''};;;
|
||||
${content.organization ? `ORG:${content.organization}` : ''}
|
||||
${content.title ? `TITLE:${content.title}` : ''}
|
||||
${content.email ? `EMAIL:${content.email}` : ''}
|
||||
${content.phone ? `TEL:${content.phone}` : ''}
|
||||
END:VCARD`;
|
||||
break;
|
||||
case 'GEO':
|
||||
const lat = content.latitude || 0;
|
||||
const lon = content.longitude || 0;
|
||||
const label = content.label ? `?q=${encodeURIComponent(content.label)}` : '';
|
||||
qrContent = `geo:${lat},${lon}${label}`;
|
||||
break;
|
||||
case 'TEXT':
|
||||
qrContent = content.text;
|
||||
break;
|
||||
case 'WHATSAPP':
|
||||
qrContent = `https://wa.me/${content.phone}${content.message ? `?text=${encodeURIComponent(content.message)}` : ''}`;
|
||||
break;
|
||||
default:
|
||||
qrContent = content.url || 'https://example.com';
|
||||
}
|
||||
|
||||
// Store the QR content in a special field
|
||||
const enrichedContent = {
|
||||
...content,
|
||||
qrContent // This is what the QR code should actually contain
|
||||
};
|
||||
|
||||
// Generate slug
|
||||
const slug = generateSlug(title);
|
||||
|
||||
// Create QR code
|
||||
const qrCode = await db.qRCode.create({
|
||||
data: {
|
||||
userId,
|
||||
title,
|
||||
type: 'STATIC',
|
||||
contentType,
|
||||
content: enrichedContent,
|
||||
tags: tags || [],
|
||||
style: style || {
|
||||
foregroundColor: '#000000',
|
||||
backgroundColor: '#FFFFFF',
|
||||
cornerStyle: 'square',
|
||||
size: 200,
|
||||
},
|
||||
slug,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(qrCode);
|
||||
} catch (error) {
|
||||
console.error('Error creating static QR code:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +1,89 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.STRIPE_CANCEL);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user with subscription info
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
stripeSubscriptionId: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Already on free plan
|
||||
if (user.plan === 'FREE') {
|
||||
return NextResponse.json({ error: 'Already on free plan' }, { status: 400 });
|
||||
}
|
||||
|
||||
// No active subscription
|
||||
if (!user.stripeSubscriptionId) {
|
||||
// Just update plan to FREE if somehow plan is not FREE but no subscription
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
plan: 'FREE',
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
},
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
// Cancel the Stripe subscription
|
||||
await stripe.subscriptions.cancel(user.stripeSubscriptionId);
|
||||
|
||||
// Update user plan to FREE
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
plan: 'FREE',
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error canceling subscription:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to cancel subscription' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.STRIPE_CANCEL);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user with subscription info
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
stripeSubscriptionId: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Already on free plan
|
||||
if (user.plan === 'FREE') {
|
||||
return NextResponse.json({ error: 'Already on free plan' }, { status: 400 });
|
||||
}
|
||||
|
||||
// No active subscription
|
||||
if (!user.stripeSubscriptionId) {
|
||||
// Just update plan to FREE if somehow plan is not FREE but no subscription
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
plan: 'FREE',
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
},
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
// Cancel the Stripe subscription
|
||||
await stripe.subscriptions.cancel(user.stripeSubscriptionId);
|
||||
|
||||
// Update user plan to FREE
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
plan: 'FREE',
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error canceling subscription:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to cancel subscription' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get user email from request body (since we're using simple auth, not NextAuth)
|
||||
const { priceId, plan, userEmail } = await request.json();
|
||||
|
||||
if (!userEmail) {
|
||||
return NextResponse.json({ error: 'Unauthorized - No user email provided' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!priceId || !plan) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing priceId or plan' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await db.user.findUnique({
|
||||
where: { email: userEmail },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Create or get Stripe customer
|
||||
let customerId = user.stripeCustomerId;
|
||||
|
||||
if (!customerId) {
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
customerId = customer.id;
|
||||
|
||||
// Update user with Stripe customer ID
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: { stripeCustomerId: customerId },
|
||||
});
|
||||
}
|
||||
|
||||
// Create Stripe Checkout Session
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
plan,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ url: checkoutSession.url });
|
||||
} catch (error) {
|
||||
console.error('Error creating checkout session:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get user email from request body (since we're using simple auth, not NextAuth)
|
||||
const { priceId, plan, userEmail } = await request.json();
|
||||
|
||||
if (!userEmail) {
|
||||
return NextResponse.json({ error: 'Unauthorized - No user email provided' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!priceId || !plan) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing priceId or plan' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await db.user.findUnique({
|
||||
where: { email: userEmail },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Create or get Stripe customer
|
||||
let customerId = user.stripeCustomerId;
|
||||
|
||||
if (!customerId) {
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
customerId = customer.id;
|
||||
|
||||
// Update user with Stripe customer ID
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: { stripeCustomerId: customerId },
|
||||
});
|
||||
}
|
||||
|
||||
// Create Stripe Checkout Session
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
plan,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ url: checkoutSession.url });
|
||||
} catch (error) {
|
||||
console.error('Error creating checkout session:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { stripe, STRIPE_PLANS } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { cookies } from 'next/headers';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get user from cookie (using userId like other routes)
|
||||
const cookieStore = await cookies();
|
||||
const userId = cookieStore.get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.STRIPE_CHECKOUT);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized - Please log in' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get plan and billing interval from request
|
||||
const { plan, billingInterval = 'month' } = await request.json();
|
||||
|
||||
if (!plan || !['PRO', 'BUSINESS'].includes(plan)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid plan. Must be PRO or BUSINESS' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get the Stripe price ID for the plan
|
||||
const planConfig = STRIPE_PLANS[plan as 'PRO' | 'BUSINESS'];
|
||||
const priceId = billingInterval === 'year' ? planConfig.priceIdYearly : planConfig.priceId;
|
||||
|
||||
if (!priceId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Stripe price ID not configured for this plan' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Create or get Stripe customer
|
||||
let customerId = user.stripeCustomerId;
|
||||
|
||||
if (!customerId) {
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
customerId = customer.id;
|
||||
|
||||
// Update user with Stripe customer ID
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: { stripeCustomerId: customerId },
|
||||
});
|
||||
}
|
||||
|
||||
// Create Stripe Checkout Session
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
plan,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ url: checkoutSession.url });
|
||||
} catch (error) {
|
||||
console.error('Error creating checkout session:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { stripe, STRIPE_PLANS } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { cookies } from 'next/headers';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get user from cookie (using userId like other routes)
|
||||
const cookieStore = await cookies();
|
||||
const userId = cookieStore.get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.STRIPE_CHECKOUT);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized - Please log in' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get plan and billing interval from request
|
||||
const { plan, billingInterval = 'month' } = await request.json();
|
||||
|
||||
if (!plan || !['PRO', 'BUSINESS'].includes(plan)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid plan. Must be PRO or BUSINESS' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get the Stripe price ID for the plan
|
||||
const planConfig = STRIPE_PLANS[plan as 'PRO' | 'BUSINESS'];
|
||||
const priceId = billingInterval === 'year' ? planConfig.priceIdYearly : planConfig.priceId;
|
||||
|
||||
if (!priceId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Stripe price ID not configured for this plan' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Create or get Stripe customer
|
||||
let customerId = user.stripeCustomerId;
|
||||
|
||||
if (!customerId) {
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
customerId = customer.id;
|
||||
|
||||
// Update user with Stripe customer ID
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: { stripeCustomerId: customerId },
|
||||
});
|
||||
}
|
||||
|
||||
// Create Stripe Checkout Session
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
plan,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ url: checkoutSession.url });
|
||||
} catch (error) {
|
||||
console.error('Error creating checkout session:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.STRIPE_PORTAL);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user with Stripe customer ID
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
stripeCustomerId: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// If user doesn't have a Stripe customer ID, they can't access the portal
|
||||
if (!user.stripeCustomerId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No active subscription found' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create Stripe Customer Portal session
|
||||
const portalSession = await stripe.billingPortal.sessions.create({
|
||||
customer: user.stripeCustomerId,
|
||||
return_url: `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/settings`,
|
||||
});
|
||||
|
||||
return NextResponse.json({ url: portalSession.url });
|
||||
} catch (error) {
|
||||
console.error('Error creating portal session:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create portal session' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.STRIPE_PORTAL);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user with Stripe customer ID
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
stripeCustomerId: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// If user doesn't have a Stripe customer ID, they can't access the portal
|
||||
if (!user.stripeCustomerId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No active subscription found' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create Stripe Customer Portal session
|
||||
const portalSession = await stripe.billingPortal.sessions.create({
|
||||
customer: user.stripeCustomerId,
|
||||
return_url: `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/settings`,
|
||||
});
|
||||
|
||||
return NextResponse.json({ url: portalSession.url });
|
||||
} catch (error) {
|
||||
console.error('Error creating portal session:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create portal session' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* Manual sync endpoint to update user subscription from Stripe
|
||||
* Use this if the automatic webhook/verify failed
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Use cookie-based auth
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!user.stripeCustomerId) {
|
||||
return NextResponse.json({ error: 'No Stripe customer ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get all subscriptions for this customer
|
||||
const subscriptions = await stripe.subscriptions.list({
|
||||
customer: user.stripeCustomerId,
|
||||
status: 'active',
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (subscriptions.data.length === 0) {
|
||||
// No active subscription - set to FREE
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
plan: 'FREE',
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan: 'FREE',
|
||||
message: 'No active subscription found, set to FREE plan',
|
||||
});
|
||||
}
|
||||
|
||||
const subscription: any = subscriptions.data[0];
|
||||
|
||||
// Determine plan from price ID
|
||||
const priceId = subscription.items.data[0]?.price?.id;
|
||||
let plan = 'PRO'; // default
|
||||
|
||||
// Check against known price IDs
|
||||
if (priceId === process.env.STRIPE_PRICE_ID_BUSINESS_MONTHLY ||
|
||||
priceId === process.env.STRIPE_PRICE_ID_BUSINESS_YEARLY) {
|
||||
plan = 'BUSINESS';
|
||||
} else if (priceId === process.env.STRIPE_PRICE_ID_PRO_MONTHLY ||
|
||||
priceId === process.env.STRIPE_PRICE_ID_PRO_YEARLY) {
|
||||
plan = 'PRO';
|
||||
}
|
||||
|
||||
// Get current_period_end
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
|
||||
const currentPeriodEnd = periodEndTimestamp
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
console.log('Syncing subscription:', {
|
||||
subscriptionId: subscription.id,
|
||||
priceId,
|
||||
plan,
|
||||
periodEndTimestamp,
|
||||
currentPeriodEnd,
|
||||
});
|
||||
|
||||
// Update user in database
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: priceId,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
plan: plan as any,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan,
|
||||
subscriptionId: subscription.id,
|
||||
currentPeriodEnd,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error syncing subscription:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* Manual sync endpoint to update user subscription from Stripe
|
||||
* Use this if the automatic webhook/verify failed
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Use cookie-based auth
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!user.stripeCustomerId) {
|
||||
return NextResponse.json({ error: 'No Stripe customer ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get all subscriptions for this customer
|
||||
const subscriptions = await stripe.subscriptions.list({
|
||||
customer: user.stripeCustomerId,
|
||||
status: 'active',
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (subscriptions.data.length === 0) {
|
||||
// No active subscription - set to FREE
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
plan: 'FREE',
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan: 'FREE',
|
||||
message: 'No active subscription found, set to FREE plan',
|
||||
});
|
||||
}
|
||||
|
||||
const subscription: any = subscriptions.data[0];
|
||||
|
||||
// Determine plan from price ID
|
||||
const priceId = subscription.items.data[0]?.price?.id;
|
||||
let plan = 'PRO'; // default
|
||||
|
||||
// Check against known price IDs
|
||||
if (priceId === process.env.STRIPE_PRICE_ID_BUSINESS_MONTHLY ||
|
||||
priceId === process.env.STRIPE_PRICE_ID_BUSINESS_YEARLY) {
|
||||
plan = 'BUSINESS';
|
||||
} else if (priceId === process.env.STRIPE_PRICE_ID_PRO_MONTHLY ||
|
||||
priceId === process.env.STRIPE_PRICE_ID_PRO_YEARLY) {
|
||||
plan = 'PRO';
|
||||
}
|
||||
|
||||
// Get current_period_end
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
|
||||
const currentPeriodEnd = periodEndTimestamp
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
console.log('Syncing subscription:', {
|
||||
subscriptionId: subscription.id,
|
||||
priceId,
|
||||
plan,
|
||||
periodEndTimestamp,
|
||||
currentPeriodEnd,
|
||||
});
|
||||
|
||||
// Update user in database
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: priceId,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
plan: plan as any,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan,
|
||||
subscriptionId: subscription.id,
|
||||
currentPeriodEnd,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error syncing subscription:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,97 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Use cookie-based auth instead of NextAuth
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!user.stripeCustomerId) {
|
||||
return NextResponse.json({ error: 'No Stripe customer ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get the most recent checkout session for this customer
|
||||
const checkoutSessions = await stripe.checkout.sessions.list({
|
||||
customer: user.stripeCustomerId,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (checkoutSessions.data.length === 0) {
|
||||
return NextResponse.json({ error: 'No checkout session found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const checkoutSession = checkoutSessions.data[0];
|
||||
|
||||
// Only process if payment was successful
|
||||
if (checkoutSession.payment_status === 'paid' && checkoutSession.subscription) {
|
||||
const subscriptionId = typeof checkoutSession.subscription === 'string'
|
||||
? checkoutSession.subscription
|
||||
: checkoutSession.subscription.id;
|
||||
|
||||
// Retrieve the full subscription object
|
||||
const subscription: any = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
|
||||
// Determine plan from metadata or price ID
|
||||
const plan = checkoutSession.metadata?.plan || 'PRO';
|
||||
|
||||
// Debug log to see the subscription structure
|
||||
console.log('Full subscription object:', JSON.stringify(subscription, null, 2));
|
||||
|
||||
// Get current_period_end - Stripe returns it as a Unix timestamp
|
||||
// Try different possible field names
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
|
||||
const currentPeriodEnd = periodEndTimestamp
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // Default to 30 days from now
|
||||
|
||||
console.log('Subscription data:', {
|
||||
id: subscription.id,
|
||||
periodEndTimestamp,
|
||||
currentPeriodEnd,
|
||||
priceId: subscription.items?.data?.[0]?.price?.id,
|
||||
});
|
||||
|
||||
// Update user in database
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
plan: plan as any,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan,
|
||||
subscriptionId: subscription.id,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Payment not completed' }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('Error verifying session:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Use cookie-based auth instead of NextAuth
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!user.stripeCustomerId) {
|
||||
return NextResponse.json({ error: 'No Stripe customer ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get the most recent checkout session for this customer
|
||||
const checkoutSessions = await stripe.checkout.sessions.list({
|
||||
customer: user.stripeCustomerId,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (checkoutSessions.data.length === 0) {
|
||||
return NextResponse.json({ error: 'No checkout session found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const checkoutSession = checkoutSessions.data[0];
|
||||
|
||||
// Only process if payment was successful
|
||||
if (checkoutSession.payment_status === 'paid' && checkoutSession.subscription) {
|
||||
const subscriptionId = typeof checkoutSession.subscription === 'string'
|
||||
? checkoutSession.subscription
|
||||
: checkoutSession.subscription.id;
|
||||
|
||||
// Retrieve the full subscription object
|
||||
const subscription: any = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
|
||||
// Determine plan from metadata or price ID
|
||||
const plan = checkoutSession.metadata?.plan || 'PRO';
|
||||
|
||||
// Debug log to see the subscription structure
|
||||
console.log('Full subscription object:', JSON.stringify(subscription, null, 2));
|
||||
|
||||
// Get current_period_end - Stripe returns it as a Unix timestamp
|
||||
// Try different possible field names
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
|
||||
const currentPeriodEnd = periodEndTimestamp
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // Default to 30 days from now
|
||||
|
||||
console.log('Subscription data:', {
|
||||
id: subscription.id,
|
||||
periodEndTimestamp,
|
||||
currentPeriodEnd,
|
||||
priceId: subscription.items?.data?.[0]?.price?.id,
|
||||
});
|
||||
|
||||
// Update user in database
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
plan: plan as any,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
plan,
|
||||
subscriptionId: subscription.id,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Payment not completed' }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('Error verifying session:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,116 +1,116 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { headers } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.text();
|
||||
const signature = headers().get('stripe-signature');
|
||||
|
||||
if (!signature) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No signature' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
body,
|
||||
signature,
|
||||
process.env.STRIPE_WEBHOOK_SECRET!
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Webhook signature verification failed:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid signature' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed': {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
|
||||
if (session.mode === 'subscription') {
|
||||
const subscription: any = await stripe.subscriptions.retrieve(
|
||||
session.subscription as string
|
||||
);
|
||||
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
|
||||
const currentPeriodEnd = periodEndTimestamp
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeCustomerId: session.customer as string,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
plan: (session.metadata?.plan || 'FREE') as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.updated': {
|
||||
const subscription: any = event.data.object as Stripe.Subscription;
|
||||
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
|
||||
const currentPeriodEnd = periodEndTimestamp
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.deleted': {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
plan: 'FREE',
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true });
|
||||
} catch (error) {
|
||||
console.error('Error processing webhook:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Webhook processing failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { headers } from 'next/headers';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { db } from '@/lib/db';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.text();
|
||||
const signature = headers().get('stripe-signature');
|
||||
|
||||
if (!signature) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No signature' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
body,
|
||||
signature,
|
||||
process.env.STRIPE_WEBHOOK_SECRET!
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Webhook signature verification failed:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid signature' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed': {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
|
||||
if (session.mode === 'subscription') {
|
||||
const subscription: any = await stripe.subscriptions.retrieve(
|
||||
session.subscription as string
|
||||
);
|
||||
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
|
||||
const currentPeriodEnd = periodEndTimestamp
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeCustomerId: session.customer as string,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
plan: (session.metadata?.plan || 'FREE') as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.updated': {
|
||||
const subscription: any = event.data.object as Stripe.Subscription;
|
||||
|
||||
const periodEndTimestamp = subscription.current_period_end
|
||||
|| subscription.currentPeriodEnd
|
||||
|| subscription.billing_cycle_anchor;
|
||||
|
||||
const currentPeriodEnd = periodEndTimestamp
|
||||
? new Date(periodEndTimestamp * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: currentPeriodEnd,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.deleted': {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db.user.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
stripeSubscriptionId: null,
|
||||
stripePriceId: null,
|
||||
stripeCurrentPeriodEnd: null,
|
||||
plan: 'FREE',
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true });
|
||||
} catch (error) {
|
||||
console.error('Error processing webhook:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Webhook processing failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
database: user,
|
||||
localStorage: 'Check in browser console',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Debug error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
database: user,
|
||||
localStorage: 'Check in browser console',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Debug error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.ACCOUNT_DELETE);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user data including Stripe information
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
stripeSubscriptionId: true,
|
||||
stripeCustomerId: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Cancel Stripe subscription if user has one
|
||||
if (user.stripeSubscriptionId && user.plan !== 'FREE') {
|
||||
try {
|
||||
await stripe.subscriptions.cancel(user.stripeSubscriptionId);
|
||||
} catch (stripeError) {
|
||||
console.error('Error canceling Stripe subscription:', stripeError);
|
||||
// Continue with deletion even if Stripe cancellation fails
|
||||
}
|
||||
}
|
||||
|
||||
// Delete user and all related data (cascading deletes should handle QR codes, scans, etc.)
|
||||
await db.user.delete({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
// Clear auth cookie
|
||||
cookies().delete('userId');
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting account:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.ACCOUNT_DELETE);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user data including Stripe information
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
stripeSubscriptionId: true,
|
||||
stripeCustomerId: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Cancel Stripe subscription if user has one
|
||||
if (user.stripeSubscriptionId && user.plan !== 'FREE') {
|
||||
try {
|
||||
await stripe.subscriptions.cancel(user.stripeSubscriptionId);
|
||||
} catch (stripeError) {
|
||||
console.error('Error canceling Stripe subscription:', stripeError);
|
||||
// Continue with deletion even if Stripe cancellation fails
|
||||
}
|
||||
}
|
||||
|
||||
// Delete user and all related data (cascading deletes should handle QR codes, scans, etc.)
|
||||
await db.user.delete({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
// Clear auth cookie
|
||||
cookies().delete('userId');
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting account:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +1,103 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { changePasswordSchema, validateRequest } from '@/lib/validationSchemas';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.PASSWORD_CHANGE);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validate request body
|
||||
const validation = await validateRequest(changePasswordSchema, body);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(validation.error, { status: 400 });
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = validation.data;
|
||||
|
||||
// Get user with password
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
password: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if user has a password (OAuth users don't have passwords)
|
||||
if (!user.password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot change password for OAuth accounts' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isPasswordValid = await bcrypt.compare(currentPassword, user.password);
|
||||
if (!isPasswordValid) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Current password is incorrect' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// Update password
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: { password: hashedPassword },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error changing password:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { changePasswordSchema, validateRequest } from '@/lib/validationSchemas';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: csrfCheck.error },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.PASSWORD_CHANGE);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validate request body
|
||||
const validation = await validateRequest(changePasswordSchema, body);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(validation.error, { status: 400 });
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = validation.data;
|
||||
|
||||
// Get user with password
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
password: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if user has a password (OAuth users don't have passwords)
|
||||
if (!user.password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot change password for OAuth accounts' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isPasswordValid = await bcrypt.compare(currentPassword, user.password);
|
||||
if (!isPasswordValid) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Current password is incorrect' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// Update password
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: { password: hashedPassword },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error changing password:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { STRIPE_PLANS } from '@/lib/stripe';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Use cookie-based auth instead of NextAuth
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
plan: true,
|
||||
stripeCurrentPeriodEnd: true,
|
||||
stripePriceId: true,
|
||||
stripeCustomerId: true,
|
||||
stripeSubscriptionId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Determine billing interval from stripePriceId
|
||||
let interval: 'month' | 'year' | null = null;
|
||||
|
||||
if (user.stripePriceId) {
|
||||
// Check if the current price ID matches any yearly price ID
|
||||
const isYearly =
|
||||
user.stripePriceId === STRIPE_PLANS.PRO.priceIdYearly ||
|
||||
user.stripePriceId === STRIPE_PLANS.BUSINESS.priceIdYearly;
|
||||
|
||||
interval = isYearly ? 'year' : 'month';
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
plan: user.plan || 'FREE',
|
||||
interval,
|
||||
currentPeriodEnd: user.stripeCurrentPeriodEnd,
|
||||
priceId: user.stripePriceId,
|
||||
stripeCustomerId: user.stripeCustomerId,
|
||||
stripeSubscriptionId: user.stripeSubscriptionId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching user plan:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { STRIPE_PLANS } from '@/lib/stripe';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Use cookie-based auth instead of NextAuth
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
plan: true,
|
||||
stripeCurrentPeriodEnd: true,
|
||||
stripePriceId: true,
|
||||
stripeCustomerId: true,
|
||||
stripeSubscriptionId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Determine billing interval from stripePriceId
|
||||
let interval: 'month' | 'year' | null = null;
|
||||
|
||||
if (user.stripePriceId) {
|
||||
// Check if the current price ID matches any yearly price ID
|
||||
const isYearly =
|
||||
user.stripePriceId === STRIPE_PLANS.PRO.priceIdYearly ||
|
||||
user.stripePriceId === STRIPE_PLANS.BUSINESS.priceIdYearly;
|
||||
|
||||
interval = isYearly ? 'year' : 'month';
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
plan: user.plan || 'FREE',
|
||||
interval,
|
||||
currentPeriodEnd: user.stripeCurrentPeriodEnd,
|
||||
priceId: user.stripePriceId,
|
||||
stripeCustomerId: user.stripeCustomerId,
|
||||
stripeSubscriptionId: user.stripeSubscriptionId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching user plan:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { updateProfileSchema, validateRequest } from '@/lib/validationSchemas';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.PROFILE_UPDATE);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validate request body
|
||||
const validation = await validateRequest(updateProfileSchema, body);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(validation.error, { status: 400 });
|
||||
}
|
||||
|
||||
const { name } = validation.data;
|
||||
|
||||
// Update user name in database
|
||||
const updatedUser = await db.user.update({
|
||||
where: { id: userId },
|
||||
data: { name: name.trim() },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: updatedUser,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { updateProfileSchema, validateRequest } from '@/lib/validationSchemas';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
// CSRF Protection
|
||||
const csrfCheck = csrfProtection(request);
|
||||
if (!csrfCheck.valid) {
|
||||
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
|
||||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
const rateLimitResult = rateLimit(clientId, RateLimits.PROFILE_UPDATE);
|
||||
|
||||
if (!rateLimitResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too many requests. Please try again later.',
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000)
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validate request body
|
||||
const validation = await validateRequest(updateProfileSchema, body);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(validation.error, { status: 400 });
|
||||
}
|
||||
|
||||
const { name } = validation.data;
|
||||
|
||||
// Update user name in database
|
||||
const updatedUser = await db.user.update({
|
||||
where: { id: userId },
|
||||
data: { name: name.trim() },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: updatedUser,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// Force dynamic rendering (required for cookies)
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* GET /api/user
|
||||
* Get current user information
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(user);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
// Force dynamic rendering (required for cookies)
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* GET /api/user
|
||||
* Get current user information
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(user);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user with plan info
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Count dynamic QR codes
|
||||
const dynamicQRCount = await db.qRCode.count({
|
||||
where: {
|
||||
userId,
|
||||
type: 'DYNAMIC',
|
||||
},
|
||||
});
|
||||
|
||||
// Count static QR codes
|
||||
const staticQRCount = await db.qRCode.count({
|
||||
where: {
|
||||
userId,
|
||||
type: 'STATIC',
|
||||
},
|
||||
});
|
||||
|
||||
// Determine limits based on plan
|
||||
let dynamicLimit = 3; // FREE plan default
|
||||
if (user.plan === 'PRO') {
|
||||
dynamicLimit = 50;
|
||||
} else if (user.plan === 'BUSINESS') {
|
||||
dynamicLimit = 500;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
dynamicUsed: dynamicQRCount,
|
||||
dynamicLimit,
|
||||
staticUsed: staticQRCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching user stats:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user with plan info
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Count dynamic QR codes
|
||||
const dynamicQRCount = await db.qRCode.count({
|
||||
where: {
|
||||
userId,
|
||||
type: 'DYNAMIC',
|
||||
},
|
||||
});
|
||||
|
||||
// Count static QR codes
|
||||
const staticQRCount = await db.qRCode.count({
|
||||
where: {
|
||||
userId,
|
||||
type: 'STATIC',
|
||||
},
|
||||
});
|
||||
|
||||
// Determine limits based on plan
|
||||
let dynamicLimit = 3; // FREE plan default
|
||||
if (user.plan === 'PRO') {
|
||||
dynamicLimit = 50;
|
||||
} else if (user.plan === 'BUSINESS') {
|
||||
dynamicLimit = 500;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
dynamicUsed: dynamicQRCount,
|
||||
dynamicLimit,
|
||||
staticUsed: staticQRCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching user stats:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user