@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
JavaScript
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