Rebuild as InnungsApp project: replace stadtwerke analysis with full documentation
- PRD: vollständige Produktspezifikation (5 Module, Scope, Akzeptanzkriterien) - ARCHITECTURE: Tech Stack, Ordnerstruktur, Multi-Tenancy, Push, Kosten - DATABASE_SCHEMA: Vollständiges SQL-Schema mit RLS Policies und Views - USER_STORIES: 40+ Stories nach Rolle (Admin, Mitglied, Azubi, Obermeister) - PERSONAS: 5 detaillierte Nutzerprofile mit Alltag, Zitaten und Erwartungen - BUSINESS_MODEL: Preistabellen, Unit Economics, Revenue-Projektionen, Distribution - ROADMAP: 6 Phasen, Sprint-Planung, Meilensteine und KPIs - COMPETITIVE_ANALYSIS: Wettbewerbsmatrix, USPs, Preispositionierung - API_DESIGN: Supabase Query Patterns, Edge Functions, Realtime Subscriptions - ONBOARDING_FLOWS: 7 User Flows von Setup bis Fehlerfall - GTM_STRATEGY: 3-Phasen-Vertrieb, Outreach-Sequenz, Einwandbehandlung - AZUBI_MODULE: Video-Feed, 1-Click-Apply, Chat, Berichtsheft, Quiz - DSGVO_KONZEPT: Rechtsgrundlagen, TOMs, AVV, Minderjährige, Incident Response - FEATURES_BACKLOG: 72 Features nach MoSCoW + Technische Schulden Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
54
innungsapp/apps/admin/app/api/upload/route.ts
Normal file
54
innungsapp/apps/admin/app/api/upload/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { writeFile, mkdir } from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { auth } from '@/lib/auth'
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR ?? './uploads'
|
||||
const MAX_SIZE_BYTES = Number(process.env.UPLOAD_MAX_SIZE_MB ?? 10) * 1024 * 1024
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// Auth check
|
||||
const session = await auth.api.getSession({ headers: req.headers })
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file') as File | null
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (file.size > MAX_SIZE_BYTES) {
|
||||
return NextResponse.json({ error: 'File too large' }, { status: 413 })
|
||||
}
|
||||
|
||||
// Only allow safe file types
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
]
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return NextResponse.json({ error: 'File type not allowed' }, { status: 415 })
|
||||
}
|
||||
|
||||
const ext = path.extname(file.name)
|
||||
const fileName = `${randomUUID()}${ext}`
|
||||
const uploadPath = path.join(process.cwd(), UPLOAD_DIR)
|
||||
|
||||
await mkdir(uploadPath, { recursive: true })
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
await writeFile(path.join(uploadPath, fileName), buffer)
|
||||
|
||||
return NextResponse.json({
|
||||
storagePath: fileName,
|
||||
name: file.name,
|
||||
sizeBytes: file.size,
|
||||
url: `/uploads/${fileName}`,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user