UNPKG

@fdm-monster/server

Version:

FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.

90 lines (89 loc) 3.47 kB
import { NotFoundException } from "../../exceptions/runtime.exceptions.js"; import { BaseService } from "./base.service.js"; import { Role } from "../../entities/role.entity.js"; import { ApiKey } from "../../entities/api-key.entity.js"; import "../../entities/index.js"; import { ApiKeyDto } from "../interfaces/api-key.dto.js"; import { In } from "typeorm"; import { createHash, randomBytes, timingSafeEqual } from "node:crypto"; //#region src/services/orm/api-key.service.ts const TOKEN_PREFIX = "fdmm_api_"; const SECRET_BYTES = 32; const PREFIX_LEN = 16; function generateToken() { const secret = randomBytes(SECRET_BYTES).toString("base64url"); const token = `${TOKEN_PREFIX}${secret}`; return { token, prefix: secret.slice(0, PREFIX_LEN), hashedSecret: createHash("sha256").update(token).digest("hex") }; } var ApiKeyService = class ApiKeyService extends BaseService(ApiKey, ApiKeyDto) { logger; constructor(loggerFactory, typeormService) { super(typeormService); this.logger = loggerFactory(ApiKeyService.name); } get roleRepo() { return this.typeormService.getDataSource().getRepository(Role); } toDto(entity) { return { id: entity.id, createdByUserId: entity.createdByUserId, label: entity.label, prefix: entity.prefix, createdAt: entity.createdAt, lastUsedAt: entity.lastUsedAt, roles: (entity.roles ?? []).map((r) => r.name) }; } looksLikeApiKey(token) { return typeof token === "string" && token.startsWith(TOKEN_PREFIX) && token.length > 9 + PREFIX_LEN; } async create(createdByUserId, label, roleIds) { const trimmed = label?.trim(); if (!trimmed?.length) throw new Error("API key label is required"); if (!roleIds?.length) throw new Error("At least one role must be assigned to an API key"); const roles = await this.roleRepo.find({ where: { id: In(roleIds) } }); if (roles.length !== roleIds.length) throw new NotFoundException("One or more roleIds do not exist"); const { token, prefix, hashedSecret } = generateToken(); const entity = await this.repository.save(this.repository.create({ createdByUserId, label: trimmed, prefix, hashedSecret, lastUsedAt: null, roles })); this.logger.log(`Created API key id=${entity.id} prefix=${prefix} by user ${createdByUserId} with roles ${roleIds}`); return { ...this.toDto(entity), token }; } async list() { return (await this.repository.find({ order: { createdAt: "DESC" } })).map((r) => this.toDto(r)); } async delete(id) { const row = await this.repository.findOneBy({ id }); if (!row) throw new NotFoundException(`API key ${id} not found`); await this.repository.delete(id); this.logger.log(`Deleted API key id=${id} prefix=${row.prefix}`); } async verify(token) { if (!this.looksLikeApiKey(token)) return null; const prefix = token.slice(9).slice(0, PREFIX_LEN); const candidate = await this.repository.findOne({ where: { prefix } }); if (!candidate) return null; const presented = createHash("sha256").update(token).digest(); const stored = Buffer.from(candidate.hashedSecret, "hex"); if (presented.length !== stored.length || !timingSafeEqual(presented, stored)) return null; this.repository.update({ id: candidate.id }, { lastUsedAt: /* @__PURE__ */ new Date() }).catch((err) => this.logger.warn(`Failed to bump lastUsedAt for api key ${candidate.id}: ${err}`)); return candidate; } }; //#endregion export { ApiKeyService }; //# sourceMappingURL=api-key.service.js.map