From 58652bc3000668c12b77c6f8e7b6f1c4f2cd7abd Mon Sep 17 00:00:00 2001 From: aaldebs99 Date: Thu, 30 Oct 2025 22:10:04 +0000 Subject: [PATCH 1/2] Feat(UI): hide UI login when ALLOW_LOCAL_AUTH is set to false --- frontend/src/components/Auth/Login.tsx | 331 ++++++++++-------- .../Auth/__tests__/Login.oidc.test.tsx | 16 +- frontend/src/components/Register.tsx | 43 ++- src/routes/auth.rs | 29 +- 4 files changed, 272 insertions(+), 147 deletions(-) diff --git a/frontend/src/components/Auth/Login.tsx b/frontend/src/components/Auth/Login.tsx index dff0736..9fe0ffc 100644 --- a/frontend/src/components/Auth/Login.tsx +++ b/frontend/src/components/Auth/Login.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, Card, @@ -12,6 +12,7 @@ import { IconButton, Fade, Grow, + CircularProgress, } from '@mui/material'; import { Visibility, @@ -34,7 +35,14 @@ interface LoginFormData { password: string; } +interface AuthConfig { + allow_local_auth: boolean; + oidc_enabled: boolean; +} + const Login: React.FC = () => { + const [authConfig, setAuthConfig] = useState(null); + const [configLoading, setConfigLoading] = useState(true); const [showPassword, setShowPassword] = useState(false); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); @@ -45,6 +53,30 @@ const Login: React.FC = () => { const theme = useMuiTheme(); const { t } = useTranslation(); + // Fetch authentication configuration from backend + useEffect(() => { + const fetchAuthConfig = async () => { + try { + const response = await fetch('/api/auth/config'); + if (response.ok) { + const config = await response.json(); + setAuthConfig(config); + } else { + // Default to allowing both if config fetch fails + setAuthConfig({ allow_local_auth: true, oidc_enabled: true }); + } + } catch (err) { + console.error('Failed to fetch auth config:', err); + // Default to allowing both if config fetch fails + setAuthConfig({ allow_local_auth: true, oidc_enabled: true }); + } finally { + setConfigLoading(false); + } + }; + + fetchAuthConfig(); + }, []); + const { register, handleSubmit, @@ -178,18 +210,26 @@ const Login: React.FC = () => { + {/* Loading state while fetching config */} + {configLoading && ( + + + + )} + {/* Login Card */} - - + { )} - - - - ), - }} - sx={{ mb: 2 }} - /> + {authConfig.allow_local_auth && ( + <> + + + + ), + }} + sx={{ mb: 2 }} + /> - - - - ), - endAdornment: ( - - - {showPassword ? : } - - - ), - }} - sx={{ mb: 3 }} - /> + + + + ), + endAdornment: ( + + + {showPassword ? : } + + + ), + }} + sx={{ mb: 3 }} + /> - + - - + + {t('common.or')} + + + + )} + + {authConfig.oidc_enabled && ( + + {oidcLoading ? t('auth.redirecting') : t('auth.signInWithOIDC')} + + )} @@ -359,6 +405,7 @@ const Login: React.FC = () => { + )} {/* Footer */} diff --git a/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx b/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx index 2eb89da..edc3ae4 100644 --- a/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx +++ b/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx @@ -1,11 +1,16 @@ -import { describe, test, expect } from 'vitest'; +import { describe, test, expect, vi, beforeEach } from 'vitest'; import Login from '../Login'; // Basic existence test for Login component -// More complex auth tests require comprehensive context mocking which +// More complex auth tests require comprehensive context mocking which // is causing infrastructure issues describe('Login - OIDC Features - Simplified', () => { + beforeEach(() => { + // Mock fetch for auth config endpoint + global.fetch = vi.fn(); + }); + test('Test file exists and can run', () => { // This is a basic test to ensure the test file is valid expect(true).toBe(true); @@ -16,4 +21,11 @@ describe('Login - OIDC Features - Simplified', () => { expect(Login).toBeDefined(); expect(typeof Login).toBe('function'); }); + + test('Component fetches auth config at runtime', () => { + // The component now fetches /api/auth/config to determine + // which authentication methods are available + // Actual rendering logic is tested in E2E tests + expect(global.fetch).toBeDefined(); + }); }); \ No newline at end of file diff --git a/frontend/src/components/Register.tsx b/frontend/src/components/Register.tsx index c9cf729..bb099fb 100644 --- a/frontend/src/components/Register.tsx +++ b/frontend/src/components/Register.tsx @@ -1,17 +1,46 @@ -import React, { useState } from 'react' -import { Link } from 'react-router-dom' +import React, { useState, useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useAuth } from '../contexts/AuthContext' +interface AuthConfig { + allow_local_auth: boolean; + oidc_enabled: boolean; +} + function Register() { + const navigate = useNavigate() const { t } = useTranslation() const [username, setUsername] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) + const [configLoading, setConfigLoading] = useState(true) const { register } = useAuth() + // Fetch authentication configuration and redirect if local auth is disabled + useEffect(() => { + const fetchAuthConfig = async () => { + try { + const response = await fetch('/api/auth/config'); + if (response.ok) { + const config: AuthConfig = await response.json(); + if (!config.allow_local_auth) { + // Redirect to login if local auth is disabled + navigate('/login'); + } + } + } catch (err) { + console.error('Failed to fetch auth config:', err); + } finally { + setConfigLoading(false); + } + }; + + fetchAuthConfig(); + }, [navigate]) + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError('') @@ -26,6 +55,16 @@ function Register() { } } + if (configLoading) { + return ( +
+
+

{t('common.loading', 'Loading...')}

+
+
+ ) + } + return (
diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 791db32..a4fbcbb 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -5,7 +5,7 @@ use axum::{ routing::{get, post}, Router, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::sync::Arc; use crate::{ @@ -20,10 +20,17 @@ pub fn router() -> Router> { .route("/register", post(register)) .route("/login", post(login)) .route("/me", get(me)) + .route("/config", get(get_auth_config)) .route("/oidc/login", get(oidc_login)) .route("/oidc/callback", get(oidc_callback)) } +#[derive(Serialize, utoipa::ToSchema)] +struct AuthConfig { + allow_local_auth: bool, + oidc_enabled: bool, +} + #[utoipa::path( post, @@ -82,6 +89,26 @@ async fn register( } } +#[utoipa::path( + get, + path = "/api/auth/config", + tag = "auth", + responses( + (status = 200, description = "Authentication configuration", body = AuthConfig), + ) +)] +async fn get_auth_config( + State(state): State>, +) -> Json { + let allow_local_auth = state.config.allow_local_auth.unwrap_or(true); + let oidc_enabled = state.oidc_client.is_some(); + + Json(AuthConfig { + allow_local_auth, + oidc_enabled, + }) +} + #[utoipa::path( post, path = "/api/auth/login", From 706b71eb40367aa79f2f1fbeb7b2801e8ba342ab Mon Sep 17 00:00:00 2001 From: aaldebs99 Date: Thu, 30 Oct 2025 22:10:52 +0000 Subject: [PATCH 2/2] Feat(docs): add OIDC samples to env example --- .env.example | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.env.example b/.env.example index 3be5a30..a810dfc 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,19 @@ DATABASE_URL=postgresql://readur:readur_password@localhost:5432/readur JWT_SECRET=your-super-secret-jwt-key-change-this-in-production SERVER_ADDRESS=0.0.0.0:8000 +# Authentication Configuration +# Enable/disable local username/password authentication (default: true) +# When disabled, only OIDC authentication is available +ALLOW_LOCAL_AUTH=true + +# OIDC Configuration (optional - see docs/oidc-setup.md for details) +# OIDC_ENABLED=true +# OIDC_CLIENT_ID=your-client-id +# OIDC_CLIENT_SECRET=your-client-secret +# OIDC_ISSUER_URL=https://accounts.google.com +# OIDC_REDIRECT_URI=https://your-domain.com/api/auth/oidc/callback +# OIDC_AUTO_REGISTER=true + # File Storage & Upload UPLOAD_PATH=./uploads ALLOWED_FILE_TYPES=pdf,png,jpg,jpeg,tiff,bmp,gif,txt,rtf,doc,docx