Add Docker Registry AIO with Nuxt Token Auth and admin UI.
Compose brings up registry:3 plus a Prisma/SQLite Nuxt app for docker login, ACL management, and image browsing with shadcn-vue. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
0fd00bfa37
120 files changed
+17094
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const bodySchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await ensureBootstrap()
|
||||
const body = bodySchema.parse(await readBody(event))
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { username: body.username } })
|
||||
if (!user || !user.active) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' })
|
||||
}
|
||||
|
||||
const ok = await verifyPassword(body.password, user.passwordHash)
|
||||
if (!ok) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' })
|
||||
}
|
||||
|
||||
const token = await createSessionToken({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
})
|
||||
setSessionCookie(event, token)
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
clearSessionCookie(event)
|
||||
return { ok: true }
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = await readSessionUser(event)
|
||||
if (!user) {
|
||||
return { user: null }
|
||||
}
|
||||
return { user }
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export default defineEventHandler(() => {
|
||||
return { ok: true, service: 'registry-aio' }
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const bodySchema = z.object({
|
||||
name: z.string().min(1),
|
||||
tag: z.string().min(1),
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await requireSession(event)
|
||||
await ensureBootstrap()
|
||||
|
||||
const body = bodySchema.parse(await readBody(event))
|
||||
|
||||
if (session.role !== 'ADMIN') {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.id },
|
||||
include: { acls: true },
|
||||
})
|
||||
if (!user) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
||||
}
|
||||
const allowed = allowedActionsForRepo(user, body.name)
|
||||
if (!allowed.includes('delete') && !allowed.includes('*')) {
|
||||
throw createError({ statusCode: 403, statusMessage: 'Delete not allowed' })
|
||||
}
|
||||
}
|
||||
|
||||
const digest = await getManifestDigest(body.name, body.tag)
|
||||
if (!digest) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Tag not found' })
|
||||
}
|
||||
|
||||
await deleteManifest(body.name, digest)
|
||||
return { ok: true, name: body.name, tag: body.tag, digest }
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireSession(event)
|
||||
await ensureBootstrap()
|
||||
|
||||
const query = getQuery(event)
|
||||
const name = typeof query.name === 'string' ? query.name : ''
|
||||
if (!name) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Missing name query parameter' })
|
||||
}
|
||||
|
||||
const tags = await listTags(name)
|
||||
return { name, tags }
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireSession(event)
|
||||
await ensureBootstrap()
|
||||
|
||||
try {
|
||||
const repositories = await listRepositories()
|
||||
return { repositories }
|
||||
} catch (err: any) {
|
||||
// Registry may be unreachable during local UI-only boot
|
||||
if (err?.statusCode) throw err
|
||||
throw createError({
|
||||
statusCode: 502,
|
||||
statusMessage: err?.message || 'Failed to reach registry',
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireSession(event)
|
||||
await ensureBootstrap()
|
||||
|
||||
const [userCount, repositories] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
listRepositories().catch(() => [] as string[]),
|
||||
])
|
||||
|
||||
return {
|
||||
users: userCount,
|
||||
repositories: repositories.length,
|
||||
authPublicUrl: process.env.AUTH_PUBLIC_URL || '',
|
||||
registryInternalUrl: process.env.REGISTRY_INTERNAL_URL || '',
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await requireAdmin(event)
|
||||
await ensureBootstrap()
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
if (!id) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Missing id' })
|
||||
}
|
||||
|
||||
if (session.id === id) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Cannot delete yourself' })
|
||||
}
|
||||
|
||||
const existing = await prisma.user.findUnique({ where: { id } })
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'User not found' })
|
||||
}
|
||||
|
||||
if (existing.username === (process.env.ADMIN_USERNAME || 'admin')) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Cannot delete bootstrap admin' })
|
||||
}
|
||||
|
||||
await prisma.user.delete({ where: { id } })
|
||||
return { ok: true }
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAdmin(event)
|
||||
await ensureBootstrap()
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
if (!id) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Missing id' })
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: { acls: true },
|
||||
})
|
||||
if (!user) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'User not found' })
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
active: user.active,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
acls: user.acls,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const aclSchema = z.object({
|
||||
repository: z.string().min(1),
|
||||
canPull: z.boolean().default(true),
|
||||
canPush: z.boolean().default(false),
|
||||
canDelete: z.boolean().default(false),
|
||||
})
|
||||
|
||||
const bodySchema = z.object({
|
||||
password: z.string().min(6).optional(),
|
||||
role: z.enum(['ADMIN', 'USER']).optional(),
|
||||
active: z.boolean().optional(),
|
||||
acls: z.array(aclSchema).optional(),
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await requireAdmin(event)
|
||||
await ensureBootstrap()
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
if (!id) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Missing id' })
|
||||
}
|
||||
|
||||
const existing = await prisma.user.findUnique({ where: { id } })
|
||||
if (!existing) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'User not found' })
|
||||
}
|
||||
|
||||
const body = bodySchema.parse(await readBody(event))
|
||||
|
||||
if (existing.username === (process.env.ADMIN_USERNAME || 'admin') && body.active === false) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Cannot deactivate bootstrap admin' })
|
||||
}
|
||||
|
||||
if (session.id === id && body.role && body.role !== 'ADMIN') {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Cannot demote yourself' })
|
||||
}
|
||||
|
||||
const user = await prisma.$transaction(async (tx) => {
|
||||
if (body.acls) {
|
||||
await tx.acl.deleteMany({ where: { userId: id } })
|
||||
await tx.acl.createMany({
|
||||
data: body.acls.map((acl) => ({
|
||||
userId: id,
|
||||
repository: acl.repository,
|
||||
canPull: acl.canPull,
|
||||
canPush: acl.canPush,
|
||||
canDelete: acl.canDelete,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
return tx.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.role ? { role: body.role } : {}),
|
||||
...(typeof body.active === 'boolean' ? { active: body.active } : {}),
|
||||
...(body.password ? { passwordHash: await hashPassword(body.password) } : {}),
|
||||
},
|
||||
include: { acls: true },
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
active: user.active,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
acls: user.acls,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAdmin(event)
|
||||
await ensureBootstrap()
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
orderBy: { username: 'asc' },
|
||||
include: { acls: true },
|
||||
})
|
||||
|
||||
return {
|
||||
users: users.map((u) => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
role: u.role,
|
||||
active: u.active,
|
||||
createdAt: u.createdAt,
|
||||
updatedAt: u.updatedAt,
|
||||
acls: u.acls,
|
||||
})),
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const aclSchema = z.object({
|
||||
repository: z.string().min(1),
|
||||
canPull: z.boolean().default(true),
|
||||
canPush: z.boolean().default(false),
|
||||
canDelete: z.boolean().default(false),
|
||||
})
|
||||
|
||||
const bodySchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(/^[a-zA-Z0-9._-]+$/, 'Username may contain letters, numbers, . _ -'),
|
||||
password: z.string().min(6),
|
||||
role: z.enum(['ADMIN', 'USER']).default('USER'),
|
||||
active: z.boolean().default(true),
|
||||
acls: z.array(aclSchema).default([]),
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAdmin(event)
|
||||
await ensureBootstrap()
|
||||
|
||||
const body = bodySchema.parse(await readBody(event))
|
||||
const existing = await prisma.user.findUnique({ where: { username: body.username } })
|
||||
if (existing) {
|
||||
throw createError({ statusCode: 409, statusMessage: 'Username already exists' })
|
||||
}
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username: body.username,
|
||||
passwordHash: await hashPassword(body.password),
|
||||
role: body.role,
|
||||
active: body.active,
|
||||
acls: {
|
||||
create: body.acls.map((acl) => ({
|
||||
repository: acl.repository,
|
||||
canPull: acl.canPull,
|
||||
canPush: acl.canPush,
|
||||
canDelete: acl.canDelete,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: { acls: true },
|
||||
})
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
active: user.active,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
acls: user.acls,
|
||||
},
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user