sync: update from private repo (63a6e76)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-09 03:17:43 +00:00
parent d6d8e83867
commit 3848b5efd7
20 changed files with 386 additions and 29 deletions
+63 -11
View File
@@ -9,9 +9,10 @@ import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { Strategy as OAuth2Strategy } from 'passport-oauth2';
import type { Database } from 'better-sqlite3';
import type { AuthConfig } from '../config.js';
import type { AuthConfig, AuthProviderConfig } from '../config.js';
import type { Repository } from '../db/repository.js';
import { logger } from '../logger.js';
import { randomBytes } from 'crypto';
/**
* WebSocket upgrade(生 IncomingMessage)から認証済みユーザーを解決するチェッカー。
@@ -43,6 +44,36 @@ function escapeHtml(s: string): string {
.replace(/'/g, ''');
}
/**
* A provider is usable only when ALL fields needed to complete an OAuth round
* trip are present (Gitea additionally needs base_url). Used to gate auth
* activation and login-button visibility so a partial config saved from the
* Settings UI can't enable auth in a state where nobody can log in.
*/
export function isProviderConfigured(
p: AuthProviderConfig | undefined,
kind: 'google' | 'gitea',
): p is AuthProviderConfig {
if (!p || !p.clientId || !p.clientSecret || !p.callbackUrl) return false;
if (kind === 'gitea' && !p.baseUrl) return false;
return true;
}
/**
* Whether a provider should actually be LIVE (strategy + /auth/<kind> route).
* A valid `primaryProvider` restricts auth to that single provider, so the
* restriction is enforced at the route layer too — not just hidden on the login
* page (otherwise the "disabled" provider's route stayed open to direct URLs).
* An invalid primary (pointing at an unconfigured provider) is ignored.
*/
export function isProviderActive(authConfig: AuthConfig, kind: 'google' | 'gitea'): boolean {
if (!isProviderConfigured(authConfig.providers[kind], kind)) return false;
const primary = authConfig.primaryProvider;
if (primary === 'google' && isProviderConfigured(authConfig.providers.google, 'google')) return kind === 'google';
if (primary === 'gitea' && isProviderConfigured(authConfig.providers.gitea, 'gitea')) return kind === 'gitea';
return true;
}
/**
* auth-login.html をレンダリングする。
* primary_provider 設定と各プロバイダの configured 状態に応じて
@@ -51,9 +82,15 @@ function escapeHtml(s: string): string {
*/
function renderLoginPage(authConfig: AuthConfig, branding: LoginBranding = DEFAULT_LOGIN_BRANDING): string {
const raw = readFileSync(path.join(__authDirname, 'auth-login.html'), 'utf-8');
const primary = authConfig.primaryProvider;
const googleConfigured = !!authConfig.providers.google?.clientId;
const giteaConfigured = !!authConfig.providers.gitea?.clientId;
const googleConfigured = isProviderConfigured(authConfig.providers.google, 'google');
const giteaConfigured = isProviderConfigured(authConfig.providers.gitea, 'gitea');
// Ignore a primaryProvider that points to an unconfigured provider — otherwise
// it would hide the only working login button and lock the operator out.
const primary =
(authConfig.primaryProvider === 'google' && googleConfigured) ||
(authConfig.primaryProvider === 'gitea' && giteaConfigured)
? authConfig.primaryProvider
: undefined;
// Decide which buttons to show
let showGoogle: boolean;
@@ -315,7 +352,8 @@ export async function fetchGiteaOrgsForUser(
function registerGoogleStrategy(repo: Repository, authConfig: AuthConfig): void {
const googleConfig = authConfig.providers.google;
if (!googleConfig) return;
if (!isProviderConfigured(googleConfig, 'google')) return;
if (!isProviderActive(authConfig, 'google')) return;
passport.use(
new GoogleStrategy(
@@ -331,7 +369,7 @@ function registerGoogleStrategy(repo: Repository, authConfig: AuthConfig): void
await handleOAuthCallback(
repo,
authConfig.adminEmails,
authConfig.adminEmails ?? [],
'google',
profile.id,
email,
@@ -346,7 +384,8 @@ function registerGoogleStrategy(repo: Repository, authConfig: AuthConfig): void
function registerGiteaStrategy(repo: Repository, authConfig: AuthConfig): void {
const giteaConfig = authConfig.providers.gitea;
if (!giteaConfig) return;
if (!isProviderConfigured(giteaConfig, 'gitea')) return;
if (!isProviderActive(authConfig, 'gitea')) return;
const baseUrl = giteaConfig.baseUrl ?? '';
@@ -401,7 +440,7 @@ function registerGiteaStrategy(repo: Repository, authConfig: AuthConfig): void {
name,
avatarUrl,
});
if (user.status === 'pending' && authConfig.adminEmails.includes(email)) {
if (user.status === 'pending' && (authConfig.adminEmails ?? []).includes(email)) {
repo.updateUser(user.id, { status: 'active', role: 'admin' });
const updated = repo.getUserById(user.id);
if (updated) user = updated;
@@ -457,7 +496,7 @@ function createAuthRouter(
});
// Google OAuth
if (authConfig.providers.google) {
if (isProviderActive(authConfig, 'google')) {
router.get('/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
router.get(
@@ -475,7 +514,7 @@ function createAuthRouter(
}
// Gitea OAuth
if (authConfig.providers.gitea) {
if (isProviderActive(authConfig, 'gitea')) {
router.get('/gitea', passport.authenticate('gitea'));
router.get(
@@ -533,9 +572,22 @@ export function setupAuth(
): AuthMiddlewares {
const db = repo.getDb();
// express-session throws "secret option required" (→ 500 on every request) if
// the secret is empty. Auth can be enabled from the Settings UI before a
// session_secret is set, so fall back to a random per-process secret with a
// warning rather than bricking the server. Sessions reset on restart until a
// stable value is configured.
let sessionSecret = authConfig.sessionSecret;
if (!sessionSecret || sessionSecret.length === 0) {
sessionSecret = randomBytes(32).toString('hex');
logger.warn(
'[auth] auth.session_secret is unset — using a random per-process secret; sessions reset on restart. Set a stable value in Settings → Authentication.',
);
}
// セッションミドルウェア
const sessionMiddleware = session({
secret: authConfig.sessionSecret,
secret: sessionSecret,
resave: false,
saveUninitialized: false,
store: createSqliteSessionStore(db),