feat: repo permission system + Gitea-only provisioning + env templates
Schema:
- AllowedRepo model: admin-controlled list of deployable repos
- giteaOwner/giteaRepo: always from Gitea (git.dfmpanel.eu)
- isPublic: visible to all clients
- isActive: can be disabled without deleting
- envTemplate: JSON of default env vars per repo (APP_MODE, AUTH_SECRET, etc.)
- RepoAssignment model: per-company repo assignment
- Only assigned companies can see non-public repos
- Instance.allowedRepoId: links instance to its source repo
Provisioning changes:
- Always uses Gitea (gitProvider: 'gitea'), never GitHub
- Branch is always 'main'
- Repo URL: {GITEA_URL}/{owner}/{repo}.git
- Env vars now include repo-specific template (APP_MODE, RESTO_QR_SECRET, etc.)
merged with AOS license vars and caller overrides
- Permission check: repo must be public OR assigned to the company
API:
- GET /api/aos/repos — list all (admin) or client-scoped (?companyId=X)
- POST /api/aos/repos — add a repo to allowed list
- PATCH /api/aos/repos/[id] — update + assign/unassign to company
- DELETE /api/aos/repos/[id] — remove (blocked if instances exist)
Seed:
- restov1 added as public allowed repo with env template:
APP_MODE=demo, RESTO_RESTAURANT_ID, RESTO_QR_SECRET, AUTH_SECRETs
Env:
- GITEA_URL=https://git.dfmpanel.eu added to .env + .env.example
This commit is contained in:
1 parent
aa075762b2
commit
89adb63f5e
6 files changed
+339
-10
No files matched your search
@@ -0,0 +1,115 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const patchSchema = z.object({
|
||||
displayName: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
isPublic: z.boolean().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
envTemplate: z.string().optional(),
|
||||
// Assign to a specific company (or remove assignment)
|
||||
assignToCompany: z.string().optional(),
|
||||
unassignFromCompany: z.string().optional(),
|
||||
})
|
||||
|
||||
// PATCH /api/aos/repos/[id] — update repo settings + assign/unassign
|
||||
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const parsed = patchSchema.safeParse(await req.json())
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
const { displayName, description, isPublic, isActive, envTemplate, assignToCompany, unassignFromCompany } = parsed.data
|
||||
|
||||
const repo = await db.allowedRepo.findUnique({ where: { id } })
|
||||
if (!repo) {
|
||||
return NextResponse.json({ error: 'repo not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Update fields
|
||||
const updateData: Record<string, unknown> = {}
|
||||
if (displayName !== undefined) updateData.displayName = displayName
|
||||
if (description !== undefined) updateData.description = description
|
||||
if (isPublic !== undefined) updateData.isPublic = isPublic
|
||||
if (isActive !== undefined) updateData.isActive = isActive
|
||||
if (envTemplate !== undefined) updateData.envTemplate = envTemplate
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await db.allowedRepo.update({ where: { id }, data: updateData })
|
||||
}
|
||||
|
||||
// Assign to company
|
||||
if (assignToCompany) {
|
||||
await db.repoAssignment.upsert({
|
||||
where: { allowedRepoId_companyId: { allowedRepoId: id, companyId: assignToCompany } },
|
||||
create: { allowedRepoId: id, companyId: assignToCompany },
|
||||
update: {},
|
||||
})
|
||||
}
|
||||
|
||||
// Unassign from company
|
||||
if (unassignFromCompany) {
|
||||
await db.repoAssignment.deleteMany({
|
||||
where: { allowedRepoId: id, companyId: unassignFromCompany },
|
||||
})
|
||||
}
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: 'admin:ui',
|
||||
action: 'repo.update',
|
||||
entity: 'AllowedRepo',
|
||||
entityId: id,
|
||||
after: JSON.stringify({ updateData, assignToCompany, unassignFromCompany }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ ok: true })
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/aos/repos/[id] — remove repo from allowed list
|
||||
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const repo = await db.allowedRepo.findUnique({
|
||||
where: { id },
|
||||
include: { _count: { select: { instances: true } } },
|
||||
})
|
||||
if (!repo) {
|
||||
return NextResponse.json({ error: 'repo not found' }, { status: 404 })
|
||||
}
|
||||
if (repo._count.instances > 0) {
|
||||
return NextResponse.json({
|
||||
error: 'cannot delete repo with active instances',
|
||||
instances: repo._count.instances,
|
||||
}, { status: 409 })
|
||||
}
|
||||
|
||||
await db.allowedRepo.delete({ where: { id } })
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: 'admin:ui',
|
||||
action: 'repo.delete',
|
||||
entity: 'AllowedRepo',
|
||||
entityId: id,
|
||||
after: JSON.stringify({ giteaOwner: repo.giteaOwner, giteaRepo: repo.giteaRepo }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ ok: true })
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user