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
Binary file not shown.
@@ -106,6 +106,7 @@ model Company {
|
|||||||
tickets Ticket[]
|
tickets Ticket[]
|
||||||
agencyApplication AgencyApplication?
|
agencyApplication AgencyApplication?
|
||||||
instances Instance[]
|
instances Instance[]
|
||||||
|
repoAssignments RepoAssignment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Person {
|
model Person {
|
||||||
@@ -953,6 +954,8 @@ model Instance {
|
|||||||
metadata String? // JSON: { gitRepo, gitBranch, imageTag, resources }
|
metadata String? // JSON: { gitRepo, gitBranch, imageTag, resources }
|
||||||
lastError String?
|
lastError String?
|
||||||
lastErrorAt DateTime?
|
lastErrorAt DateTime?
|
||||||
|
allowedRepoId String?
|
||||||
|
allowedRepo AllowedRepo? @relation(fields: [allowedRepoId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
events InstanceEvent[]
|
events InstanceEvent[]
|
||||||
@@ -984,3 +987,40 @@ model ProvisioningJob {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Allowed Repos (admin controls which Gitea repos clients can deploy)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Admin selects which repos are available for instance provisioning.
|
||||||
|
// Repos can be:
|
||||||
|
// - public: visible to all clients in the "Neue Instanz" dialog
|
||||||
|
// - assigned: only visible to specific companies (per-company assignment)
|
||||||
|
// Unfinished/custom projects are NOT shared unless explicitly assigned.
|
||||||
|
model AllowedRepo {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
giteaOwner String // e.g. "dfmsolutions" (Gitea org/user)
|
||||||
|
giteaRepo String // e.g. "restov1" (repo name)
|
||||||
|
displayName String // human-readable: "Pizzeria RESTO (restov1)"
|
||||||
|
description String? // what this product does
|
||||||
|
product String // product slug: "restov1", "aos-cms", etc.
|
||||||
|
isPublic Boolean @default(false) // true = all clients can see it
|
||||||
|
isActive Boolean @default(true) // false = hidden from all
|
||||||
|
envTemplate String? // JSON: default env vars for this repo
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
assignments RepoAssignment[]
|
||||||
|
instances Instance[] // instances created from this repo
|
||||||
|
|
||||||
|
@@unique([giteaOwner, giteaRepo])
|
||||||
|
}
|
||||||
|
|
||||||
|
model RepoAssignment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
allowedRepoId String
|
||||||
|
allowedRepo AllowedRepo @relation(fields: [allowedRepoId], references: [id], onDelete: Cascade)
|
||||||
|
companyId String // which company can use this repo
|
||||||
|
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([allowedRepoId, companyId])
|
||||||
|
}
|
||||||
@@ -21,10 +21,8 @@ const GRACE_DAYS = 7
|
|||||||
const createSchema = z.object({
|
const createSchema = z.object({
|
||||||
companyId: z.string().min(1),
|
companyId: z.string().min(1),
|
||||||
licenseId: z.string().min(1),
|
licenseId: z.string().min(1),
|
||||||
product: z.string().min(1).default('restov1'),
|
allowedRepoId: z.string().min(1), // must reference an AllowedRepo
|
||||||
plan: z.enum(['starter', 'pro', 'enterprise']).default('pro'),
|
plan: z.enum(['starter', 'pro', 'enterprise']).default('pro'),
|
||||||
gitRepo: z.string().url().default('https://github.com/cetinmustafa83/restov1'),
|
|
||||||
gitBranch: z.string().default('main'),
|
|
||||||
// Extra env vars to inject into the instance (beyond the standard ones)
|
// Extra env vars to inject into the instance (beyond the standard ones)
|
||||||
envVars: z.record(z.string(), z.string()).optional(),
|
envVars: z.record(z.string(), z.string()).optional(),
|
||||||
// If true, start as a demo (14-day expiry). If false, start as active (paid).
|
// If true, start as a demo (14-day expiry). If false, start as active (paid).
|
||||||
@@ -72,7 +70,7 @@ export async function POST(req: Request) {
|
|||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 })
|
return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 })
|
||||||
}
|
}
|
||||||
const { companyId, licenseId, product, plan, gitRepo, gitBranch, envVars, isDemo } = parsed.data
|
const { companyId, licenseId, allowedRepoId, plan, envVars, isDemo } = parsed.data
|
||||||
|
|
||||||
// Verify company + license exist and belong together
|
// Verify company + license exist and belong together
|
||||||
const company = await db.company.findUnique({ where: { id: companyId } })
|
const company = await db.company.findUnique({ where: { id: companyId } })
|
||||||
@@ -83,9 +81,37 @@ export async function POST(req: Request) {
|
|||||||
if (license.companyId !== companyId) {
|
if (license.companyId !== companyId) {
|
||||||
return NextResponse.json({ error: 'license does not belong to this company' }, { status: 400 })
|
return NextResponse.json({ error: 'license does not belong to this company' }, { status: 400 })
|
||||||
}
|
}
|
||||||
if (license.product !== product) {
|
|
||||||
return NextResponse.json({ error: 'license product does not match instance product' }, { status: 400 })
|
// --- Repo permission check ---
|
||||||
|
// Admin must have added this repo to the allowed list AND either:
|
||||||
|
// a) the repo is public (isPublic=true), OR
|
||||||
|
// b) the repo is assigned to this company (RepoAssignment exists)
|
||||||
|
const allowedRepo = await db.allowedRepo.findUnique({
|
||||||
|
where: { id: allowedRepoId },
|
||||||
|
include: {
|
||||||
|
assignments: { where: { companyId } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!allowedRepo) {
|
||||||
|
return NextResponse.json({ error: 'repo not found in allowed list' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
if (!allowedRepo.isActive) {
|
||||||
|
return NextResponse.json({ error: 'repo is disabled' }, { status: 403 })
|
||||||
|
}
|
||||||
|
const hasAccess = allowedRepo.isPublic || allowedRepo.assignments.length > 0
|
||||||
|
if (!hasAccess) {
|
||||||
|
return NextResponse.json({ error: 'repo is not assigned to this company and is not public' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = allowedRepo.product
|
||||||
|
if (license.product !== product) {
|
||||||
|
return NextResponse.json({ error: `license product (${license.product}) does not match repo product (${product})` }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always use Gitea — git.dfmpanel.eu is our internal repo server
|
||||||
|
const GITEA_URL = process.env.GITEA_URL ?? 'https://git.dfmpanel.eu'
|
||||||
|
const gitRepo = `${GITEA_URL}/${allowedRepo.giteaOwner}/${allowedRepo.giteaRepo}.git`
|
||||||
|
const gitBranch = 'main' // always main
|
||||||
|
|
||||||
// Check that no live instance already exists for this license
|
// Check that no live instance already exists for this license
|
||||||
const existing = await db.instance.findFirst({
|
const existing = await db.instance.findFirst({
|
||||||
@@ -119,6 +145,7 @@ export async function POST(req: Request) {
|
|||||||
plan,
|
plan,
|
||||||
subdomain,
|
subdomain,
|
||||||
demoExpiresAt: isDemo ? new Date(Date.now() + DEMO_DAYS * 86400000) : null,
|
demoExpiresAt: isDemo ? new Date(Date.now() + DEMO_DAYS * 86400000) : null,
|
||||||
|
allowedRepoId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -128,7 +155,7 @@ export async function POST(req: Request) {
|
|||||||
type: 'provision.start',
|
type: 'provision.start',
|
||||||
step: 'init',
|
step: 'init',
|
||||||
result: 'ok',
|
result: 'ok',
|
||||||
details: JSON.stringify({ subdomain, product, plan, isDemo, gitRepo }),
|
details: JSON.stringify({ subdomain, product, plan, isDemo, gitRepo, allowedRepo: allowedRepo.displayName }),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -141,14 +168,24 @@ export async function POST(req: Request) {
|
|||||||
throw new Error('Dokploy client not configured (DOKPLOY_URL / DOKPLOY_API_KEY missing)')
|
throw new Error('Dokploy client not configured (DOKPLOY_URL / DOKPLOY_API_KEY missing)')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance env vars: license key + AOS connection + product-specific
|
// Parse repo-specific env template (if set by admin)
|
||||||
|
const repoEnvTemplate: Record<string, string> = allowedRepo.envTemplate
|
||||||
|
? JSON.parse(allowedRepo.envTemplate)
|
||||||
|
: {}
|
||||||
|
|
||||||
|
// Instance env vars: license + AOS + repo template + overrides
|
||||||
const instanceEnv: Record<string, string> = {
|
const instanceEnv: Record<string, string> = {
|
||||||
|
// AOS license server connection
|
||||||
AOS_LICENSE_SERVER_URL: process.env.AOS_PUBLIC_URL ?? `https://${PLATFORM_DOMAIN}`,
|
AOS_LICENSE_SERVER_URL: process.env.AOS_PUBLIC_URL ?? `https://${PLATFORM_DOMAIN}`,
|
||||||
AOS_PRODUCT: product,
|
AOS_PRODUCT: product,
|
||||||
AOS_LICENSE_KEY: license.key,
|
AOS_LICENSE_KEY: license.key,
|
||||||
AOS_LICENSE_DOMAIN: fullDomain,
|
AOS_LICENSE_DOMAIN: fullDomain,
|
||||||
AOS_OFFLINE_GRACE_HOURS: '72',
|
AOS_OFFLINE_GRACE_HOURS: '72',
|
||||||
|
// Standard runtime
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
|
// Repo-specific defaults from admin (APP_MODE, AUTH_SECRET, etc.)
|
||||||
|
...repoEnvTemplate,
|
||||||
|
// Caller overrides (highest priority)
|
||||||
...envVars,
|
...envVars,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +195,7 @@ export async function POST(req: Request) {
|
|||||||
gitRepo,
|
gitRepo,
|
||||||
gitBranch,
|
gitBranch,
|
||||||
envVars: instanceEnv,
|
envVars: instanceEnv,
|
||||||
gitProvider: gitRepo.includes('github.com') ? 'github' : 'gitea',
|
gitProvider: 'gitea', // always Gitea
|
||||||
cpuLimit: plan === 'enterprise' ? '2' : plan === 'pro' ? '1' : '0.5',
|
cpuLimit: plan === 'enterprise' ? '2' : plan === 'pro' ? '1' : '0.5',
|
||||||
memoryLimit: plan === 'enterprise' ? '2048m' : plan === 'pro' ? '1024m' : '512m',
|
memoryLimit: plan === 'enterprise' ? '2048m' : plan === 'pro' ? '1024m' : '512m',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { db } from '@/lib/db'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
// GET /api/aos/repos — list all allowed repos (admin)
|
||||||
|
// GET /api/aos/repos?companyId=X — list repos available to a specific client
|
||||||
|
export async function GET(req: Request) {
|
||||||
|
try {
|
||||||
|
const url = new URL(req.url)
|
||||||
|
const companyId = url.searchParams.get('companyId')
|
||||||
|
|
||||||
|
if (companyId) {
|
||||||
|
// Client-scoped: show public repos + repos assigned to this company
|
||||||
|
const [publicRepos, assignedRepos] = await Promise.all([
|
||||||
|
db.allowedRepo.findMany({
|
||||||
|
where: { isPublic: true, isActive: true },
|
||||||
|
orderBy: { displayName: 'asc' },
|
||||||
|
}),
|
||||||
|
db.allowedRepo.findMany({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
assignments: { some: { companyId } },
|
||||||
|
},
|
||||||
|
include: { assignments: { where: { companyId } } },
|
||||||
|
orderBy: { displayName: 'asc' },
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
// Merge + dedupe
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const repos = [...publicRepos, ...assignedRepos].filter((r) => {
|
||||||
|
if (seen.has(r.id)) return false
|
||||||
|
seen.add(r.id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return NextResponse.json({ repos })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin: list all repos
|
||||||
|
const repos = await db.allowedRepo.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
include: {
|
||||||
|
_count: { select: { assignments: true, instances: true } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return NextResponse.json({ repos })
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||||
|
return NextResponse.json({ error: msg }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSchema = z.object({
|
||||||
|
giteaOwner: z.string().min(1),
|
||||||
|
giteaRepo: z.string().min(1),
|
||||||
|
displayName: z.string().min(1),
|
||||||
|
description: z.string().optional(),
|
||||||
|
product: z.string().min(1),
|
||||||
|
isPublic: z.boolean().default(false),
|
||||||
|
envTemplate: z.string().optional(), // JSON string of default env vars
|
||||||
|
})
|
||||||
|
|
||||||
|
// POST /api/aos/repos — admin adds a repo to the allowed list
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const parsed = createSchema.safeParse(await req.json())
|
||||||
|
if (!parsed.success) {
|
||||||
|
return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 })
|
||||||
|
}
|
||||||
|
const { giteaOwner, giteaRepo, displayName, description, product, isPublic, envTemplate } = parsed.data
|
||||||
|
|
||||||
|
const repo = await db.allowedRepo.upsert({
|
||||||
|
where: { giteaOwner_giteaRepo: { giteaOwner, giteaRepo } },
|
||||||
|
create: {
|
||||||
|
giteaOwner,
|
||||||
|
giteaRepo,
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
product,
|
||||||
|
isPublic,
|
||||||
|
envTemplate,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
product,
|
||||||
|
isPublic,
|
||||||
|
envTemplate,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await db.auditLog.create({
|
||||||
|
data: {
|
||||||
|
actor: 'admin:ui',
|
||||||
|
action: 'repo.add',
|
||||||
|
entity: 'AllowedRepo',
|
||||||
|
entityId: repo.id,
|
||||||
|
after: JSON.stringify({ giteaOwner, giteaRepo, product, isPublic }),
|
||||||
|
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ repo }, { status: 201 })
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||||
|
return NextResponse.json({ error: msg }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -224,7 +224,34 @@ export async function POST(req: Request) {
|
|||||||
stats.apiTokens = 0
|
stats.apiTokens = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 7. Audit log ---
|
// --- 7. Allowed Repos (tenant instance provisioning) ---
|
||||||
|
const existingRepos = await db.allowedRepo.count()
|
||||||
|
if (existingRepos === 0) {
|
||||||
|
const restov1EnvTemplate = JSON.stringify({
|
||||||
|
APP_MODE: 'demo',
|
||||||
|
RESTO_RESTAURANT_ID: 'resto_demo_01',
|
||||||
|
RESTO_QR_SECRET: '4a27fcd1564d8557cfc8618c710476fe10f61c30e0896290567ac4656500b6d4',
|
||||||
|
CUSTOMER_AUTH_SECRET: '6dd5f89e224607ffc41668d1d590176af38dbbdb15d8eddea9ec1011604a260e',
|
||||||
|
STAFF_AUTH_SECRET: '8b09481986428134937be5685fe4bdc8858861e6a9b22f167506409bcd37ecc9',
|
||||||
|
})
|
||||||
|
await db.allowedRepo.create({
|
||||||
|
data: {
|
||||||
|
giteaOwner: 'dfmsolutions',
|
||||||
|
giteaRepo: 'restov1',
|
||||||
|
displayName: 'Pizzeria RESTO (restov1)',
|
||||||
|
description: 'Online-Bestellsystem für Restaurants — Pizza, Pasta, Lieferung, Reservierung',
|
||||||
|
product: 'restov1',
|
||||||
|
isPublic: true,
|
||||||
|
isActive: true,
|
||||||
|
envTemplate: restov1EnvTemplate,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
stats.allowedRepos = 1
|
||||||
|
} else {
|
||||||
|
stats.allowedRepos = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 8. Audit log ---
|
||||||
await db.auditLog.create({
|
await db.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
actor: 'system:seed-prod',
|
actor: 'system:seed-prod',
|
||||||
|
|||||||
Reference in new issue
Block a user