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