Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions nilcc-api/migrations/1771977600000-ApiTokensTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { MigrationInterface, QueryRunner } from "typeorm";

export class ApiTokensTable1771977600000 implements MigrationInterface {
name = "ApiTokensTable1771977600000";

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE api_tokens (
id character varying NOT NULL,
token character varying NOT NULL,
account_id character varying NOT NULL,
created_at TIMESTAMP NOT NULL,
CONSTRAINT pk_api_tokens PRIMARY KEY (id)
)
`);
await queryRunner.query(
"CREATE UNIQUE INDEX api_tokens_token_idx ON api_tokens (token)",
);
await queryRunner.query(`
ALTER TABLE api_tokens
ADD CONSTRAINT fk_api_tokens_account_id
FOREIGN KEY (account_id) REFERENCES accounts(id)
`);
await queryRunner.query(`
INSERT INTO api_tokens (id, token, created_at)
SELECT gen_random_uuid()::text, api_token, NOW()
FROM accounts
`);
await queryRunner.query("DROP INDEX account_entity_api_token_idx");
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE accounts ALTER COLUMN api_token SET NOT NULL",
);
await queryRunner.query(
"CREATE UNIQUE INDEX account_entity_api_token_idx ON accounts (api_token)",
);
await queryRunner.query(
"ALTER TABLE api_tokens DROP CONSTRAINT fk_api_tokens_account_id",
);
}
}
7 changes: 6 additions & 1 deletion nilcc-api/src/account/account.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,14 @@ export function create(options: ControllerOptions) {
}),
adminAuthentication(bindings),
payloadValidator(CreateAccountRequest),
transactionMiddleware(bindings.dataSource),
async (c) => {
const payload = c.req.valid("json");
const account = await bindings.services.account.create(bindings, payload);
const account = await bindings.services.account.create(
bindings,
payload,
c.get("txQueryRunner"),
);
return c.json(accountMapper.entityToResponse(account));
},
);
Expand Down
2 changes: 1 addition & 1 deletion nilcc-api/src/account/account.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class AccountEntity {
@Column({ type: "varchar", unique: true })
name: string;

@Column({ type: "varchar", unique: true })
@Column({ type: "varchar" })
apiToken: string;

@Column({ type: "int" })
Expand Down
28 changes: 23 additions & 5 deletions nilcc-api/src/account/account.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as crypto from "node:crypto";
import { In, type QueryRunner, type Repository } from "typeorm";
import { v4 as uuidv4 } from "uuid";
import { ApiTokenEntity } from "#/api-token/api-token.entity";
import {
EntityAlreadyExists,
EntityNotFound,
Expand Down Expand Up @@ -31,16 +32,26 @@ export class AccountService {
async create(
bindings: AppBindings,
request: CreateAccountRequest,
tx: QueryRunner,
): Promise<AccountEntity> {
const repository = this.getRepository(bindings);
const accountRepository = this.getRepository(bindings, tx);
const apiTokensRepository = tx.manager.getRepository(ApiTokenEntity);
try {
return await repository.save({
const token = crypto.randomBytes(API_TOKEN_BYTE_LENGTH).toString("hex");
const account = await accountRepository.save({
id: uuidv4(),
name: request.name,
apiToken: crypto.randomBytes(API_TOKEN_BYTE_LENGTH).toString("hex"),
apiToken: token,
createdAt: new Date(),
credits: request.credits,
});
await apiTokensRepository.save({
id: uuidv4(),
token,
account,
createdAt: bindings.services.time.getTime(),
});
return account;
} catch (e: unknown) {
if (isUniqueConstraint(e)) {
throw new EntityAlreadyExists("account");
Expand Down Expand Up @@ -72,8 +83,15 @@ export class AccountService {
bindings: AppBindings,
apiToken: string,
): Promise<AccountEntity | null> {
const repository = this.getRepository(bindings);
return await repository.findOneBy({ apiToken });
const tokenRepository = bindings.dataSource.getRepository(ApiTokenEntity);
const tokens = await tokenRepository.find({
where: { token: apiToken },
relations: ["account"],
});
if (tokens.length === 0) {
return null;
}
return tokens[0].account;
}

async list(bindings: AppBindings): Promise<AccountEntity[]> {
Expand Down
17 changes: 17 additions & 0 deletions nilcc-api/src/api-token/api-token.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
import { AccountEntity } from "#/account/account.entity";

@Entity({ name: "api_tokens" })
export class ApiTokenEntity {
@PrimaryColumn({ type: "varchar" })
id: string;

@Column({ type: "varchar", unique: true })
token: string;

@ManyToOne(() => AccountEntity, { onDelete: "CASCADE" })
account: AccountEntity;

@Column({ type: "timestamp" })
createdAt: Date;
}
6 changes: 5 additions & 1 deletion nilcc-api/src/data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { CreateArtifactsTable1758563696154 } from "migrations/1758563696154-Crea
import { MetalInstanceArtifactVersions1758645450546 } from "migrations/1758645450546-MetalInstanceArtifactVersions";
import { ArtifactVersionMandatory1758656531192 } from "migrations/1758656531192-ArtifactVersionMandatory";
import { WorkloadHeartbeats1765485856928 } from "migrations/1765485856928-WorkloadHeartbeats";
import { ApiTokensTable1771977600000 } from "migrations/1771977600000-ApiTokensTable";
import { DataSource } from "typeorm";
import type { EnvVars } from "#/env";
import { MetalInstanceEntity } from "#/metal-instance/metal-instance.entity";
Expand All @@ -32,6 +33,7 @@ import {
WorkloadEventEntity,
} from "#/workload/workload.entity";
import { AccountEntity } from "./account/account.entity";
import { ApiTokenEntity } from "./api-token/api-token.entity";
import { ArtifactEntity } from "./artifact/artifact.entity";
import { WorkloadTierEntity } from "./workload-tier/workload-tier.entity";

Expand All @@ -41,9 +43,10 @@ export async function buildDataSource(config: EnvVars): Promise<DataSource> {
url: config.dbUri,
entities: [
AccountEntity,
ApiTokenEntity,
ArtifactEntity,
WorkloadEntity,
MetalInstanceEntity,
WorkloadEntity,
WorkloadEventEntity,
WorkloadTierEntity,
],
Expand All @@ -64,6 +67,7 @@ export async function buildDataSource(config: EnvVars): Promise<DataSource> {
MetalInstanceArtifactVersions1758645450546,
ArtifactVersionMandatory1758656531192,
WorkloadHeartbeats1765485856928,
ApiTokensTable1771977600000,
],
synchronize: false,
logging: false,
Expand Down
Loading