mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: harden plugin api for separate repos, signed updates and generic external login
This commit is contained in:
1 parent
fb9c168b6b
commit
80944301dd
310 files changed
+6577
-4927
No files matched your search
@@ -11,7 +11,7 @@ export interface PublicLoginMethod {
|
||||
kind: "redirect" | "form";
|
||||
labelKey: string;
|
||||
icon?: string;
|
||||
instances: Array<{ id: string; label: string }>;
|
||||
instances: Array<{ id: string; label: string; autoStart?: boolean }>;
|
||||
}
|
||||
|
||||
export interface SecondFactorRef {
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface UptimeInfo {
|
||||
export interface RecentActivityItem {
|
||||
id: number;
|
||||
userId: string;
|
||||
/** Core records file_manager and tunnel; plugin tabs record their own. */
|
||||
/** The activity type; each plugin records its own. */
|
||||
type: string;
|
||||
hostId: number;
|
||||
hostName: string;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { authApi, handleApiError } from "@/main-axios";
|
||||
|
||||
// EXTERNAL ACCOUNT LINKING
|
||||
// ============================================================================
|
||||
|
||||
export async function linkExternalToPasswordAccount(
|
||||
externalUserId: string,
|
||||
targetUsername: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await authApi.post("/users/link-external-to-password", {
|
||||
externalUserId,
|
||||
targetUsername,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "link external account to password account");
|
||||
}
|
||||
}
|
||||
|
||||
export async function unlinkExternalFromPasswordAccount(
|
||||
userId: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await authApi.post(
|
||||
"/users/unlink-external-from-password",
|
||||
{ userId },
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(
|
||||
error,
|
||||
"unlink external sign-in from password account",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { authApi, handleApiError } from "@/main-axios";
|
||||
|
||||
// OIDC ACCOUNT LINKING
|
||||
// ============================================================================
|
||||
|
||||
export async function linkOIDCToPasswordAccount(
|
||||
oidcUserId: string,
|
||||
targetUsername: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await authApi.post("/users/link-oidc-to-password", {
|
||||
oidcUserId,
|
||||
targetUsername,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "link OIDC account to password account");
|
||||
}
|
||||
}
|
||||
|
||||
export async function unlinkOIDCFromPasswordAccount(
|
||||
userId: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await authApi.post("/users/unlink-oidc-from-password", {
|
||||
userId,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "unlink OIDC from password account");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import { authApi } from "@/main-axios";
|
||||
import { createTtlRequestCache } from "@/lib/ttl-request-cache";
|
||||
import type { CustomKeybinding } from "@/types/keybindings";
|
||||
import { invokeAction, isActionRegistered } from "@/shell/action-registry";
|
||||
|
||||
const SHARED_WITH_ME_ACTION = "sessions.sharedWithMe";
|
||||
|
||||
// OPEN TABS API
|
||||
// ============================================================================
|
||||
@@ -45,10 +42,6 @@ export interface ActiveSessionInfo {
|
||||
tabInstanceId: string | null;
|
||||
isConnected: boolean;
|
||||
createdAt: number;
|
||||
isOwnSession: boolean;
|
||||
sharedByUsername: string | null;
|
||||
permissionLevel: string | null;
|
||||
shareId: string | null;
|
||||
}
|
||||
|
||||
// Negative IDs identify remote-only shared hosts, absent from the local DB.
|
||||
@@ -86,21 +79,11 @@ export async function addOpenTab(tab: OpenTabUpsertPayload): Promise<void> {
|
||||
await authApi.post("/open-tabs", tab);
|
||||
}
|
||||
|
||||
/**
|
||||
* The caller's live sessions, plus sessions other users shared with them,
|
||||
* which the session sharing plugin answers through an action (none while it
|
||||
* is off).
|
||||
*/
|
||||
/** The caller's own live sessions. */
|
||||
export async function getActiveSessions(): Promise<ActiveSessionInfo[]> {
|
||||
return activeSessionsCache.get(async () => {
|
||||
const [response, shared] = await Promise.all([
|
||||
authApi.get("/open-tabs/active-sessions"),
|
||||
isActionRegistered(SHARED_WITH_ME_ACTION)
|
||||
? invokeAction(SHARED_WITH_ME_ACTION).catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
const own = Array.isArray(response.data) ? response.data : [];
|
||||
return Array.isArray(shared) ? [...own, ...shared] : own;
|
||||
const response = await authApi.get("/open-tabs/active-sessions");
|
||||
return Array.isArray(response.data) ? response.data : [];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -78,19 +78,10 @@ export interface PluginContributions {
|
||||
uiPresets?: PluginUiPresets;
|
||||
}
|
||||
|
||||
/** A secret as it arrives from the server. The value never leaves the server. */
|
||||
export interface RedactedSecret {
|
||||
set: boolean;
|
||||
}
|
||||
|
||||
export function isRedactedSecret(value: unknown): value is RedactedSecret {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
typeof (value as RedactedSecret).set === "boolean"
|
||||
);
|
||||
}
|
||||
export {
|
||||
isRedactedSecret,
|
||||
type RedactedSecret,
|
||||
} from "@termix/plugin-sdk/settings";
|
||||
|
||||
export type PluginSettingsValues = Record<string, unknown>;
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export async function updateAnalyticsEnabled(
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TERMINAL IMAGE STORAGE SETTINGS
|
||||
// BRANDING SETTINGS
|
||||
// ============================================================================
|
||||
|
||||
export interface BrandingSettings {
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
export interface ServerSentEvent {
|
||||
event: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export async function streamServerSentEvents(
|
||||
url: string,
|
||||
init: RequestInit,
|
||||
onEvent: (event: ServerSentEvent) => void,
|
||||
): Promise<void> {
|
||||
const response = await fetch(url, init);
|
||||
if (!response.ok) {
|
||||
throw new Error(`SSE request failed with status ${response.status}`);
|
||||
}
|
||||
if (!response.body) throw new Error("SSE response has no body");
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let eventName = "message";
|
||||
let data: string[] = [];
|
||||
|
||||
const dispatch = () => {
|
||||
if (data.length > 0) onEvent({ event: eventName, data: data.join("\n") });
|
||||
eventName = "message";
|
||||
data = [];
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
buffer += decoder.decode(value, { stream: !done });
|
||||
const lines = buffer.split(/\r?\n/);
|
||||
buffer = done ? "" : (lines.pop() ?? "");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === "") dispatch();
|
||||
else if (line.startsWith("event:")) eventName = line.slice(6).trimStart();
|
||||
else if (line.startsWith("data:")) data.push(line.slice(5).trimStart());
|
||||
}
|
||||
|
||||
if (done) {
|
||||
dispatch();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,20 +221,22 @@ export async function updateRegistrationAllowed(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOidcAutoProvision(): Promise<{ enabled: boolean }> {
|
||||
export async function getExternalAutoProvision(): Promise<{
|
||||
enabled: boolean;
|
||||
}> {
|
||||
try {
|
||||
const response = await authApi.get("/users/oidc-auto-provision");
|
||||
const response = await authApi.get("/users/external-auto-provision");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "check OIDC auto-provision status");
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateOidcAutoProvision(
|
||||
export async function updateExternalAutoProvision(
|
||||
enabled: boolean,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.patch("/users/oidc-auto-provision", {
|
||||
const response = await authApi.patch("/users/external-auto-provision", {
|
||||
enabled,
|
||||
});
|
||||
return response.data;
|
||||
@@ -270,31 +272,6 @@ export async function updateSecondFactorAfterExternalLogin(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOidcSilentLoginDefault(): Promise<{
|
||||
enabled: boolean;
|
||||
locked?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const response = await authApi.get("/users/oidc-silent-login-default");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "get OIDC silent login default");
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateOidcSilentLoginDefault(
|
||||
enabled: boolean,
|
||||
): Promise<{ enabled: boolean }> {
|
||||
try {
|
||||
const response = await authApi.patch("/users/oidc-silent-login-default", {
|
||||
enabled,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "update OIDC silent login default");
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePasswordLoginAllowed(
|
||||
allowed: boolean,
|
||||
): Promise<{ allowed: boolean }> {
|
||||
|
||||
Reference in new issue
Block a user