Added partner app
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import { UserManager, WebStorageStateStore } from 'oidc-client-ts';
|
||||
import { getSecuritySettings, loadRuntimeConfig } from '../config';
|
||||
import { parseJwtClaims } from './jwt';
|
||||
import { syncUserAccount, clearUserSyncInBrowser, isActivationPending } from '../services/userSyncService';
|
||||
import { onSessionExpired, clearSessionAndRedirectToLanding } from '../services/sessionExpiration';
|
||||
|
||||
export interface AuthUserProfile {
|
||||
id: string;
|
||||
sub: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
tenant: string;
|
||||
tenant_id: number | string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: AuthUserProfile | null;
|
||||
sub: string;
|
||||
claims: Record<string, any>;
|
||||
accessToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
isPendingActivation: boolean;
|
||||
login: () => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Derives the user profile strictly from JWT token claims.
|
||||
* If name, email, tenant, or tenant_id are not present, returns null (user is signed out).
|
||||
*/
|
||||
export function deriveUserProfileFromTokenClaims(claims: Record<string, any>, rawSub?: string): AuthUserProfile | null {
|
||||
if (!claims || typeof claims !== 'object' || Object.keys(claims).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Derive name
|
||||
const name =
|
||||
claims.name ||
|
||||
claims.given_name ||
|
||||
claims.preferred_username ||
|
||||
claims.nickname ||
|
||||
(claims.first_name && claims.last_name ? `${claims.first_name} ${claims.last_name}` : undefined);
|
||||
|
||||
// Derive email
|
||||
const email =
|
||||
claims.email ||
|
||||
claims.upn ||
|
||||
claims.email_address ||
|
||||
claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'];
|
||||
|
||||
// Derive tenant (tenant name)
|
||||
const tenant =
|
||||
claims.tenant ||
|
||||
claims.tenant_name ||
|
||||
claims.tenantName ||
|
||||
claims.organisation ||
|
||||
claims.org ||
|
||||
claims.organization;
|
||||
|
||||
// Derive tenant_id
|
||||
const rawTenantId =
|
||||
claims.tenant_id ??
|
||||
claims.tenantId ??
|
||||
claims.tenantid ??
|
||||
claims.org_id ??
|
||||
claims.organisation_id ??
|
||||
claims.organization_id ??
|
||||
claims.tid;
|
||||
|
||||
// If any required property is missing, user is signed out
|
||||
if (!name || !email || !tenant || rawTenantId === undefined || rawTenantId === null || String(rawTenantId).trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sub = claims.sub || rawSub || String(rawTenantId);
|
||||
const role =
|
||||
claims.role ||
|
||||
(Array.isArray(claims.roles) ? claims.roles[0] : claims.roles) ||
|
||||
'Partner';
|
||||
|
||||
return {
|
||||
id: String(sub),
|
||||
sub: String(sub),
|
||||
name: String(name).trim(),
|
||||
email: String(email).trim(),
|
||||
role: String(role).trim(),
|
||||
tenant: String(tenant).trim(),
|
||||
tenant_id: typeof rawTenantId === 'number' ? rawTenantId : String(rawTenantId).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<AuthUserProfile | null>(null);
|
||||
const [accessToken, setAccessToken] = useState<string | null>(() => {
|
||||
return typeof window !== 'undefined' ? localStorage.getItem('pfm_access_token') : null;
|
||||
});
|
||||
const [claims, setClaims] = useState<Record<string, any>>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [userManager, setUserManager] = useState<UserManager | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
async function initOidc() {
|
||||
try {
|
||||
await loadRuntimeConfig();
|
||||
const sec = getSecuritySettings();
|
||||
const authorityUrl = sec.Authority.replace(/\/+$/, '');
|
||||
|
||||
const mgr = new UserManager({
|
||||
authority: authorityUrl,
|
||||
client_id: sec.ClientId,
|
||||
client_secret: sec.ClientSecret,
|
||||
redirect_uri: window.location.origin,
|
||||
post_logout_redirect_uri: window.location.origin,
|
||||
response_type: 'code',
|
||||
scope: sec.Scope || 'openid profile email roles pfm-api-dev',
|
||||
userStore: new WebStorageStateStore({ store: window.localStorage }),
|
||||
metadata: {
|
||||
issuer: authorityUrl,
|
||||
authorization_endpoint: `${authorityUrl}/connect/authorize`,
|
||||
token_endpoint: `${authorityUrl}/connect/token`,
|
||||
userinfo_endpoint: `${authorityUrl}/connect/userinfo`,
|
||||
end_session_endpoint: `${authorityUrl}/connect/endsession`,
|
||||
jwks_uri: `${authorityUrl}/.well-known/openid-configuration/jwks`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!active) return;
|
||||
setUserManager(mgr);
|
||||
|
||||
// 1. Process OIDC redirect code if returning from STS
|
||||
if (window.location.search.includes('code=')) {
|
||||
try {
|
||||
const oidcUser = await mgr.signinCallback();
|
||||
if (oidcUser && oidcUser.access_token) {
|
||||
const tokenClaims = parseJwtClaims(oidcUser.access_token);
|
||||
const combinedClaims = { ...tokenClaims, ...(oidcUser.profile || {}) };
|
||||
const derivedProfile = deriveUserProfileFromTokenClaims(combinedClaims, oidcUser.profile?.sub);
|
||||
|
||||
if (derivedProfile) {
|
||||
localStorage.setItem('pfm_access_token', oidcUser.access_token);
|
||||
localStorage.setItem('pfm_user_sub', derivedProfile.sub);
|
||||
localStorage.setItem('pfm_auth_user', JSON.stringify(derivedProfile));
|
||||
localStorage.setItem('pfm_user_claims', JSON.stringify(combinedClaims));
|
||||
|
||||
setAccessToken(oidcUser.access_token);
|
||||
setClaims(combinedClaims);
|
||||
setUser(derivedProfile);
|
||||
|
||||
try {
|
||||
await syncUserAccount(derivedProfile.sub, oidcUser.access_token, derivedProfile);
|
||||
} catch {}
|
||||
} else {
|
||||
clearUserSyncInBrowser();
|
||||
localStorage.removeItem('pfm_access_token');
|
||||
localStorage.removeItem('pfm_auth_user');
|
||||
setAccessToken(null);
|
||||
setUser(null);
|
||||
}
|
||||
}
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
} catch (err) {
|
||||
console.error('[Auth] Callback error:', err);
|
||||
}
|
||||
} else {
|
||||
// 2. Evaluate existing stored token
|
||||
const storedToken = localStorage.getItem('pfm_access_token');
|
||||
if (storedToken) {
|
||||
const tokenClaims = parseJwtClaims(storedToken);
|
||||
const derivedProfile = deriveUserProfileFromTokenClaims(tokenClaims);
|
||||
|
||||
if (derivedProfile) {
|
||||
setAccessToken(storedToken);
|
||||
setClaims(tokenClaims);
|
||||
setUser(derivedProfile);
|
||||
localStorage.setItem('pfm_auth_user', JSON.stringify(derivedProfile));
|
||||
} else {
|
||||
clearUserSyncInBrowser();
|
||||
localStorage.removeItem('pfm_access_token');
|
||||
localStorage.removeItem('pfm_auth_user');
|
||||
setAccessToken(null);
|
||||
setUser(null);
|
||||
}
|
||||
} else {
|
||||
setUser(null);
|
||||
setAccessToken(null);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[Auth] OIDC initialization notice:', err);
|
||||
setUser(null);
|
||||
setAccessToken(null);
|
||||
} finally {
|
||||
if (active) setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
initOidc();
|
||||
const unsubSession = onSessionExpired(() => clearSessionAndRedirectToLanding());
|
||||
return () => {
|
||||
active = false;
|
||||
unsubSession();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const login = async () => {
|
||||
if (userManager) {
|
||||
try {
|
||||
await userManager.signinRedirect();
|
||||
} catch (err) {
|
||||
console.error('[Auth] Redirect error:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
clearUserSyncInBrowser();
|
||||
clearSessionAndRedirectToLanding();
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
sub: user?.sub || '',
|
||||
claims,
|
||||
accessToken,
|
||||
isAuthenticated: !!user && !!accessToken,
|
||||
isLoading,
|
||||
isPendingActivation: isActivationPending(),
|
||||
login,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
||||
return ctx;
|
||||
};
|
||||
Reference in New Issue
Block a user