diff --git a/studio-admin-ingress.yaml b/k8s/studio-admin-ingress.yaml similarity index 100% rename from studio-admin-ingress.yaml rename to k8s/studio-admin-ingress.yaml diff --git a/studio-launcher-ingress.yaml b/k8s/studio-launcher-ingress.yaml similarity index 100% rename from studio-launcher-ingress.yaml rename to k8s/studio-launcher-ingress.yaml diff --git a/src/admin-auth/admin-auth.module.ts b/src/admin-auth/admin-auth.module.ts index f1477bd..db656b6 100644 --- a/src/admin-auth/admin-auth.module.ts +++ b/src/admin-auth/admin-auth.module.ts @@ -1,13 +1,12 @@ import { Module } from '@nestjs/common'; -import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module'; import { GiteaModule } from '../gitea/gitea.module'; import { PrismaModule } from '../prisma/prisma.module'; import { AdminAuthController } from './admin-auth.controller'; import { AdminAuthService } from './admin-auth.service'; @Module({ - imports: [AuthPrismaModule, GiteaModule, PrismaModule], + imports: [GiteaModule, PrismaModule], controllers: [AdminAuthController], providers: [AdminAuthService], }) diff --git a/src/admin-auth/admin-auth.service.ts b/src/admin-auth/admin-auth.service.ts index 90f0e6b..d7378a1 100644 --- a/src/admin-auth/admin-auth.service.ts +++ b/src/admin-auth/admin-auth.service.ts @@ -4,10 +4,8 @@ import { DeleteObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client import { BadRequestException, Injectable, NotFoundException, UnauthorizedException } 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 { AdminLoginDto, ChangeAdminPasswordDto } from './dto'; @Injectable() @@ -15,7 +13,6 @@ export class AdminAuthService { private r2Client?: S3Client; constructor( - private readonly authPrisma: AuthPrismaService, private readonly configService: ConfigService, private readonly giteaService: GiteaService, private readonly prisma: PrismaService, @@ -74,7 +71,7 @@ export class AdminAuthService { }), ]); - const avatarUrl = await this.getAdminAvatarUrl(admin.email); + const avatarUrl = this.getAdminAvatarUrl(admin.id); return { token, @@ -125,7 +122,7 @@ export class AdminAuthService { email: admin.email, name: admin.name, role: admin.role, - avatarUrl: await this.getAdminAvatarUrl(admin.email), + avatarUrl: this.getAdminAvatarUrl(admin.id), appPermissions: admin.appPermissions.map((permission) => permission.appId), sshKeys: admin.sshKeys, }, @@ -145,45 +142,28 @@ export class AdminAuthService { throw new BadRequestException('프로필 이미지는 80KB 이하로 업로드해 주세요.'); } - const { admin, user } = await this.getLinkedAuthUser(adminId); + const admin = await this.getAdminForAvatar(adminId); const giteaUsername = await this.ensureGiteaUser(admin); - await this.uploadAvatarToR2(user.id, imageData, mimeType); + await this.uploadAvatarToR2(admin.id, imageData, mimeType); await this.giteaService.updateUserAvatar(giteaUsername, imageData, mimeType); - await this.authPrisma.user.update({ - where: { id: user.id }, - data: { - profileImageData: null, - profileImageMime: mimeType, - profileImageUpdatedAt: new Date(), - }, - }); return { - avatarUrl: await this.getAdminAvatarUrl(admin.email), + avatarUrl: this.getAdminAvatarUrl(admin.id), }; } async deleteAvatar(adminId: string) { - const { admin, user } = await this.getLinkedAuthUser(adminId); + const admin = await this.getAdminForAvatar(adminId); const giteaUsername = this.resolveGiteaUsername(admin); if (this.isR2Enabled()) { - await this.deleteAvatarFromR2(user.id); + await this.deleteAvatarFromR2(admin.id); } await this.giteaService.deleteUserAvatar(giteaUsername); - await this.authPrisma.user.update({ - where: { id: user.id }, - data: { - profileImageData: null, - profileImageMime: null, - profileImageUpdatedAt: new Date(), - }, - }); - return { - avatarUrl: await this.getAdminAvatarUrl(admin.email), + avatarUrl: this.getAdminAvatarUrl(admin.id), }; } @@ -217,24 +197,13 @@ export class AdminAuthService { return { ok: true }; } - private async getAdminAvatarUrl(email: string) { - const user = await this.authPrisma.user.findUnique({ - where: { email }, - select: { - id: true, - profileImageMime: true, - }, - }); - - const authBaseUrl = this.configService.get('AUTH_PUBLIC_URL') - ?? this.configService.get('ATNMY_AUTH_URL') - ?? this.configService.get('AUTH_EXTERNAL_URL') - ?? 'https://auth.autonomystudio.net'; - - return getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl); + private getAdminAvatarUrl(adminId: string) { + const publicBaseUrl = this.configService.get('R2_PUBLIC_BASE_URL')?.replace(/\/+$/, ''); + if (!publicBaseUrl) return null; + return `${publicBaseUrl}/${this.createAvatarObjectKey(adminId)}`; } - private async getLinkedAuthUser(adminId: string) { + private async getAdminForAvatar(adminId: string) { const admin = await this.prisma.admin.findUnique({ where: { id: adminId }, select: { @@ -254,25 +223,7 @@ export class AdminAuthService { throw new UnauthorizedException('관리자 세션이 유효하지 않습니다.'); } - let user = await this.authPrisma.user.findUnique({ - where: { email: admin.email }, - select: { id: true }, - }); - - if (!user) { - user = await this.authPrisma.user.create({ - data: { - email: admin.email, - passwordHash: hashPassword(randomBytes(24).toString('hex')), - name: admin.name, - role: 'ADMIN', - isActive: true, - }, - select: { id: true }, - }); - } - - return { admin, user }; + return admin; } private async ensureGiteaUser(admin: { email: string; name: string; sshKeys?: Array<{ giteaUsername: string | null }> }) { @@ -297,7 +248,7 @@ export class AdminAuthService { return admin.sshKeys?.find((key) => key.giteaUsername?.trim())?.giteaUsername?.trim() || null; } - private async uploadAvatarToR2(userId: string, imageData: Buffer, mimeType: string) { + private async uploadAvatarToR2(adminId: string, imageData: Buffer, mimeType: string) { const bucket = this.configService.get('R2_BUCKET'); const accessKeyId = this.configService.get('R2_ACCESS_KEY_ID'); const secretAccessKey = this.configService.get('R2_SECRET_ACCESS_KEY'); @@ -308,14 +259,14 @@ export class AdminAuthService { await this.getR2Client(accessKeyId, secretAccessKey).send(new PutObjectCommand({ Bucket: bucket, - Key: this.createAvatarObjectKey(userId), + Key: this.createAvatarObjectKey(adminId), Body: imageData, ContentType: mimeType, CacheControl: 'public, max-age=60', })); } - private async deleteAvatarFromR2(userId: string) { + private async deleteAvatarFromR2(adminId: string) { const bucket = this.configService.get('R2_BUCKET'); const accessKeyId = this.configService.get('R2_ACCESS_KEY_ID'); const secretAccessKey = this.configService.get('R2_SECRET_ACCESS_KEY'); @@ -324,7 +275,7 @@ export class AdminAuthService { await this.getR2Client(accessKeyId, secretAccessKey).send(new DeleteObjectCommand({ Bucket: bucket, - Key: this.createAvatarObjectKey(userId), + Key: this.createAvatarObjectKey(adminId), })); } @@ -355,9 +306,9 @@ export class AdminAuthService { ); } - private createAvatarObjectKey(userId: string) { + private createAvatarObjectKey(adminId: string) { const prefix = this.configService.get('R2_PUBLIC_FILE_PREFIX', 'studio-files/public'); - return `${prefix.replace(/^\/+|\/+$/g, '')}/users/${userId}/avatar`; + return `${prefix.replace(/^\/+|\/+$/g, '')}/admins/${adminId}/avatar`; } } diff --git a/src/admins/admins.module.ts b/src/admins/admins.module.ts index 4b797f4..2464787 100644 --- a/src/admins/admins.module.ts +++ b/src/admins/admins.module.ts @@ -1,13 +1,12 @@ import { Module } from '@nestjs/common'; -import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module'; import { GiteaModule } from '../gitea/gitea.module'; import { PrismaModule } from '../prisma/prisma.module'; import { AdminsController } from './admins.controller'; import { AdminsService } from './admins.service'; @Module({ - imports: [AuthPrismaModule, GiteaModule, PrismaModule], + imports: [GiteaModule, PrismaModule], controllers: [AdminsController], providers: [AdminsService], }) diff --git a/src/admins/admins.service.ts b/src/admins/admins.service.ts index 18a4af1..90e9568 100644 --- a/src/admins/admins.service.ts +++ b/src/admins/admins.service.ts @@ -3,10 +3,8 @@ 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, @@ -21,7 +19,6 @@ import { @Injectable() export class AdminsService { constructor( - private readonly authPrisma: AuthPrismaService, private readonly configService: ConfigService, private readonly giteaService: GiteaService, private readonly prisma: PrismaService, @@ -98,27 +95,12 @@ export class AdminsService { this.prisma.admin.count({ where }), ]); - 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('AUTH_PUBLIC_URL') - ?? this.configService.get('ATNMY_AUTH_URL') - ?? this.configService.get('AUTH_EXTERNAL_URL') - ?? 'https://auth.autonomystudio.net'; - return { admins: admins.map((admin, index) => { - const user = userByEmail.get(admin.email); return { ...admin, no: total - ((page - 1) * limit + index), - avatarUrl: getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl), + avatarUrl: getAdminAvatarUrl(admin.id, this.configService), }; }), total, @@ -352,6 +334,13 @@ export class AdminsService { } } +function getAdminAvatarUrl(adminId: string, configService: ConfigService) { + const publicBaseUrl = configService.get('R2_PUBLIC_BASE_URL')?.replace(/\/+$/, ''); + if (!publicBaseUrl) return null; + const prefix = configService.get('R2_PUBLIC_FILE_PREFIX', 'studio-files/public'); + return `${publicBaseUrl}/${prefix.replace(/^\/+|\/+$/g, '')}/admins/${adminId}/avatar`; +} + const adminSelect = { id: true, email: true,