2026-07-19 12:29:52 -05:00
|
|
|
import { and, eq } from "drizzle-orm";
|
|
|
|
|
import { opksshTokens } from "../db/schema.js";
|
|
|
|
|
import type { DatabaseContext } from "./database-context.js";
|
2026-08-06 14:41:39 -05:00
|
|
|
import { rowsAffected } from "./mutation-result.js";
|
|
|
|
|
import { upsert } from "./returning.js";
|
2026-07-19 12:29:52 -05:00
|
|
|
|
|
|
|
|
export type OpksshTokenRecord = typeof opksshTokens.$inferSelect;
|
|
|
|
|
|
|
|
|
|
export interface OpksshTokenUpsertInput {
|
|
|
|
|
userId: string;
|
|
|
|
|
hostId: number;
|
|
|
|
|
sshCert: string;
|
|
|
|
|
privateKey: string;
|
|
|
|
|
email?: string | null;
|
|
|
|
|
sub?: string | null;
|
|
|
|
|
issuer?: string | null;
|
|
|
|
|
audience?: string | null;
|
|
|
|
|
expiresAt: string;
|
|
|
|
|
createdAt?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class OpksshTokenRepository {
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly context: DatabaseContext,
|
|
|
|
|
private readonly onWrite?: () => void | Promise<void>,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async upsert(input: OpksshTokenUpsertInput): Promise<void> {
|
|
|
|
|
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
await upsert(
|
|
|
|
|
this.context,
|
|
|
|
|
opksshTokens,
|
|
|
|
|
{
|
2026-07-19 12:29:52 -05:00
|
|
|
userId: input.userId,
|
|
|
|
|
hostId: input.hostId,
|
|
|
|
|
sshCert: input.sshCert,
|
|
|
|
|
privateKey: input.privateKey,
|
|
|
|
|
email: input.email,
|
|
|
|
|
sub: input.sub,
|
|
|
|
|
issuer: input.issuer,
|
|
|
|
|
audience: input.audience,
|
|
|
|
|
expiresAt: input.expiresAt,
|
2026-08-06 14:41:39 -05:00
|
|
|
},
|
|
|
|
|
{
|
2026-07-19 12:29:52 -05:00
|
|
|
target: [opksshTokens.userId, opksshTokens.hostId],
|
|
|
|
|
set: {
|
|
|
|
|
sshCert: input.sshCert,
|
|
|
|
|
privateKey: input.privateKey,
|
|
|
|
|
email: input.email,
|
|
|
|
|
sub: input.sub,
|
|
|
|
|
issuer: input.issuer,
|
|
|
|
|
audience: input.audience,
|
|
|
|
|
expiresAt: input.expiresAt,
|
|
|
|
|
createdAt,
|
|
|
|
|
},
|
2026-08-06 14:41:39 -05:00
|
|
|
},
|
|
|
|
|
);
|
2026-07-19 12:29:52 -05:00
|
|
|
|
|
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async findByUserAndHost(
|
|
|
|
|
userId: string,
|
|
|
|
|
hostId: number,
|
|
|
|
|
): Promise<OpksshTokenRecord | null> {
|
|
|
|
|
const rows = await this.context.drizzle
|
|
|
|
|
.select()
|
|
|
|
|
.from(opksshTokens)
|
|
|
|
|
.where(
|
|
|
|
|
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
|
|
|
|
|
)
|
|
|
|
|
.limit(1);
|
|
|
|
|
|
|
|
|
|
return rows[0] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateLastUsed(
|
|
|
|
|
userId: string,
|
|
|
|
|
hostId: number,
|
|
|
|
|
lastUsed = new Date().toISOString(),
|
|
|
|
|
): Promise<boolean> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const result = await this.context.drizzle
|
2026-07-19 12:29:52 -05:00
|
|
|
.update(opksshTokens)
|
|
|
|
|
.set({ lastUsed })
|
|
|
|
|
.where(
|
|
|
|
|
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
|
2026-08-06 14:41:39 -05:00
|
|
|
);
|
2026-07-19 12:29:52 -05:00
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
if (rowsAffected(result) > 0) {
|
2026-07-19 12:29:52 -05:00
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
return rowsAffected(result) > 0;
|
2026-07-19 12:29:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteByUserAndHost(userId: string, hostId: number): Promise<boolean> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const result = await this.context.drizzle
|
2026-07-19 12:29:52 -05:00
|
|
|
.delete(opksshTokens)
|
|
|
|
|
.where(
|
|
|
|
|
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
|
2026-08-06 14:41:39 -05:00
|
|
|
);
|
2026-07-19 12:29:52 -05:00
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
if (rowsAffected(result) > 0) {
|
2026-07-19 12:29:52 -05:00
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
return rowsAffected(result) > 0;
|
2026-07-19 12:29:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteByUserId(userId: string): Promise<number> {
|
2026-08-06 14:41:39 -05:00
|
|
|
const result = await this.context.drizzle
|
2026-07-19 12:29:52 -05:00
|
|
|
.delete(opksshTokens)
|
2026-08-06 14:41:39 -05:00
|
|
|
.where(eq(opksshTokens.userId, userId));
|
2026-07-19 12:29:52 -05:00
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
if (rowsAffected(result) > 0) {
|
2026-07-19 12:29:52 -05:00
|
|
|
await this.afterWrite();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 14:41:39 -05:00
|
|
|
return rowsAffected(result);
|
2026-07-19 12:29:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async afterWrite(): Promise<void> {
|
|
|
|
|
await this.onWrite?.();
|
|
|
|
|
}
|
|
|
|
|
}
|