octa change to production sso

This commit is contained in:
Aaditya Jaiswal 2026-04-10 19:32:22 +05:30
parent 729a0d2d26
commit 80e28fb0eb
4 changed files with 197 additions and 68 deletions

View File

@ -1 +1 @@
import{a as s}from"./index-D495l21h.js";import"./radix-vendor-CLtqm-Ae.js";import"./charts-vendor-CmYZJIYl.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-DgwXkk2Y.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-HW_ujxKo.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion}; import{a as s}from"./index-CPucVe9U.js";import"./radix-vendor-CLtqm-Ae.js";import"./charts-vendor-CmYZJIYl.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-DgwXkk2Y.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-HW_ujxKo.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};

File diff suppressed because one or more lines are too long

View File

@ -13,7 +13,7 @@
<!-- Preload essential fonts and icons --> <!-- Preload essential fonts and icons -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script type="module" crossorigin src="/assets/index-D495l21h.js"></script> <script type="module" crossorigin src="/assets/index-CPucVe9U.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-CmYZJIYl.js"> <link rel="modulepreload" crossorigin href="/assets/charts-vendor-CmYZJIYl.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CLtqm-Ae.js"> <link rel="modulepreload" crossorigin href="/assets/radix-vendor-CLtqm-Ae.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js"> <link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">

View File

