Files
pocketbase/ui/src/collections/oauth2/oidcOptions.js
2026-04-18 16:50:39 +03:00

235 lines
10 KiB
JavaScript

window.app = window.app || {};
window.app.oauth2 = window.app.oauth2 || {};
// note: data is the providerSettingsModal form store
window.app.oauth2.oidc = function(providerInfo, namePrefix, data) {
const uniqueId = "oidc_" + app.utils.randomString();
const userInfoOptions = [
{ label: "User info URL", value: true },
{ label: "ID Token", value: false },
];
const local = store({
useUserInfoUrl: false,
});
const watchers = [];
return t.div(
{
pbEvent: "oauth2OIDCOptions",
className: "oauth2-oidc-options",
// init defaults
onmount: (el) => {
if (typeof data.config.displayName == "undefined") {
data.config.displayName = "OIDC";
}
if (typeof data.config.pkce == "undefined") {
data.config.pkce = true;
}
if (data.config.userInfoURL || !data.config.extra) {
local.useUserInfoUrl = true;
}
// unset the id_token or info url fields based on the toggle state
watchers.push(
watch(() => local.useUserInfoUrl, (useURL, oldUseURL) => {
if (useURL) {
// note: null because {} will just result in JSON unmarshal merge with the existing data
data.config.extra = null;
} else {
data.config.userInfoURL = "";
// note: fallback to empty object to distinguish from the null state since all id_token fields are optional
data.config.extra = data.config.extra || {};
}
}),
);
},
onunmount: () => {
watchers.forEach((w) => w?.unwatch());
},
},
t.div(
{ className: "grid" },
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".displayName" }, "Display name"),
t.input({
id: uniqueId + ".displayName",
name: namePrefix + ".displayName",
type: "text",
required: true,
value: () => data.config.displayName || "",
oninput: (e) => data.config.displayName = e.target.value,
}),
),
),
t.div(
{ className: "col-12" },
t.p({ className: "txt-bold" }, "Endpoints"),
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".authURL" }, "Auth URL"),
t.input({
id: uniqueId + ".authURL",
name: namePrefix + ".authURL",
type: "url",
required: true,
value: () => data.config.authURL || "",
oninput: (e) => data.config.authURL = e.target.value,
}),
),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".tokenURL" }, "Token URL"),
t.input({
id: uniqueId + ".tokenURL",
name: namePrefix + ".tokenURL",
type: "url",
required: true,
value: () => data.config.tokenURL || "",
oninput: (e) => data.config.tokenURL = e.target.value,
}),
),
),
// User info
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".userInfoSelect" }, "Fetch user info from"),
app.components.select({
id: uniqueId + ".userInfoSelect",
required: true,
options: userInfoOptions,
value: () => local.useUserInfoUrl,
onchange: (selectedOpts) => local.useUserInfoUrl = selectedOpts?.[0]?.value,
}),
),
t.div({ className: "oidc-userinfo-options m-t-10" }, () => {
if (local.useUserInfoUrl) {
return t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".userInfoURL" }, "User info URL"),
t.input({
id: uniqueId + ".userInfoURL",
name: namePrefix + ".userInfoURL",
type: "url",
required: true,
value: () => data.config.userInfoURL || "",
oninput: (e) => data.config.userInfoURL = e.target.value,
}),
);
}
return t.div(
{ className: "grid sm" },
t.div(
{ className: "col-12 txt-hint txt-sm" },
t.em(
null,
"Both fields are considered optional because the parsed ",
t.code(null, "id_token"),
" is a direct result of the TLS code->token exchange server response.",
),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label(
{ htmlFor: uniqueId + ".extra.jwksURL" },
t.span({ className: "txt" }, "JWKS verification URL"),
t.i({
ariaHidden: true,
className: "ri-information-line link-hint",
ariaDescription: app.attrs.tooltip(
"URL to the public token verification keys.",
),
}),
),
t.input({
id: uniqueId + ".extra.jwksURL",
name: namePrefix + ".extra.jwksURL",
type: "url",
value: () => data.config.extra?.jwksURL || "",
oninput: (e) => {
data.config.extra = data.config.extra || {};
data.config.extra.jwksURL = e.target.value;
},
}),
),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label(
{ htmlFor: uniqueId + ".extra.issuers" },
t.span({ className: "txt" }, "Issuers"),
t.i({
ariaHidden: true,
className: "ri-information-line link-hint",
ariaDescription: app.attrs.tooltip(
"Comma separated list of accepted values for the iss token claim validation.",
),
}),
),
t.input({
id: uniqueId + ".extra.issuers",
name: namePrefix + ".extra.issuers",
type: "text",
value: () => app.utils.joinNonEmpty(data.config.extra?.issuers),
oninput: (e) => {
const newValue = app.utils.splitNonEmpty(e.target.value, ",");
const newStr = app.utils.joinNonEmpty(newValue);
const oldStr = app.utils.joinNonEmpty(data.config.extra?.issuers);
// has an actual change
if (oldStr != newStr) {
data.config.extra = data.config.extra || {};
data.config.extra.issuers = newValue;
}
},
}),
),
),
);
}),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.input({
id: uniqueId + ".pkce",
name: namePrefix + ".pkce",
type: "checkbox",
checked: () => data.config.pkce || false,
onchange: (e) => data.config.pkce = e.target.checked,
}),
t.label(
{ htmlFor: uniqueId + ".pkce" },
t.span({ className: "txt", textContent: "Support PKCE" }),
t.i({
className: "ri-information-line link-hint",
ariaHidden: true,
ariaDescription: app.attrs.tooltip(
"Usually it should be safe to be always enabled as most providers will just ignore the extra query parameters if they don't support PKCE.",
),
}),
),
),
),
),
);
};