113 lines
3.2 KiB
TypeScript
113 lines
3.2 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Injectable,
|
|
NotFoundException,
|
|
UnauthorizedException,
|
|
} from '@nestjs/common';
|
|
import { createHash, randomBytes } from 'crypto';
|
|
import type {
|
|
CreateOrganizationApiKeyRequest,
|
|
CreateOrganizationApiKeyResponse,
|
|
OrganizationApiKeyDto,
|
|
} from '@mento/shared';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
@Injectable()
|
|
export class ApiKeysService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async list(organizationId: string): Promise<OrganizationApiKeyDto[]> {
|
|
const rows = await this.prisma.organizationApiKey.findMany({
|
|
where: { organizationId },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
return rows.map((row) => this.toDto(row));
|
|
}
|
|
|
|
async create(
|
|
organizationId: string,
|
|
dto: CreateOrganizationApiKeyRequest,
|
|
): Promise<CreateOrganizationApiKeyResponse> {
|
|
const name = dto.name?.trim();
|
|
if (!name) throw new BadRequestException('نام کلید الزامی است');
|
|
|
|
const rawKey = `mk_${randomBytes(24).toString('hex')}`;
|
|
const prefix = rawKey.slice(0, 10);
|
|
const keyHash = this.hashKey(rawKey);
|
|
|
|
const row = await this.prisma.organizationApiKey.create({
|
|
data: {
|
|
organizationId,
|
|
name,
|
|
prefix,
|
|
keyHash,
|
|
},
|
|
});
|
|
|
|
return { ...this.toDto(row), rawKey };
|
|
}
|
|
|
|
async revoke(organizationId: string, id: string): Promise<OrganizationApiKeyDto> {
|
|
const existing = await this.prisma.organizationApiKey.findFirst({
|
|
where: { id, organizationId },
|
|
});
|
|
if (!existing) throw new NotFoundException('کلید پیدا نشد');
|
|
if (existing.revokedAt) return this.toDto(existing);
|
|
|
|
const row = await this.prisma.organizationApiKey.update({
|
|
where: { id },
|
|
data: { revokedAt: new Date() },
|
|
});
|
|
return this.toDto(row);
|
|
}
|
|
|
|
async authenticate(
|
|
rawKey: string,
|
|
): Promise<{ organizationId: string; apiKeyId: string; name: string; role: 'owner' }> {
|
|
const key = rawKey.trim();
|
|
if (!key.startsWith('mk_') || key.length < 20) {
|
|
throw new UnauthorizedException('کلید API نامعتبر است');
|
|
}
|
|
const keyHash = this.hashKey(key);
|
|
const row = await this.prisma.organizationApiKey.findFirst({
|
|
where: { keyHash, revokedAt: null },
|
|
select: { id: true, organizationId: true, name: true },
|
|
});
|
|
if (!row) throw new UnauthorizedException('کلید API نامعتبر است');
|
|
|
|
await this.prisma.organizationApiKey.update({
|
|
where: { id: row.id },
|
|
data: { lastUsedAt: new Date() },
|
|
});
|
|
|
|
return {
|
|
organizationId: row.organizationId,
|
|
apiKeyId: row.id,
|
|
name: row.name,
|
|
role: 'owner',
|
|
};
|
|
}
|
|
|
|
hashKey(raw: string): string {
|
|
return createHash('sha256').update(raw).digest('hex');
|
|
}
|
|
|
|
private toDto(row: {
|
|
id: string;
|
|
name: string;
|
|
prefix: string;
|
|
lastUsedAt: Date | null;
|
|
revokedAt: Date | null;
|
|
createdAt: Date;
|
|
}): OrganizationApiKeyDto {
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
prefix: row.prefix,
|
|
lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
|
|
revokedAt: row.revokedAt?.toISOString() ?? null,
|
|
createdAt: row.createdAt.toISOString(),
|
|
};
|
|
}
|
|
}
|