merge newui branch

This commit is contained in:
Gani Georgiev
2026-04-18 16:29:34 +03:00
parent 58f605e90c
commit 4c44044c0c
804 changed files with 58660 additions and 56663 deletions
+236
View File
@@ -0,0 +1,236 @@
window.app = window.app || {};
window.app.oauth2 = window.app.oauth2 || {};
// note: data is the providerSettingsModal form store
window.app.oauth2.apple = function(providerInfo, namePrefix, data) {
const uniqueId = "apple_" + app.utils.randomString();
return t.div(
{ pbEvent: "oauth2AppleOptions", className: "oauth2-apple-options" },
t.button(
{
type: "button",
className: "btn sm secondary",
onclick: () => {
app.modals.openAppleSecretGenerator({
ongenerate: (secret) => {
data.config.clientSecret = secret;
},
});
},
},
t.i({ className: "ri-key-line" }),
t.span({ className: "txt" }, "Generate secret"),
),
);
};
window.app.modals = window.app.modals || {};
window.app.modals.openAppleSecretGenerator = function(modalSettings = {
onbeforeopen: null,
onafteropen: null,
onbeforeclose: null,
onafterclose: null,
ongenerate: null, // (secret) => {}
}) {
const modal = appleSecretGeneratorModal(modalSettings);
if (!modal) {
return;
}
document.body.appendChild(modal);
app.modals.open(modal);
};
function appleSecretGeneratorModal(modalSettings = {}) {
let modal;
const uniqueId = "secret_generator_" + app.utils.randomString();
const maxDuration = 15777000; // 6 months
const data = store({
clientId: "",
teamId: "",
keyId: "",
privateKey: "",
duration: maxDuration,
isSubmitting: false,
});
async function submit() {
data.isSubmitting = true;
try {
const result = await app.pb.settings.generateAppleClientSecret(
data.clientId,
data.teamId,
data.keyId,
data.privateKey.trim(),
data.duration,
);
data.isSubmitting = false;
app.toasts.success("Successfully generated client secret.");
modalSettings.ongenerate?.(result.secret);
app.modals.close(modal);
} catch (err) {
if (!err.isAbort) {
app.checkApiError(err);
data.isSubmitting = false;
}
}
}
modal = t.div(
{
pbEvent: "appleSecretGeneratorModal",
className: "modal popup apple-secret-generator-modal",
onbeforeopen: (el) => {
return modalSettings.onbeforeopen?.(el);
},
onafteropen: (el) => {
modalSettings.onafteropen?.(el);
},
onbeforeclose: (el) => {
return modalSettings.onbeforeclose?.(el);
},
onafterclose: (el) => {
modalSettings.onafterclose?.(el);
el?.remove();
},
},
t.header(
{ className: "modal-header" },
t.h5({ className: "m-auto" }, "Generate Apple client secret"),
),
t.form(
{
id: uniqueId + "_form",
className: "modal-content",
onsubmit: (e) => {
e.preventDefault();
submit();
},
},
t.div(
{ className: "grid" },
t.div(
{ className: "col-sm-6" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".clientId" }, "Client ID"),
t.input({
id: uniqueId + ".clientId",
name: "clientId",
type: "text",
required: true,
value: () => data.clientId || "",
oninput: (e) => data.clientId = e.target.value,
}),
),
),
t.div(
{ className: "col-sm-6" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".teamId" }, "Team ID"),
t.input({
id: uniqueId + ".teamId",
name: "teamId",
type: "text",
required: true,
value: () => data.teamId || "",
oninput: (e) => data.teamId = e.target.value,
}),
),
),
t.div(
{ className: "col-sm-6" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".keyId" }, "Key ID"),
t.input({
id: uniqueId + ".keyId",
name: "keyId",
type: "text",
required: true,
value: () => data.keyId || "",
oninput: (e) => data.keyId = e.target.value,
}),
),
),
t.div(
{ className: "col-sm-6" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".duration" }, "Duration (in seconds)"),
t.input({
id: uniqueId + ".duration",
name: "duration",
type: "number",
min: 0,
step: 1,
max: maxDuration,
required: true,
value: () => data.duration || 0,
oninput: (e) => data.duration = parseInt(e.target.value, 10),
}),
),
t.div(
{ className: "field-help" },
`Max ${maxDuration} seconds (~${(maxDuration / (60 * 60 * 24 * 30)) << 0} months).`,
),
),
t.div(
{ className: "col-sm-12" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".privateKey" }, "Private key"),
t.textarea({
id: uniqueId + ".privateKey",
name: "privateKey",
type: "text",
required: true,
rows: 8,
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
value: () => data.privateKey || "",
oninput: (e) => data.privateKey = e.target.value,
}),
),
t.div(
{ className: "field-help" },
"The key is not stored on the server and it is used only for generating the signed JWT.",
),
),
),
),
t.footer(
{ className: "modal-footer" },
t.button(
{
type: "button",
className: "btn transparent m-r-auto",
onclick: () => app.modals.close(modal),
},
t.span({ className: "txt" }, "Close"),
),
t.button(
{
"html-form": uniqueId + "_form",
type: "submit",
className: "btn expanded",
},
t.i({ className: "ri-key-line" }),
t.span({ className: "txt" }, "Generate secret"),
),
),
);
return modal;
}
+73
View File
@@ -0,0 +1,73 @@
window.app = window.app || {};
window.app.oauth2 = window.app.oauth2 || {};
// note: data is the providerSettingsModal form store
window.app.oauth2.lark = function(providerInfo, namePrefix, data) {
const uniqueId = "lark_" + app.utils.randomString();
const domainOptions = [
{ label: "Feishu (China)", value: "feishu.cn" },
{ label: "Lark (International)", value: "larksuite.com" },
];
const local = store({
domain: data.config.authURL?.includes(domainOptions[1].value)
? domainOptions[1].value
: domainOptions[0].value,
});
const watchers = [
watch(() => local.domain, (domain) => {
if (domain) {
data.config.authURL = `https://accounts.${domain}/open-apis/authen/v1/authorize`;
data.config.tokenURL = `https://open.${domain}/open-apis/authen/v2/oauth/token`;
data.config.userInfoURL = `https://open.${domain}/open-apis/authen/v1/user_info`;
}
}),
];
return t.div(
{
pbEvent: "oauth2LarkOptions",
className: "oauth2-lark-options",
onunmount: () => {
watchers.forEach((w) => w?.unwatch());
},
},
t.div(
{ className: "grid" },
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".site" }, "Site"),
app.components.select({
options: domainOptions,
required: true,
value: () => local.domain || "",
onchange: (selectedOpts) => {
local.domain = selectedOpts?.[0]?.value;
},
}),
),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "alert info" },
"Note that the Lark user's ",
t.strong(null, "Union ID"),
" will be used for the association with the PocketBase user (see ",
t.a({
href:
"https://open.feishu.cn/document/platform-overveiw/basic-concepts/user-identity-introduction/introduction#3f2d4b63",
target: "_blank",
rel: "noopener noreferrer",
textContent: "Different Types of Lark User IDs",
}),
").",
),
),
),
);
};
@@ -0,0 +1,53 @@
window.app = window.app || {};
window.app.oauth2 = window.app.oauth2 || {};
// note: data is the providerSettingsModal form store
window.app.oauth2.microsoft = function(providerInfo, namePrefix, data) {
const uniqueId = "microsoft_" + app.utils.randomString();
return t.div(
{ pbEvent: "oauth2MicrosoftOptions", className: "oauth2-microsoft-options" },
t.p({ className: "txt-bold" }, "Azure AD endpoints"),
t.div(
{ className: "grid" },
t.div(
{ className: "col-12" },
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: "field-help" },
"Ex. https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/authorize",
),
),
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,
}),
),
t.div(
{ className: "field-help" },
"Ex. https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/token",
),
),
),
);
};
+234
View File
@@ -0,0 +1,234 @@
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.",
),
}),
),
),
),
),
);
};
@@ -0,0 +1,115 @@
window.app = window.app || {};
window.app.components = window.app.components || {};
// note: data is the providerSettingsModal form store
window.app.components.oauth2EndpointFields = function(providerInfo, namePrefix, data, settingsArg = {}) {
const uniqueId = "endpoints_" + app.utils.randomString();
const settings = store({
required: true,
title: "Provider endpoints",
});
const watchers = app.utils.extendStore(settings, settingsArg);
return t.div(
{
pbEvent: "oauth2Endpoints",
className: "oauth2-endpoints",
onunmount: () => {
watchers.forEach((w) => w?.unwatch());
},
},
t.p(
{ className: "txt-bold" },
(el) => {
if (typeof settings.title == "function") {
settings.title(el);
}
return settings.title;
},
),
t.div(
{ className: "grid" },
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".authURL" }, "Auth URL"),
t.input({
id: uniqueId + ".authURL",
name: namePrefix + ".authURL",
type: "url",
required: () => !!settings.required,
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: () => !!settings.required,
value: () => data.config.tokenURL || "",
oninput: (e) => data.config.tokenURL = e.target.value,
}),
),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label({ htmlFor: uniqueId + ".userInfoURL" }, "User info URL"),
t.input({
id: uniqueId + ".userInfoURL",
name: namePrefix + ".userInfoURL",
type: "url",
required: () => !!settings.required,
value: () => data.config.userInfoURL || "",
oninput: (e) => data.config.userInfoURL = e.target.value,
}),
),
),
),
);
};
window.app.oauth2 = window.app.oauth2 || {};
window.app.oauth2.gitlab = function(providerInfo, namePrefix, data) {
return app.components.oauth2EndpointFields(
providerInfo,
namePrefix,
data,
{
required: false,
title: "Self-hosted endpoints (optional)",
},
);
};
window.app.oauth2.gitea = function(providerInfo, namePrefix, data) {
return app.components.oauth2EndpointFields(
providerInfo,
namePrefix,
data,
{
required: false,
title: "Self-hosted endpoints (optional)",
},
);
};
window.app.oauth2.mailcow = function(providerInfo, namePrefix, data) {
return app.components.oauth2EndpointFields(
providerInfo,
namePrefix,
data,
);
};