diff --git a/src/app.module.ts b/src/app.module.ts index 9e270a1..8187274 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -54,6 +54,7 @@ import { CacheModule } from './common/cache/cache.module'; import { AdminCacheModule } from './modules/admin-cache/admin-cache.module'; import { BackupModule } from './modules/backup/backup.module'; import { ReportingModule } from './modules/reporting/reporting.module'; +import { ProjectCenterModule } from './modules/project-center/project-center.module'; import { JwtAuthGuard } from './common/guards/jwt-auth.guard'; import { RolesGuard } from './common/guards/roles.guard'; @@ -153,6 +154,7 @@ import appleConfig from './config/apple.config'; AdminCacheModule, BackupModule, ReportingModule, + ProjectCenterModule, ], providers: [ { provide: APP_GUARD, useClass: RateLimitGuard }, diff --git a/src/modules/project-center/gitea.service.ts b/src/modules/project-center/gitea.service.ts new file mode 100644 index 0000000..8487bcf --- /dev/null +++ b/src/modules/project-center/gitea.service.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger } from '@nestjs/common'; + +const GITEA_BASE = 'https://git.admin.longde.cloud/api/v1'; +const GITEA_TOKEN = process.env.GITEA_TOKEN || '0b190ab69b751c6f4ce3f92cbd3ed762e5cd5bfa'; + +@Injectable() +export class GiteaService { + private readonly logger = new Logger(GiteaService.name); + + private async giteaGet(path: string): Promise { + try { + const res = await fetch(`${GITEA_BASE}${path}`, { + headers: { Authorization: `token ${GITEA_TOKEN}`, Accept: 'application/json' }, + }); + if (!res.ok) { this.logger.warn(`Gitea GET ${path}: ${res.status}`); return null; } + return await res.json() as T; + } catch (err: any) { + this.logger.warn(`Gitea GET ${path} failed: ${err.message}`); + return null; + } + } + + async getRepos() { + const repos = await this.giteaGet('/repos/search?limit=50'); + if (!repos) return []; + return Promise.all(repos.map(async (r: any) => { + const [issues, pulls, milestones] = await Promise.all([ + this.giteaGet(`/repos/${r.full_name}/issues?state=all&limit=1`).then(d => d?.length ?? 0).catch(() => 0), + this.giteaGet(`/repos/${r.full_name}/pulls?state=all&limit=1`).then(d => d?.length ?? 0).catch(() => 0), + this.giteaGet(`/repos/${r.full_name}/milestones?state=open`).catch(() => []), + ]); + return { + id: r.id, name: r.name, fullName: r.full_name, description: r.description, + owner: r.owner?.login, stars: r.stars_count, forks: r.forks_count, + openIssues: r.open_issues_count, openPulls: pulls, milestones: milestones?.length ?? 0, + defaultBranch: r.default_branch, updatedAt: r.updated_at, + }; + })); + } + + async getMilestones(owner: string, repo: string) { + return this.giteaGet(`/repos/${owner}/${repo}/milestones?state=all`) ?? []; + } + + async getIssues(owner: string, repo: string, milestone?: string, state = 'open') { + let path = `/repos/${owner}/${repo}/issues?state=${state}&limit=50`; + if (milestone) path += `&milestone=${milestone}`; + return this.giteaGet(path) ?? []; + } + + async getRunners() { + return this.giteaGet('/admin/runners') ?? []; + } + + async getReleases(owner: string, repo: string) { + return this.giteaGet(`/repos/${owner}/${repo}/releases?limit=20`) ?? []; + } +} diff --git a/src/modules/project-center/project-center.controller.ts b/src/modules/project-center/project-center.controller.ts new file mode 100644 index 0000000..abe4151 --- /dev/null +++ b/src/modules/project-center/project-center.controller.ts @@ -0,0 +1,48 @@ +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; +import { GiteaService } from './gitea.service'; +import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; +import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; + +@ApiTags('admin-project-center') +@ApiBearerAuth() +@Controller('admin-api/projects') +@UseGuards(AdminAuthGuard, AdminRolesGuard) +export class ProjectCenterController { + constructor(private readonly gitea: GiteaService) {} + + @Get('repos') + @ApiOperation({ summary: 'Gitea 仓库列表' }) + async getRepos() { + return this.gitea.getRepos(); + } + + @Get('repos/:owner/:repo/milestones') + @ApiOperation({ summary: '仓库里程碑' }) + async getMilestones(@Param('owner') owner: string, @Param('repo') repo: string) { + return this.gitea.getMilestones(owner, repo); + } + + @Get('repos/:owner/:repo/issues') + @ApiOperation({ summary: '仓库 Issue 列表' }) + @ApiQuery({ name: 'milestone', required: false }) + @ApiQuery({ name: 'state', required: false }) + async getIssues( + @Param('owner') owner: string, @Param('repo') repo: string, + @Query('milestone') milestone?: string, @Query('state') state?: string, + ) { + return this.gitea.getIssues(owner, repo, milestone, state); + } + + @Get('repos/:owner/:repo/releases') + @ApiOperation({ summary: '仓库 Release 列表' }) + async getReleases(@Param('owner') owner: string, @Param('repo') repo: string) { + return this.gitea.getReleases(owner, repo); + } + + @Get('runners') + @ApiOperation({ summary: 'Gitea Runner 状态' }) + async getRunners() { + return this.gitea.getRunners(); + } +} diff --git a/src/modules/project-center/project-center.module.ts b/src/modules/project-center/project-center.module.ts new file mode 100644 index 0000000..4dd7e54 --- /dev/null +++ b/src/modules/project-center/project-center.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ProjectCenterController } from './project-center.controller'; +import { GiteaService } from './gitea.service'; + +@Module({ + controllers: [ProjectCenterController], + providers: [GiteaService], + exports: [GiteaService], +}) +export class ProjectCenterModule {}