Commit iniziale

This commit is contained in:
2026-05-18 15:25:38 +02:00
commit a8d4c158b8
79 changed files with 8730 additions and 0 deletions
+103
View File
@@ -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 } })
}
+70
View File
@@ -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}`,
})
}
+13
View File
@@ -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
+39
View File
@@ -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)
}
+115
View File
@@ -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>