Commit iniziale
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { cookies } from 'next/headers'
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { prisma } from './prisma'
|
||||
import type { User, Session } from '@prisma/client'
|
||||
|
||||
const COOKIE_NAME = 'session_token'
|
||||
const SESSION_EXPIRY_DAYS = 30
|
||||
const BCRYPT_COST = 12
|
||||
|
||||
export function hashToken(token: string): string {
|
||||
return createHash('sha256').update(token).digest('hex')
|
||||
}
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, BCRYPT_COST)
|
||||
}
|
||||
|
||||
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash)
|
||||
}
|
||||
|
||||
export function validatePasswordStrength(password: string): string | null {
|
||||
if (password.length < 12) return 'Password must be at least 12 characters'
|
||||
if (!/[A-Z]/.test(password)) return 'Password must contain at least one uppercase letter'
|
||||
if (!/[a-z]/.test(password)) return 'Password must contain at least one lowercase letter'
|
||||
if (!/[0-9]/.test(password)) return 'Password must contain at least one number'
|
||||
if (!/[^A-Za-z0-9]/.test(password)) return 'Password must contain at least one symbol'
|
||||
return null
|
||||
}
|
||||
|
||||
export async function createSession(userId: string): Promise<string> {
|
||||
const token = randomBytes(32).toString('hex')
|
||||
const tokenHash = hashToken(token)
|
||||
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000)
|
||||
|
||||
await prisma.session.create({
|
||||
data: {
|
||||
userId,
|
||||
tokenHash,
|
||||
expiresAt,
|
||||
},
|
||||
})
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
export async function setSessionCookie(token: string): Promise<void> {
|
||||
const cookieStore = cookies()
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
cookieStore.set(COOKIE_NAME, token, {
|
||||
httpOnly: true,
|
||||
secure: isProd,
|
||||
sameSite: 'lax',
|
||||
expires: new Date(Date.now() + SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000),
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
|
||||
export async function clearSessionCookie(): Promise<void> {
|
||||
const cookieStore = cookies()
|
||||
cookieStore.delete(COOKIE_NAME)
|
||||
}
|
||||
|
||||
export async function getSessionToken(): Promise<string | null> {
|
||||
const cookieStore = cookies()
|
||||
const cookie = cookieStore.get(COOKIE_NAME)
|
||||
return cookie?.value ?? null
|
||||
}
|
||||
|
||||
export async function getSession(): Promise<(Session & { user: User }) | null> {
|
||||
const token = await getSessionToken()
|
||||
if (!token) return null
|
||||
|
||||
const tokenHash = hashToken(token)
|
||||
|
||||
const session = await prisma.session.findUnique({
|
||||
where: { tokenHash },
|
||||
include: { user: true },
|
||||
})
|
||||
|
||||
if (!session) return null
|
||||
if (session.expiresAt < new Date()) {
|
||||
await prisma.session.delete({ where: { id: session.id } })
|
||||
return null
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
export async function getCurrentUser(): Promise<User | null> {
|
||||
const session = await getSession()
|
||||
return session?.user ?? null
|
||||
}
|
||||
|
||||
export async function deleteSession(token: string): Promise<void> {
|
||||
const tokenHash = hashToken(token)
|
||||
await prisma.session.deleteMany({ where: { tokenHash } })
|
||||
}
|
||||
|
||||
export async function deleteAllUserSessions(userId: string): Promise<void> {
|
||||
await prisma.session.deleteMany({ where: { userId } })
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST || 'localhost',
|
||||
port: parseInt(process.env.SMTP_PORT || '1025'),
|
||||
secure: false,
|
||||
auth:
|
||||
process.env.SMTP_USER
|
||||
? {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASSWORD,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
|
||||
export async function sendEmail({
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
text,
|
||||
}: {
|
||||
to: string
|
||||
subject: string
|
||||
html: string
|
||||
text?: string
|
||||
}): Promise<void> {
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM || 'noreply@localhost',
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
text,
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendOrderConfirmationEmail(
|
||||
to: string,
|
||||
orderDetails: { orderId: string; grandTotal: number; currency: string }
|
||||
): Promise<void> {
|
||||
const amount = (orderDetails.grandTotal / 100).toFixed(2)
|
||||
await sendEmail({
|
||||
to,
|
||||
subject: `Order Confirmation #${orderDetails.orderId}`,
|
||||
html: `
|
||||
<h1>Thank you for your order!</h1>
|
||||
<p>Your order <strong>#${orderDetails.orderId}</strong> has been confirmed.</p>
|
||||
<p>Total: <strong>${amount} ${orderDetails.currency}</strong></p>
|
||||
<p>You can track your order in your <a href="${process.env.APP_URL}/account/orders">account</a>.</p>
|
||||
`,
|
||||
text: `Order #${orderDetails.orderId} confirmed. Total: ${amount} ${orderDetails.currency}`,
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendPasswordResetEmail(
|
||||
to: string,
|
||||
resetToken: string
|
||||
): Promise<void> {
|
||||
const resetUrl = `${process.env.APP_URL}/reset-password?token=${resetToken}`
|
||||
await sendEmail({
|
||||
to,
|
||||
subject: 'Password Reset Request',
|
||||
html: `
|
||||
<h1>Password Reset</h1>
|
||||
<p>Click the link below to reset your password. This link expires in 1 hour.</p>
|
||||
<a href="${resetUrl}">${resetUrl}</a>
|
||||
<p>If you did not request a password reset, please ignore this email.</p>
|
||||
`,
|
||||
text: `Reset your password: ${resetUrl}`,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
||||
@@ -0,0 +1,39 @@
|
||||
import Stripe from 'stripe'
|
||||
|
||||
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: '2024-06-20',
|
||||
})
|
||||
|
||||
export async function createCheckoutSession({
|
||||
orderId,
|
||||
lineItems,
|
||||
customerEmail,
|
||||
successUrl,
|
||||
cancelUrl,
|
||||
}: {
|
||||
orderId: string
|
||||
lineItems: Stripe.Checkout.SessionCreateParams.LineItem[]
|
||||
customerEmail: string
|
||||
successUrl: string
|
||||
cancelUrl: string
|
||||
}): Promise<Stripe.Checkout.Session> {
|
||||
return stripe.checkout.sessions.create({
|
||||
payment_method_types: ['card'],
|
||||
mode: 'payment',
|
||||
customer_email: customerEmail,
|
||||
line_items: lineItems,
|
||||
success_url: successUrl,
|
||||
cancel_url: cancelUrl,
|
||||
metadata: {
|
||||
orderId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function constructWebhookEvent(
|
||||
payload: string | Buffer,
|
||||
signature: string,
|
||||
secret: string
|
||||
): Stripe.Event {
|
||||
return stripe.webhooks.constructEvent(payload, signature, secret)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
})
|
||||
|
||||
export const registerSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z
|
||||
.string()
|
||||
.min(12, 'Password must be at least 12 characters')
|
||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||
.regex(/[0-9]/, 'Password must contain at least one number')
|
||||
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one symbol'),
|
||||
name: z.string().min(1, 'Name is required').max(100),
|
||||
})
|
||||
|
||||
export const changePasswordSchema = z.object({
|
||||
currentPassword: z.string().min(1, 'Current password is required'),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(12, 'Password must be at least 12 characters')
|
||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||
.regex(/[0-9]/, 'Password must contain at least one number')
|
||||
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one symbol'),
|
||||
})
|
||||
|
||||
export const productTypeSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(100),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1, 'Slug is required')
|
||||
.max(100)
|
||||
.regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
||||
schema: z.record(z.any()),
|
||||
})
|
||||
|
||||
export const productSchema = z.object({
|
||||
typeId: z.string().min(1, 'Product type is required'),
|
||||
title: z.string().min(1, 'Title is required').max(200),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1, 'Slug is required')
|
||||
.max(200)
|
||||
.regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
||||
description: z.string().min(1, 'Description is required'),
|
||||
basePrice: z.number().int().min(0, 'Price must be non-negative'),
|
||||
currency: z.string().length(3, 'Currency must be 3 characters'),
|
||||
status: z.enum(['DRAFT', 'PUBLISHED', 'ARCHIVED']),
|
||||
attributes: z.record(z.any()),
|
||||
stock: z.number().int().min(0).nullable().optional(),
|
||||
categoryIds: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const categorySchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(100),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1, 'Slug is required')
|
||||
.max(100)
|
||||
.regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens'),
|
||||
parentId: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export const reviewSchema = z.object({
|
||||
productId: z.string().min(1, 'Product ID is required'),
|
||||
rating: z.number().int().min(1).max(5),
|
||||
title: z.string().max(200).optional(),
|
||||
comment: z.string().max(2000).optional(),
|
||||
})
|
||||
|
||||
export const cartItemSchema = z.object({
|
||||
productId: z.string().min(1),
|
||||
quantity: z.number().int().min(1).max(100),
|
||||
variantId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const checkoutSchema = z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
productId: z.string(),
|
||||
quantity: z.number().int().min(1),
|
||||
variantId: z.string().optional(),
|
||||
})
|
||||
).min(1, 'Cart is empty'),
|
||||
})
|
||||
|
||||
export const settingSchema = z.object({
|
||||
key: z.string().min(1),
|
||||
value: z.any(),
|
||||
})
|
||||
|
||||
export const adminUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100),
|
||||
role: z.enum(['ADMIN', 'OWNER']),
|
||||
password: z
|
||||
.string()
|
||||
.min(12, 'Password must be at least 12 characters')
|
||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||
.regex(/[0-9]/, 'Password must contain at least one number')
|
||||
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one symbol'),
|
||||
})
|
||||
|
||||
export type LoginInput = z.infer<typeof loginSchema>
|
||||
export type RegisterInput = z.infer<typeof registerSchema>
|
||||
export type ProductTypeInput = z.infer<typeof productTypeSchema>
|
||||
export type ProductInput = z.infer<typeof productSchema>
|
||||
export type CategoryInput = z.infer<typeof categorySchema>
|
||||
export type ReviewInput = z.infer<typeof reviewSchema>
|
||||
export type CheckoutInput = z.infer<typeof checkoutSchema>
|
||||
Reference in New Issue
Block a user