πŸ”’

Security & Privacy

Lucia Auth sessies, RBAC, Zod-validatie, Drizzle ORM, rate limiting en GDPR/CCPA compliance

Lucia Auth Fastify Drizzle ORM Zod Schemas GDPR Β· CCPA Socket.IO

πŸ›‘οΈ Security Lagen

πŸ”‘

Auth

Lucia Auth Β· session-based Β· HTTP-only cookies

πŸ›‚

RBAC

8 rollen Β· Fastify middleware Β· route-guards

🚦

Rate Limiting

@fastify/rate-limit Β· global / auth / AI limieten

πŸ“œ

Compliance

GDPR Β· CCPA Β· consent ledger Β· audit logs

πŸ”‘ Authenticatie β€” Lucia Auth

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.

⚠️ Geen JWT in localStorage. Vroegere versies gebruikten JWT tokens. Alle auth is volledig omgezet naar Lucia sessies. Elke poging om JWT's client-side op te slaan is een securityrisico en niet toegestaan.

Sessie flow

βœ“
Login
bcrypt wachtwoord-vergelijking β†’ Lucia sessie aanmaken β†’ session-id in HTTP-only cookie zetten
βœ“
Request validatie
Fastify preHandler: lucia.validateSession(sessionId) uit cookie β†’ user object op request.user
βœ“
Logout
Sessie rij verwijderd uit PostgreSQL β€” cookie gewist β€” onmiddellijke invalidatie
βœ“
Sessie opslag
PostgreSQL via Lucia sessie-adapter tabel (user_session)

Wachtwoord hashing

// 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
})

πŸ›‚ RBAC β€” 8 Rollen

Role-Based Access Control via Fastify route-middleware. Elke route declareert welke rollen toegang hebben. Rollen zijn opgeslagen in de users.role kolom.

Super Admin
Volledige toegang
Admin
Platform beheer
Moderator
Content review
Creator
Content publiceren
Provider
Services aanbieden
Member
Basis abonnee
Plus
Betaald lid
Viewer
Alleen lezen

Betaalde member tiers

basic plus premium elite
// 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' })
  }
}

βœ… Input Validatie β€” Zod

Alle API-endpoints valideren input via Zod schemas uit het gedeelde pakket @teezu/types. Geen enkel endpoint accepteert ongevalideerde input.

Schema definitie (@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>

Endpoint gebruik

// 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)
})

πŸ›‘οΈ SQL Injection Preventie β€” Drizzle ORM

Alle databasetoegang gaat via Drizzle ORM met geparametriseerde queries. Raw SQL is verboden in applicatiecode.

βœ“ Correct (Drizzle)

// 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(),
})

βœ— Verboden (raw SQL)

// 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}%'`

🚦 Rate Limiting

Geconfigureerd via @fastify/rate-limit. Drie niveaus: globaal, auth-endpoints en AI-endpoints.

100
/ minuut
Globaal β€” alle endpoints
10
/ minuut
Auth β€” /login, /register, /reset-password
20
/ minuut
AI β€” /ai/*, /genie/*, /prompts/*
// 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)

πŸͺ– Security Headers & CORS

HTTP-beveiligingsheaders via @fastify/helmet. CORS geconfigureerd in Fastify voor toegestane origins.

Helmet.js headers

    βœ“
    Content-Security-Policy
    Voorkomt XSS & inline-script injectie
    βœ“
    Strict-Transport-Security
    HTTPS-only (HSTS) afdwingen
    βœ“
    X-Frame-Options
    Clickjacking-bescherming
    βœ“
    X-Content-Type-Options
    MIME-type sniffing blokkeren
    βœ“
    Referrer-Policy
    Beperkt referrer-informatie

CORS configuratie

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'],
})

πŸ” Data Encryptie

In Transit

  • β€’ TLS 1.3 voor alle verbindingen
  • β€’ HTTPS-only (HSTS enabled)
  • β€’ Socket.IO over WSS (TLS)
  • β€’ HTTP-only cookies β€” niet toegankelijk via JS

At Rest

  • β€’ AES-256 PostgreSQL database encryptie
  • β€’ Private S3 buckets β€” publieke URLs verboden
  • β€’ Signed URLs voor premium media (ttl: 15 min)
  • β€’ Key rotation policy

πŸ–ΌοΈ Media Beveiliging β€” Private S3

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.

Toegangsstrategie

βœ“
Server valideert Lucia sessie + tier-rechten
βœ“
Signed URL gegenereerd met 15 min TTL
βœ“
URL bevat user-id voor misbruik-tracking
βœ“
Verlopen URL's geven direct 403 terug
βœ“
Toegang gelogd in audit tabel

Signed URL flow

// 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
}

⚑ Real-time β€” Socket.IO Beveiliging

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

πŸ”ž Content Policy & Consent

TeezU is een 18+ platform. Leeftijdsverificatie en consent worden gelogd in een onveranderlijk consent ledger in PostgreSQL.

Consent flow

β‘ 
Gebruiker registreert β†’ leeftijdscheck (18+)
β‘‘
ConsentModal getoond bij eerste 18+ content
β‘’
Acceptatie gelogd in consent_log tabel
β‘£
Timestamp, IP-hash, user-id, consent-versie opgeslagen
β‘€
Intrekking ook gelogd β€” content verborgen

Content Moderation

Image Scanning

AI-screening op NSFW-overtredingen, geweld, hate symbols bij elke upload

Text Analyse

Spam, harassment, hate speech detectie in chat en posts

Gedragspatronen

Verdachte activiteit, bots, ban-ontwijking detectie

πŸ”’ Privacy Controls

Profiel Privacy

  • Zichtbaarheid
    Publiek / PrivΓ© / Premium-only
  • Locatie precisie
    Exact / Stad / Land / Verborgen
  • Online status
    Tonen / Verbergen / Alleen volgers

Content Privacy

  • Post zichtbaarheid
    Publiek / Volgers / Premium / PrivΓ©
  • Media download
    Toestaan / Blokkeren (signed URLs)
  • Gebruikersblokkering
    Blokkeer / Mute / Rapporteer

πŸ“œ GDPR & CCPA Compliance

Gebruikersrechten

  • β€’ Recht op inzage (data export)
  • β€’ Recht op rectificatie
  • β€’ Recht op verwijdering ("right to be forgotten")
  • β€’ Recht op dataportabiliteit (JSON export)
  • β€’ Recht op bezwaar (opt-out marketing)

Platform verplichtingen

  • β€’ Duidelijke consent-mechanismen (consent ledger)
  • β€’ Data minimalisatie β€” alleen noodzakelijke data
  • β€’ Doelbeperking β€” data niet hergebruikt
  • β€’ Datalek melding binnen 72u
  • β€’ Privacy by design in alle nieuwe features
  • β€’ Audit logs in PostgreSQL (onveranderlijk)
πŸ“‹ Audit logs worden opgeslagen in de 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.