Add public API keys, outbound webhooks, and bulk SMS campaigns with settings UI and org modules.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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; 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 },
|
||||
});
|
||||
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,
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user