modify nav..
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-13 16:56:13 +09:00
parent aac1d01526
commit 195a893bd1
6 changed files with 30 additions and 92 deletions
+1 -2
View File
@@ -1,13 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
import { GiteaModule } from '../gitea/gitea.module'; import { GiteaModule } from '../gitea/gitea.module';
import { PrismaModule } from '../prisma/prisma.module'; import { PrismaModule } from '../prisma/prisma.module';
import { AdminAuthController } from './admin-auth.controller'; import { AdminAuthController } from './admin-auth.controller';
import { AdminAuthService } from './admin-auth.service'; import { AdminAuthService } from './admin-auth.service';
@Module({ @Module({
imports: [AuthPrismaModule, GiteaModule, PrismaModule], imports: [GiteaModule, PrismaModule],
controllers: [AdminAuthController], controllers: [AdminAuthController],
providers: [AdminAuthService], providers: [AdminAuthService],
}) })
+20 -69
View File
@@ -4,10 +4,8 @@ import { DeleteObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client
import { BadRequestException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { BadRequestException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
import { GiteaService } from '../gitea/gitea.service'; import { GiteaService } from '../gitea/gitea.service';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { getUserAvatarUrl } from '../users/users.service';
import { AdminLoginDto, ChangeAdminPasswordDto } from './dto'; import { AdminLoginDto, ChangeAdminPasswordDto } from './dto';
@Injectable() @Injectable()
@@ -15,7 +13,6 @@ export class AdminAuthService {
private r2Client?: S3Client; private r2Client?: S3Client;
constructor( constructor(
private readonly authPrisma: AuthPrismaService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly giteaService: GiteaService, private readonly giteaService: GiteaService,
private readonly prisma: PrismaService, 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 { return {
token, token,
@@ -125,7 +122,7 @@ export class AdminAuthService {
email: admin.email, email: admin.email,
name: admin.name, name: admin.name,
role: admin.role, role: admin.role,
avatarUrl: await this.getAdminAvatarUrl(admin.email), avatarUrl: this.getAdminAvatarUrl(admin.id),
appPermissions: admin.appPermissions.map((permission) => permission.appId), appPermissions: admin.appPermissions.map((permission) => permission.appId),
sshKeys: admin.sshKeys, sshKeys: admin.sshKeys,
}, },
@@ -145,45 +142,28 @@ export class AdminAuthService {
throw new BadRequestException('프로필 이미지는 80KB 이하로 업로드해 주세요.'); throw new BadRequestException('프로필 이미지는 80KB 이하로 업로드해 주세요.');
} }
const { admin, user } = await this.getLinkedAuthUser(adminId); const admin = await this.getAdminForAvatar(adminId);
const giteaUsername = await this.ensureGiteaUser(admin); 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.giteaService.updateUserAvatar(giteaUsername, imageData, mimeType);
await this.authPrisma.user.update({
where: { id: user.id },
data: {
profileImageData: null,
profileImageMime: mimeType,
profileImageUpdatedAt: new Date(),
},
});
return { return {
avatarUrl: await this.getAdminAvatarUrl(admin.email), avatarUrl: this.getAdminAvatarUrl(admin.id),
}; };
} }
async deleteAvatar(adminId: string) { async deleteAvatar(adminId: string) {
const { admin, user } = await this.getLinkedAuthUser(adminId); const admin = await this.getAdminForAvatar(adminId);
const giteaUsername = this.resolveGiteaUsername(admin); const giteaUsername = this.resolveGiteaUsername(admin);
if (this.isR2Enabled()) { if (this.isR2Enabled()) {
await this.deleteAvatarFromR2(user.id); await this.deleteAvatarFromR2(admin.id);
} }
await this.giteaService.deleteUserAvatar(giteaUsername); await this.giteaService.deleteUserAvatar(giteaUsername);
await this.authPrisma.user.update({
where: { id: user.id },
data: {
profileImageData: null,
profileImageMime: null,
profileImageUpdatedAt: new Date(),
},
});
return { return {
avatarUrl: await this.getAdminAvatarUrl(admin.email), avatarUrl: this.getAdminAvatarUrl(admin.id),
}; };
} }
@@ -217,24 +197,13 @@ export class AdminAuthService {
return { ok: true }; return { ok: true };
} }
private async getAdminAvatarUrl(email: string) { private getAdminAvatarUrl(adminId: string) {
const user = await this.authPrisma.user.findUnique({ const publicBaseUrl = this.configService.get<string>('R2_PUBLIC_BASE_URL')?.replace(/\/+$/, '');
where: { email }, if (!publicBaseUrl) return null;
select: { return `${publicBaseUrl}/${this.createAvatarObjectKey(adminId)}`;
id: true,
profileImageMime: true,
},
});
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 getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl);
} }
private async getLinkedAuthUser(adminId: string) { private async getAdminForAvatar(adminId: string) {
const admin = await this.prisma.admin.findUnique({ const admin = await this.prisma.admin.findUnique({
where: { id: adminId }, where: { id: adminId },
select: { select: {
@@ -254,25 +223,7 @@ export class AdminAuthService {
throw new UnauthorizedException('관리자 세션이 유효하지 않습니다.'); throw new UnauthorizedException('관리자 세션이 유효하지 않습니다.');
} }
let user = await this.authPrisma.user.findUnique({ return admin;
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 };
} }
private async ensureGiteaUser(admin: { email: string; name: string; sshKeys?: Array<{ giteaUsername: string | null }> }) { 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; 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<string>('R2_BUCKET'); const bucket = this.configService.get<string>('R2_BUCKET');
const accessKeyId = this.configService.get<string>('R2_ACCESS_KEY_ID'); const accessKeyId = this.configService.get<string>('R2_ACCESS_KEY_ID');
const secretAccessKey = this.configService.get<string>('R2_SECRET_ACCESS_KEY'); const secretAccessKey = this.configService.get<string>('R2_SECRET_ACCESS_KEY');
@@ -308,14 +259,14 @@ export class AdminAuthService {
await this.getR2Client(accessKeyId, secretAccessKey).send(new PutObjectCommand({ await this.getR2Client(accessKeyId, secretAccessKey).send(new PutObjectCommand({
Bucket: bucket, Bucket: bucket,
Key: this.createAvatarObjectKey(userId), Key: this.createAvatarObjectKey(adminId),
Body: imageData, Body: imageData,
ContentType: mimeType, ContentType: mimeType,
CacheControl: 'public, max-age=60', CacheControl: 'public, max-age=60',
})); }));
} }
private async deleteAvatarFromR2(userId: string) { private async deleteAvatarFromR2(adminId: string) {
const bucket = this.configService.get<string>('R2_BUCKET'); const bucket = this.configService.get<string>('R2_BUCKET');
const accessKeyId = this.configService.get<string>('R2_ACCESS_KEY_ID'); const accessKeyId = this.configService.get<string>('R2_ACCESS_KEY_ID');
const secretAccessKey = this.configService.get<string>('R2_SECRET_ACCESS_KEY'); const secretAccessKey = this.configService.get<string>('R2_SECRET_ACCESS_KEY');
@@ -324,7 +275,7 @@ export class AdminAuthService {
await this.getR2Client(accessKeyId, secretAccessKey).send(new DeleteObjectCommand({ await this.getR2Client(accessKeyId, secretAccessKey).send(new DeleteObjectCommand({
Bucket: bucket, 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<string>('R2_PUBLIC_FILE_PREFIX', 'studio-files/public'); const prefix = this.configService.get<string>('R2_PUBLIC_FILE_PREFIX', 'studio-files/public');
return `${prefix.replace(/^\/+|\/+$/g, '')}/users/${userId}/avatar`; return `${prefix.replace(/^\/+|\/+$/g, '')}/admins/${adminId}/avatar`;
} }
} }
+1 -2
View File
@@ -1,13 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
import { GiteaModule } from '../gitea/gitea.module'; import { GiteaModule } from '../gitea/gitea.module';
import { PrismaModule } from '../prisma/prisma.module'; import { PrismaModule } from '../prisma/prisma.module';
import { AdminsController } from './admins.controller'; import { AdminsController } from './admins.controller';
import { AdminsService } from './admins.service'; import { AdminsService } from './admins.service';
@Module({ @Module({
imports: [AuthPrismaModule, GiteaModule, PrismaModule], imports: [GiteaModule, PrismaModule],
controllers: [AdminsController], controllers: [AdminsController],
providers: [AdminsService], providers: [AdminsService],
}) })
+8 -19
View File
@@ -3,10 +3,8 @@ import { createHash, randomBytes, scryptSync } from 'node:crypto';
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
import { GiteaService } from '../gitea/gitea.service'; import { GiteaService } from '../gitea/gitea.service';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { getUserAvatarUrl } from '../users/users.service';
import { AdminRole } from './admin-role'; import { AdminRole } from './admin-role';
import { import {
CreateAdminSshKeyDto, CreateAdminSshKeyDto,
@@ -21,7 +19,6 @@ import {
@Injectable() @Injectable()
export class AdminsService { export class AdminsService {
constructor( constructor(
private readonly authPrisma: AuthPrismaService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly giteaService: GiteaService, private readonly giteaService: GiteaService,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
@@ -98,27 +95,12 @@ export class AdminsService {
this.prisma.admin.count({ where }), 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<string>('AUTH_PUBLIC_URL')
?? this.configService.get<string>('ATNMY_AUTH_URL')
?? this.configService.get<string>('AUTH_EXTERNAL_URL')
?? 'https://auth.autonomystudio.net';
return { return {
admins: admins.map((admin, index) => { admins: admins.map((admin, index) => {
const user = userByEmail.get(admin.email);
return { return {
...admin, ...admin,
no: total - ((page - 1) * limit + index), no: total - ((page - 1) * limit + index),
avatarUrl: getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl), avatarUrl: getAdminAvatarUrl(admin.id, this.configService),
}; };
}), }),
total, total,
@@ -352,6 +334,13 @@ export class AdminsService {
} }
} }
function getAdminAvatarUrl(adminId: string, configService: ConfigService) {
const publicBaseUrl = configService.get<string>('R2_PUBLIC_BASE_URL')?.replace(/\/+$/, '');
if (!publicBaseUrl) return null;
const prefix = configService.get<string>('R2_PUBLIC_FILE_PREFIX', 'studio-files/public');
return `${publicBaseUrl}/${prefix.replace(/^\/+|\/+$/g, '')}/admins/${adminId}/avatar`;
}
const adminSelect = { const adminSelect = {
id: true, id: true,
email: true, email: true,