@ -30,14 +30,54 @@ function parseDeviceFromUserAgent(ua?: string): string {
export class AuthService { export class AuthService {
private resolveOktaConfigForRedirectUri(redirectUri: string): {
domain: string;
clientId: string;
clientSecret: string;
apiToken: string;
profile: 'localhost' | 'default';
} {
let host = '';
try {
host = new URL(redirectUri).hostname.toLowerCase();
} catch {
host = '';
}
const isLocalhostRedirect = host === 'localhost' || host === '127.0.0.1';
if (isLocalhostRedirect) {
return {
domain: process.env.OKTA_DOMAIN_LOCALHOST || ssoConfig.oktaDomain,
clientId: process.env.OKTA_CLIENT_ID_LOCALHOST || ssoConfig.oktaClientId,
clientSecret: process.env.OKTA_CLIENT_SECRET_LOCALHOST || ssoConfig.oktaClientSecret,
apiToken: process.env.OKTA_API_TOKEN_LOCALHOST || ssoConfig.oktaApiToken || '',
profile: 'localhost',
};
}
return {
domain: ssoConfig.oktaDomain,
clientId: ssoConfig.oktaClientId,
clientSecret: ssoConfig.oktaClientSecret,
apiToken: ssoConfig.oktaApiToken || '',
profile: 'default',
};
}
/** /**
* Fetch user details from Okta Users API (full profile with manager, employeeID, etc.) * Fetch user details from Okta Users API (full profile with manager, employeeID, etc.)
* Falls back to userinfo endpoint if Users API fails or token is not configured * Falls back to userinfo endpoint if Users API fails or token is not configured
*/ */
private async fetchUserFromOktaUsersAPI(oktaSub: string, email: string, accessToken: string): Promise<any> { private async fetchUserFromOktaUsersAPI(
oktaSub: string,
email: string,
accessToken: string,
oktaDomainOverride?: string,
oktaApiTokenOverride?: string
): Promise<any> {
try { try {
const oktaDomain = oktaDomainOverride || ssoConfig.oktaDomain;
const oktaApiToken = oktaApiTokenOverride || ssoConfig.oktaApiToken;
// Check if API token is configured // Check if API token is configured
if (!ssoConfig.oktaApiToken || ssoConfig.oktaApiToken.trim() === '') { if (!oktaApiToken || oktaApiToken.trim() === '') {
logger.info('OKTA_API_TOKEN not configured, will use userinfo endpoint as fallback'); logger.info('OKTA_API_TOKEN not configured, will use userinfo endpoint as fallback');
return null; return null;
} }
@ -48,17 +88,17 @@ export class AuthService {
// First attempt: Use email (preferred method as shown in curl example) // First attempt: Use email (preferred method as shown in curl example)
if (email) { if (email) {
const usersApiEndpoint = `${ssoConfig.oktaDomain}/api/v1/users/${encodeURIComponent(email)}`; const usersApiEndpoint = `${oktaDomain}/api/v1/users/${encodeURIComponent(email)}`;
logger.info('Fetching user from Okta Users API (using email)', { logger.info('Fetching user from Okta Users API (using email)', {
endpoint: usersApiEndpoint.replace(email, email.substring(0, 5) + '...'), endpoint: usersApiEndpoint.replace(email, email.substring(0, 5) + '...'),
hasApiToken: !!ssoConfig.oktaApiToken, hasApiToken: !!oktaApiToken,
}); });
try { try {
const response = await axios.get(usersApiEndpoint, { const response = await axios.get(usersApiEndpoint, {
headers: { headers: {
'Authorization': `SSWS ${ssoConfig.oktaApiToken}`, 'Authorization': `SSWS ${oktaApiToken}`,
'Accept': 'application/json', 'Accept': 'application/json',
}, },
validateStatus: (status) => status < 500, // Don't throw on 4xx errors validateStatus: (status) => status < 500, // Don't throw on 4xx errors
@ -81,17 +121,17 @@ export class AuthService {
// Second attempt: Use oktaSub (user ID) if email lookup failed // Second attempt: Use oktaSub (user ID) if email lookup failed
if (oktaSub) { if (oktaSub) {
const usersApiEndpoint = `${ssoConfig.oktaDomain}/api/v1/users/${encodeURIComponent(oktaSub)}`; const usersApiEndpoint = `${oktaDomain}/api/v1/users/${encodeURIComponent(oktaSub)}`;
logger.info('Fetching user from Okta Users API (using oktaSub)', { logger.info('Fetching user from Okta Users API (using oktaSub)', {
endpoint: usersApiEndpoint.replace(oktaSub, oktaSub.substring(0, 10) + '...'), endpoint: usersApiEndpoint.replace(oktaSub, oktaSub.substring(0, 10) + '...'),
hasApiToken: !!ssoConfig.oktaApiToken, hasApiToken: !!oktaApiToken,
}); });
try { try {
const response = await axios.get(usersApiEndpoint, { const response = await axios.get(usersApiEndpoint, {
headers: { headers: {
'Authorization': `SSWS ${ssoConfig.oktaApiToken}`, 'Authorization': `SSWS ${oktaApiToken}`,
'Accept': 'application/json', 'Accept': 'application/json',
}, },
validateStatus: (status) => status < 500, validateStatus: (status) => status < 500,
@ -269,6 +309,20 @@ export class AuthService {
throw new Error('Email and Okta sub are required'); throw new Error('Email and Okta sub are required');
} }
const norm = (value?: unknown): string | undefined => {
const s = String(value ?? '').trim();
return s ? s : undefined;
};
const limit = (value: unknown, max: number): string | undefined => {
const s = norm(value);
return s ? s.slice(0, max) : undefined;
};
const normalizedEmail = norm(userData.email)?.toLowerCase();
const normalizedOktaSub = norm(userData.oktaSub);
if (!normalizedEmail || !normalizedOktaSub) {
throw new Error('Email and Okta sub are required');
}
// Prepare user data with defaults for missing fields // Prepare user data with defaults for missing fields
// If firstName/lastName are missing, try to extract from displayName // If firstName/lastName are missing, try to extract from displayName
let firstName = userData.firstName || ''; let firstName = userData.firstName || '';
@ -294,13 +348,17 @@ export class AuthService {
displayName = userData.email.split('@')[0] || 'User'; displayName = userData.email.split('@')[0] || 'User';
} }
firstName = limit(firstName, 100) || '';
lastName = limit(lastName, 100) || '';
displayName = limit(displayName, 200) || normalizedEmail.split('@')[0] || 'User';
const sessionToken = uuidv4(); const sessionToken = uuidv4();
const lastLoginDevice = parseDeviceFromUserAgent(userAgent); const lastLoginDevice = parseDeviceFromUserAgent(userAgent);
// Prepare update/create data - always include required fields // Prepare update/create data - always include required fields
const userUpdateData: any = { const userUpdateData: any = {
email: userData.email, email: normalizedEmail,
oktaSub: userData.oktaSub, oktaSub: normalizedOktaSub,
lastLogin: new Date(), lastLogin: new Date(),
sessionToken, sessionToken,
lastLoginDevice, lastLoginDevice,
@ -311,25 +369,36 @@ export class AuthService {
if (firstName) userUpdateData.firstName = firstName; if (firstName) userUpdateData.firstName = firstName;
if (lastName) userUpdateData.lastName = lastName; if (lastName) userUpdateData.lastName = lastName;
if (displayName) userUpdateData.displayName = displayName; if (displayName) userUpdateData.displayName = displayName;
if (userData.employeeId) userUpdateData.employeeId = userData.employeeId; // Optional if (limit(userData.employeeId, 50)) userUpdateData.employeeId = limit(userData.employeeId, 50); // Optional
if (userData.department) userUpdateData.department = userData.department; if (limit(userData.department, 100)) userUpdateData.department = limit(userData.department, 100);
if (userData.designation) userUpdateData.designation = userData.designation; if (limit(userData.designation, 100)) userUpdateData.designation = limit(userData.designation, 100);
if (userData.phone) userUpdateData.phone = userData.phone; if (limit(userData.phone, 20)) userUpdateData.phone = limit(userData.phone, 20);
if (userData.manager) userUpdateData.manager = userData.manager; // Manager name from SSO if (limit(userData.manager, 200)) userUpdateData.manager = limit(userData.manager, 200); // Manager name from SSO
if (userData.jobTitle) userUpdateData.jobTitle = userData.jobTitle; // Job title from SSO if (limit(userData.jobTitle, 3000)) userUpdateData.jobTitle = limit(userData.jobTitle, 3000); // Job title from SSO
if (userData.postalAddress) userUpdateData.postalAddress = userData.postalAddress; // Address from SSO if (limit(userData.postalAddress, 500)) userUpdateData.postalAddress = limit(userData.postalAddress, 500); // Address from SSO
if (userData.mobilePhone) userUpdateData.mobilePhone = userData.mobilePhone; // Mobile phone from SSO if (limit(userData.mobilePhone, 20)) userUpdateData.mobilePhone = limit(userData.mobilePhone, 20); // Mobile phone from SSO
if (userData.employeeNumber || userData.dealerCode) { if (limit(userData.secondEmail, 255)) userUpdateData.secondEmail = limit(userData.secondEmail, 255);
userUpdateData.employeeNumber = userData.employeeNumber || userData.dealerCode; const employeeNumber = limit(userData.employeeNumber || userData.dealerCode, 50);
if (employeeNumber) {
userUpdateData.employeeNumber = employeeNumber;
} }
if (userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0) { if (userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0) {
userUpdateData.adGroups = userData.adGroups; // Group memberships from SSO userUpdateData.adGroups = userData.adGroups
.map((group: unknown) => limit(group, 255))
.filter(Boolean)
.slice(0, 200); // Group memberships from SSO
} }
// Check if user exists by email (primary identifier) // Prefer matching by oktaSub, then fallback to email.
// This avoids collisions when email changes in IdP over time.
let user = await User.findOne({ let user = await User.findOne({
where: { email: userData.email } where: { oktaSub: normalizedOktaSub }
}); });
if (!user) {
user = await User.findOne({
where: { email: normalizedEmail }
});
}
if (user) { if (user) {
// Update existing user - update oktaSub if different, and other fields // Update existing user - update oktaSub if different, and other fields
@ -353,21 +422,22 @@ export class AuthService {
} else { } else {
// Create new user with required fields (email and oktaSub) // Create new user with required fields (email and oktaSub)
user = await User.create({ user = await User.create({
email: userData.email, email: normalizedEmail,
oktaSub: userData.oktaSub, oktaSub: normalizedOktaSub,
employeeId: userData.employeeId || null, // Optional employeeId: userData.employeeId || null, // Optional
firstName: firstName || null, firstName: firstName || null,
lastName: lastName || null, lastName: lastName || null,
displayName: displayName, displayName: displayName,
department: userData.department || null, department: limit(userData.department, 100) || null,
designation: userData.designation || null, designation: limit(userData.designation, 100) || null,
phone: userData.phone || null, phone: limit(userData.phone, 20) || null,
manager: userData.manager || null, // Manager name from SSO manager: limit(userData.manager, 200) || null, // Manager name from SSO
jobTitle: userData.jobTitle || null, // Job title from SSO jobTitle: limit(userData.jobTitle, 3000) || null, // Job title from SSO
postalAddress: userData.postalAddress || null, // Address from SSO postalAddress: limit(userData.postalAddress, 500) || null, // Address from SSO
mobilePhone: userData.mobilePhone || null, mobilePhone: limit(userData.mobilePhone, 20) || null,
adGroups: userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0 ? userData.adGroups : null, secondEmail: limit(userData.secondEmail, 255) || null,
employeeNumber: userData.employeeNumber || userData.dealerCode || null, adGroups: userUpdateData.adGroups && Array.isArray(userUpdateData.adGroups) && userUpdateData.adGroups.length > 0 ? userUpdateData.adGroups : null,
employeeNumber: limit(userData.employeeNumber || userData.dealerCode, 50) || null,
isActive: true, isActive: true,
role: 'USER', role: 'USER',
lastLogin: new Date(), lastLogin: new Date(),
@ -860,11 +930,12 @@ export class AuthService {
*/ */
async exchangeCodeForTokens(code: string, redirectUri: string, userAgent?: string): Promise<LoginResponse> { async exchangeCodeForTokens(code: string, redirectUri: string, userAgent?: string): Promise<LoginResponse> {
try { try {
const oktaConfigForRequest = this.resolveOktaConfigForRedirectUri(redirectUri);
// Validate configuration // Validate configuration
if (!ssoConfig.oktaClientId || ssoConfig.oktaClientId.trim() === '') { if (!oktaConfigForRequest.clientId || oktaConfigForRequest.clientId.trim() === '') {
throw new Error('OKTA_CLIENT_ID is not configured. Please set it in your .env file.'); throw new Error('OKTA_CLIENT_ID is not configured. Please set it in your .env file.');
} }
if (!ssoConfig.oktaClientSecret || ssoConfig.oktaClientSecret.trim() === '' || ssoConfig.oktaClientSecret.includes('your_okta_client_secret')) { if (!oktaConfigForRequest.clientSecret || oktaConfigForRequest.clientSecret.trim() === '' || oktaConfigForRequest.clientSecret.includes('your_okta_client_secret')) {
throw new Error('OKTA_CLIENT_SECRET is not configured. Please set it in your .env file.'); throw new Error('OKTA_CLIENT_SECRET is not configured. Please set it in your .env file.');
} }
if (!code || code.trim() === '') { if (!code || code.trim() === '') {
@ -876,8 +947,27 @@ export class AuthService {
const normalize = (s: string) => s.trim().replace(/\/+$/, ''); const normalize = (s: string) => s.trim().replace(/\/+$/, '');
const providedRedirectUri = normalize(redirectUri); const providedRedirectUri = normalize(redirectUri);
const frontendBase = process.env.FRONTEND_URL ? normalize(process.env.FRONTEND_URL) : ''; const configuredFrontendBases = (process.env.FRONTEND_URL || '')
const canonicalRedirectUri = frontendBase ? `${frontendBase}/login/callback` : providedRedirectUri; .split(',')
.map((s) => normalize(s))
.filter(Boolean);
const providedOrigin = (() => {
try {
return normalize(new URL(providedRedirectUri).origin);
} catch {
return '';
}
})();
const matchingConfiguredBase = configuredFrontendBases.find((base) => {
try {
return normalize(new URL(base).origin).toLowerCase() === providedOrigin.toLowerCase();
} catch {
return false;
}
});
const fallbackConfiguredBase = configuredFrontendBases[0] || '';
const selectedFrontendBase = matchingConfiguredBase || fallbackConfiguredBase;
const canonicalRedirectUri = selectedFrontendBase ? `${selectedFrontendBase}/login/callback` : providedRedirectUri;
const isSecureEnv = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'uat'; const isSecureEnv = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'uat';
const effectiveRedirectUri = isSecureEnv ? canonicalRedirectUri : providedRedirectUri; const effectiveRedirectUri = isSecureEnv ? canonicalRedirectUri : providedRedirectUri;
@ -885,13 +975,16 @@ export class AuthService {
redirectUri: effectiveRedirectUri, redirectUri: effectiveRedirectUri,
providedRedirectUri, providedRedirectUri,
canonicalRedirectUri, canonicalRedirectUri,
configuredFrontendBases,
selectedFrontendBase,
oktaProfile: oktaConfigForRequest.profile,
codePrefix: code.substring(0, 10) + '...', codePrefix: code.substring(0, 10) + '...',
oktaDomain: ssoConfig.oktaDomain, oktaDomain: oktaConfigForRequest.domain,
clientId: ssoConfig.oktaClientId, clientId: oktaConfigForRequest.clientId,
hasClientSecret: !!ssoConfig.oktaClientSecret && !ssoConfig.oktaClientSecret.includes('your_okta_client_secret'), hasClientSecret: !!oktaConfigForRequest.clientSecret && !oktaConfigForRequest.clientSecret.includes('your_okta_client_secret'),
}); });
const tokenEndpoint = `${ssoConfig.oktaDomain}/oauth2/default/v1/token`; const tokenEndpoint = `${oktaConfigForRequest.domain}/oauth2/default/v1/token`;
// Exchange authorization code for tokens // Exchange authorization code for tokens
// redirect_uri here must match the one used when requesting the authorization code // redirect_uri here must match the one used when requesting the authorization code
@ -901,8 +994,8 @@ export class AuthService {
grant_type: 'authorization_code', grant_type: 'authorization_code',
code, code,
redirect_uri: effectiveRedirectUri, // Must match authorize request redirect_uri exactly redirect_uri: effectiveRedirectUri, // Must match authorize request redirect_uri exactly
client_id: ssoConfig.oktaClientId, client_id: oktaConfigForRequest.clientId,
client_secret: ssoConfig.oktaClientSecret, client_secret: oktaConfigForRequest.clientSecret,
}), }),
{ {
headers: { headers: {
@ -957,7 +1050,7 @@ export class AuthService {
// Step 1: Try to get user info from Okta Users API (full profile with manager, employeeID, etc.) // Step 1: Try to get user info from Okta Users API (full profile with manager, employeeID, etc.)
// First, get oktaSub from userinfo to use as user ID // First, get oktaSub from userinfo to use as user ID
const userInfoEndpoint = `${ssoConfig.oktaDomain}/oauth2/default/v1/userinfo`; const userInfoEndpoint = `${oktaConfigForRequest.domain}/oauth2/default/v1/userinfo`;
const userInfoResponse = await axios.get(userInfoEndpoint, { const userInfoResponse = await axios.get(userInfoEndpoint, {
headers: { headers: {
Authorization: `Bearer ${access_token}`, Authorization: `Bearer ${access_token}`,
@ -973,7 +1066,13 @@ export class AuthService {
// Try Users API first (provides full profile including manager, employeeID, etc.) // Try Users API first (provides full profile including manager, employeeID, etc.)
let userData: SSOUserData | null = null; let userData: SSOUserData | null = null;
const usersApiResponse = await this.fetchUserFromOktaUsersAPI(oktaSub, oktaUserInfo.email || '', access_token); const usersApiResponse = await this.fetchUserFromOktaUsersAPI(
oktaSub,
oktaUserInfo.email || '',
access_token,
oktaConfigForRequest.domain,
oktaConfigForRequest.apiToken
);
if (usersApiResponse) { if (usersApiResponse) {
userData = this.extractUserDataFromUsersAPI(usersApiResponse, oktaSub); userData = this.extractUserDataFromUsersAPI(usersApiResponse, oktaSub);
@ -1062,8 +1161,38 @@ export class AuthService {
throw new Error('Redirect URI is required'); throw new Error('Redirect URI is required');
} }
const normalize = (s: string) => s.trim().replace(/\/+$/, '');
const providedRedirectUri = normalize(redirectUri);
const configuredFrontendBases = (process.env.FRONTEND_URL || '')
.split(',')
.map((s) => normalize(s))
.filter(Boolean);
const providedOrigin = (() => {
try {
return normalize(new URL(providedRedirectUri).origin);
} catch {
return '';
}
})();
const matchingConfiguredBase = configuredFrontendBases.find((base) => {
try {
return normalize(new URL(base).origin).toLowerCase() === providedOrigin.toLowerCase();
} catch {
return false;
}
});
const fallbackConfiguredBase = configuredFrontendBases[0] || '';
const selectedFrontendBase = matchingConfiguredBase || fallbackConfiguredBase;
const canonicalRedirectUri = selectedFrontendBase ? `${selectedFrontendBase}/login/callback` : providedRedirectUri;
const isSecureEnv = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'uat';
const effectiveRedirectUri = isSecureEnv ? canonicalRedirectUri : providedRedirectUri;
logger.info('Exchanging code with Tanflow', { logger.info('Exchanging code with Tanflow', {
redirectUri, redirectUri: effectiveRedirectUri,
providedRedirectUri,
canonicalRedirectUri,
configuredFrontendBases,
selectedFrontendBase,
codePrefix: code.substring(0, 10) + '...', codePrefix: code.substring(0, 10) + '...',
tanflowBaseUrl: ssoConfig.tanflowBaseUrl, tanflowBaseUrl: ssoConfig.tanflowBaseUrl,
clientId: ssoConfig.tanflowClientId, clientId: ssoConfig.tanflowClientId,
@ -1078,7 +1207,7 @@ export class AuthService {
new URLSearchParams({ new URLSearchParams({
grant_type: 'authorization_code', grant_type: 'authorization_code',
code, code,
redirect_uri: redirectUri, redirect_uri: effectiveRedirectUri,
client_id: ssoConfig.tanflowClientId!, client_id: ssoConfig.tanflowClientId!,
client_secret: ssoConfig.tanflowClientSecret!, client_secret: ssoConfig.tanflowClientSecret!,
}), }),