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 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<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/radix-vendor-CLtqm-Ae.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 {
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.)
* 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 {
const oktaDomain = oktaDomainOverride || ssoConfig.oktaDomain;
const oktaApiToken = oktaApiTokenOverride || ssoConfig.oktaApiToken;
// 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');
return null;
}
@ -48,17 +88,17 @@ export class AuthService {
// First attempt: Use email (preferred method as shown in curl example)
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)', {
endpoint: usersApiEndpoint.replace(email, email.substring(0, 5) + '...'),
hasApiToken: !!ssoConfig.oktaApiToken,
hasApiToken: !!oktaApiToken,
});
try {
const response = await axios.get(usersApiEndpoint, {
headers: {
'Authorization': `SSWS ${ssoConfig.oktaApiToken}`,
'Authorization': `SSWS ${oktaApiToken}`,
'Accept': 'application/json',
},
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
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)', {
endpoint: usersApiEndpoint.replace(oktaSub, oktaSub.substring(0, 10) + '...'),
hasApiToken: !!ssoConfig.oktaApiToken,
hasApiToken: !!oktaApiToken,
});
try {
const response = await axios.get(usersApiEndpoint, {
headers: {
'Authorization': `SSWS ${ssoConfig.oktaApiToken}`,
'Authorization': `SSWS ${oktaApiToken}`,
'Accept': 'application/json',
},
validateStatus: (status) => status < 500,
@ -269,6 +309,20 @@ export class AuthService {
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
// If firstName/lastName are missing, try to extract from displayName
let firstName = userData.firstName || '';
@ -294,13 +348,17 @@ export class AuthService {
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 lastLoginDevice = parseDeviceFromUserAgent(userAgent);
// Prepare update/create data - always include required fields
const userUpdateData: any = {
email: userData.email,
oktaSub: userData.oktaSub,
email: normalizedEmail,
oktaSub: normalizedOktaSub,
lastLogin: new Date(),
sessionToken,
lastLoginDevice,
@ -311,25 +369,36 @@ export class AuthService {
if (firstName) userUpdateData.firstName = firstName;
if (lastName) userUpdateData.lastName = lastName;
if (displayName) userUpdateData.displayName = displayName;
if (userData.employeeId) userUpdateData.employeeId = userData.employeeId; // Optional
if (userData.department) userUpdateData.department = userData.department;
if (userData.designation) userUpdateData.designation = userData.designation;
if (userData.phone) userUpdateData.phone = userData.phone;
if (userData.manager) userUpdateData.manager = userData.manager; // Manager name from SSO
if (userData.jobTitle) userUpdateData.jobTitle = userData.jobTitle; // Job title from SSO
if (userData.postalAddress) userUpdateData.postalAddress = userData.postalAddress; // Address from SSO
if (userData.mobilePhone) userUpdateData.mobilePhone = userData.mobilePhone; // Mobile phone from SSO
if (userData.employeeNumber || userData.dealerCode) {
userUpdateData.employeeNumber = userData.employeeNumber || userData.dealerCode;
if (limit(userData.employeeId, 50)) userUpdateData.employeeId = limit(userData.employeeId, 50); // Optional
if (limit(userData.department, 100)) userUpdateData.department = limit(userData.department, 100);
if (limit(userData.designation, 100)) userUpdateData.designation = limit(userData.designation, 100);
if (limit(userData.phone, 20)) userUpdateData.phone = limit(userData.phone, 20);
if (limit(userData.manager, 200)) userUpdateData.manager = limit(userData.manager, 200); // Manager name from SSO
if (limit(userData.jobTitle, 3000)) userUpdateData.jobTitle = limit(userData.jobTitle, 3000); // Job title from SSO
if (limit(userData.postalAddress, 500)) userUpdateData.postalAddress = limit(userData.postalAddress, 500); // Address from SSO
if (limit(userData.mobilePhone, 20)) userUpdateData.mobilePhone = limit(userData.mobilePhone, 20); // Mobile phone from SSO
if (limit(userData.secondEmail, 255)) userUpdateData.secondEmail = limit(userData.secondEmail, 255);
const employeeNumber = limit(userData.employeeNumber || userData.dealerCode, 50);
if (employeeNumber) {
userUpdateData.employeeNumber = employeeNumber;
}
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({
where: { email: userData.email }
where: { oktaSub: normalizedOktaSub }
});
if (!user) {
user = await User.findOne({
where: { email: normalizedEmail }
});
}
if (user) {
// Update existing user - update oktaSub if different, and other fields
@ -353,21 +422,22 @@ export class AuthService {
} else {
// Create new user with required fields (email and oktaSub)
user = await User.create({
email: userData.email,
oktaSub: userData.oktaSub,
email: normalizedEmail,
oktaSub: normalizedOktaSub,
employeeId: userData.employeeId || null, // Optional
firstName: firstName || null,
lastName: lastName || null,
displayName: displayName,
department: userData.department || null,
designation: userData.designation || null,
phone: userData.phone || null,
manager: userData.manager || null, // Manager name from SSO
jobTitle: userData.jobTitle || null, // Job title from SSO
postalAddress: userData.postalAddress || null, // Address from SSO
mobilePhone: userData.mobilePhone || null,
adGroups: userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0 ? userData.adGroups : null,
employeeNumber: userData.employeeNumber || userData.dealerCode || null,
department: limit(userData.department, 100) || null,
designation: limit(userData.designation, 100) || null,
phone: limit(userData.phone, 20) || null,
manager: limit(userData.manager, 200) || null, // Manager name from SSO
jobTitle: limit(userData.jobTitle, 3000) || null, // Job title from SSO
postalAddress: limit(userData.postalAddress, 500) || null, // Address from SSO
mobilePhone: limit(userData.mobilePhone, 20) || null,
secondEmail: limit(userData.secondEmail, 255) || 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,
role: 'USER',
lastLogin: new Date(),
@ -860,11 +930,12 @@ export class AuthService {
*/
async exchangeCodeForTokens(code: string, redirectUri: string, userAgent?: string): Promise<LoginResponse> {
try {
const oktaConfigForRequest = this.resolveOktaConfigForRedirectUri(redirectUri);
// 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.');
}
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.');
}
if (!code || code.trim() === '') {
@ -876,8 +947,27 @@ export class AuthService {
const normalize = (s: string) => s.trim().replace(/\/+$/, '');
const providedRedirectUri = normalize(redirectUri);
const frontendBase = process.env.FRONTEND_URL ? normalize(process.env.FRONTEND_URL) : '';
const canonicalRedirectUri = frontendBase ? `${frontendBase}/login/callback` : providedRedirectUri;
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;
@ -885,13 +975,16 @@ export class AuthService {
redirectUri: effectiveRedirectUri,
providedRedirectUri,
canonicalRedirectUri,
configuredFrontendBases,
selectedFrontendBase,
oktaProfile: oktaConfigForRequest.profile,
codePrefix: code.substring(0, 10) + '...',
oktaDomain: ssoConfig.oktaDomain,
clientId: ssoConfig.oktaClientId,
hasClientSecret: !!ssoConfig.oktaClientSecret && !ssoConfig.oktaClientSecret.includes('your_okta_client_secret'),
oktaDomain: oktaConfigForRequest.domain,
clientId: oktaConfigForRequest.clientId,
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
// redirect_uri here must match the one used when requesting the authorization code
@ -901,8 +994,8 @@ export class AuthService {
grant_type: 'authorization_code',
code,
redirect_uri: effectiveRedirectUri, // Must match authorize request redirect_uri exactly
client_id: ssoConfig.oktaClientId,
client_secret: ssoConfig.oktaClientSecret,
client_id: oktaConfigForRequest.clientId,
client_secret: oktaConfigForRequest.clientSecret,
}),
{
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.)
// 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, {
headers: {
Authorization: `Bearer ${access_token}`,
@ -973,7 +1066,13 @@ export class AuthService {
// Try Users API first (provides full profile including manager, employeeID, etc.)
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) {
userData = this.extractUserDataFromUsersAPI(usersApiResponse, oktaSub);
@ -1062,8 +1161,38 @@ export class AuthService {
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', {
redirectUri,
redirectUri: effectiveRedirectUri,
providedRedirectUri,
canonicalRedirectUri,
configuredFrontendBases,
selectedFrontendBase,
codePrefix: code.substring(0, 10) + '...',
tanflowBaseUrl: ssoConfig.tanflowBaseUrl,
clientId: ssoConfig.tanflowClientId,
@ -1078,7 +1207,7 @@ export class AuthService {
new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
redirect_uri: effectiveRedirectUri,
client_id: ssoConfig.tanflowClientId!,
client_secret: ssoConfig.tanflowClientSecret!,
}),