mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 21:33:41 +00:00
refactor(db): route remaining raw DB access through repositories
proxmox, session-log, oidc-utils, webauthn and guacamole recording now use repositories (new WebauthnCredentialRepository; SsoProviderRepository listEnabled; HostRepository findDecryptedByIdAs/listProxmoxEnabled). Remaining raw access: db boot code, simple-db-ops and docker.ts, which are removed/restructured in later phases.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
|
import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
|
||||||
import { getDb, getSqlite } from "../db/index.js";
|
import { getDb, getSqlite } from "../db/index.js";
|
||||||
import type { DatabaseContext } from "./database-context.js";
|
import type { DatabaseContext } from "./database-context.js";
|
||||||
|
import { WebauthnCredentialRepository } from "./webauthn-credential-repository.js";
|
||||||
import { AlertRepository } from "./alert-repository.js";
|
import { AlertRepository } from "./alert-repository.js";
|
||||||
import { ApiKeyRepository } from "./api-key-repository.js";
|
import { ApiKeyRepository } from "./api-key-repository.js";
|
||||||
import { AuditLogRepository } from "./audit-log-repository.js";
|
import { AuditLogRepository } from "./audit-log-repository.js";
|
||||||
@@ -68,6 +69,13 @@ export function getCurrentSettingValue(key: string): string | null {
|
|||||||
return row?.value ?? null;
|
return row?.value ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createCurrentWebauthnCredentialRepository(): WebauthnCredentialRepository {
|
||||||
|
return new WebauthnCredentialRepository(
|
||||||
|
createCurrentRepositoryContext(),
|
||||||
|
createCurrentRepositoryWriteHook("webauthn_credential_repository_write"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function createCurrentAlertRepository(): AlertRepository {
|
export function createCurrentAlertRepository(): AlertRepository {
|
||||||
return new AlertRepository(
|
return new AlertRepository(
|
||||||
createCurrentRepositoryContext(),
|
createCurrentRepositoryContext(),
|
||||||
|
|||||||
@@ -78,6 +78,32 @@ export class HostRepository {
|
|||||||
return rows[0] ?? null;
|
return rows[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findDecryptedByIdAs(
|
||||||
|
userId: string,
|
||||||
|
hostId: number,
|
||||||
|
): Promise<HostRecord | null> {
|
||||||
|
const row = await this.findById(hostId);
|
||||||
|
if (!row) return null;
|
||||||
|
|
||||||
|
const userDataKey = DataCrypto.getUserDataKey(userId);
|
||||||
|
if (!userDataKey) return null;
|
||||||
|
|
||||||
|
return DataCrypto.decryptRecord("ssh_data", row, userId, userDataKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listProxmoxEnabled(): Promise<
|
||||||
|
Pick<HostRecord, "id" | "userId" | "proxmoxConfig">[]
|
||||||
|
> {
|
||||||
|
return this.context.drizzle
|
||||||
|
.select({
|
||||||
|
id: hosts.id,
|
||||||
|
userId: hosts.userId,
|
||||||
|
proxmoxConfig: hosts.proxmoxConfig,
|
||||||
|
})
|
||||||
|
.from(hosts)
|
||||||
|
.where(eq(hosts.enableProxmox, true));
|
||||||
|
}
|
||||||
|
|
||||||
async listByUserId(userId: string): Promise<HostRecord[]> {
|
async listByUserId(userId: string): Promise<HostRecord[]> {
|
||||||
return this.context.drizzle
|
return this.context.drizzle
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ export class SsoProviderRepository {
|
|||||||
.orderBy(asc(ssoProviders.displayOrder), asc(ssoProviders.id));
|
.orderBy(asc(ssoProviders.displayOrder), asc(ssoProviders.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listEnabled(): Promise<SsoProviderRecord[]> {
|
||||||
|
return this.context.drizzle
|
||||||
|
.select()
|
||||||
|
.from(ssoProviders)
|
||||||
|
.where(eq(ssoProviders.enabled, true))
|
||||||
|
.orderBy(asc(ssoProviders.displayOrder), asc(ssoProviders.id));
|
||||||
|
}
|
||||||
|
|
||||||
async listAll(): Promise<SsoProviderRecord[]> {
|
async listAll(): Promise<SsoProviderRecord[]> {
|
||||||
return this.context.drizzle
|
return this.context.drizzle
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { webauthnCredentials } from "../db/schema.js";
|
||||||
|
import type { DatabaseContext } from "./database-context.js";
|
||||||
|
|
||||||
|
export type WebauthnCredentialRecord = typeof webauthnCredentials.$inferSelect;
|
||||||
|
export type NewWebauthnCredentialRecord =
|
||||||
|
typeof webauthnCredentials.$inferInsert;
|
||||||
|
|
||||||
|
export interface WebauthnCredentialAuthState {
|
||||||
|
counter: number;
|
||||||
|
backedUp: boolean;
|
||||||
|
deviceType: string | null;
|
||||||
|
lastUsedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebauthnCredentialRepository {
|
||||||
|
constructor(
|
||||||
|
private readonly context: DatabaseContext,
|
||||||
|
private readonly onWrite?: () => void | Promise<void>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async listByUserId(userId: string): Promise<WebauthnCredentialRecord[]> {
|
||||||
|
return this.context.drizzle
|
||||||
|
.select()
|
||||||
|
.from(webauthnCredentials)
|
||||||
|
.where(eq(webauthnCredentials.userId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByCredentialId(
|
||||||
|
credentialId: string,
|
||||||
|
): Promise<WebauthnCredentialRecord | null> {
|
||||||
|
const rows = await this.context.drizzle
|
||||||
|
.select()
|
||||||
|
.from(webauthnCredentials)
|
||||||
|
.where(eq(webauthnCredentials.credentialId, credentialId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
record: NewWebauthnCredentialRecord,
|
||||||
|
): Promise<WebauthnCredentialRecord> {
|
||||||
|
const rows = await this.context.drizzle
|
||||||
|
.insert(webauthnCredentials)
|
||||||
|
.values(record)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
await this.afterWrite();
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAuthState(
|
||||||
|
id: string,
|
||||||
|
state: WebauthnCredentialAuthState,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.context.drizzle
|
||||||
|
.update(webauthnCredentials)
|
||||||
|
.set(state)
|
||||||
|
.where(eq(webauthnCredentials.id, id));
|
||||||
|
|
||||||
|
await this.afterWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteForUser(userId: string, id: string): Promise<boolean> {
|
||||||
|
const rows = await this.context.drizzle
|
||||||
|
.delete(webauthnCredentials)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(webauthnCredentials.id, id),
|
||||||
|
eq(webauthnCredentials.userId, userId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning({ id: webauthnCredentials.id });
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
await this.afterWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async afterWrite(): Promise<void> {
|
||||||
|
await this.onWrite?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { Client as SSHClient } from "ssh2";
|
import { Client as SSHClient } from "ssh2";
|
||||||
import { db, getDb, DatabaseSaveTrigger } from "../db/index.js";
|
|
||||||
import { hosts, sshCredentials } from "../db/schema.js";
|
|
||||||
import { eq, and } from "drizzle-orm";
|
|
||||||
import { logger } from "../../utils/logger.js";
|
import { logger } from "../../utils/logger.js";
|
||||||
import { SimpleDBOps } from "../../utils/simple-db-ops.js";
|
import { DataCrypto } from "../../utils/data-crypto.js";
|
||||||
|
import {
|
||||||
|
createCurrentCredentialRepository,
|
||||||
|
createCurrentHostRepository,
|
||||||
|
} from "../repositories/factory.js";
|
||||||
import { AuthManager } from "../../utils/auth-manager.js";
|
import { AuthManager } from "../../utils/auth-manager.js";
|
||||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||||
import type { SSHHost } from "../../../types/index.js";
|
import type { SSHHost } from "../../../types/index.js";
|
||||||
@@ -288,25 +289,24 @@ async function discoverProxmoxGuestsForHost(
|
|||||||
defaultCredentialId: number | null;
|
defaultCredentialId: number | null;
|
||||||
config: ReturnType<typeof parseProxmoxConfig>;
|
config: ReturnType<typeof parseProxmoxConfig>;
|
||||||
}> {
|
}> {
|
||||||
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
if (!DataCrypto.canUserAccessData(userId)) {
|
||||||
const error = new Error("Session expired — please log in again");
|
const error = new Error("Session expired — please log in again");
|
||||||
(error as Error & { code?: string }).code = "SESSION_EXPIRED";
|
(error as Error & { code?: string }).code = "SESSION_EXPIRED";
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostResults = await SimpleDBOps.select(
|
const hostRecord = await createCurrentHostRepository().findDecryptedByIdAs(
|
||||||
getDb().select().from(hosts).where(eq(hosts.id, parsedHostId)),
|
|
||||||
"ssh_data",
|
|
||||||
userId,
|
userId,
|
||||||
|
parsedHostId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hostResults.length) {
|
if (!hostRecord) {
|
||||||
const error = new Error("Host not found");
|
const error = new Error("Host not found");
|
||||||
(error as Error & { status?: number }).status = 404;
|
(error as Error & { status?: number }).status = 404;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = hostResults[0] as unknown as SSHHost;
|
const host = hostRecord as unknown as SSHHost;
|
||||||
const proxmoxCfgRaw = parseJsonObject(host.proxmoxConfig);
|
const proxmoxCfgRaw = parseJsonObject(host.proxmoxConfig);
|
||||||
const config = parseProxmoxConfig(proxmoxCfgRaw);
|
const config = parseProxmoxConfig(proxmoxCfgRaw);
|
||||||
|
|
||||||
@@ -362,21 +362,13 @@ async function discoverProxmoxGuestsForHost(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const creds = await SimpleDBOps.select(
|
const cred =
|
||||||
getDb()
|
await createCurrentCredentialRepository().findDecryptedByIdForUser(
|
||||||
.select()
|
|
||||||
.from(sshCredentials)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(sshCredentials.id, host.credentialId as number),
|
|
||||||
eq(sshCredentials.userId, userId),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
"ssh_credentials",
|
|
||||||
userId,
|
userId,
|
||||||
|
host.credentialId as number,
|
||||||
);
|
);
|
||||||
if (creds.length > 0) {
|
if (cred) {
|
||||||
const c = creds[0];
|
const c = cred;
|
||||||
resolvedCredentials = {
|
resolvedCredentials = {
|
||||||
password: c.password as string | undefined,
|
password: c.password as string | undefined,
|
||||||
sshKey: (c.key || c.privateKey) as string | undefined,
|
sshKey: (c.key || c.privateKey) as string | undefined,
|
||||||
@@ -634,11 +626,10 @@ async function syncProxmoxHost(
|
|||||||
);
|
);
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
const existingHosts = await SimpleDBOps.select<Record<string, unknown>>(
|
const existingHosts =
|
||||||
db.select().from(hosts).where(eq(hosts.userId, userId)),
|
(await createCurrentHostRepository().listDecryptedByUserId(
|
||||||
"ssh_data",
|
|
||||||
userId,
|
userId,
|
||||||
);
|
)) as unknown as Record<string, unknown>[];
|
||||||
const existingBySource = new Map<string, Record<string, unknown>>();
|
const existingBySource = new Map<string, Record<string, unknown>>();
|
||||||
for (const host of existingHosts) {
|
for (const host of existingHosts) {
|
||||||
const source = getProxmoxSource(host);
|
const source = getProxmoxSource(host);
|
||||||
@@ -712,12 +703,10 @@ async function syncProxmoxHost(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await SimpleDBOps.update(
|
await createCurrentHostRepository().updateEncryptedForUser(
|
||||||
hosts,
|
|
||||||
"ssh_data",
|
|
||||||
eq(hosts.id, existing.id as number),
|
|
||||||
update,
|
|
||||||
userId,
|
userId,
|
||||||
|
existing.id as number,
|
||||||
|
update,
|
||||||
);
|
);
|
||||||
result.updated++;
|
result.updated++;
|
||||||
continue;
|
continue;
|
||||||
@@ -732,17 +721,13 @@ async function syncProxmoxHost(
|
|||||||
enableRdp: connectionType === "rdp",
|
enableRdp: connectionType === "rdp",
|
||||||
});
|
});
|
||||||
|
|
||||||
await SimpleDBOps.insert(
|
await createCurrentHostRepository().createEncryptedForUser(userId, {
|
||||||
hosts,
|
|
||||||
"ssh_data",
|
|
||||||
{
|
|
||||||
...update,
|
...update,
|
||||||
userId,
|
userId,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
pin: false,
|
pin: false,
|
||||||
authType: connectionType === "rdp" ? "password" : importAuth.authType,
|
authType: connectionType === "rdp" ? "password" : importAuth.authType,
|
||||||
credentialId:
|
credentialId: connectionType === "ssh" ? importAuth.credentialId : null,
|
||||||
connectionType === "ssh" ? importAuth.credentialId : null,
|
|
||||||
overrideCredentialUsername: importAuth.overrideCredentialUsername,
|
overrideCredentialUsername: importAuth.overrideCredentialUsername,
|
||||||
password: null,
|
password: null,
|
||||||
key: null,
|
key: null,
|
||||||
@@ -780,9 +765,7 @@ async function syncProxmoxHost(
|
|||||||
showTunnelInSidebar: 0,
|
showTunnelInSidebar: 0,
|
||||||
showDockerInSidebar: 0,
|
showDockerInSidebar: 0,
|
||||||
showServerStatsInSidebar: 0,
|
showServerStatsInSidebar: 0,
|
||||||
},
|
});
|
||||||
userId,
|
|
||||||
);
|
|
||||||
result.created++;
|
result.created++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,10 +776,9 @@ async function syncProxmoxHost(
|
|||||||
const source = getProxmoxSource(existing);
|
const source = getProxmoxSource(existing);
|
||||||
if (!source) continue;
|
if (!source) continue;
|
||||||
const missingSince = source.missingSince || now;
|
const missingSince = source.missingSince || now;
|
||||||
await SimpleDBOps.update(
|
await createCurrentHostRepository().updateEncryptedForUser(
|
||||||
hosts,
|
userId,
|
||||||
"ssh_data",
|
existing.id as number,
|
||||||
eq(hosts.id, existing.id as number),
|
|
||||||
{
|
{
|
||||||
tags: mergeTags(existing.tags, ["proxmox-missing"]),
|
tags: mergeTags(existing.tags, ["proxmox-missing"]),
|
||||||
proxmoxConfig: JSON.stringify({
|
proxmoxConfig: JSON.stringify({
|
||||||
@@ -808,7 +790,6 @@ async function syncProxmoxHost(
|
|||||||
}),
|
}),
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
},
|
},
|
||||||
userId,
|
|
||||||
);
|
);
|
||||||
result.markedMissing++;
|
result.markedMissing++;
|
||||||
}
|
}
|
||||||
@@ -842,18 +823,13 @@ async function writeSyncStatus(
|
|||||||
hostId: number,
|
hostId: number,
|
||||||
patch: Record<string, unknown>,
|
patch: Record<string, unknown>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const rows = await db
|
const hostRepository = createCurrentHostRepository();
|
||||||
.select({ proxmoxConfig: hosts.proxmoxConfig })
|
const row = await hostRepository.findByIdForUser(userId, hostId);
|
||||||
.from(hosts)
|
if (!row) return;
|
||||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
|
const config = parseJsonObject(row.proxmoxConfig);
|
||||||
.limit(1);
|
await hostRepository.updateForUser(userId, hostId, {
|
||||||
if (!rows.length) return;
|
proxmoxConfig: JSON.stringify({ ...config, ...patch }),
|
||||||
const config = parseJsonObject(rows[0].proxmoxConfig);
|
});
|
||||||
await db
|
|
||||||
.update(hosts)
|
|
||||||
.set({ proxmoxConfig: JSON.stringify({ ...config, ...patch }) })
|
|
||||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)));
|
|
||||||
DatabaseSaveTrigger.triggerSave("proxmox_sync_status");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
router.post("/sync", authenticateJWT, requireDataAccess, async (req, res) => {
|
router.post("/sync", authenticateJWT, requireDataAccess, async (req, res) => {
|
||||||
@@ -886,22 +862,14 @@ router.post("/sync", authenticateJWT, requireDataAccess, async (req, res) => {
|
|||||||
|
|
||||||
async function runDueProxmoxAutoSyncs(): Promise<void> {
|
async function runDueProxmoxAutoSyncs(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const rows = await db
|
const rows = await createCurrentHostRepository().listProxmoxEnabled();
|
||||||
.select({
|
|
||||||
id: hosts.id,
|
|
||||||
userId: hosts.userId,
|
|
||||||
enableProxmox: hosts.enableProxmox,
|
|
||||||
proxmoxConfig: hosts.proxmoxConfig,
|
|
||||||
})
|
|
||||||
.from(hosts)
|
|
||||||
.where(eq(hosts.enableProxmox, true));
|
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const configRaw = parseJsonObject(row.proxmoxConfig);
|
const configRaw = parseJsonObject(row.proxmoxConfig);
|
||||||
const config = parseProxmoxConfig(configRaw);
|
const config = parseProxmoxConfig(configRaw);
|
||||||
if (!config.autoSyncEnabled) continue;
|
if (!config.autoSyncEnabled) continue;
|
||||||
if (!SimpleDBOps.isUserDataUnlocked(row.userId)) continue;
|
if (!DataCrypto.canUserAccessData(row.userId)) continue;
|
||||||
|
|
||||||
const lastSyncAt =
|
const lastSyncAt =
|
||||||
typeof configRaw.lastSyncAt === "string"
|
typeof configRaw.lastSyncAt === "string"
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import { apiLogger } from "../../utils/logger.js";
|
|||||||
import { AuthManager } from "../../utils/auth-manager.js";
|
import { AuthManager } from "../../utils/auth-manager.js";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { PermissionManager } from "../../utils/permission-manager.js";
|
import { PermissionManager } from "../../utils/permission-manager.js";
|
||||||
import { createCurrentSessionRecordingRepository } from "../repositories/factory.js";
|
import {
|
||||||
import { getDb } from "../db/index.js";
|
createCurrentSessionRecordingRepository,
|
||||||
|
createCurrentSettingsRepository,
|
||||||
|
getCurrentSettingValue,
|
||||||
|
} from "../repositories/factory.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -29,12 +32,10 @@ function getRetentionDays(): number {
|
|||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const row = getDb()
|
const configured = parseInt(
|
||||||
.$client.prepare(
|
getCurrentSettingValue("session_recording_retention_days") || "",
|
||||||
"SELECT value FROM settings WHERE key = 'session_recording_retention_days'",
|
10,
|
||||||
)
|
);
|
||||||
.get() as { value?: string } | undefined;
|
|
||||||
const configured = parseInt(row?.value || "", 10);
|
|
||||||
if (configured >= 1 && configured <= 3650) return configured;
|
if (configured >= 1 && configured <= 3650) return configured;
|
||||||
} catch {
|
} catch {
|
||||||
// use environment/default below
|
// use environment/default below
|
||||||
@@ -164,11 +165,10 @@ router.put(
|
|||||||
.status(400)
|
.status(400)
|
||||||
.json({ error: "Retention must be between 1 and 3650 days" });
|
.json({ error: "Retention must be between 1 and 3650 days" });
|
||||||
}
|
}
|
||||||
getDb()
|
await createCurrentSettingsRepository().upsert(
|
||||||
.$client.prepare(
|
"session_recording_retention_days",
|
||||||
"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
String(retentionDays),
|
||||||
)
|
);
|
||||||
.run("session_recording_retention_days", String(retentionDays));
|
|
||||||
void pruneOldLogs();
|
void pruneOldLogs();
|
||||||
res.json({ retentionDays });
|
res.json({ retentionDays });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { authLogger } from "../../utils/logger.js";
|
|||||||
import type { SSOProviderType } from "../../../types/index.js";
|
import type { SSOProviderType } from "../../../types/index.js";
|
||||||
import { DataCrypto } from "../../utils/data-crypto.js";
|
import { DataCrypto } from "../../utils/data-crypto.js";
|
||||||
import { Agent } from "undici";
|
import { Agent } from "undici";
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { getDb } from "../db/index.js";
|
|
||||||
import { ssoProviders } from "../db/schema.js";
|
|
||||||
import {
|
import {
|
||||||
createCurrentSettingsRepository,
|
createCurrentSettingsRepository,
|
||||||
createCurrentSsoProviderRepository,
|
createCurrentSsoProviderRepository,
|
||||||
@@ -426,10 +423,7 @@ export async function resolveProviderByIssuer(issuer: string): Promise<{
|
|||||||
const target = normalizeIssuer(issuer);
|
const target = normalizeIssuer(issuer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rows = await getDb()
|
const rows = await createCurrentSsoProviderRepository().listEnabled();
|
||||||
.select()
|
|
||||||
.from(ssoProviders)
|
|
||||||
.where(eq(ssoProviders.enabled, true));
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
if (!["oidc", "github", "google"].includes(row.type)) continue;
|
if (!["oidc", "github", "google"].includes(row.type)) continue;
|
||||||
let parsed: Record<string, unknown>;
|
let parsed: Record<string, unknown>;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
verifyAuthenticationResponse,
|
verifyAuthenticationResponse,
|
||||||
verifyRegistrationResponse,
|
verifyRegistrationResponse,
|
||||||
} from "@simplewebauthn/server";
|
} from "@simplewebauthn/server";
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import type { AuthenticatedRequest } from "../../../types/index.js";
|
import type { AuthenticatedRequest } from "../../../types/index.js";
|
||||||
import { AuthManager } from "../../utils/auth-manager.js";
|
import { AuthManager } from "../../utils/auth-manager.js";
|
||||||
@@ -21,8 +20,12 @@ import {
|
|||||||
generateDeviceFingerprint,
|
generateDeviceFingerprint,
|
||||||
parseUserAgent,
|
parseUserAgent,
|
||||||
} from "../../utils/user-agent-parser.js";
|
} from "../../utils/user-agent-parser.js";
|
||||||
import { db, saveMemoryDatabaseToFile } from "../db/index.js";
|
import {
|
||||||
import { users, webauthnCredentials } from "../db/schema.js";
|
createCurrentUserRepository,
|
||||||
|
createCurrentWebauthnCredentialRepository,
|
||||||
|
getCurrentSettingValue,
|
||||||
|
} from "../repositories/factory.js";
|
||||||
|
import type { WebauthnCredentialRecord } from "../repositories/webauthn-credential-repository.js";
|
||||||
|
|
||||||
type UserVerification = "discouraged" | "preferred" | "required";
|
type UserVerification = "discouraged" | "preferred" | "required";
|
||||||
type NativeAppRequestChecker = (req: Request) => boolean;
|
type NativeAppRequestChecker = (req: Request) => boolean;
|
||||||
@@ -113,7 +116,7 @@ function parseTransports(value: string | null): AuthenticatorTransportFuture[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCredentialForVerification(
|
function getCredentialForVerification(
|
||||||
credential: typeof webauthnCredentials.$inferSelect,
|
credential: WebauthnCredentialRecord,
|
||||||
): WebAuthnCredential {
|
): WebAuthnCredential {
|
||||||
return {
|
return {
|
||||||
id: credential.credentialId as Base64URLString,
|
id: credential.credentialId as Base64URLString,
|
||||||
@@ -135,10 +138,8 @@ export function registerUserWebAuthnRoutes(
|
|||||||
return res.status(401).json({ error: "Authentication required" });
|
return res.status(401).json({ error: "Authentication required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = await db
|
const credentials =
|
||||||
.select()
|
await createCurrentWebauthnCredentialRepository().listByUserId(userId);
|
||||||
.from(webauthnCredentials)
|
|
||||||
.where(eq(webauthnCredentials.userId, userId));
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
credentials: credentials.map((credential) => ({
|
credentials: credentials.map((credential) => ({
|
||||||
@@ -169,15 +170,13 @@ export function registerUserWebAuthnRoutes(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await db.select().from(users).where(eq(users.id, userId));
|
const user = await createCurrentUserRepository().findById(userId);
|
||||||
if (!user.length) {
|
if (!user) {
|
||||||
return res.status(404).json({ error: "User not found" });
|
return res.status(404).json({ error: "User not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await db
|
const existing =
|
||||||
.select()
|
await createCurrentWebauthnCredentialRepository().listByUserId(userId);
|
||||||
.from(webauthnCredentials)
|
|
||||||
.where(eq(webauthnCredentials.userId, userId));
|
|
||||||
|
|
||||||
const origin = getRequestOrigin(req);
|
const origin = getRequestOrigin(req);
|
||||||
const rpID = getRpID(origin);
|
const rpID = getRpID(origin);
|
||||||
@@ -189,8 +188,8 @@ export function registerUserWebAuthnRoutes(
|
|||||||
rpName: "Termix",
|
rpName: "Termix",
|
||||||
rpID,
|
rpID,
|
||||||
userID: Buffer.from(userId, "utf8"),
|
userID: Buffer.from(userId, "utf8"),
|
||||||
userName: user[0].username,
|
userName: user.username,
|
||||||
userDisplayName: user[0].username,
|
userDisplayName: user.username,
|
||||||
attestationType: "none",
|
attestationType: "none",
|
||||||
excludeCredentials: existing.map((credential) => ({
|
excludeCredentials: existing.map((credential) => ({
|
||||||
id: credential.credentialId as Base64URLString,
|
id: credential.credentialId as Base64URLString,
|
||||||
@@ -259,7 +258,7 @@ export function registerUserWebAuthnRoutes(
|
|||||||
? req.body.name.trim().slice(0, 80)
|
? req.body.name.trim().slice(0, 80)
|
||||||
: "Passkey";
|
: "Passkey";
|
||||||
|
|
||||||
await db.insert(webauthnCredentials).values({
|
await createCurrentWebauthnCredentialRepository().create({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
userId,
|
userId,
|
||||||
name,
|
name,
|
||||||
@@ -273,7 +272,6 @@ export function registerUserWebAuthnRoutes(
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveMemoryDatabaseToFile();
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
authLogger.warn("WebAuthn registration failed", {
|
authLogger.warn("WebAuthn registration failed", {
|
||||||
@@ -301,19 +299,14 @@ export function registerUserWebAuthnRoutes(
|
|||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
const user = await db
|
const user = await createCurrentUserRepository().findByUsername(username);
|
||||||
.select()
|
if (!user) {
|
||||||
.from(users)
|
|
||||||
.where(eq(users.username, username));
|
|
||||||
if (!user.length) {
|
|
||||||
return res.status(404).json({ error: "No passkeys found" });
|
return res.status(404).json({ error: "No passkeys found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
userId = user[0].id;
|
userId = user.id;
|
||||||
const credentials = await db
|
const credentials =
|
||||||
.select()
|
await createCurrentWebauthnCredentialRepository().listByUserId(userId);
|
||||||
.from(webauthnCredentials)
|
|
||||||
.where(eq(webauthnCredentials.userId, userId));
|
|
||||||
|
|
||||||
if (!credentials.length) {
|
if (!credentials.length) {
|
||||||
return res.status(404).json({ error: "No passkeys found" });
|
return res.status(404).json({ error: "No passkeys found" });
|
||||||
@@ -360,16 +353,15 @@ export function registerUserWebAuthnRoutes(
|
|||||||
return res.status(400).json({ error: "Invalid passkey response" });
|
return res.status(400).json({ error: "Invalid passkey response" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = await db
|
const credential =
|
||||||
.select()
|
await createCurrentWebauthnCredentialRepository().findByCredentialId(
|
||||||
.from(webauthnCredentials)
|
response.id,
|
||||||
.where(eq(webauthnCredentials.credentialId, response.id));
|
);
|
||||||
|
|
||||||
if (!credentials.length) {
|
if (!credential) {
|
||||||
return res.status(401).json({ error: "Passkey not recognized" });
|
return res.status(401).json({ error: "Passkey not recognized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const credential = credentials[0];
|
|
||||||
if (challenge.userId && challenge.userId !== credential.userId) {
|
if (challenge.userId && challenge.userId !== credential.userId) {
|
||||||
return res.status(401).json({ error: "Passkey not recognized" });
|
return res.status(401).json({ error: "Passkey not recognized" });
|
||||||
}
|
}
|
||||||
@@ -391,15 +383,13 @@ export function registerUserWebAuthnRoutes(
|
|||||||
return res.status(401).json({ error: "Passkey authentication failed" });
|
return res.status(401).json({ error: "Passkey authentication failed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await db
|
const userRecord = await createCurrentUserRepository().findById(
|
||||||
.select()
|
credential.userId,
|
||||||
.from(users)
|
);
|
||||||
.where(eq(users.id, credential.userId));
|
if (!userRecord) {
|
||||||
if (!user.length) {
|
|
||||||
return res.status(404).json({ error: "User not found" });
|
return res.status(404).json({ error: "User not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRecord = user[0];
|
|
||||||
const deviceInfo = parseUserAgent(req);
|
const deviceInfo = parseUserAgent(req);
|
||||||
const dataUnlocked = await authManager.authenticateWebAuthnUser(
|
const dataUnlocked = await authManager.authenticateWebAuthnUser(
|
||||||
userRecord.id,
|
userRecord.id,
|
||||||
@@ -413,15 +403,15 @@ export function registerUserWebAuthnRoutes(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await db
|
await createCurrentWebauthnCredentialRepository().updateAuthState(
|
||||||
.update(webauthnCredentials)
|
credential.id,
|
||||||
.set({
|
{
|
||||||
counter: verification.authenticationInfo.newCounter,
|
counter: verification.authenticationInfo.newCounter,
|
||||||
backedUp: verification.authenticationInfo.credentialBackedUp,
|
backedUp: verification.authenticationInfo.credentialBackedUp,
|
||||||
deviceType: verification.authenticationInfo.credentialDeviceType,
|
deviceType: verification.authenticationInfo.credentialDeviceType,
|
||||||
lastUsedAt: new Date().toISOString(),
|
lastUsedAt: new Date().toISOString(),
|
||||||
})
|
},
|
||||||
.where(eq(webauthnCredentials.id, credential.id));
|
);
|
||||||
|
|
||||||
if (userRecord.totpEnabled) {
|
if (userRecord.totpEnabled) {
|
||||||
const deviceFingerprint = generateDeviceFingerprint(deviceInfo);
|
const deviceFingerprint = generateDeviceFingerprint(deviceInfo);
|
||||||
@@ -431,7 +421,6 @@ export function registerUserWebAuthnRoutes(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isTrusted) {
|
if (!isTrusted) {
|
||||||
await saveMemoryDatabaseToFile();
|
|
||||||
const tempToken = await authManager.generateJWTToken(userRecord.id, {
|
const tempToken = await authManager.generateJWTToken(userRecord.id, {
|
||||||
pendingTOTP: true,
|
pendingTOTP: true,
|
||||||
expiresIn: "10m",
|
expiresIn: "10m",
|
||||||
@@ -451,15 +440,9 @@ export function registerUserWebAuthnRoutes(
|
|||||||
deviceInfo: deviceInfo.deviceInfo,
|
deviceInfo: deviceInfo.deviceInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveMemoryDatabaseToFile();
|
const timeoutSetting = getCurrentSettingValue("session_timeout_hours");
|
||||||
|
const timeoutHours = timeoutSetting
|
||||||
const timeoutRow = db.$client
|
? parseInt(timeoutSetting, 10) || 24
|
||||||
.prepare(
|
|
||||||
"SELECT value FROM settings WHERE key = 'session_timeout_hours'",
|
|
||||||
)
|
|
||||||
.get() as { value: string } | undefined;
|
|
||||||
const timeoutHours = timeoutRow
|
|
||||||
? parseInt(timeoutRow.value, 10) || 24
|
|
||||||
: 24;
|
: 24;
|
||||||
const maxAge = req.body?.rememberMe
|
const maxAge = req.body?.rememberMe
|
||||||
? 30 * 24 * 60 * 60 * 1000
|
? 30 * 24 * 60 * 60 * 1000
|
||||||
@@ -498,15 +481,10 @@ export function registerUserWebAuthnRoutes(
|
|||||||
|
|
||||||
const credentialId = String(req.params.credentialId);
|
const credentialId = String(req.params.credentialId);
|
||||||
|
|
||||||
await db
|
await createCurrentWebauthnCredentialRepository().deleteForUser(
|
||||||
.delete(webauthnCredentials)
|
userId,
|
||||||
.where(
|
credentialId,
|
||||||
and(
|
|
||||||
eq(webauthnCredentials.id, credentialId),
|
|
||||||
eq(webauthnCredentials.userId, userId),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
await saveMemoryDatabaseToFile();
|
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { getCurrentSettingValue } from "../database/repositories/factory.js";
|
|||||||
import { resolveGuacdOptions } from "../utils/guacd-config.js";
|
import { resolveGuacdOptions } from "../utils/guacd-config.js";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { getDb } from "../database/db/index.js";
|
import { createCurrentSessionRecordingRepository } from "../database/repositories/factory.js";
|
||||||
import { sessionRecordings } from "../database/db/schema.js";
|
|
||||||
import type { GuacamoleRecordingMetadata } from "./token-service.js";
|
import type { GuacamoleRecordingMetadata } from "./token-service.js";
|
||||||
|
|
||||||
const tokenService = GuacamoleTokenService.getInstance();
|
const tokenService = GuacamoleTokenService.getInstance();
|
||||||
@@ -63,9 +62,7 @@ async function persistGuacamoleRecording(
|
|||||||
|
|
||||||
const endedAt = new Date();
|
const endedAt = new Date();
|
||||||
const startedAt = new Date(recording.startedAt);
|
const startedAt = new Date(recording.startedAt);
|
||||||
await getDb()
|
await createCurrentSessionRecordingRepository().create({
|
||||||
.insert(sessionRecordings)
|
|
||||||
.values({
|
|
||||||
hostId: recording.hostId,
|
hostId: recording.hostId,
|
||||||
userId: recording.userId,
|
userId: recording.userId,
|
||||||
startedAt: startedAt.toISOString(),
|
startedAt: startedAt.toISOString(),
|
||||||
|
|||||||
Reference in New Issue
Block a user