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
@@ -55,5 +55,11 @@
|
||||
"adminGroupDesc": "Users in this group are granted admin. Leave empty to disable group sync.",
|
||||
"caCert": "Custom CA Certificate",
|
||||
"caCertDesc": "Optional. PEM-encoded CA certificate for OIDC providers using a private or self-signed CA. Leave empty to use the system trust store."
|
||||
},
|
||||
"settings": {
|
||||
"silentLoginDefault": {
|
||||
"label": "Sign in with SSO automatically",
|
||||
"description": "Send visitors straight to the first SSO provider instead of showing the login form. The OIDC_SILENT_LOGIN_DEFAULT environment variable overrides this."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,14 @@
|
||||
"type": "custom",
|
||||
"component": "providers",
|
||||
"permission": "manage"
|
||||
},
|
||||
{
|
||||
"key": "silentLoginDefault",
|
||||
"type": "boolean",
|
||||
"labelKey": "settings.silentLoginDefault.label",
|
||||
"descriptionKey": "settings.silentLoginDefault.description",
|
||||
"default": false,
|
||||
"permission": "manage"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -6,14 +6,30 @@
|
||||
"scripts": {
|
||||
"build": "termix-plugin build",
|
||||
"test": "termix-plugin test",
|
||||
"typecheck": "tsc -b ../../tsconfig.plugins.backend.json ../../tsconfig.plugins.frontend.json",
|
||||
"typecheck": "tsc -p .",
|
||||
"validate": "termix-plugin validate"
|
||||
},
|
||||
"dependencies": {
|
||||
"jose": "^6.2.12",
|
||||
"lucide-react": "^1.33.0",
|
||||
"undici": "^8.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@termix/plugin-sdk": "*"
|
||||
"@termix/plugin-sdk": "^1.0.0",
|
||||
"@testing-library/dom": "^10.4.2",
|
||||
"@testing-library/react": "^16.3.3",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"better-sqlite3": "^13.0.3",
|
||||
"jsdom": "^30.0.1",
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@termix/plugin-sdk": "^1.0.0",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"express": "^5.2.1",
|
||||
"react": "^19.2.8",
|
||||
"sonner": "^2.0.8"
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,16 @@ import { createProviderStore } from "./providers.js";
|
||||
import { createSsoLogin, METHOD_ID } from "./login.js";
|
||||
import { PUBLIC_PATHS, registerSsoRoutes } from "./routes.js";
|
||||
|
||||
/**
|
||||
* Whether the login page starts the first provider without a click. The
|
||||
* OIDC_SILENT_LOGIN_DEFAULT environment variable pins it either way.
|
||||
*/
|
||||
export async function silentLoginDefault(ctx: PluginContext): Promise<boolean> {
|
||||
const pinned = process.env.OIDC_SILENT_LOGIN_DEFAULT;
|
||||
if (pinned !== undefined) return pinned.trim().toLowerCase() === "true";
|
||||
return (await ctx.settings.get("silentLoginDefault")) === true;
|
||||
}
|
||||
|
||||
export async function activate(ctx: PluginContext) {
|
||||
const table = await ctx.db.define(providers);
|
||||
const store = createProviderStore(ctx, table);
|
||||
@@ -17,11 +27,14 @@ export async function activate(ctx: PluginContext) {
|
||||
icon: "key-round",
|
||||
kind: "redirect",
|
||||
external: true,
|
||||
describe: async () =>
|
||||
(await store.listInstances()).map((instance) => ({
|
||||
describe: async () => {
|
||||
const autoStart = await silentLoginDefault(ctx);
|
||||
return (await store.listInstances()).map((instance, index) => ({
|
||||
...instance,
|
||||
enabled: true,
|
||||
})),
|
||||
...(autoStart && index === 0 ? { autoStart: true } : {}),
|
||||
}));
|
||||
},
|
||||
start: login.start,
|
||||
callback: login.callback,
|
||||
});
|
||||
|
||||
@@ -57,6 +57,7 @@ export async function startServer(
|
||||
permissions?: string[];
|
||||
linkedUsers?: Record<string, number>;
|
||||
before?: (sqlite: TestDb["sqlite"]) => void;
|
||||
settings?: Record<string, unknown>;
|
||||
} = {},
|
||||
): Promise<TestServer> {
|
||||
const db = await createTestDb(pluginDir, { before: options.before });
|
||||
@@ -74,6 +75,7 @@ export async function startServer(
|
||||
permissions: options.permissions,
|
||||
linkedUsers: options.linkedUsers,
|
||||
baseUrl: "https://termix.test",
|
||||
settings: options.settings,
|
||||
});
|
||||
|
||||
const { activate } = await import("../../src/backend/index.js");
|
||||
|
||||
@@ -185,6 +185,31 @@ describe("the login method", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("marks the first provider to start on its own with silent sign-in on", async () => {
|
||||
const s = await startLegacy({ settings: { silentLoginDefault: true } });
|
||||
expect(await s.mock.auth.loginMethods[0].describe!()).toEqual([
|
||||
{
|
||||
id: "3",
|
||||
label: "Keycloak",
|
||||
type: "oidc",
|
||||
enabled: true,
|
||||
autoStart: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("lets OIDC_SILENT_LOGIN_DEFAULT override the setting", async () => {
|
||||
const s = await startLegacy({ settings: { silentLoginDefault: true } });
|
||||
vi.stubEnv("OIDC_SILENT_LOGIN_DEFAULT", "false");
|
||||
const [instance] = await s.mock.auth.loginMethods[0].describe!();
|
||||
expect(instance.autoStart).toBeUndefined();
|
||||
vi.stubEnv("OIDC_SILENT_LOGIN_DEFAULT", "true");
|
||||
s.mock.settings.clear();
|
||||
expect((await s.mock.auth.loginMethods[0].describe!())[0].autoStart).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the provider configured through the environment", async () => {
|
||||
server = await startServer();
|
||||
vi.stubEnv("OIDC_CLIENT_ID", "env-client");
|
||||
|
||||
Reference in new issue
Block a user