Compare commits
12 Commits
feat/Multi
...
added-code
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74cdad76ba | ||
|
|
1adc885ecf | ||
|
|
c925f9f39c | ||
|
|
71effb1a66 | ||
|
|
e3acd18c07 | ||
|
|
bef99e7927 | ||
|
|
c80507d1a6 | ||
|
|
4b72518813 | ||
|
|
4c8d2e090b | ||
|
|
dc6e5c104d | ||
|
|
1688310e04 | ||
|
|
d059189dab |
5
.codeql-config.yml
Normal file
5
.codeql-config.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
paths:
|
||||
- "**/test/**"
|
||||
- "**/__tests__/**"
|
||||
- "**/*.spec.*"
|
||||
- "**/*.test.*"
|
||||
@@ -424,11 +424,9 @@ APPLE_PRIVATE_KEY_PATH=
|
||||
APPLE_CALLBACK_URL=/oauth/apple/callback
|
||||
|
||||
# OpenID
|
||||
OPENID_ENABLED=
|
||||
OPENID_MULTI_TENANT=
|
||||
#OPENID_CLIENT_ID=
|
||||
#OPENID_CLIENT_SECRET=
|
||||
#OPENID_ISSUER=
|
||||
OPENID_CLIENT_ID=
|
||||
OPENID_CLIENT_SECRET=
|
||||
OPENID_ISSUER=
|
||||
OPENID_SESSION_SECRET=
|
||||
OPENID_SCOPE="openid profile email"
|
||||
OPENID_CALLBACK_URL=/oauth/openid/callback
|
||||
|
||||
100
.github/workflows/codeql.yml
vendored
Normal file
100
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '39 17 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
# or others). This is typically only required for manual builds.
|
||||
# - name: Setup runtime (example)
|
||||
# uses: actions/setup-example@v1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -52,9 +52,10 @@ router.get('/', async function (req, res) {
|
||||
!!process.env.APPLE_KEY_ID &&
|
||||
!!process.env.APPLE_PRIVATE_KEY_PATH,
|
||||
openidLoginEnabled:
|
||||
!!process.env.OPENID_ENABLED &&
|
||||
!!process.env.OPENID_CLIENT_ID &&
|
||||
!!process.env.OPENID_CLIENT_SECRET &&
|
||||
!!process.env.OPENID_ISSUER &&
|
||||
!!process.env.OPENID_SESSION_SECRET,
|
||||
openidMultiTenantEnabled: !!process.env.OPENID_MULTI_TENANT,
|
||||
openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID',
|
||||
openidImageUrl: process.env.OPENID_IMAGE_URL,
|
||||
openidAutoRedirect: isEnabled(process.env.OPENID_AUTO_REDIRECT),
|
||||
|
||||
@@ -10,7 +10,6 @@ const {
|
||||
} = require('~/server/middleware');
|
||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||
const { logger } = require('~/config');
|
||||
const { chooseOpenIdStrategy } = require('~/server/utils/openidHelper');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -95,32 +94,20 @@ router.get(
|
||||
/**
|
||||
* OpenID Routes
|
||||
*/
|
||||
router.get('/openid', async (req, res, next) => {
|
||||
try {
|
||||
const strategy = await chooseOpenIdStrategy(req);
|
||||
console.log('OpenID login using strategy:', strategy);
|
||||
passport.authenticate(strategy, {
|
||||
session: false,
|
||||
})(req, res, next);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router.get(
|
||||
'/openid',
|
||||
passport.authenticate('openid', {
|
||||
session: false,
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/openid/callback',
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const strategy = await chooseOpenIdStrategy(req);
|
||||
passport.authenticate(strategy, {
|
||||
failureRedirect: `${domains.client}/oauth/error`,
|
||||
failureMessage: true,
|
||||
session: false,
|
||||
})(req, res, next);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
passport.authenticate('openid', {
|
||||
failureRedirect: `${domains.client}/oauth/error`,
|
||||
failureMessage: true,
|
||||
session: false,
|
||||
}),
|
||||
setBalanceConfig,
|
||||
oauthHandler,
|
||||
);
|
||||
|
||||
@@ -36,6 +36,7 @@ function loadTurnstileConfig(config, configDefaults) {
|
||||
logger.info('Turnstile is DISABLED (no siteKey provided).');
|
||||
}
|
||||
|
||||
|
||||
return loadedTurnstile;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ const keyvRedis = require('~/cache/keyvRedis');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Express.Application} app
|
||||
*/
|
||||
const configureSocialLogins = (app) => {
|
||||
@@ -37,7 +38,10 @@ const configureSocialLogins = (app) => {
|
||||
passport.use(appleLogin());
|
||||
}
|
||||
if (
|
||||
process.env.OPENID_ENABLED &&
|
||||
process.env.OPENID_CLIENT_ID &&
|
||||
process.env.OPENID_CLIENT_SECRET &&
|
||||
process.env.OPENID_ISSUER &&
|
||||
process.env.OPENID_SCOPE &&
|
||||
process.env.OPENID_SESSION_SECRET
|
||||
) {
|
||||
logger.info('Configuring OpenID Connect...');
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
const { logger } = require('~/config');
|
||||
const { getCustomConfig } = require('~/server/services/Config');
|
||||
|
||||
/**
|
||||
* Loads the tenant configurations from the custom configuration.
|
||||
* @returns {Promise<Array>} Array of tenant configurations.
|
||||
*/
|
||||
async function getOpenIdTenants() {
|
||||
try {
|
||||
const customConfig = await getCustomConfig();
|
||||
if (customConfig?.openid?.tenants) {
|
||||
return customConfig.openid.tenants;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to load custom configuration for OpenID tenants:', err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the OpenID strategy name based on the email domain.
|
||||
* It consults the global tenant mapping (built in setupOpenId).
|
||||
* @param {import('express').Request} req - The Express request object.
|
||||
* @returns {Promise<string>} - The chosen strategy name.
|
||||
*/
|
||||
async function chooseOpenIdStrategy(req) {
|
||||
if (req.query.email) {
|
||||
const email = req.query.email;
|
||||
const domain = email.split('@')[1].toLowerCase();
|
||||
const tenants = await getOpenIdTenants();
|
||||
|
||||
// Iterate over the tenants and return the strategy name of the first matching tenant
|
||||
for (const tenant of tenants) {
|
||||
if (tenant.domains) {
|
||||
const tenantDomains = tenant.domains.split(',').map(s => s.trim().toLowerCase());
|
||||
if (tenantDomains.includes(domain)) {
|
||||
// Look up the registered strategy via the global mapping.
|
||||
if (tenant.name && tenant.name.trim() && global.__openidTenantMapping) {
|
||||
const mapped = global.__openidTenantMapping.get(tenant.name.trim().toLowerCase());
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
return 'openid'; // Fallback if no mapping exists.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'openid';
|
||||
}
|
||||
|
||||
module.exports = { getOpenIdTenants, chooseOpenIdStrategy };
|
||||
@@ -1,6 +1,6 @@
|
||||
const fetch = require('node-fetch');
|
||||
const passport = require('passport');
|
||||
const { decode: jwtDecode } = require('jsonwebtoken');
|
||||
const jwtDecode = require('jsonwebtoken/decode');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { Issuer, Strategy: OpenIDStrategy, custom } = require('openid-client');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
@@ -8,7 +8,6 @@ const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
||||
const { hashToken } = require('~/server/utils/crypto');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
const { getOpenIdTenants } = require('~/server/utils/openidHelper');
|
||||
|
||||
let crypto;
|
||||
try {
|
||||
@@ -106,18 +105,16 @@ function convertToUsername(input, defaultValue = '') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a single OpenID strategy for the given tenant configuration.
|
||||
* @param {Object} tenant - The tenant’s OpenID config (issuer, clientId, etc.).
|
||||
* @param {string} tenant.issuer
|
||||
* @param {string} tenant.clientId
|
||||
* @param {string} tenant.clientSecret
|
||||
* @param {string} strategyName - Unique name for the strategy.
|
||||
*/
|
||||
async function setupSingleStrategy(tenant, strategyName) {
|
||||
async function setupOpenId() {
|
||||
try {
|
||||
// Discover the issuer (this performs the .well-known lookup).
|
||||
const issuer = await Issuer.discover(tenant.issuer);
|
||||
if (process.env.PROXY) {
|
||||
const proxyAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||
custom.setHttpOptionsDefaults({
|
||||
agent: proxyAgent,
|
||||
});
|
||||
logger.info(`[openidStrategy] proxy agent added: ${process.env.PROXY}`);
|
||||
}
|
||||
const issuer = await Issuer.discover(process.env.OPENID_ISSUER);
|
||||
/* Supported Algorithms, openid-client v5 doesn't set it automatically as discovered from server.
|
||||
- id_token_signed_response_alg // defaults to 'RS256'
|
||||
- request_object_signing_alg // defaults to 'RS256'
|
||||
@@ -127,8 +124,8 @@ async function setupSingleStrategy(tenant, strategyName) {
|
||||
*/
|
||||
/** @type {import('openid-client').ClientMetadata} */
|
||||
const clientMetadata = {
|
||||
client_id: tenant.clientId,
|
||||
client_secret: tenant.clientSecret,
|
||||
client_id: process.env.OPENID_CLIENT_ID,
|
||||
client_secret: process.env.OPENID_CLIENT_SECRET,
|
||||
redirect_uris: [process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL],
|
||||
};
|
||||
if (isEnabled(process.env.OPENID_SET_FIRST_SUPPORTED_ALGORITHM)) {
|
||||
@@ -149,7 +146,7 @@ async function setupSingleStrategy(tenant, strategyName) {
|
||||
async (tokenset, userinfo, done) => {
|
||||
try {
|
||||
logger.info(`[openidStrategy] verify login openidId: ${userinfo.sub}`);
|
||||
logger.debug('[openidStrategy] verify login tokenset and userinfo', { tokenset, userinfo });
|
||||
logger.debug('[openidStrategy] very login tokenset and userinfo', { tokenset, userinfo });
|
||||
|
||||
let user = await findUser({ openidId: userinfo.sub });
|
||||
logger.info(
|
||||
@@ -268,65 +265,7 @@ async function setupSingleStrategy(tenant, strategyName) {
|
||||
},
|
||||
);
|
||||
|
||||
passport.use(strategyName, openidLogin);
|
||||
logger.info(`Configured OpenID strategy [${strategyName}] for issuer: ${tenant.issuer}`);
|
||||
} catch (err) {
|
||||
logger.error(`[openidStrategy] Error configuring strategy "${strategyName}":`, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the YAML configuration and registers strategies for multi-tenant OpenID Connect.
|
||||
*/
|
||||
async function setupOpenId() {
|
||||
try {
|
||||
// If a proxy is configured, set it for openid-client.
|
||||
|
||||
// Set global HTTP options for openid-client
|
||||
if (process.env.PROXY) {
|
||||
const proxyAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||
custom.setHttpOptionsDefaults({
|
||||
agent: proxyAgent,
|
||||
timeout: 10000, // 10,000ms = 10 seconds
|
||||
});
|
||||
logger.info(`[openidStrategy] Proxy agent added: ${process.env.PROXY} with timeout 10000ms`);
|
||||
} else {
|
||||
custom.setHttpOptionsDefaults({
|
||||
timeout: 10000, // Increase the default timeout
|
||||
});
|
||||
logger.info('[openidStrategy] Set default timeout to 10000ms');
|
||||
}
|
||||
|
||||
const tenants = await getOpenIdTenants();
|
||||
|
||||
// Global mapping: tenant name (lowercase) -> strategy name.
|
||||
const tenantMapping = new Map();
|
||||
|
||||
// If there is one tenant with no domains specified, register it as the default "openid" strategy.
|
||||
if (tenants.length === 1 && (!tenants[0].domains || tenants[0].domains.trim() === '')) {
|
||||
await setupSingleStrategy(tenants[0].openid, 'openid');
|
||||
tenantMapping.set(tenants[0].name?.trim().toLowerCase() || 'openid', 'openid');
|
||||
logger.info('Configured single-tenant OpenID strategy as "openid"');
|
||||
} else {
|
||||
// Otherwise, iterate over each tenant.
|
||||
for (const tenantCfg of tenants) {
|
||||
const openidCfg = tenantCfg.openid;
|
||||
let strategyName = 'openid';
|
||||
if (tenantCfg.name && tenantCfg.name.trim()) {
|
||||
strategyName = `openid_${tenantCfg.name.trim()}`;
|
||||
}else {
|
||||
logger.warn(
|
||||
`[openidStrategy] Tenant with issuer ${openidCfg.issuer} has no domains specified; defaulting strategy name to "openid".`,
|
||||
);
|
||||
}
|
||||
await setupSingleStrategy(openidCfg, strategyName);
|
||||
if (tenantCfg.name && tenantCfg.name.trim()) {
|
||||
tenantMapping.set(tenantCfg.name.trim().toLowerCase(), strategyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Store the tenant mapping globally so that the helper can choose the correct strategy.
|
||||
global.__openidTenantMapping = tenantMapping;
|
||||
passport.use('openid', openidLogin);
|
||||
} catch (err) {
|
||||
logger.error('[openidStrategy]', err);
|
||||
}
|
||||
|
||||
@@ -33,15 +33,6 @@ jest.mock('~/config', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// IMPORTANT: Mock the openid helper to return our desired tenant configuration.
|
||||
jest.mock('~/server/utils/openidHelper', () => ({
|
||||
getOpenIdTenants: jest.fn(),
|
||||
chooseOpenIdStrategy: jest.fn(), // Not used in these tests.
|
||||
}));
|
||||
|
||||
// Import our mocked helper so we can set its return value.
|
||||
const { getOpenIdTenants } = require('~/server/utils/openidHelper');
|
||||
|
||||
// Mock Issuer.discover so that setupOpenId gets a fake issuer and client
|
||||
Issuer.discover = jest.fn().mockResolvedValue({
|
||||
id_token_signing_alg_values_supported: ['RS256'],
|
||||
@@ -106,21 +97,6 @@ describe('setupOpenId', () => {
|
||||
delete process.env.OPENID_NAME_CLAIM;
|
||||
delete process.env.PROXY;
|
||||
|
||||
// Set up our mocked tenant configuration.
|
||||
// Here we simulate a single tenant with an empty domains field.
|
||||
// (Our updated multi-tenant code uses the tenant name to build the strategy.)
|
||||
getOpenIdTenants.mockResolvedValue([
|
||||
{
|
||||
name: 'tenant1',
|
||||
domains: '', // Using an empty string so the single-tenant branch is taken.
|
||||
openid: {
|
||||
issuer: process.env.OPENID_ISSUER,
|
||||
clientId: process.env.OPENID_CLIENT_ID,
|
||||
clientSecret: process.env.OPENID_CLIENT_SECRET,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Default jwtDecode mock returns a token that includes the required role.
|
||||
jwtDecode.mockReturnValue({
|
||||
roles: ['requiredRole'],
|
||||
|
||||
@@ -29,8 +29,10 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||
const { data: config } = useGetStartupConfig();
|
||||
const useUsernameLogin = config?.ldap?.username;
|
||||
const validTheme = theme === 'dark' ? 'dark' : 'light';
|
||||
|
||||
const requireCaptcha = Boolean(startupConfig.turnstile?.siteKey);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (error && error.includes('422') && !showResendLink) {
|
||||
setShowResendLink(true);
|
||||
@@ -150,6 +152,7 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||
</a>
|
||||
)}
|
||||
|
||||
|
||||
{requireCaptcha && (
|
||||
<div className="my-4 flex justify-center">
|
||||
<Turnstile
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { OpenIDIcon } from '~/components';
|
||||
|
||||
interface MultiTenantOpenIDProps {
|
||||
serverDomain: string;
|
||||
openidLabel: string;
|
||||
openidImageUrl: string;
|
||||
localize: (key: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* When multi‑tenant mode is enabled (startupConfig.emailLoginEnabled === true),
|
||||
* we render a form for the user to enter their email. When submitted, we perform a GET
|
||||
* request (via redirect) to /oauth/openid with the email as a query parameter.
|
||||
* If, for some reason, no email is provided, we simply redirect to /oauth/openid.
|
||||
*/
|
||||
function MultiTenantOpenID({
|
||||
serverDomain,
|
||||
openidLabel,
|
||||
openidImageUrl,
|
||||
localize,
|
||||
}: MultiTenantOpenIDProps) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<{ email: string }>();
|
||||
|
||||
const onSubmit = (data: { email: string }) => {
|
||||
// If an email is provided, include it as a query parameter.
|
||||
// Otherwise, simply redirect without an email.
|
||||
const emailQuery =
|
||||
data.email && data.email.trim() !== ''
|
||||
? `?email=${encodeURIComponent(data.email)}`
|
||||
: '';
|
||||
window.location.href = `${serverDomain}/oauth/openid${emailQuery}`;
|
||||
};
|
||||
|
||||
const renderError = (fieldName: string) => {
|
||||
const errorMessage = errors[fieldName]?.message;
|
||||
return errorMessage ? (
|
||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
||||
{String(errorMessage)}
|
||||
</span>
|
||||
) : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mt-2">
|
||||
<div className="mb-4">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
autoComplete="email"
|
||||
aria-label={localize('com_auth_email')}
|
||||
{...register('email', {
|
||||
required: localize('com_auth_email_required'),
|
||||
maxLength: { value: 120, message: localize('com_auth_email_max_length') },
|
||||
pattern: {
|
||||
value: /\S+@\S+\.\S+/,
|
||||
message: localize('com_auth_email_pattern'),
|
||||
},
|
||||
})}
|
||||
aria-invalid={!!errors.email}
|
||||
className="
|
||||
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
|
||||
bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none
|
||||
"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="
|
||||
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
|
||||
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
|
||||
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500
|
||||
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
|
||||
"
|
||||
>
|
||||
{localize('com_auth_email_address')}
|
||||
</label>
|
||||
</div>
|
||||
{renderError('email')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full items-center space-x-3 rounded-2xl border border-border-light bg-surface-primary px-5 py-3 text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||
data-testid="openid"
|
||||
>
|
||||
{openidImageUrl ? (
|
||||
<img src={openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
|
||||
) : (
|
||||
<OpenIDIcon />
|
||||
)}
|
||||
<p>{openidLabel}</p>
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default MultiTenantOpenID;
|
||||
@@ -33,6 +33,7 @@ const Registration: React.FC = () => {
|
||||
const token = queryParams.get('token');
|
||||
const validTheme = theme === 'dark' ? 'dark' : 'light';
|
||||
|
||||
|
||||
// only require captcha if we have a siteKey
|
||||
const requireCaptcha = Boolean(startupConfig?.turnstile?.siteKey);
|
||||
|
||||
@@ -179,6 +180,7 @@ const Registration: React.FC = () => {
|
||||
})}
|
||||
|
||||
{startupConfig?.turnstile?.siteKey && (
|
||||
|
||||
<div className="my-4 flex justify-center">
|
||||
<Turnstile
|
||||
siteKey={startupConfig.turnstile.siteKey}
|
||||
@@ -198,6 +200,7 @@ const Registration: React.FC = () => {
|
||||
disabled={
|
||||
Object.keys(errors).length > 0 ||
|
||||
isSubmitting ||
|
||||
|
||||
(requireCaptcha && !turnstileToken)
|
||||
}
|
||||
type="submit"
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
GoogleIcon,
|
||||
FacebookIcon,
|
||||
OpenIDIcon,
|
||||
GithubIcon,
|
||||
DiscordIcon,
|
||||
AppleIcon,
|
||||
} from '~/components';
|
||||
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon, AppleIcon } from '~/components';
|
||||
|
||||
import SocialButton from './SocialButton';
|
||||
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
import { TStartupConfig } from 'librechat-data-provider';
|
||||
import MultiTenantOpenID from './MultiTenantOpenID';
|
||||
|
||||
function SocialLoginRender({
|
||||
startupConfig,
|
||||
@@ -79,37 +73,23 @@ function SocialLoginRender({
|
||||
id="apple"
|
||||
/>
|
||||
),
|
||||
openid:
|
||||
startupConfig.openidLoginEnabled &&
|
||||
(startupConfig.openidMultiTenantEnabled ? (
|
||||
<MultiTenantOpenID
|
||||
key="openid"
|
||||
openidImageUrl={startupConfig.openidImageUrl}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
openidLabel={startupConfig.openidLabel}
|
||||
localize={localize}
|
||||
/>
|
||||
) : (
|
||||
<SocialButton
|
||||
key="openid"
|
||||
enabled={startupConfig.openidLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="openid"
|
||||
Icon={() =>
|
||||
startupConfig.openidImageUrl ? (
|
||||
<img
|
||||
src={startupConfig.openidImageUrl}
|
||||
alt="OpenID Logo"
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
) : (
|
||||
<OpenIDIcon />
|
||||
)
|
||||
}
|
||||
label={startupConfig.openidLabel}
|
||||
id="openid"
|
||||
/>
|
||||
)),
|
||||
openid: startupConfig.openidLoginEnabled && (
|
||||
<SocialButton
|
||||
key="openid"
|
||||
enabled={startupConfig.openidLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="openid"
|
||||
Icon={() =>
|
||||
startupConfig.openidImageUrl ? (
|
||||
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
|
||||
) : (
|
||||
<OpenIDIcon />
|
||||
)
|
||||
}
|
||||
label={startupConfig.openidLabel}
|
||||
id="openid"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -53,6 +53,7 @@ export default function AgentFooter({
|
||||
const showButtons = activePanel === Panel.builder;
|
||||
|
||||
return (
|
||||
|
||||
<div className="mb-1 flex w-full flex-col gap-2">
|
||||
{showButtons && <AdvancedButton setActivePanel={setActivePanel} />}
|
||||
{showButtons && agent_id && <VersionButton setActivePanel={setActivePanel} />}
|
||||
|
||||
@@ -84,31 +84,6 @@ registration:
|
||||
# allowedDomains:
|
||||
# - "gmail.com"
|
||||
|
||||
# Single‑Tenant YAML
|
||||
#openid:
|
||||
# tenants:
|
||||
# - name: "default"
|
||||
# domains: ""
|
||||
# openid:
|
||||
# clientId: "client-id-for-tenant1"
|
||||
# clientSecret: "client-secret-for-tenant1"
|
||||
# issuer: "https://example.com/oidc"
|
||||
|
||||
# Add your multi-tenant OpenID settings:
|
||||
openid:
|
||||
tenants:
|
||||
- name: "tenant1"
|
||||
domains: "first.com,example.com"
|
||||
openid:
|
||||
clientId: "client-id-for-tenant1"
|
||||
clientSecret: "client-secret-for-tenant1"
|
||||
issuer: "https://example.com/oidc"
|
||||
- name: "tenant2"
|
||||
domains: "another.com,one.com"
|
||||
openid:
|
||||
clientId: "client-id-for-tenant2"
|
||||
clientSecret: "client-secret-for-tenant2"
|
||||
issuer: "https://example.com/oidc2"
|
||||
|
||||
# Example Balance settings
|
||||
# balance:
|
||||
|
||||
@@ -540,7 +540,6 @@ export type TStartupConfig = {
|
||||
githubLoginEnabled: boolean;
|
||||
googleLoginEnabled: boolean;
|
||||
openidLoginEnabled: boolean;
|
||||
openidMultiTenantEnabled: boolean;
|
||||
appleLoginEnabled: boolean;
|
||||
openidLabel: string;
|
||||
openidImageUrl: string;
|
||||
@@ -647,23 +646,6 @@ export const configSchema = z.object({
|
||||
message: 'At least one `endpoints` field must be provided.',
|
||||
})
|
||||
.optional(),
|
||||
openid: z
|
||||
.object({
|
||||
tenants: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
domains: z.string(),
|
||||
openid: z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
issuer: z.string(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const getConfigDefaults = () => getSchemaDefaults(configSchema);
|
||||
|
||||
Reference in New Issue
Block a user