v1.2.2: Fix Nominatim CSP, Tenant Admin kann eigene Symbole hochladen

This commit is contained in:
Pepe Ziberi
2026-02-24 22:43:05 +01:00
parent f480905bb9
commit 8ddeb7b377
5 changed files with 21 additions and 18 deletions

View File

@@ -51,7 +51,7 @@ const nextConfig = {
"style-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https://*.tile.openstreetmap.org https://api.maptiler.com https://server.arcgisonline.com https://*.geo.admin.ch http://localhost:9000 http://minio:9000", "img-src 'self' data: blob: https://*.tile.openstreetmap.org https://api.maptiler.com https://server.arcgisonline.com https://*.geo.admin.ch http://localhost:9000 http://minio:9000",
"font-src 'self' data:", "font-src 'self' data:",
"connect-src 'self' ws: wss: https://api.maptiler.com https://*.tile.openstreetmap.org https://api.open-meteo.com https://server.arcgisonline.com https://*.geo.admin.ch", "connect-src 'self' ws: wss: https://api.maptiler.com https://*.tile.openstreetmap.org https://nominatim.openstreetmap.org https://api.open-meteo.com https://server.arcgisonline.com https://*.geo.admin.ch",
"frame-ancestors 'self'", "frame-ancestors 'self'",
"base-uri 'self'", "base-uri 'self'",
"form-action 'self'", "form-action 'self'",

View File

@@ -1,6 +1,6 @@
{ {
"name": "lageplan", "name": "lageplan",
"version": "1.2.1", "version": "1.2.2",
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation", "description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@@ -552,7 +552,8 @@ export default function AdminPage() {
setUploadFiles(null) setUploadFiles(null)
setUploadCategory('') setUploadCategory('')
setUploadIconName('') setUploadIconName('')
fetchData() if (user?.role === 'TENANT_ADMIN') fetchTenantSymbols()
else fetchData()
} catch (error) { } catch (error) {
toast({ title: 'Upload-Fehler', description: error instanceof Error ? error.message : 'Fehler', variant: 'destructive' }) toast({ title: 'Upload-Fehler', description: error instanceof Error ? error.message : 'Fehler', variant: 'destructive' })
} finally { setIsUploading(false) } } finally { setIsUploading(false) }
@@ -896,7 +897,7 @@ export default function AdminPage() {
placeholder="Symbole suchen..." placeholder="Symbole suchen..."
value={symbolSearch} value={symbolSearch}
onChange={e => setSymbolSearch(e.target.value)} onChange={e => setSymbolSearch(e.target.value)}
className="w-48" className="w-full sm:w-64"
/> />
<Select value={symbolCatFilter} onValueChange={setSymbolCatFilter}> <Select value={symbolCatFilter} onValueChange={setSymbolCatFilter}>
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
@@ -909,9 +910,13 @@ export default function AdminPage() {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<span className="text-sm text-muted-foreground ml-auto"> <span className="text-sm text-muted-foreground mr-auto">
{tenantSymbols.filter(s => s.isActive).length} aktiv / {tenantSymbols.length} gesamt {tenantSymbols.filter(s => s.isActive).length} aktiv / {tenantSymbols.length} gesamt
</span> </span>
<Button onClick={() => setIsUploadDialogOpen(true)}>
<Upload className="w-4 h-4 mr-2" />
Eigene Symbole hochladen
</Button>
</div> </div>
{/* Bulk category action */} {/* Bulk category action */}
@@ -1050,7 +1055,7 @@ export default function AdminPage() {
))} ))}
</div> </div>
)} )}
</> </>
)} )}
</TabsContent> </TabsContent>

View File

@@ -10,7 +10,7 @@ const MAX_SIZE = 5 * 1024 * 1024 // 5MB
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const user = await getSession() const user = await getSession()
if (!user || !isAdmin(user.role)) { if (!user || (user.role !== 'SERVER_ADMIN' && user.role !== 'TENANT_ADMIN')) {
return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 403 }) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 403 })
} }
@@ -55,27 +55,25 @@ export async function POST(req: NextRequest) {
// Generate safe filename // Generate safe filename
const ext = file.name.split('.').pop()?.toLowerCase() || 'png' const ext = file.name.split('.').pop()?.toLowerCase() || 'png'
const isTenantAdmin = user.role === 'TENANT_ADMIN'
const prefix = isTenantAdmin ? `tenant-${user.tenantId}/icons` : 'icons'
const safeFileName = `${uuidv4()}.${ext}` const safeFileName = `${uuidv4()}.${ext}`
const fileKey = `icons/${safeFileName}` const fileKey = `${prefix}/${safeFileName}`
// Upload to MinIO // Upload to MinIO
const buffer = Buffer.from(await file.arrayBuffer()) const buffer = Buffer.from(await file.arrayBuffer())
await uploadFile(fileKey, buffer, file.type) await uploadFile(fileKey, buffer, file.type)
// TENANT_ADMIN: icons get tenantId. SERVER_ADMIN: global icons (tenantId=null) // Save to DB
const tenantId = user.role === 'SERVER_ADMIN' ? null : user.tenantId || null
// Create database entry
const icon = await (prisma as any).iconAsset.create({ const icon = await (prisma as any).iconAsset.create({
data: { data: {
name: name.trim(), name: name.trim(),
fileKey,
mimeType: file.type,
categoryId, categoryId,
iconType: iconType as any, iconType: iconType as any,
isSystem: false, fileKey,
isActive: true, mimeType: file.type,
tenantId, isSystem: !isTenantAdmin, // true für Server Admin, false für Tenant Admin
tenantId: isTenantAdmin ? user.tenantId : null,
ownerId: user.id, ownerId: user.id,
}, },
include: { include: {

View File

@@ -32,7 +32,7 @@ export async function GET() {
icons: { icons: {
where: user?.tenantId where: user?.tenantId
? { isActive: true, OR: [{ tenantId: null }, { tenantId: user.tenantId }] } ? { isActive: true, OR: [{ tenantId: null }, { tenantId: user.tenantId }] }
: { isActive: true }, : { isActive: true, tenantId: null },
orderBy: { name: 'asc' }, orderBy: { name: 'asc' },
}, },
}, },