track requesters for builds releases deployments and migrations
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-10 10:43:02 +09:00
parent dd09471267
commit 3873ce3169
17 changed files with 690 additions and 61 deletions
+62 -14
View File
@@ -1,9 +1,12 @@
import { createHash, randomBytes, scryptSync } from 'node:crypto';
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
import { GiteaService } from '../gitea/gitea.service';
import { PrismaService } from '../prisma/prisma.service';
import { getUserAvatarUrl } from '../users/users.service';
import { AdminRole } from './admin-role';
import {
CreateAdminSshKeyDto,
@@ -17,6 +20,8 @@ import {
@Injectable()
export class AdminsService {
constructor(
private readonly authPrisma: AuthPrismaService,
private readonly configService: ConfigService,
private readonly giteaService: GiteaService,
private readonly prisma: PrismaService,
) {}
@@ -38,8 +43,8 @@ export class AdminsService {
});
}
list() {
return this.prisma.admin.findMany({
async list() {
const admins = await this.prisma.admin.findMany({
orderBy: [{ role: 'asc' }, { createdAt: 'desc' }],
select: {
id: true,
@@ -69,15 +74,46 @@ export class AdminsService {
},
},
});
const users = await this.authPrisma.user.findMany({
where: { email: { in: admins.map((admin) => admin.email) } },
select: {
id: true,
email: true,
profileImageMime: true,
},
});
const userByEmail = new Map(users.map((user) => [user.email, user]));
const authBaseUrl = this.configService.get<string>('AUTH_PUBLIC_URL')
?? this.configService.get<string>('ATNMY_AUTH_URL')
?? this.configService.get<string>('AUTH_EXTERNAL_URL')
?? 'https://auth.autonomystudio.net';
return admins.map((admin) => {
const user = userByEmail.get(admin.email);
return {
...admin,
avatarUrl: getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl),
};
});
}
async create(body: CreateAdminDto) {
await this.ensureRoleEnabled(body.role);
const email = body.email.trim().toLowerCase();
const name = body.name.trim();
await this.giteaService.createUser({
username: createGiteaUsername(email),
email,
password: body.temporaryPassword,
fullName: name,
});
const admin = await this.prisma.admin.create({
data: {
email: body.email.trim().toLowerCase(),
name: body.name.trim(),
email,
name,
role: body.role,
passwordHash: hashPassword(body.temporaryPassword),
},
@@ -186,20 +222,18 @@ export class AdminsService {
}
async createSshKey(adminId: string, body: CreateAdminSshKeyDto) {
await this.ensureExists(adminId);
const admin = await this.ensureExists(adminId);
const parsedKey = parseSshPublicKey(body.publicKey);
const label = body.label?.trim() || `${parsedKey.keyType} ${parsedKey.fingerprint}`;
const giteaUsername = body.giteaUsername?.trim() || null;
const giteaUsername = createGiteaUsername(admin.email);
let giteaKeyId: number | null = null;
if (giteaUsername) {
const giteaKey = await this.giteaService.createUserSshKey(giteaUsername, {
title: label,
key: parsedKey.normalizedKey,
});
giteaKeyId = giteaKey.id;
}
const giteaKey = await this.giteaService.createUserSshKey(giteaUsername, {
title: label,
key: parsedKey.normalizedKey,
});
giteaKeyId = giteaKey.id;
const sshKey = await this.prisma.adminSshKey.create({
data: {
@@ -273,12 +307,14 @@ export class AdminsService {
private async ensureExists(id: string) {
const admin = await this.prisma.admin.findUnique({
where: { id },
select: { id: true },
select: { id: true, email: true },
});
if (!admin) {
throw new NotFoundException('관리자를 찾을 수 없습니다.');
}
return admin;
}
}
@@ -309,6 +345,18 @@ function hashPassword(password: string) {
return `scrypt:${salt}:${hash}`;
}
function createGiteaUsername(email: string) {
const account = email.split('@')[0] || email;
const username = account
.trim()
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/^[._-]+|[._-]+$/g, '')
.slice(0, 40);
return username || `admin-${createHash('sha1').update(email).digest('hex').slice(0, 8)}`;
}
function parseSshPublicKey(publicKey: string) {
const normalizedKey = publicKey.trim().replace(/\s+/g, ' ');
const [keyType, keyBody] = normalizedKey.split(' ');