This commit is contained in:
@@ -15,6 +15,7 @@ import { NoticesModule } from './notices/notices.module';
|
|||||||
import { PrismaModule } from './prisma/prisma.module';
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
import { RuntimeModule } from './runtime/runtime.module';
|
import { RuntimeModule } from './runtime/runtime.module';
|
||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from './users/users.module';
|
||||||
|
import { WoodpeckerModule } from './woodpecker/woodpecker.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -39,6 +40,7 @@ import { UsersModule } from './users/users.module';
|
|||||||
PrismaModule,
|
PrismaModule,
|
||||||
RuntimeModule,
|
RuntimeModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
WoodpeckerModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -137,15 +137,19 @@ export class UsersService {
|
|||||||
async terminateSession(sessionId: string) {
|
async terminateSession(sessionId: string) {
|
||||||
const session = await this.authPrisma.refreshToken.findUnique({
|
const session = await this.authPrisma.refreshToken.findUnique({
|
||||||
where: { id: sessionId },
|
where: { id: sessionId },
|
||||||
select: { id: true },
|
select: { id: true, userId: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new NotFoundException('세션을 찾을 수 없습니다.');
|
throw new NotFoundException('세션을 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.authPrisma.refreshToken.update({
|
await this.authPrisma.refreshToken.updateMany({
|
||||||
where: { id: sessionId },
|
where: {
|
||||||
|
userId: session.userId,
|
||||||
|
revokedAt: null,
|
||||||
|
expiresAt: { gt: new Date() },
|
||||||
|
},
|
||||||
data: { revokedAt: new Date() },
|
data: { revokedAt: new Date() },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,5 +175,5 @@ function getSessionState(
|
|||||||
return '만료';
|
return '만료';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '활성';
|
return '유효토큰';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WoodpeckerService } from './woodpecker.service';
|
||||||
|
|
||||||
|
@Controller('woodpecker')
|
||||||
|
export class WoodpeckerController {
|
||||||
|
constructor(private readonly woodpeckerService: WoodpeckerService) {}
|
||||||
|
|
||||||
|
@Get('repos')
|
||||||
|
listRepos() {
|
||||||
|
return this.woodpeckerService.listRepos();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WoodpeckerController } from './woodpecker.controller';
|
||||||
|
import { WoodpeckerService } from './woodpecker.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [WoodpeckerController],
|
||||||
|
providers: [WoodpeckerService],
|
||||||
|
})
|
||||||
|
export class WoodpeckerModule {}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { BadGatewayException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
type WoodpeckerRepo = Record<string, unknown>;
|
||||||
|
type WoodpeckerPipeline = Record<string, unknown>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WoodpeckerService {
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
private readonly token?: string;
|
||||||
|
|
||||||
|
constructor(configService: ConfigService) {
|
||||||
|
this.baseUrl = configService
|
||||||
|
.get<string>('WOODPECKER_BASE_URL', 'https://woodpecker.autonomystudio.net')
|
||||||
|
.replace(/\/$/, '');
|
||||||
|
this.token =
|
||||||
|
configService.get<string>('WOODPECKER_TOKEN') ||
|
||||||
|
configService.get<string>('WOODPECKER_ADMIN_TOKEN') ||
|
||||||
|
undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRepos() {
|
||||||
|
this.assertToken();
|
||||||
|
|
||||||
|
const body = await this.request<unknown>('/api/repos?latest=true');
|
||||||
|
const repos = Array.isArray(body)
|
||||||
|
? body
|
||||||
|
: isRecord(body) && Array.isArray(body.repos)
|
||||||
|
? body.repos
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
repos: repos.map((repo) => normalizeRepo(repo)).sort((a, b) => b.createdAt - a.createdAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(path: string): Promise<T> {
|
||||||
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${this.token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const detail = await response.text().catch(() => '');
|
||||||
|
throw new BadGatewayException({
|
||||||
|
message: 'Woodpecker API 요청이 실패했습니다.',
|
||||||
|
status: response.status,
|
||||||
|
detail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertToken() {
|
||||||
|
if (!this.token) {
|
||||||
|
throw new UnauthorizedException('WOODPECKER_TOKEN이 설정되어 있지 않습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRepo(repo: WoodpeckerRepo) {
|
||||||
|
const pipeline = getPipeline(repo);
|
||||||
|
const createdAt = getPipelineTime(pipeline);
|
||||||
|
const repository = getString(repo.full_name) || getString(repo.fullName) || joinRepoName(repo) || getString(repo.name) || '-';
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${getString(repo.id) || repository}-${getString(pipeline.id) || getString(pipeline.number) || createdAt}`,
|
||||||
|
repository,
|
||||||
|
message: getString(pipeline.message) || getString(pipeline.title) || getString(pipeline.event) || '-',
|
||||||
|
branch: getString(pipeline.branch) || '-',
|
||||||
|
status: getString(pipeline.status) || '-',
|
||||||
|
createdAt,
|
||||||
|
url: getString(repo.link_url) || getString(repo.forge_url) || getString(repo.url) || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPipeline(repo: WoodpeckerRepo): WoodpeckerPipeline {
|
||||||
|
const pipeline = repo.last_pipeline ?? repo.pipeline;
|
||||||
|
return isRecord(pipeline) ? pipeline : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPipelineTime(pipeline: WoodpeckerPipeline) {
|
||||||
|
const raw = pipeline.created ?? pipeline.created_at ?? pipeline.started ?? pipeline.started_at ?? pipeline.finished ?? pipeline.updated_at;
|
||||||
|
if (typeof raw === 'number') return raw > 10_000_000_000 ? raw : raw * 1000;
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
const numeric = Number(raw);
|
||||||
|
if (Number.isFinite(numeric) && numeric > 0) return numeric > 10_000_000_000 ? numeric : numeric * 1000;
|
||||||
|
const parsed = Date.parse(raw);
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinRepoName(repo: WoodpeckerRepo) {
|
||||||
|
const owner = getString(repo.owner);
|
||||||
|
const name = getString(repo.name);
|
||||||
|
return owner && name ? `${owner} / ${name}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getString(value: unknown) {
|
||||||
|
return typeof value === 'string' || typeof value === 'number' ? String(value) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user