Lucia Auth sessies, RBAC, Zod-validatie, Drizzle ORM, rate limiting en GDPR/CCPA compliance
Lucia Auth Β· session-based Β· HTTP-only cookies
8 rollen Β· Fastify middleware Β· route-guards
@fastify/rate-limit Β· global / auth / AI limieten
GDPR Β· CCPA Β· consent ledger Β· audit logs
TeezU gebruikt Lucia Auth voor server-side sessie-authenticatie. Geen JWT tokens in localStorage β alle sessies leven in PostgreSQL en worden getransporteerd via HTTP-only cookies.
lucia.validateSession(sessionId) uit cookie β user object op request.useruser_session)// Registratie
import { hash, verify } from '@node-rs/bcrypt'
const hashedPassword = await hash(plainPassword, 12)
// Login verificatie
const valid = await verify(plainPassword, storedHash)
if (!valid) throw new Error('Invalid credentials')
bcrypt cost factor 12 β passwords nooit in plaintext opgeslagen of gelogd.
// Fastify auth preHandler (vereenvoudigd)
fastify.addHook('preHandler', async (request, reply) => {
const sessionId = lucia.readSessionCookie(request.headers.cookie ?? '')
if (!sessionId) return reply.status(401).send({ error: 'Unauthorized' })
const { session, user } = await lucia.validateSession(sessionId)
if (!session) return reply.status(401).send({ error: 'Session expired' })
// Ververs cookie indien bijna verlopen
if (session.fresh) {
reply.header('Set-Cookie', lucia.createSessionCookie(session.id).serialize())
}
request.user = user
request.session = session
})
Role-Based Access Control via Fastify route-middleware. Elke route declareert welke rollen toegang hebben. Rollen zijn opgeslagen in de users.role kolom.
// Fastify RBAC middleware
import { requireRole } from '@/middleware/rbac'
// Route met rolbeperking
fastify.get('/admin/users', {
preHandler: [requireRole(['super_admin', 'admin'])]
}, async (request, reply) => { ... })
fastify.post('/live/start', {
preHandler: [requireRole(['creator'])]
}, async (request, reply) => { ... })
// requireRole implementatie
export const requireRole = (roles: Role[]) => async (request, reply) => {
if (!roles.includes(request.user.role)) {
return reply.status(403).send({ error: 'Forbidden' })
}
}
Alle API-endpoints valideren input via Zod schemas uit het gedeelde pakket @teezu/types. Geen enkel endpoint accepteert ongevalideerde input.
@teezu/types)// packages/types/src/schemas/chat.ts
import { z } from 'zod'
export const chatMessageSchema = z.object({
recipientId: z.string().uuid(),
content: z.string().min(1).max(500),
type: z.enum(['text', 'image', 'tip']),
})
export type ChatMessageInput =
z.infer<typeof chatMessageSchema>
// server/routes/chat.ts
import { chatMessageSchema } from '@teezu/types'
fastify.post('/messages', {
preHandler: [requireAuth]
}, async (request, reply) => {
const input = chatMessageSchema.safeParse(request.body)
if (!input.success) {
return reply.status(400).send({
error: 'Validation failed',
issues: input.error.issues
})
}
// input.data is volledig getypeerd
await chatService.send(request.user.id, input.data)
})
Alle databasetoegang gaat via Drizzle ORM met geparametriseerde queries. Raw SQL is verboden in applicatiecode.
// Geparametriseerde query via Drizzle
const user = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1)
// Insert met typed input
await db.insert(messages).values({
senderId: request.user.id,
content: input.data.content,
createdAt: new Date(),
})
// NOOIT β raw string interpolatie
const user = await db.execute(
`SELECT * FROM users WHERE id = '${userId}'`
)
// NOOIT β user input in query string
const q = `SELECT * FROM posts
WHERE title LIKE '%${searchTerm}%'`
Geconfigureerd via @fastify/rate-limit. Drie niveaus: globaal, auth-endpoints en AI-endpoints.
// fastify plugin registratie
await fastify.register(import('@fastify/rate-limit'), {
global: true,
max: 100,
timeWindow: '1 minute',
})
// Override per route
fastify.post('/auth/login', {
config: { rateLimit: { max: 10, timeWindow: '1 minute' } }
}, loginHandler)
fastify.post('/ai/genie', {
config: { rateLimit: { max: 20, timeWindow: '1 minute' } }
}, genieHandler)
HTTP-beveiligingsheaders via @fastify/helmet. CORS geconfigureerd in Fastify voor toegestane origins.
await fastify.register(import('@fastify/cors'), {
origin: [
'https://teezu.com',
'https://app.teezu.com',
process.env.NODE_ENV === 'development'
? 'http://localhost:5173'
: false,
].filter(Boolean),
credentials: true, // cookies doorsturen
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
})
Alle premium media wordt opgeslagen in private S3 buckets. Directe publieke toegang is geblokkeerd. Content wordt geleverd via server-gegenereerde signed URLs met een korte TTL.
// server/services/media.ts
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { GetObjectCommand } from '@aws-sdk/client-s3'
export async function getMediaUrl(
s3Key: string,
userId: string
): Promise<string> {
return getSignedUrl(s3Client, new GetObjectCommand({
Bucket: process.env.S3_PRIVATE_BUCKET,
Key: s3Key,
}), { expiresIn: 900 }) // 15 min
}
Socket.IO verbindingen worden gevalideerd met de Lucia sessie bij het verbinden. Geen geldige sessie = verbinding geweigerd.
// server/socket/auth.middleware.ts
io.use(async (socket, next) => {
try {
const cookieHeader = socket.handshake.headers.cookie ?? ''
const sessionId = lucia.readSessionCookie(cookieHeader)
if (!sessionId) return next(new Error('No session cookie'))
const { session, user } = await lucia.validateSession(sessionId)
if (!session) return next(new Error('Invalid or expired session'))
// User beschikbaar op socket
socket.data.user = user
socket.data.session = session
next()
} catch (err) {
next(new Error('Authentication failed'))
}
})
// Kamer-toegang ook op rol gebaseerd
socket.on('join:room', (roomId) => {
const { user } = socket.data
if (!canJoinRoom(user, roomId)) {
socket.emit('error', { code: 'FORBIDDEN' })
return
}
socket.join(roomId)
})
TeezU is een 18+ platform. Leeftijdsverificatie en consent worden gelogd in een onveranderlijk consent ledger in PostgreSQL.
consent_log tabelAI-screening op NSFW-overtredingen, geweld, hate symbols bij elke upload
Spam, harassment, hate speech detectie in chat en posts
Verdachte activiteit, bots, ban-ontwijking detectie
audit_log tabel in PostgreSQL. Elke wijziging aan gebruikersdata, content-access, consent-acties en admin-handelingen wordt gelogd met timestamp, actor-id en IP-hash.