diff --git a/prisma/launcher.schema.prisma b/prisma/launcher.schema.prisma index f0523f4..ff86a6f 100644 --- a/prisma/launcher.schema.prisma +++ b/prisma/launcher.schema.prisma @@ -46,16 +46,20 @@ model LauncherTerm { } model LauncherManualDocument { - id String @id @default(uuid()) @db.Uuid - appKey String @map("app_key") - title String - body String - isPublished Boolean @default(true) @map("is_published") - sortOrder Int @default(0) @map("sort_order") - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) - updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) + id String @id @default(uuid()) @db.Uuid + appKey String @map("app_key") + title String + body String + isPublished Boolean @default(true) @map("is_published") + sortOrder Int @default(0) @map("sort_order") + createdById String? @map("created_by_id") @db.Uuid + createdByName String? @map("created_by_name") + createdByEmail String? @map("created_by_email") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) @@index([appKey, isPublished, sortOrder]) + @@index([createdByEmail]) @@map("launcher_manual_documents") } diff --git a/prisma/migrations/20260714110000_add_launcher_manual_document_creator/migration.sql b/prisma/migrations/20260714110000_add_launcher_manual_document_creator/migration.sql new file mode 100644 index 0000000..69605c1 --- /dev/null +++ b/prisma/migrations/20260714110000_add_launcher_manual_document_creator/migration.sql @@ -0,0 +1,7 @@ +ALTER TABLE "launcher_manual_documents" + ADD COLUMN IF NOT EXISTS "created_by_id" UUID, + ADD COLUMN IF NOT EXISTS "created_by_name" TEXT, + ADD COLUMN IF NOT EXISTS "created_by_email" TEXT; + +CREATE INDEX IF NOT EXISTS "launcher_manual_documents_created_by_email_idx" + ON "launcher_manual_documents"("created_by_email"); diff --git a/src/admins/admin-avatar.ts b/src/admins/admin-avatar.ts new file mode 100644 index 0000000..5b6d608 --- /dev/null +++ b/src/admins/admin-avatar.ts @@ -0,0 +1,9 @@ +import { ConfigService } from '@nestjs/config'; + +export function getAdminAvatarUrl(adminId: string | null | undefined, configService: ConfigService) { + const publicBaseUrl = configService.get('R2_PUBLIC_BASE_URL')?.replace(/\/+$/, ''); + if (!adminId || !publicBaseUrl) return null; + + const prefix = configService.get('R2_PUBLIC_FILE_PREFIX', 'studio-files/public'); + return `${publicBaseUrl}/${prefix.replace(/^\/+|\/+$/g, '')}/admins/${adminId}/avatar`; +} diff --git a/src/admins/admins.service.ts b/src/admins/admins.service.ts index 90e9568..da31a92 100644 --- a/src/admins/admins.service.ts +++ b/src/admins/admins.service.ts @@ -5,6 +5,7 @@ import { ConfigService } from '@nestjs/config'; import { GiteaService } from '../gitea/gitea.service'; import { PrismaService } from '../prisma/prisma.service'; +import { getAdminAvatarUrl } from './admin-avatar'; import { AdminRole } from './admin-role'; import { CreateAdminSshKeyDto, @@ -334,13 +335,6 @@ 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, diff --git a/src/apps/apps.module.ts b/src/apps/apps.module.ts index 85b835c..50bf93f 100644 --- a/src/apps/apps.module.ts +++ b/src/apps/apps.module.ts @@ -2,13 +2,12 @@ import { Module } from '@nestjs/common'; import { LauncherModule } from '../launcher/launcher.module'; import { PrismaModule } from '../prisma/prisma.module'; -import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module'; import { AppsController } from './apps.controller'; import { AppsService } from './apps.service'; import { BuildRunnerService } from './build-runner.service'; @Module({ - imports: [PrismaModule, LauncherModule, AuthPrismaModule], + imports: [PrismaModule, LauncherModule], controllers: [AppsController], providers: [AppsService, BuildRunnerService], }) diff --git a/src/apps/apps.service.ts b/src/apps/apps.service.ts index 5f3d75e..1b64f07 100644 --- a/src/apps/apps.service.ts +++ b/src/apps/apps.service.ts @@ -8,10 +8,9 @@ import * as path from 'node:path'; import { promisify } from 'node:util'; import { LinkComponentSourceDto } from '../gitea/dto'; +import { getAdminAvatarUrl } from '../admins/admin-avatar'; import { LauncherService } from '../launcher/launcher.service'; import { PrismaService } from '../prisma/prisma.service'; -import { AuthPrismaService } from '../auth-prisma/auth-prisma.service'; -import { getUserAvatarUrl } from '../users/users.service'; import { BuildRunnerService } from './build-runner.service'; import { CreateAppComponentDto, @@ -46,7 +45,6 @@ export class AppsService { private readonly configService: ConfigService, private readonly buildRunnerService: BuildRunnerService, private readonly launcherService: LauncherService, - private readonly authPrisma: AuthPrismaService, ) {} private requesterData(requester?: AdminRequester) { @@ -59,31 +57,11 @@ export class AppsService { : {}; } - private async withRequesterAvatars(items: T[]) { - const emails = [...new Set(items.map((item) => item.requestedByEmail).filter((email): email is string => Boolean(email)))]; - if (emails.length === 0) return items.map((item) => ({ ...item, requestedByAvatarUrl: null })); - - const users = await this.authPrisma.user.findMany({ - where: { email: { in: emails } }, - 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 items.map((item) => { - const user = item.requestedByEmail ? userByEmail.get(item.requestedByEmail) : null; - return { - ...item, - requestedByAvatarUrl: getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl), - }; - }); + private withRequesterAvatars(items: T[]) { + return items.map((item) => ({ + ...item, + requestedByAvatarUrl: getAdminAvatarUrl(item.requestedById, this.configService), + })); } async listApps() { diff --git a/src/launcher/launcher.module.ts b/src/launcher/launcher.module.ts index 968d50e..5f35493 100644 --- a/src/launcher/launcher.module.ts +++ b/src/launcher/launcher.module.ts @@ -2,12 +2,11 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from '../prisma/prisma.module'; import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module'; -import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module'; import { LauncherController } from './launcher.controller'; import { LauncherService } from './launcher.service'; @Module({ - imports: [PrismaModule, LauncherPrismaModule, AuthPrismaModule], + imports: [PrismaModule, LauncherPrismaModule], controllers: [LauncherController], providers: [LauncherService], exports: [LauncherService], diff --git a/src/launcher/launcher.service.ts b/src/launcher/launcher.service.ts index 6ec9997..60d20d7 100644 --- a/src/launcher/launcher.service.ts +++ b/src/launcher/launcher.service.ts @@ -10,8 +10,7 @@ import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service'; import { PrismaService } from '../prisma/prisma.service'; -import { AuthPrismaService } from '../auth-prisma/auth-prisma.service'; -import { getUserAvatarUrl } from '../users/users.service'; +import { getAdminAvatarUrl } from '../admins/admin-avatar'; import { CreateLauncherAppLinkDto, CreateLauncherBuildJobDto, @@ -63,7 +62,6 @@ export class LauncherService { private readonly prisma: PrismaService, private readonly launcherPrisma: LauncherPrismaService, private readonly configService: ConfigService, - private readonly authPrisma: AuthPrismaService, ) {} async listVersions() { @@ -453,31 +451,11 @@ export class LauncherService { }); } - private async withLauncherBuildRequesters(jobs: T[]) { - const emails = [...new Set(jobs.map((job) => job.requestedByEmail).filter((email): email is string => Boolean(email)))]; - if (emails.length === 0) return jobs.map((job) => ({ ...job, requestedByAvatarUrl: null })); - - const users = await this.authPrisma.user.findMany({ - where: { email: { in: emails } }, - 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 jobs.map((job) => { - const user = job.requestedByEmail ? userByEmail.get(job.requestedByEmail) : null; - return { - ...job, - requestedByAvatarUrl: getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl), - }; - }); + private withLauncherBuildRequesters(jobs: T[]) { + return jobs.map((job) => ({ + ...job, + requestedByAvatarUrl: getAdminAvatarUrl(job.requestedById, this.configService), + })); } diff --git a/src/manuals/manuals.controller.ts b/src/manuals/manuals.controller.ts index 86b52cf..75b4e90 100644 --- a/src/manuals/manuals.controller.ts +++ b/src/manuals/manuals.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Req } from '@nestjs/common'; import { Public } from '../admin-auth/decorators/admin-auth.decorators'; import { CreateManualDocumentDto, ListManualDocumentsQuery, UpdateManualDocumentDto } from './dto'; @@ -20,8 +20,11 @@ export class ManualsController { } @Post() - create(@Body() body: CreateManualDocumentDto) { - return this.manualsService.create(body); + create( + @Body() body: CreateManualDocumentDto, + @Req() request: { admin?: { id: string; name: string; email: string } }, + ) { + return this.manualsService.create(body, request.admin); } @Patch(':id') diff --git a/src/manuals/manuals.service.ts b/src/manuals/manuals.service.ts index 9250ada..6df51de 100644 --- a/src/manuals/manuals.service.ts +++ b/src/manuals/manuals.service.ts @@ -1,12 +1,34 @@ import { Injectable, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Prisma } from '@prisma/launcher-client'; +import { getAdminAvatarUrl } from '../admins/admin-avatar'; import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service'; import { CreateManualDocumentDto, ListManualDocumentsQuery, UpdateManualDocumentDto } from './dto'; @Injectable() export class ManualsService { - constructor(private readonly launcherPrisma: LauncherPrismaService) {} + constructor( + private readonly configService: ConfigService, + private readonly launcherPrisma: LauncherPrismaService, + ) {} + + private creatorData(requester?: AdminRequester) { + return requester + ? { + createdById: requester.id, + createdByName: requester.name, + createdByEmail: requester.email, + } + : {}; + } + + private withCreatorAvatars(items: T[]) { + return items.map((item) => ({ + ...item, + createdByAvatarUrl: getAdminAvatarUrl(item.createdById, this.configService), + })); + } async list(query: ListManualDocumentsQuery) { const page = toPositiveInt(query.page, 1); @@ -35,10 +57,10 @@ export class ManualsService { ]); return { - documents: documents.map((document, index) => ({ + documents: await this.withCreatorAvatars(documents.map((document, index) => ({ ...document, no: total - ((page - 1) * limit + index), - })), + }))), total, page, limit, @@ -61,7 +83,7 @@ export class ManualsService { return { documents }; } - async create(body: CreateManualDocumentDto) { + async create(body: CreateManualDocumentDto, requester?: AdminRequester) { const document = await this.launcherPrisma.launcherManualDocument.create({ data: { appKey: body.appKey.trim(), @@ -69,6 +91,7 @@ export class ManualsService { body: body.body.trim(), isPublished: body.isPublished ?? true, sortOrder: body.sortOrder ?? 0, + ...this.creatorData(requester), }, }); @@ -105,6 +128,12 @@ export class ManualsService { } +type AdminRequester = { + id: string; + name: string; + email: string; +}; + function toPositiveInt(value: string | undefined, fallback: number) { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback; diff --git a/src/notices/notices.module.ts b/src/notices/notices.module.ts index d3b6286..4d1b9c8 100644 --- a/src/notices/notices.module.ts +++ b/src/notices/notices.module.ts @@ -1,12 +1,11 @@ import { Module } from '@nestjs/common'; -import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module'; import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module'; import { NoticesController } from './notices.controller'; import { NoticesService } from './notices.service'; @Module({ - imports: [AuthPrismaModule, LauncherPrismaModule], + imports: [LauncherPrismaModule], controllers: [NoticesController], providers: [NoticesService], }) diff --git a/src/notices/notices.service.ts b/src/notices/notices.service.ts index 978ed89..64e6583 100644 --- a/src/notices/notices.service.ts +++ b/src/notices/notices.service.ts @@ -2,9 +2,8 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Prisma } from '@prisma/launcher-client'; -import { AuthPrismaService } from '../auth-prisma/auth-prisma.service'; +import { getAdminAvatarUrl } from '../admins/admin-avatar'; import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service'; -import { getUserAvatarUrl } from '../users/users.service'; import { CreateLauncherNoticeDto, ListLauncherNoticesQuery, @@ -20,7 +19,6 @@ type AdminRequester = { @Injectable() export class NoticesService { constructor( - private readonly authPrisma: AuthPrismaService, private readonly configService: ConfigService, private readonly launcherPrisma: LauncherPrismaService, ) {} @@ -35,31 +33,11 @@ export class NoticesService { : {}; } - private async withCreatorAvatars(items: T[]) { - const emails = [...new Set(items.map((item) => item.createdByEmail).filter((email): email is string => Boolean(email)))]; - if (emails.length === 0) return items.map((item) => ({ ...item, createdByAvatarUrl: null })); - - const users = await this.authPrisma.user.findMany({ - where: { email: { in: emails } }, - 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 items.map((item) => { - const user = item.createdByEmail ? userByEmail.get(item.createdByEmail) : null; - return { - ...item, - createdByAvatarUrl: getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl), - }; - }); + private withCreatorAvatars(items: T[]) { + return items.map((item) => ({ + ...item, + createdByAvatarUrl: getAdminAvatarUrl(item.createdById, this.configService), + })); } async list(query: ListLauncherNoticesQuery) { diff --git a/src/terms/terms.module.ts b/src/terms/terms.module.ts index f51b2b5..b3f4d86 100644 --- a/src/terms/terms.module.ts +++ b/src/terms/terms.module.ts @@ -1,12 +1,11 @@ import { Module } from '@nestjs/common'; -import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module'; import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module'; import { TermsController } from './terms.controller'; import { TermsService } from './terms.service'; @Module({ - imports: [AuthPrismaModule, LauncherPrismaModule], + imports: [LauncherPrismaModule], controllers: [TermsController], providers: [TermsService], }) diff --git a/src/terms/terms.service.ts b/src/terms/terms.service.ts index f678ec3..71ef220 100644 --- a/src/terms/terms.service.ts +++ b/src/terms/terms.service.ts @@ -2,9 +2,8 @@ import { ConflictException, Injectable, NotFoundException } from '@nestjs/common import { ConfigService } from '@nestjs/config'; import { Prisma } from '@prisma/launcher-client'; -import { AuthPrismaService } from '../auth-prisma/auth-prisma.service'; +import { getAdminAvatarUrl } from '../admins/admin-avatar'; import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service'; -import { getUserAvatarUrl } from '../users/users.service'; import { CreateTermDto, ListTermsQuery, UpdateTermDto } from './dto'; type AdminRequester = { @@ -16,7 +15,6 @@ type AdminRequester = { @Injectable() export class TermsService { constructor( - private readonly authPrisma: AuthPrismaService, private readonly configService: ConfigService, private readonly launcherPrisma: LauncherPrismaService, ) {} @@ -115,31 +113,11 @@ export class TermsService { : {}; } - private async withCreatorAvatars(items: T[]) { - const emails = [...new Set(items.map((item) => item.createdByEmail).filter((email): email is string => Boolean(email)))]; - if (emails.length === 0) return items.map((item) => ({ ...item, createdByAvatarUrl: null })); - - const users = await this.authPrisma.user.findMany({ - where: { email: { in: emails } }, - 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 items.map((item) => { - const user = item.createdByEmail ? userByEmail.get(item.createdByEmail) : null; - return { - ...item, - createdByAvatarUrl: getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl), - }; - }); + private withCreatorAvatars(items: T[]) { + return items.map((item) => ({ + ...item, + createdByAvatarUrl: getAdminAvatarUrl(item.createdById, this.configService), + })); } private async ensureExists(id: string) {