@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,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],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user