Fix auth for optical-dev.oliver.solutions/ppt-tool/ deployment
- AZURE_AD_REDIRECT_URI set to https://optical-dev.oliver.solutions/ppt-tool/ - Root page intercepts ?code= from Azure AD and forwards to backend callback - Post-OAuth redirect uses NEXT_PUBLIC_BASE_PATH env var (/ppt-tool/dashboard) - Cookie secure flag driven by COOKIE_SECURE env var (true in prod) - Dev login now works alongside Azure AD when DEV_AUTH_PASSWORD is set - Login page shows both Microsoft SSO and dev form when both modes enabled - docker-compose.prod.yml: add COOKIE_SECURE=true and NEXT_PUBLIC_BASE_PATH Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1e28574512
commit
19222ab36d
7 changed files with 73 additions and 21 deletions
|
|
@ -8,7 +8,7 @@ REDIS_URL=redis://redis:6379/0
|
|||
AZURE_AD_TENANT_ID=
|
||||
AZURE_AD_CLIENT_ID=
|
||||
AZURE_AD_CLIENT_SECRET=
|
||||
AZURE_AD_REDIRECT_URI=http://localhost/api/v1/auth/callback
|
||||
AZURE_AD_REDIRECT_URI=https://yourdomain.com/api/v1/auth/callback
|
||||
|
||||
# JWT
|
||||
JWT_SECRET_KEY=change-me-to-a-random-256-bit-key
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, Request
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
|
|
@ -20,8 +21,12 @@ class DevLoginRequest(BaseModel):
|
|||
|
||||
@AUTH_ROUTER.get("/dev-status")
|
||||
async def dev_status():
|
||||
"""Check if dev auth mode is enabled."""
|
||||
return {"dev_mode": auth_service.is_dev_mode}
|
||||
"""Check auth modes available."""
|
||||
return {
|
||||
"dev_mode": auth_service.is_dev_mode,
|
||||
"dev_login_enabled": auth_service.dev_login_enabled,
|
||||
"azure_enabled": not auth_service.is_dev_mode,
|
||||
}
|
||||
|
||||
|
||||
@AUTH_ROUTER.get("/login")
|
||||
|
|
@ -63,12 +68,14 @@ async def callback(
|
|||
user = await auth_service.get_or_create_user(claims, session)
|
||||
token = auth_service.create_session_jwt(user)
|
||||
|
||||
response = RedirectResponse(url="/dashboard", status_code=302)
|
||||
base_path = os.environ.get("NEXT_PUBLIC_BASE_PATH", "")
|
||||
is_https = os.environ.get("COOKIE_SECURE", "false").lower() == "true"
|
||||
response = RedirectResponse(url=f"{base_path}/dashboard", status_code=302)
|
||||
response.set_cookie(
|
||||
key="session_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=False, # Set True in production with HTTPS
|
||||
secure=is_https,
|
||||
samesite="lax",
|
||||
max_age=86400, # 24 hours
|
||||
)
|
||||
|
|
@ -86,8 +93,8 @@ async def dev_login(
|
|||
body: DevLoginRequest,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""Dev-mode login with email and password. Only available when Azure AD is not configured."""
|
||||
if not auth_service.is_dev_mode:
|
||||
"""Dev login with email and password. Available when DEV_AUTH_PASSWORD is set."""
|
||||
if not auth_service.dev_login_enabled:
|
||||
raise HTTPException(status_code=404, detail="Dev login not available")
|
||||
|
||||
user = await auth_service.dev_login(body.email, body.password, session)
|
||||
|
|
@ -96,6 +103,7 @@ async def dev_login(
|
|||
|
||||
token = auth_service.create_session_jwt(user)
|
||||
|
||||
is_https = os.environ.get("COOKIE_SECURE", "false").lower() == "true"
|
||||
response = JSONResponse(
|
||||
content={
|
||||
"id": str(user.id),
|
||||
|
|
@ -108,7 +116,7 @@ async def dev_login(
|
|||
key="session_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=False,
|
||||
secure=is_https,
|
||||
samesite="lax",
|
||||
max_age=86400,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ class AuthService:
|
|||
def is_dev_mode(self) -> bool:
|
||||
return not self.tenant_id
|
||||
|
||||
@property
|
||||
def dev_login_enabled(self) -> bool:
|
||||
"""Dev login is available whenever DEV_AUTH_PASSWORD is set, regardless of Azure AD config."""
|
||||
return bool(self.dev_password)
|
||||
|
||||
@property
|
||||
def msal_app(self):
|
||||
if self._msal_app is None and not self.is_dev_mode:
|
||||
|
|
@ -148,8 +153,8 @@ class AuthService:
|
|||
return user
|
||||
|
||||
async def dev_login(self, email: str, password: str, session) -> Optional[UserModel]:
|
||||
"""Dev-mode login: validate password, get or create user."""
|
||||
if not self.is_dev_mode:
|
||||
"""Dev login: validate password, get or create user. Works even when Azure AD is configured."""
|
||||
if not self.dev_login_enabled:
|
||||
return None
|
||||
if password != self.dev_password:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ services:
|
|||
- "127.0.0.1:${API_PORT:-8000}:8000"
|
||||
environment:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
NEXT_PUBLIC_BASE_PATH: "/ppt-tool"
|
||||
COOKIE_SECURE: "true"
|
||||
|
||||
worker:
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -4,17 +4,18 @@ import { useEffect, useState } from 'react';
|
|||
import { apiFetch } from '../../lib/apiFetch';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '@/store/store';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const { isAuthenticated, isDevMode } = useSelector(
|
||||
const searchParams = useSearchParams();
|
||||
const { isAuthenticated, isDevMode, devLoginEnabled, azureEnabled } = useSelector(
|
||||
(state: RootState) => state.auth
|
||||
);
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [error, setError] = useState(searchParams.get('error') || '');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -74,7 +75,7 @@ export default function LoginPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{!isDevMode && (
|
||||
{azureEnabled && (
|
||||
<button
|
||||
onClick={handleMicrosoftLogin}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#2F2F2F] text-white rounded-lg px-4 py-3 font-medium hover:bg-[#1a1a1a] transition-colors"
|
||||
|
|
@ -89,11 +90,22 @@ export default function LoginPage() {
|
|||
</button>
|
||||
)}
|
||||
|
||||
{isDevMode && (
|
||||
{devLoginEnabled && azureEnabled && (
|
||||
<div className="relative my-4">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-border" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-white px-2 text-muted-foreground">or</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{devLoginEnabled && (
|
||||
<form onSubmit={handleDevLogin} className="space-y-4">
|
||||
<div className="bg-[hsl(var(--warning)/0.08)] border border-[hsl(var(--warning)/0.3)] rounded-lg p-3 mb-4">
|
||||
<p className="text-xs text-[hsl(var(--warning))] font-medium">
|
||||
Development Mode — Azure AD not configured
|
||||
{isDevMode ? 'Development Mode — Azure AD not configured' : 'Dev login (temporary)'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,22 @@ export default function RootPage() {
|
|||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.replace("/dashboard");
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const code = params.get("code");
|
||||
const state = params.get("state");
|
||||
const error = params.get("error");
|
||||
const errorDescription = params.get("error_description");
|
||||
|
||||
if (code) {
|
||||
// Azure AD OAuth callback — forward to backend for code exchange
|
||||
let callbackUrl = `/api/v1/auth/callback?code=${encodeURIComponent(code)}`;
|
||||
if (state) callbackUrl += `&state=${encodeURIComponent(state)}`;
|
||||
window.location.href = callbackUrl;
|
||||
} else if (error) {
|
||||
router.replace(`/login?error=${encodeURIComponent(errorDescription || error)}`);
|
||||
} else {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ interface AuthState {
|
|||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
isDevMode: boolean;
|
||||
devLoginEnabled: boolean;
|
||||
azureEnabled: boolean;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
|
|
@ -21,6 +23,8 @@ const initialState: AuthState = {
|
|||
isLoading: true,
|
||||
isAuthenticated: false,
|
||||
isDevMode: false,
|
||||
devLoginEnabled: false,
|
||||
azureEnabled: false,
|
||||
};
|
||||
|
||||
export const fetchCurrentUser = createAsyncThunk(
|
||||
|
|
@ -48,11 +52,15 @@ export const checkDevMode = createAsyncThunk(
|
|||
const response = await apiFetch("/api/v1/auth/dev-status");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data.dev_mode ?? false;
|
||||
return {
|
||||
dev_mode: data.dev_mode ?? false,
|
||||
dev_login_enabled: data.dev_login_enabled ?? false,
|
||||
azure_enabled: data.azure_enabled ?? false,
|
||||
};
|
||||
}
|
||||
return false;
|
||||
return { dev_mode: false, dev_login_enabled: false, azure_enabled: false };
|
||||
} catch {
|
||||
return false;
|
||||
return { dev_mode: false, dev_login_enabled: false, azure_enabled: false };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -90,7 +98,9 @@ const authSlice = createSlice({
|
|||
state.isLoading = false;
|
||||
})
|
||||
.addCase(checkDevMode.fulfilled, (state, action) => {
|
||||
state.isDevMode = action.payload;
|
||||
state.isDevMode = action.payload.dev_mode;
|
||||
state.devLoginEnabled = action.payload.dev_login_enabled;
|
||||
state.azureEnabled = action.payload.azure_enabled;
|
||||
})
|
||||
.addCase(logoutUser.fulfilled, (state) => {
|
||||
state.user = null;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue