search console SEO ableitungen

This commit is contained in:
2026-03-23 19:01:52 -05:00
parent d47108d27c
commit e6b19e7a1c
150 changed files with 26257 additions and 25909 deletions

View File

@@ -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 };

View File

@@ -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 }
);
}
}

View File

@@ -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`
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 });
}
}

View File

@@ -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 });
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}