Add authentication, user management, and database migration features

- Implement OAuth 2.0 and PAT authentication methods
- Add user management, roles, and profile functionality
- Add database migrations and admin user scripts
- Update services for authentication and user settings
- Add protected routes and permission hooks
- Update documentation for authentication and database access
This commit is contained in:
2026-01-15 03:20:50 +01:00
parent f3637b85e1
commit 1fa424efb9
70 changed files with 15597 additions and 2098 deletions

View File

@@ -48,7 +48,9 @@ import { calculateRequiredEffortWithMinMax } from './effortCalculation.js';
import { calculateApplicationCompleteness } from './dataCompletenessConfig.js';
// Determine if we should use real Jira Assets or mock data
const useJiraAssets = !!(config.jiraPat && config.jiraSchemaId);
// Jira PAT is now configured per-user, so we check if schema is configured
// The actual PAT is provided per-request via middleware
const useJiraAssets = !!config.jiraSchemaId;
if (useJiraAssets) {
logger.info('DataService: Using CMDB cache layer with Jira Assets API');
@@ -121,9 +123,40 @@ async function lookupReferences<T extends CMDBObject>(
/**
* Convert ObjectReference to ReferenceValue format used by frontend
* Try to enrich with description from jiraAssetsService cache if available
* If not in cache or cache entry has no description, fetch it async
*/
function toReferenceValue(ref: ObjectReference | null | undefined): ReferenceValue | null {
async function toReferenceValue(ref: ObjectReference | null | undefined): Promise<ReferenceValue | null> {
if (!ref) return null;
// Try to get enriched ReferenceValue from jiraAssetsService cache (includes description if available)
const enriched = useJiraAssets ? jiraAssetsService.getEnrichedReferenceValue(ref.objectKey, ref.objectId) : null;
if (enriched && enriched.description) {
// Use enriched value with description
return enriched;
}
// Cache miss or no description - fetch it async if using Jira Assets
if (useJiraAssets && enriched && !enriched.description) {
// We have a cached value but it lacks description - fetch it
const fetched = await jiraAssetsService.fetchEnrichedReferenceValue(ref.objectKey, ref.objectId);
if (fetched) {
return fetched;
}
// If fetch failed, return the cached value anyway
return enriched;
}
if (useJiraAssets) {
// Cache miss - fetch it
const fetched = await jiraAssetsService.fetchEnrichedReferenceValue(ref.objectKey, ref.objectId);
if (fetched) {
return fetched;
}
}
// Fallback to basic conversion without description (if fetch failed or not using Jira Assets)
return {
objectId: ref.objectId,
key: ref.objectKey,
@@ -188,25 +221,49 @@ function extractDisplayValue(value: unknown): string | null {
* References are now stored as ObjectReference objects directly (not IDs)
*/
async function toApplicationDetails(app: ApplicationComponent): Promise<ApplicationDetails> {
// Debug logging for confluenceSpace from cache
logger.info(`[toApplicationDetails] Converting cached object ${app.objectKey || app.id} to ApplicationDetails`);
logger.info(`[toApplicationDetails] confluenceSpace from cache: ${app.confluenceSpace} (type: ${typeof app.confluenceSpace})`);
// Handle confluenceSpace - it can be a string (URL) or number (legacy), convert to string
const confluenceSpaceValue = app.confluenceSpace !== null && app.confluenceSpace !== undefined
? (typeof app.confluenceSpace === 'string' ? app.confluenceSpace : String(app.confluenceSpace))
: null;
// Ensure factor caches are loaded for factor value lookup
await ensureFactorCaches();
// Convert ObjectReference to ReferenceValue format
const governanceModel = toReferenceValue(app.ictGovernanceModel);
// Note: applicationManagementSubteam and applicationManagementTeam are not in the generated schema
// They are only available when fetching directly from Jira API (via jiraAssetsClient)
const applicationSubteam = toReferenceValue((app as any).applicationManagementSubteam);
const applicationTeam = toReferenceValue((app as any).applicationManagementTeam);
const applicationType = toReferenceValue(app.applicationManagementApplicationType);
const applicationManagementHosting = toReferenceValue(app.applicationManagementHosting);
const applicationManagementTAM = toReferenceValue(app.applicationManagementTAM);
const hostingType = toReferenceValue(app.applicationComponentHostingType);
const businessImpactAnalyse = toReferenceValue(app.businessImpactAnalyse);
const platform = toReferenceValue(app.platform);
const organisation = toReferenceValue(app.organisation);
const businessImportance = toReferenceValue(app.businessImportance);
// Fetch descriptions async if not in cache
// Use Promise.all to fetch all reference values in parallel for better performance
const [
governanceModel,
applicationSubteam,
applicationTeam,
applicationType,
applicationManagementHosting,
applicationManagementTAM,
hostingType,
businessImpactAnalyse,
platform,
organisation,
businessImportance,
] = await Promise.all([
toReferenceValue(app.ictGovernanceModel),
toReferenceValue((app as any).applicationManagementSubteam),
toReferenceValue((app as any).applicationManagementTeam),
toReferenceValue(app.applicationManagementApplicationType),
toReferenceValue(app.applicationManagementHosting),
toReferenceValue(app.applicationManagementTAM),
toReferenceValue(app.applicationComponentHostingType),
toReferenceValue(app.businessImpactAnalyse),
toReferenceValue(app.platform),
toReferenceValue(app.organisation),
toReferenceValue(app.businessImportance),
]);
// Look up factor values from cached factor objects (same as toMinimalDetailsForEffort)
// Also include descriptions from cache if available
let dynamicsFactor: ReferenceValue | null = null;
if (app.applicationManagementDynamicsFactor && typeof app.applicationManagementDynamicsFactor === 'object') {
const factorObj = dynamicsFactorCache?.get(app.applicationManagementDynamicsFactor.objectId);
@@ -215,6 +272,7 @@ async function toApplicationDetails(app: ApplicationComponent): Promise<Applicat
key: app.applicationManagementDynamicsFactor.objectKey,
name: app.applicationManagementDynamicsFactor.label,
factor: factorObj?.factor ?? undefined,
description: factorObj?.description ?? undefined, // Include description from cache
};
}
@@ -226,6 +284,7 @@ async function toApplicationDetails(app: ApplicationComponent): Promise<Applicat
key: app.applicationManagementComplexityFactor.objectKey,
name: app.applicationManagementComplexityFactor.label,
factor: factorObj?.factor ?? undefined,
description: factorObj?.description ?? undefined, // Include description from cache
};
}
@@ -237,6 +296,7 @@ async function toApplicationDetails(app: ApplicationComponent): Promise<Applicat
key: app.applicationManagementNumberOfUsers.objectKey,
name: app.applicationManagementNumberOfUsers.label,
factor: factorObj?.factor ?? undefined,
description: (factorObj as any)?.description ?? undefined, // Include description from cache (may not be in generated types)
};
}
@@ -286,6 +346,12 @@ async function toApplicationDetails(app: ApplicationComponent): Promise<Applicat
// Override
overrideFTE: app.applicationManagementOverrideFTE ?? null,
requiredEffortApplicationManagement: null,
// Enterprise Architect reference
reference: app.reference || null,
// Confluence Space (URL string)
confluenceSpace: confluenceSpaceValue,
};
// Calculate data completeness percentage
@@ -356,6 +422,12 @@ async function toApplicationDetails(app: ApplicationComponent): Promise<Applicat
overrideFTE: app.applicationManagementOverrideFTE ?? null,
requiredEffortApplicationManagement: null,
dataCompletenessPercentage: Math.round(completenessPercentage * 10) / 10, // Round to 1 decimal
// Enterprise Architect reference
reference: app.reference || null,
// Confluence Space (URL string)
confluenceSpace: confluenceSpaceValue,
};
}
@@ -397,11 +469,18 @@ function clearFactorCaches(): void {
* This avoids the overhead of toApplicationDetails while providing enough data for effort calculation
* Note: ensureFactorCaches() must be called before using this function
*/
function toMinimalDetailsForEffort(app: ApplicationComponent): ApplicationDetails {
const governanceModel = toReferenceValue(app.ictGovernanceModel);
const applicationType = toReferenceValue(app.applicationManagementApplicationType);
const businessImpactAnalyse = toReferenceValue(app.businessImpactAnalyse);
const applicationManagementHosting = toReferenceValue(app.applicationManagementHosting);
async function toMinimalDetailsForEffort(app: ApplicationComponent): Promise<ApplicationDetails> {
const [
governanceModel,
applicationType,
businessImpactAnalyse,
applicationManagementHosting,
] = await Promise.all([
toReferenceValue(app.ictGovernanceModel),
toReferenceValue(app.applicationManagementApplicationType),
toReferenceValue(app.businessImpactAnalyse),
toReferenceValue(app.applicationManagementHosting),
]);
// Look up factor values from cached factor objects
let dynamicsFactor: ReferenceValue | null = null;
@@ -434,6 +513,7 @@ function toMinimalDetailsForEffort(app: ApplicationComponent): ApplicationDetail
key: app.applicationManagementNumberOfUsers.objectKey,
name: app.applicationManagementNumberOfUsers.label,
factor: factorObj?.factor ?? undefined,
description: (factorObj as any)?.description ?? undefined, // Include description from cache (may not be in generated types)
};
}
@@ -474,23 +554,38 @@ function toMinimalDetailsForEffort(app: ApplicationComponent): ApplicationDetail
/**
* Convert ApplicationComponent to ApplicationListItem (lighter weight, for lists)
*/
function toApplicationListItem(app: ApplicationComponent): ApplicationListItem {
async function toApplicationListItem(app: ApplicationComponent): Promise<ApplicationListItem> {
// Use direct ObjectReference conversion instead of lookups
const governanceModel = toReferenceValue(app.ictGovernanceModel);
const dynamicsFactor = toReferenceValue(app.applicationManagementDynamicsFactor);
const complexityFactor = toReferenceValue(app.applicationManagementComplexityFactor);
// Note: Team/Subteam fields are not in generated schema, use type assertion
const applicationSubteam = toReferenceValue((app as any).applicationManagementSubteam);
const applicationTeam = toReferenceValue((app as any).applicationManagementTeam);
const applicationType = toReferenceValue(app.applicationManagementApplicationType);
const platform = toReferenceValue(app.platform);
// Fetch all reference values in parallel
const [
governanceModel,
dynamicsFactor,
complexityFactor,
applicationSubteam,
applicationTeam,
applicationType,
platform,
applicationManagementHosting,
applicationManagementTAM,
businessImpactAnalyse,
minimalDetails,
] = await Promise.all([
toReferenceValue(app.ictGovernanceModel),
toReferenceValue(app.applicationManagementDynamicsFactor),
toReferenceValue(app.applicationManagementComplexityFactor),
toReferenceValue((app as any).applicationManagementSubteam),
toReferenceValue((app as any).applicationManagementTeam),
toReferenceValue(app.applicationManagementApplicationType),
toReferenceValue(app.platform),
toReferenceValue(app.applicationManagementHosting),
toReferenceValue(app.applicationManagementTAM),
toReferenceValue(app.businessImpactAnalyse),
toMinimalDetailsForEffort(app),
]);
const applicationFunctions = toReferenceValues(app.applicationFunction);
const applicationManagementHosting = toReferenceValue(app.applicationManagementHosting);
const applicationManagementTAM = toReferenceValue(app.applicationManagementTAM);
const businessImpactAnalyse = toReferenceValue(app.businessImpactAnalyse);
// Calculate effort using minimal details
const minimalDetails = toMinimalDetailsForEffort(app);
const effortResult = calculateRequiredEffortWithMinMax(minimalDetails);
const result: ApplicationListItem = {
@@ -518,12 +613,17 @@ function toApplicationListItem(app: ApplicationComponent): ApplicationListItem {
// Calculate data completeness percentage
// Convert ApplicationListItem to format expected by completeness calculator
const [organisationRef, hostingTypeRef] = await Promise.all([
toReferenceValue(app.organisation),
toReferenceValue(app.applicationComponentHostingType),
]);
const appForCompleteness = {
organisation: toReferenceValue(app.organisation)?.name || null,
organisation: organisationRef?.name || null,
applicationFunctions: result.applicationFunctions,
status: result.status,
businessImpactAnalyse: result.businessImpactAnalyse,
hostingType: toReferenceValue(app.applicationComponentHostingType),
hostingType: hostingTypeRef,
supplierProduct: app.supplierProduct?.label || null,
businessOwner: app.businessOwner?.label || null,
systemOwner: app.systemOwner?.label || null,
@@ -535,7 +635,7 @@ function toApplicationListItem(app: ApplicationComponent): ApplicationListItem {
applicationManagementTAM: result.applicationManagementTAM,
dynamicsFactor: result.dynamicsFactor,
complexityFactor: result.complexityFactor,
numberOfUsers: toReferenceValue(app.applicationManagementNumberOfUsers),
numberOfUsers: await toReferenceValue(app.applicationManagementNumberOfUsers),
};
const completenessPercentage = calculateApplicationCompleteness(appForCompleteness);
@@ -718,8 +818,8 @@ export const dataService = {
// Ensure factor caches are loaded for effort calculation
await ensureFactorCaches();
// Convert to list items (synchronous now)
const applications = paginatedApps.map(toApplicationListItem);
// Convert to list items (async now to fetch descriptions)
const applications = await Promise.all(paginatedApps.map(toApplicationListItem));
return {
applications,
@@ -1221,8 +1321,8 @@ export const dataService = {
for (const app of apps) {
// Get team from application (via subteam lookup if needed)
let team: ReferenceValue | null = null;
const applicationSubteam = toReferenceValue((app as any).applicationManagementSubteam);
const applicationTeam = toReferenceValue((app as any).applicationManagementTeam);
const applicationSubteam = await toReferenceValue((app as any).applicationManagementSubteam);
const applicationTeam = await toReferenceValue((app as any).applicationManagementTeam);
// Prefer direct team assignment, otherwise try to get from subteam
if (applicationTeam) {
@@ -1265,7 +1365,7 @@ export const dataService = {
// Get BIA value
if (app.businessImpactAnalyse) {
const biaRef = toReferenceValue(app.businessImpactAnalyse);
const biaRef = await toReferenceValue(app.businessImpactAnalyse);
if (biaRef) {
const biaNum = biaToNumeric(biaRef.name);
if (biaNum !== null) metrics.biaValues.push(biaNum);
@@ -1274,7 +1374,7 @@ export const dataService = {
// Get governance maturity
if (app.ictGovernanceModel) {
const govRef = toReferenceValue(app.ictGovernanceModel);
const govRef = await toReferenceValue(app.ictGovernanceModel);
if (govRef) {
const maturity = governanceToMaturity(govRef.name);
if (maturity !== null) metrics.governanceValues.push(maturity);
@@ -1327,6 +1427,10 @@ export const dataService = {
async testConnection(): Promise<boolean> {
if (!useJiraAssets) return true;
// Only test connection if token is configured
if (!jiraAssetsClient.hasToken()) {
return false;
}
return jiraAssetsClient.testConnection();
},
@@ -1413,7 +1517,7 @@ export const dataService = {
if (!app.id || !app.label) continue;
// Extract Business Importance from app object
const businessImportanceRef = toReferenceValue(app.businessImportance);
const businessImportanceRef = await toReferenceValue(app.businessImportance);
const businessImportanceName = businessImportanceRef?.name || null;
// Normalize Business Importance
@@ -1436,7 +1540,7 @@ export const dataService = {
}
// Extract BIA from app object
const businessImpactAnalyseRef = toReferenceValue(app.businessImpactAnalyse);
const businessImpactAnalyseRef = await toReferenceValue(app.businessImpactAnalyse);
// Normalize BIA Class
let biaClass: string | null = null;