This commit is contained in:
LukeGus
2026-05-28 22:29:20 -05:00
parent 33dcde0827
commit 5777351145
238 changed files with 62303 additions and 98955 deletions
+17 -8
View File
@@ -147,6 +147,7 @@ router.post(
key,
keyPassword,
keyType,
certPublicKey,
} = req.body;
if (!isNonEmptyString(userId) || !isNonEmptyString(name)) {
@@ -230,6 +231,8 @@ router.post(
keyPassword: plainKeyPassword,
keyType: keyType || null,
detectedKeyType: keyInfo?.keyType || null,
certPublicKey:
authType === "key" && certPublicKey ? certPublicKey.trim() : null,
usageCount: 0,
lastUsed: null,
};
@@ -431,15 +434,14 @@ router.get(
if (credential.password) {
output.password = credential.password;
}
if (credential.key) {
output.key = credential.key;
}
if (credential.privateKey) {
output.privateKey = credential.privateKey;
}
output.hasKey = !!credential.key;
output.hasKeyPassword = !!credential.keyPassword;
if (credential.publicKey) {
output.publicKey = credential.publicKey;
}
if (credential.certPublicKey) {
output.certPublicKey = credential.certPublicKey;
}
if (credential.keyPassword) {
output.keyPassword = credential.keyPassword;
}
@@ -572,6 +574,9 @@ router.put(
if (updateData.keyPassword !== undefined) {
updateFields.keyPassword = updateData.keyPassword || null;
}
if (updateData.certPublicKey !== undefined) {
updateFields.certPublicKey = updateData.certPublicKey?.trim() || null;
}
if (Object.keys(updateFields).length === 0) {
const existing = await SimpleDBOps.select(
@@ -947,6 +952,7 @@ function formatCredentialOutput(
authType: credential.authType,
username: credential.username || null,
publicKey: credential.publicKey,
hasCertPublicKey: !!credential.certPublicKey,
keyType: credential.keyType,
detectedKeyType: credential.detectedKeyType,
usageCount: credential.usageCount || 0,
@@ -1445,7 +1451,7 @@ router.post(
const publicKeyString =
typeof publicKeyPem === "string"
? publicKeyPem
: publicKeyPem.toString("utf8");
: (publicKeyPem as Buffer).toString("utf8");
let keyType = "unknown";
const asymmetricKeyType = privateKeyObj.asymmetricKeyType;
@@ -1617,9 +1623,12 @@ async function deploySSHKeyToHost(
const escapedKey = actualPublicKey
.replace(/\\/g, "\\\\")
.replace(/'/g, "'\\''");
const escapedName = credData.name
.replace(/\\/g, "\\\\")
.replace(/'/g, "'\\''");
conn.exec(
`printf '%s\n' '${escapedKey} ${credData.name}@Termix' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
`printf '%s\n' '${escapedKey} ${escapedName}@Termix' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
(err, stream) => {
if (err) {
clearTimeout(addTimeout);
+88 -33
View File
@@ -43,6 +43,8 @@ const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
const STATS_SERVER_URL = "http://localhost:30005";
function notifyStatsHostUpdated(
hostId: number,
headers: Pick<Request["headers"], "authorization" | "cookie">,
@@ -50,7 +52,7 @@ function notifyStatsHostUpdated(
): void {
axios
.post(
"http://localhost:30005/host-updated",
`${STATS_SERVER_URL}/host-updated`,
{ hostId },
{
headers: {
@@ -244,26 +246,18 @@ function normalizeImportedHost(
}
const SENSITIVE_FIELDS = [
"password",
"key",
"keyPassword",
"sudoPassword",
"autostartPassword",
"autostartKey",
"autostartKeyPassword",
"socks5Password",
"rdpPassword",
"vncPassword",
"telnetPassword",
];
function stripSensitiveFields(
host: Record<string, unknown>,
): Record<string, unknown> {
const result = { ...host };
result.hasPassword = !!host.password;
result.hasKey = !!host.key;
result.hasSudoPassword = !!host.sudoPassword;
result.hasKeyPassword = !!host.keyPassword;
for (const field of SENSITIVE_FIELDS) {
delete result[field];
}
@@ -291,10 +285,23 @@ function transformHostResponse(
showTunnelInSidebar: !!host.showTunnelInSidebar,
showDockerInSidebar: !!host.showDockerInSidebar,
showServerStatsInSidebar: !!host.showServerStatsInSidebar,
enableSsh: host.enableSsh !== undefined ? !!host.enableSsh : true,
enableRdp: !!host.enableRdp,
enableVnc: !!host.enableVnc,
enableTelnet: !!host.enableTelnet,
// Old hosts only had connection_type set; the per-protocol enable flags didn't exist yet.
// The schema defaults (enableSsh=true, others=false) wrongly mark every old host as SSH.
// Detect this migration case: if no non-SSH protocol is explicitly enabled AND
// connectionType is set to a non-SSH value, fall back to inferring from connectionType.
...(() => {
const ct = host.connectionType;
const rdp = !!host.enableRdp;
const vnc = !!host.enableVnc;
const tel = !!host.enableTelnet;
const isMigratedNonSsh = !rdp && !vnc && !tel && ct && ct !== "ssh";
return {
enableSsh: isMigratedNonSsh ? false : !!host.enableSsh,
enableRdp: isMigratedNonSsh ? ct === "rdp" : rdp,
enableVnc: isMigratedNonSsh ? ct === "vnc" : vnc,
enableTelnet: isMigratedNonSsh ? ct === "telnet" : tel,
};
})(),
sshPort: host.sshPort ?? host.port ?? 22,
rdpPort: host.rdpPort ?? 3389,
vncPort: host.vncPort ?? 5900,
@@ -305,9 +312,6 @@ function transformHostResponse(
rdpIgnoreCert: !!host.rdpIgnoreCert,
vncUser: host.vncUser || undefined,
telnetUser: host.telnetUser || undefined,
hasRdpPassword: !!host.rdpPassword,
hasVncPassword: !!host.vncPassword,
hasTelnetPassword: !!host.telnetPassword,
tunnelConnections: host.tunnelConnections
? JSON.parse(host.tunnelConnections as string)
: [],
@@ -658,15 +662,19 @@ router.post(
authType ||
authMethod ||
(effectiveConnectionType !== "ssh" ? "password" : undefined);
const effectiveUsername =
username || rdpUser || vncUser || telnetUser || "";
const effectiveName =
name || (effectiveUsername ? `${effectiveUsername}@${ip}` : String(ip));
const sshDataObj: Record<string, unknown> = {
userId: userId,
connectionType: effectiveConnectionType,
name,
name: effectiveName,
folder: folder || null,
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
ip,
port,
username,
username: effectiveUsername,
authType: effectiveAuthType,
credentialId: credentialId || null,
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
@@ -722,7 +730,7 @@ router.post(
portKnockSequence: portKnockSequence
? JSON.stringify(portKnockSequence)
: null,
enableSsh: enableSsh !== false ? 1 : 0,
enableSsh: enableSsh ? 1 : 0,
enableRdp: enableRdp ? 1 : 0,
enableVnc: enableVnc ? 1 : 0,
enableTelnet: enableTelnet ? 1 : 0,
@@ -1190,14 +1198,18 @@ router.put(
}
const effectiveAuthType = authType || authMethod;
const effectiveUsername =
username || rdpUser || vncUser || telnetUser || "";
const effectiveName =
name || (effectiveUsername ? `${effectiveUsername}@${ip}` : String(ip));
const sshDataObj: Record<string, unknown> = {
connectionType: connectionType || "ssh",
name,
name: effectiveName,
folder,
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
ip,
port,
username,
username: effectiveUsername,
authType: effectiveAuthType,
credentialId: credentialId || null,
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
@@ -1253,7 +1265,7 @@ router.put(
portKnockSequence: portKnockSequence
? JSON.stringify(portKnockSequence)
: null,
enableSsh: enableSsh !== false ? 1 : 0,
enableSsh: enableSsh ? 1 : 0,
enableRdp: enableRdp ? 1 : 0,
enableVnc: enableVnc ? 1 : 0,
enableTelnet: enableTelnet ? 1 : 0,
@@ -1566,6 +1578,23 @@ router.get(
guacamoleConfig: hosts.guacamoleConfig,
macAddress: hosts.macAddress,
dockerConfig: hosts.dockerConfig,
enableSsh: hosts.enableSsh,
enableRdp: hosts.enableRdp,
enableVnc: hosts.enableVnc,
enableTelnet: hosts.enableTelnet,
sshPort: hosts.sshPort,
rdpPort: hosts.rdpPort,
vncPort: hosts.vncPort,
telnetPort: hosts.telnetPort,
rdpUser: hosts.rdpUser,
rdpPassword: hosts.rdpPassword,
rdpDomain: hosts.rdpDomain,
rdpSecurity: hosts.rdpSecurity,
rdpIgnoreCert: hosts.rdpIgnoreCert,
vncUser: hosts.vncUser,
vncPassword: hosts.vncPassword,
telnetUser: hosts.telnetUser,
telnetPassword: hosts.telnetPassword,
ownerId: hosts.userId,
isShared: sql<boolean>`${hostAccess.id} IS NOT NULL AND ${hosts.userId} != ${userId}`,
@@ -1891,9 +1920,21 @@ router.get(
const exportData = isRemoteDesktop
? {
...baseExportData,
domain: resolvedHost.domain || null,
security: resolvedHost.security || null,
ignoreCert: !!resolvedHost.ignoreCert,
enableRdp: !!resolvedHost.enableRdp,
enableVnc: !!resolvedHost.enableVnc,
enableTelnet: !!resolvedHost.enableTelnet,
rdpPort: resolvedHost.rdpPort || 3389,
vncPort: resolvedHost.vncPort || 5900,
telnetPort: resolvedHost.telnetPort || 23,
rdpUser: resolvedHost.rdpUser || null,
rdpPassword: resolvedHost.rdpPassword || null,
rdpDomain: resolvedHost.rdpDomain || null,
rdpSecurity: resolvedHost.rdpSecurity || null,
rdpIgnoreCert: !!resolvedHost.rdpIgnoreCert,
vncUser: resolvedHost.vncUser || null,
vncPassword: resolvedHost.vncPassword || null,
telnetUser: resolvedHost.telnetUser || null,
telnetPassword: resolvedHost.telnetPassword || null,
guacamoleConfig: resolvedHost.guacamoleConfig
? JSON.parse(resolvedHost.guacamoleConfig as string)
: null,
@@ -2217,9 +2258,8 @@ router.delete(
try {
const axios = (await import("axios")).default;
const statsPort = 30005;
await axios.post(
`http://localhost:${statsPort}/host-deleted`,
`${STATS_SERVER_URL}/host-deleted`,
{ hostId: numericHostId },
{
headers: {
@@ -3394,11 +3434,10 @@ router.delete(
try {
const axios = (await import("axios")).default;
const statsPort = 30005;
for (const host of hostsToDelete) {
try {
await axios.post(
`http://localhost:${statsPort}/host-deleted`,
`${STATS_SERVER_URL}/host-deleted`,
{ hostId: host.id },
{
headers: {
@@ -3804,6 +3843,10 @@ router.post(
overrideCredentialUsername: hostData.overrideCredentialUsername
? 1
: 0,
enableSsh: hostData.enableSsh ?? false,
enableRdp: hostData.enableRdp ?? false,
enableVnc: hostData.enableVnc ?? false,
enableTelnet: hostData.enableTelnet ?? false,
updatedAt: new Date().toISOString(),
};
@@ -3814,9 +3857,21 @@ router.post(
sshDataObj.key = null;
sshDataObj.keyPassword = null;
sshDataObj.keyType = null;
sshDataObj.domain = hostData.domain || null;
sshDataObj.security = hostData.security || null;
sshDataObj.ignoreCert = hostData.ignoreCert ? 1 : 0;
sshDataObj.rdpUser = hostData.rdpUser || null;
sshDataObj.rdpPassword = hostData.rdpPassword || null;
sshDataObj.rdpDomain = hostData.rdpDomain || null;
sshDataObj.rdpSecurity = hostData.rdpSecurity || null;
sshDataObj.rdpIgnoreCert = hostData.rdpIgnoreCert ? 1 : 0;
sshDataObj.rdpPort = hostData.rdpPort || 3389;
sshDataObj.vncUser = hostData.vncUser || null;
sshDataObj.vncPassword = hostData.vncPassword || null;
sshDataObj.vncPort = hostData.vncPort || 5900;
sshDataObj.telnetUser = hostData.telnetUser || null;
sshDataObj.telnetPassword = hostData.telnetPassword || null;
sshDataObj.telnetPort = hostData.telnetPort || 23;
sshDataObj.enableRdp = hostData.enableRdp ? 1 : 0;
sshDataObj.enableVnc = hostData.enableVnc ? 1 : 0;
sshDataObj.enableTelnet = hostData.enableTelnet ? 1 : 0;
sshDataObj.guacamoleConfig = hostData.guacamoleConfig
? JSON.stringify(hostData.guacamoleConfig)
: null;
+92 -33
View File
@@ -10,6 +10,7 @@ import {
sharedCredentials,
snippets,
snippetAccess,
sshCredentials,
} from "../db/schema.js";
import { eq, and, desc, sql, or, isNull, gte } from "drizzle-orm";
import type { Response } from "express";
@@ -128,10 +129,10 @@ router.post(
return res.status(403).json({ error: "Not host owner" });
}
if (!host[0].credentialId) {
if (!host[0].credentialId && host[0].authType !== "opkssh") {
return res.status(400).json({
error:
"Only hosts using credentials can be shared. Please create a credential and assign it to this host before sharing.",
"Only hosts using credentials or OPKSSH can be shared. Please create a credential and assign it to this host before sharing.",
code: "CREDENTIAL_REQUIRED_FOR_SHARING",
});
}
@@ -203,23 +204,25 @@ router.post(
.delete(sharedCredentials)
.where(eq(sharedCredentials.hostAccessId, existing[0].id));
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
if (targetType === "user") {
await sharedCredManager.createSharedCredentialForUser(
existing[0].id,
host[0].credentialId,
targetUserId!,
userId,
);
} else {
await sharedCredManager.createSharedCredentialsForRole(
existing[0].id,
host[0].credentialId,
targetRoleId!,
userId,
);
if (host[0].credentialId) {
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
if (targetType === "user") {
await sharedCredManager.createSharedCredentialForUser(
existing[0].id,
host[0].credentialId,
targetUserId!,
userId,
);
} else {
await sharedCredManager.createSharedCredentialsForRole(
existing[0].id,
host[0].credentialId,
targetRoleId!,
userId,
);
}
}
databaseLogger.info("Permission granted", {
operation: "rbac_permission_grant",
@@ -249,20 +252,22 @@ router.post(
await import("../../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
if (targetType === "user") {
await sharedCredManager.createSharedCredentialForUser(
result.lastInsertRowid as number,
host[0].credentialId,
targetUserId!,
userId,
);
} else {
await sharedCredManager.createSharedCredentialsForRole(
result.lastInsertRowid as number,
host[0].credentialId,
targetRoleId!,
userId,
);
if (host[0].credentialId) {
if (targetType === "user") {
await sharedCredManager.createSharedCredentialForUser(
result.lastInsertRowid as number,
host[0].credentialId,
targetUserId!,
userId,
);
} else {
await sharedCredManager.createSharedCredentialsForRole(
result.lastInsertRowid as number,
host[0].credentialId,
targetRoleId!,
userId,
);
}
}
databaseLogger.success("Host shared successfully", {
operation: "rbac_host_share_success",
@@ -1520,4 +1525,58 @@ router.get(
},
);
router.put(
"/host-access/:hostId/credential",
async (req: express.Request, res: express.Response) => {
try {
const userId = (req as AuthenticatedRequest).userId!;
const hostId = Number.parseInt(String(req.params.hostId), 10);
const { credentialId } = req.body;
if (!hostId || isNaN(hostId)) {
return res.status(400).json({ error: "Invalid host ID" });
}
const access = await db
.select()
.from(hostAccess)
.where(
and(eq(hostAccess.hostId, hostId), eq(hostAccess.userId, userId)),
)
.limit(1);
if (access.length === 0) {
return res.status(403).json({ error: "No access to this host" });
}
if (credentialId) {
const cred = await db
.select({ id: sshCredentials.id })
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, credentialId),
eq(sshCredentials.userId, userId),
),
)
.limit(1);
if (cred.length === 0) {
return res.status(404).json({ error: "Credential not found" });
}
}
await db
.update(hostAccess)
.set({ overrideCredentialId: credentialId || null })
.where(eq(hostAccess.id, access[0].id));
res.json({ success: true });
} catch (error) {
databaseLogger.error("Failed to set override credential", error);
res.status(500).json({ error: "Failed to update credential" });
}
},
);
export default router;
+8 -1
View File
@@ -93,6 +93,7 @@ async function getAccessibleSnippet(snippetId: number, userId: string) {
order: snippets.order,
createdAt: snippets.createdAt,
updatedAt: snippets.updatedAt,
hostFilter: snippets.hostFilter,
})
.from(snippetAccess)
.innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id))
@@ -1104,7 +1105,7 @@ router.post(
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { name, content, description, folder, order } = req.body;
const { name, content, description, folder, order, hostFilter } = req.body;
if (
!isNonEmptyString(userId) ||
@@ -1146,6 +1147,7 @@ router.post(
description: description?.trim() || null,
folder: folder?.trim() || null,
order: snippetOrder,
hostFilter: hostFilter ? JSON.stringify(hostFilter) : null,
};
const result = await db.insert(snippets).values(insertData).returning();
@@ -1238,6 +1240,7 @@ router.put(
description: string | null;
folder: string | null;
order: number;
hostFilter: string | null;
}> = {
updatedAt: sql`CURRENT_TIMESTAMP`,
};
@@ -1251,6 +1254,10 @@ router.put(
if (updateData.folder !== undefined)
updateFields.folder = updateData.folder?.trim() || null;
if (updateData.order !== undefined) updateFields.order = updateData.order;
if (updateData.hostFilter !== undefined)
updateFields.hostFilter = updateData.hostFilter
? JSON.stringify(updateData.hostFilter)
: null;
await db
.update(snippets)
+117 -6
View File
@@ -521,6 +521,7 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => {
name_path,
scopes,
allowed_users,
admin_group,
} = req.body;
const isDisableRequest =
@@ -579,6 +580,7 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => {
name_path,
scopes: scopes || "openid email profile",
allowed_users: allowed_users || "",
admin_group: admin_group || "",
};
let encryptedConfig;
@@ -817,7 +819,7 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
*/
router.get("/oidc/authorize", async (req, res) => {
try {
const { rememberMe } = req.query;
const { rememberMe, desktopCallbackPort } = req.query;
const origin = getRequestOriginWithForceHTTPS(req);
const backendCallbackUri = `${origin}/users/oidc/callback`;
@@ -840,7 +842,9 @@ router.get("/oidc/authorize", async (req, res) => {
const referer = req.get("Referer");
let frontendOrigin;
if (referer) {
if (desktopCallbackPort) {
frontendOrigin = `http://127.0.0.1:${desktopCallbackPort}/oidc-callback`;
} else if (referer) {
const refererUrl = new URL(referer);
frontendOrigin = `${refererUrl.protocol}//${refererUrl.host}`;
} else {
@@ -949,6 +953,18 @@ router.get("/oidc/callback", async (req, res) => {
config = JSON.parse(
(configRow as Record<string, unknown>).value as string,
);
if (config.client_secret?.startsWith("encrypted:")) {
config.client_secret = Buffer.from(
config.client_secret.substring(10),
"base64",
).toString("utf8");
} else if (config.client_secret?.startsWith("encoded:")) {
config.client_secret = Buffer.from(
config.client_secret.substring(8),
"base64",
).toString("utf8");
}
}
const tokenResponse = await fetch(config.token_url, {
@@ -1150,11 +1166,28 @@ router.get("/oidc/callback", async (req, res) => {
}
}
const oidcAllowRegistration =
(process.env.OIDC_ALLOW_REGISTRATION || "").trim().toLowerCase() ===
"true";
let oidcAutoProvision = false;
try {
const oidcProvRow = db.$client
.prepare(
"SELECT value FROM settings WHERE key = 'oidc_auto_provision'",
)
.get();
if (oidcProvRow) {
oidcAutoProvision =
(oidcProvRow as Record<string, unknown>).value === "true";
}
} catch {
// fall through to env var check
}
if (!isFirstUser && !oidcAllowRegistration) {
if (!oidcAutoProvision) {
oidcAutoProvision =
(process.env.OIDC_ALLOW_REGISTRATION || "").trim().toLowerCase() ===
"true";
}
if (!isFirstUser && !oidcAutoProvision) {
try {
const regRow = db.$client
.prepare(
@@ -1300,6 +1333,25 @@ router.get("/oidc/callback", async (req, res) => {
const userRecord = user[0];
// Sync admin status based on OIDC group membership
if (config.admin_group) {
const groups = (userInfo.groups || userInfo.roles || []) as string[];
const shouldBeAdmin = groups.includes(config.admin_group);
if (!!userRecord.isAdmin !== shouldBeAdmin) {
await db
.update(users)
.set({ isAdmin: shouldBeAdmin })
.where(eq(users.id, userRecord.id));
userRecord.isAdmin = shouldBeAdmin;
authLogger.info("OIDC admin status synced", {
operation: "oidc_admin_group_sync",
userId: userRecord.id,
group: config.admin_group,
isAdmin: shouldBeAdmin,
});
}
}
try {
await authManager.authenticateOIDCUser(userRecord.id, deviceInfo.type);
} catch (setupError) {
@@ -1333,6 +1385,8 @@ router.get("/oidc/callback", async (req, res) => {
const redirectUrl = new URL(frontendOrigin);
redirectUrl.searchParams.set("success", "true");
const isDesktopCallback = frontendOrigin.startsWith("http://127.0.0.1:");
const maxAge =
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
? 30 * 24 * 60 * 60 * 1000
@@ -1342,6 +1396,11 @@ router.get("/oidc/callback", async (req, res) => {
res.clearCookie("jwt", authManager.getClearCookieOptions(req));
if (isDesktopCallback) {
redirectUrl.searchParams.set("token", token);
return res.redirect(redirectUrl.toString());
}
return res
.cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
.redirect(redirectUrl.toString());
@@ -1883,6 +1942,56 @@ router.patch("/registration-allowed", authenticateJWT, async (req, res) => {
}
});
router.get("/oidc-auto-provision", async (_req, res) => {
try {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'oidc_auto_provision'")
.get();
res.json({
enabled: row ? (row as Record<string, unknown>).value === "true" : false,
});
} catch (err) {
authLogger.error("Failed to get OIDC auto-provision setting", err);
res
.status(500)
.json({ error: "Failed to get OIDC auto-provision setting" });
}
});
router.patch("/oidc-auto-provision", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].isAdmin) {
return res.status(403).json({ error: "Not authorized" });
}
const { enabled } = req.body;
if (typeof enabled !== "boolean") {
return res.status(400).json({ error: "Invalid value for enabled" });
}
const existing = db.$client
.prepare("SELECT value FROM settings WHERE key = 'oidc_auto_provision'")
.get();
if (existing) {
db.$client
.prepare(
"UPDATE settings SET value = ? WHERE key = 'oidc_auto_provision'",
)
.run(enabled ? "true" : "false");
} else {
db.$client
.prepare(
"INSERT INTO settings (key, value) VALUES ('oidc_auto_provision', ?)",
)
.run(enabled ? "true" : "false");
}
res.json({ enabled });
} catch (err) {
authLogger.error("Failed to set OIDC auto-provision", err);
res.status(500).json({ error: "Failed to set OIDC auto-provision" });
}
});
/**
* @openapi
* /users/password-login-allowed:
@@ -1954,6 +2063,8 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
"INSERT OR REPLACE INTO settings (key, value) VALUES ('allow_password_login', ?)",
)
.run(allowed ? "true" : "false");
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
await saveMemoryDatabaseToFile();
res.json({ allowed });
} catch (err) {
authLogger.error("Failed to set password login allowed", err);