Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6fa20c7
feat: check if system has creds saved, add new messages, extend backe…
mikicvi-SAP Oct 10, 2025
7139bf6
feat: fix translation path for info message
mikicvi-SAP Oct 10, 2025
41d4f6f
feat: remove temporaryCredentials handling from backend system logic
mikicvi-SAP Oct 10, 2025
4764c2f
feat: add warning message to the password prompt
mikicvi-SAP Oct 10, 2025
1770221
Merge branch 'main' into feat/3718/prevent-overwriting-saved-system-w…
mikicvi-SAP Oct 17, 2025
69d3306
feat: address translations feedback
mikicvi-SAP Oct 17, 2025
c32a0a9
feat: extend local type, load systems from store in helpers with cred…
mikicvi-SAP Oct 21, 2025
6ed8b5d
Merge remote-tracking branch 'origin/main' into feat/3718/prevent-ove…
mikicvi-SAP Oct 21, 2025
45e7ed5
Linting auto fix commit
github-actions[bot] Oct 21, 2025
d7e7e17
feat: unit tests for questions
mikicvi-SAP Oct 21, 2025
6c97706
feat: refactor backend system handling and credential management in p…
mikicvi-SAP Oct 23, 2025
fbe4872
Merge remote-tracking branch 'origin/main' into feat/3718/prevent-ove…
mikicvi-SAP Oct 23, 2025
8a6a04a
feat: changeset, remove unnecessary test
mikicvi-SAP Oct 23, 2025
de02eb7
feat: fix code smells
mikicvi-SAP Oct 23, 2025
7960995
Linting auto fix commit
github-actions[bot] Oct 23, 2025
ff7ebc4
feat: add prompt to confirm saving the system instead, revert some me…
mikicvi-SAP Oct 28, 2025
39d54a7
Merge branch 'main' into feat/3718/prevent-overwriting-saved-system-w…
mikicvi-SAP Oct 28, 2025
44c4609
Linting auto fix commit
github-actions[bot] Oct 28, 2025
6d4bda6
feat: tests, edit changeset
mikicvi-SAP Oct 28, 2025
e5b73ec
feat: address feedback, dont prefill username, save new sys w/o creds…
mikicvi-SAP Oct 29, 2025
ae19597
Merge branch 'main' into feat/3718/prevent-overwriting-saved-system-w…
mikicvi-SAP Oct 29, 2025
57b10b5
feat: fix lint issue
mikicvi-SAP Oct 29, 2025
c2779b6
Merge branch 'main' into feat/3718/prevent-overwriting-saved-system-w…
mikicvi-SAP Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion packages/fiori-app-sub-generator/src/fiori-app-generator/end.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export async function runPostGenerationTasks(
service: {
backendSystem?: BackendSystem & {
newOrUpdated?: boolean;
temporaryCredentials?: boolean;
};
capService?: CapService;
sapClient?: string;
Expand Down Expand Up @@ -141,14 +142,25 @@ export async function runPostGenerationTasks(
// Persist backend system connection information
const hostEnv = getHostEnvironment();

if (service.backendSystem && hostEnv !== hostEnvironment.bas && service.backendSystem.newOrUpdated) {
if (
service.backendSystem &&
hostEnv !== hostEnvironment.bas &&
service.backendSystem.newOrUpdated &&
!service.backendSystem.temporaryCredentials
) {
const storeService = await getService<BackendSystem, BackendSystemKey>({
logger: logger,
entityName: 'system'
});
// No need to await, we cannot recover anyway
// eslint-disable-next-line @typescript-eslint/no-floating-promises
storeService.write(service.backendSystem, { force: true });
} else if (
service.backendSystem?.newOrUpdated === false &&
hostEnv !== hostEnvironment.bas &&
service.backendSystem.temporaryCredentials
) {
logger.info(t('logMessages.noCredentialsBackendSystem', { system: service.backendSystem.name }));
}

// Display info message if using a cap service as it is not otherwise shown when a top level dir is not created
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@
"warningCachingNotSupported": "Warning: caching is not supported.",
"attemptingToExecutePostGenerationCommand": "Attempting to execute command after app generation: {{- command}}",
"installSkippedOptionSpecified": "Option `--skipInstall` was specified. Installation of dependencies will be skipped.",
"generatorExiting": "Application generation exiting due to error: {{error}}"
"generatorExiting": "Application generation was cancelled due to the error: {{error}}.",
"noCredentialsBackendSystem": "No credentials were found for the back-end system: {{system}}. Skipping storing credentials."
},
"error": {
"fatalError": "An error has occurred, please restart the generator.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,55 @@ describe('runPostGenerationTasks', () => {
expect(storeServiceWriteMock).toHaveBeenCalledWith(service.backendSystem, { force: true });
});

it('should NOT persist backend system when newOrUpdated is false', async () => {
const service = {
backendSystem: {
newOrUpdated: false
} as unknown as BackendSystem,
sapClient: '100',
odataVersion: OdataVersion.v2,
datasourceType: DatasourceType.sapSystem
};
const project = {
targetFolder: '/path/to/project',
name: 'testProject',
flpAppId: 'testAppId'
};

(getHostEnvironment as jest.Mock).mockReturnValue(hostEnvironment.vscode);

await runPostGenerationTasks({ service, project }, fs, logger, vscode, appWizard);

// Should not call getService or write when newOrUpdated is false
expect(getService).not.toHaveBeenCalled();
expect(storeServiceWriteMock).not.toHaveBeenCalled();
});

it('should NOT persist backend system with temporary credentials', async () => {
const service = {
backendSystem: {
newOrUpdated: false,
temporaryCredentials: true
} as unknown as BackendSystem,
sapClient: '100',
odataVersion: OdataVersion.v2,
datasourceType: DatasourceType.sapSystem
};
const project = {
targetFolder: '/path/to/project',
name: 'testProject',
flpAppId: 'testAppId'
};

(getHostEnvironment as jest.Mock).mockReturnValue(hostEnvironment.vscode);

await runPostGenerationTasks({ service, project }, fs, logger, vscode, appWizard);

// Should not call getService or write when temporaryCredentials is true
expect(getService).not.toHaveBeenCalled();
expect(storeServiceWriteMock).not.toHaveBeenCalled();
});

it('should show information message for cap projects', async () => {
const service = {
capService: {} as unknown as CapService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { promptNames } from '../../../../types';
import { PromptState, removeCircularFromServiceProvider } from '../../../../utils';
import type { ConnectionValidator } from '../../../connectionValidator';
import type { ValidationResult } from '../../../types';
import type { SystemSelectionAnswerType } from '../system-selection/prompt-helpers';
import type { SystemSelectionAnswerType, BackendSystemSelection } from '../system-selection/prompt-helpers';
import type { NewSystemAnswers } from '../new-system/types';

export enum BasicCredentialsPromptNames {
Expand Down Expand Up @@ -53,7 +53,15 @@ export function getCredentialsPrompts<T extends Answers>(
guiOptions: {
mandatory: true
},
default: '',
default: (answers: T) => {
// Prefill username from the selected backend system if available
const selectedSystem = answers?.[promptNames.systemSelection] as SystemSelectionAnswerType;
if (selectedSystem?.type === 'backendSystem') {
const backendSystem = selectedSystem.system as BackendSystemSelection;
return backendSystem.username || '';
}
return '';
},
validate: (user: string) => user?.length > 0
} as InputQuestion<T>,
{
Expand Down Expand Up @@ -112,6 +120,13 @@ export function getCredentialsPrompts<T extends Answers>(
return valResult;
},
additionalMessages: (password: string, answers: T) => {
if (PromptState.odataService.connectedSystem?.backendSystem?.newOrUpdated) {
return {
message: t('texts.passwordStoreWarning'),
severity: Severity.information
};
}

// Since the odata service URL prompt has its own credentials prompts its safe to assume
// that `ignoreCertError` when true means that the user has set the node setting to ignore cert errors and
// not that the user has chosen to ignore the cert error for this specific connection (this is only supported by the odata service URL prompts).
Expand Down Expand Up @@ -149,14 +164,21 @@ function updatePromptStateWithConnectedSystem(
};
// Update the existing backend system with the new credentials that may be used to update in the store.
if (selectedSystem?.type === 'backendSystem') {
const backendSystem = selectedSystem.system as BackendSystem;
const backendSystem = selectedSystem.system as BackendSystemSelection;

// Have the credentials changed..
if (backendSystem.username !== username || backendSystem.password !== password) {
// Determine if this is a temporary credentials scenario or an update scenario
const isTemporaryCredentials = !backendSystem.hasStoredCredentials;

PromptState.odataService.connectedSystem.backendSystem = Object.assign(backendSystem, {
username: username,
password,
userDisplayName: username,
newOrUpdated: true
// Only set newOrUpdated to true if the system had existing credentials (auth error scenario)
// For systems without credentials, set temporaryCredentials flag instead
newOrUpdated: !isTemporaryCredentials,
temporaryCredentials: isTemporaryCredentials
} as Partial<BackendSystem>);
}
// If the connection is successful and a destination was selected, assign the connected destination to the prompt state.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@sap-ux/btp-utils';
import { ERROR_TYPE } from '@sap-ux/inquirer-common';
import type { OdataVersion } from '@sap-ux/odata-service-writer';
import { type BackendSystemKey, type BackendSystem, SystemService } from '@sap-ux/store';
import { BackendSystemKey, type BackendSystem, SystemService } from '@sap-ux/store';
import type { ListChoiceOptions } from 'inquirer';
import { t } from '../../../../i18n';
import type { ConnectedSystem, DestinationFilters } from '../../../../types';
Expand All @@ -27,9 +27,14 @@ export type NewSystemChoice = typeof NewSystemChoice;
export const CfAbapEnvServiceChoice = 'cfAbapEnvService';
export type CfAbapEnvServiceChoice = typeof CfAbapEnvServiceChoice;

// Local extension of BackendSystem to include credential information
export interface BackendSystemSelection extends BackendSystem {
hasStoredCredentials?: boolean;
}

export type SystemSelectionAnswerType = {
type: 'destination' | 'backendSystem' | 'newSystemChoice' | CfAbapEnvServiceChoice;
system: Destination | BackendSystem | NewSystemChoice | CfAbapEnvServiceChoice;
system: Destination | BackendSystemSelection | NewSystemChoice | CfAbapEnvServiceChoice;
};

/**
Expand Down Expand Up @@ -72,6 +77,9 @@ export async function connectWithBackendSystem(
convertODataVersionType(requiredOdataVersion)
);
} else if (backendSystem.authenticationType === 'basic' || !backendSystem.authenticationType) {
// Check if the system has stored credentials
const hasStoredCredentials = !!(backendSystem.username && backendSystem.password);

let errorType;
({ valResult: connectValResult, errorType } = await connectionValidator.validateAuth(
backendSystem.url,
Expand All @@ -85,17 +93,18 @@ export async function connectWithBackendSystem(
));
// If authentication failed with existing credentials the user will be prompted to enter new credentials.
// We log the error in case there is another issue (unresolveable) with the stored backend configuration.
if (
errorType === ERROR_TYPE.AUTH &&
typeof backendSystem.username === 'string' &&
typeof backendSystem.password === 'string'
) {
if (errorType === ERROR_TYPE.AUTH && hasStoredCredentials) {
LoggerHelper.logger.error(
t('errors.storedSystemConnectionError', {
systemName: backendSystem.name,
error: connectValResult
})
);
(backendSystem as BackendSystemSelection).hasStoredCredentials = true;
return true;
} else if (errorType === ERROR_TYPE.AUTH && !hasStoredCredentials) {
// 1f there are no stored credentials, we defer validation to the credentials prompt but do not log an error here.
(backendSystem as BackendSystemSelection).hasStoredCredentials = false;
return true;
}
}
Expand Down Expand Up @@ -242,15 +251,36 @@ export async function createSystemChoices(
// Cache the backend systems
PromptState.backendSystemsCache = backendSystems;

systemChoices = backendSystems.map((system) => {
return {
name: getBackendSystemDisplayName(system),
value: {
system,
type: 'backendSystem'
} as SystemSelectionAnswerType
};
});
systemChoices = await Promise.all(
backendSystems.map(async (system) => {
// Check if the system has stored credentials by reading it with sensitive data
const systemWithCredentials = await new SystemService(LoggerHelper.logger).read(
BackendSystemKey.from(system) as BackendSystemKey
);
const hasStoredCredentials =
typeof systemWithCredentials?.username === 'string' &&
systemWithCredentials.username.length > 0 &&
typeof systemWithCredentials?.password === 'string' &&
systemWithCredentials.password.length > 0;
let systemSelection: BackendSystemSelection = { ...system };

if (system.authenticationType === 'basic' || !system.authenticationType) {
systemSelection = {
...systemSelection,
hasStoredCredentials,
username: systemWithCredentials?.username || system.username
};
}

return {
name: getBackendSystemDisplayName(system),
value: {
system: systemSelection,
type: 'backendSystem'
} as SystemSelectionAnswerType
};
})
);
newSystemChoice = {
name: t('prompts.systemSelection.newSystemChoiceLabel'),
value: { type: 'newSystemChoice', system: NewSystemChoice } as SystemSelectionAnswerType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { getSystemServiceQuestion } from '../service-selection/questions';
import { validateServiceUrl } from '../validators';
import {
type SystemSelectionAnswerType,
type BackendSystemSelection,
connectWithBackendSystem,
connectWithDestination,
createSystemChoices,
Expand Down Expand Up @@ -209,23 +210,35 @@ export async function getSystemConnectionQuestions(
);
},
additionalMessages: async (selectedSystem: SystemSelectionAnswerType) => {
// Backend systems credentials may need to be updated
let message;
// Check for stored credentials or authentication failure message if a backend system is selected
if (
selectedSystem.type === 'backendSystem' &&
connectionValidator.systemAuthType === 'basic' &&
(await connectionValidator.isAuthRequired())
) {
return {
message: t('prompts.systemSelection.authenticationFailedUpdateCredentials'),
severity: Severity.information
};
const backend = selectedSystem.system as BackendSystemSelection;
const missingCredentials = !backend.hasStoredCredentials;

if (missingCredentials) {
message = {
message: t('prompts.systemSelection.noStoredCredentials'),
severity: Severity.information
};
} else {
message = {
message: t('prompts.systemSelection.authenticationFailedUpdateCredentials'),
severity: Severity.information
};
}
}
if (connectionValidator.ignoreCertError) {
return {
message = {
message: t('warnings.certErrorIgnoredByNodeSetting'),
severity: Severity.warning
};
}
return message ? message : undefined;
}
} as ListQuestion<SystemSelectionAnswers>
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@
"newSystemChoiceLabel": "New System",
"hint": "Select a system configuration.",
"message": "System",
"authenticationFailedUpdateCredentials": "Authentication failed. Please try updating the credentials."
"authenticationFailedUpdateCredentials": "Authentication failed. Check your credentials are correct and try again.",
"noStoredCredentials": "This stored system has no credentials. Please provide them. They will not be saved with system details."
},
"abapOnBTPType": {
"message": "ABAP environment definition source",
Expand Down Expand Up @@ -256,6 +257,7 @@
"forUserName": "(for user [{{username}}])",
"httpStatus": "HTTP Status {{httpStatus}}",
"checkDestinationAuthConfig": "Please check the SAP BTP destination authentication configuration.",
"choiceNameNone": "None"
"choiceNameNone": "None",
"passwordStoreWarning": "Passwords are stored in your operating system's credential manager and are protected by its security policies."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ exports[`API tests getPrompts, i18n is loaded 1`] = `
"name": "storedSystem1",
"value": {
"system": {
"hasStoredCredentials": true,
"name": "storedSystem1",
"systemType": "OnPrem",
"url": "http://url1",
"username": "user1",
},
"type": "backendSystem",
},
Expand All @@ -54,9 +56,11 @@ exports[`API tests getPrompts, i18n is loaded 1`] = `
"name": "storedSystem2 (ABAP Cloud)",
"value": {
"system": {
"hasStoredCredentials": false,
"name": "storedSystem2",
"systemType": "BTP",
"url": "http://url2",
"username": undefined,
},
"type": "backendSystem",
},
Expand All @@ -75,7 +79,7 @@ exports[`API tests getPrompts, i18n is loaded 1`] = `
"when": [Function],
},
{
"default": "",
"default": [Function],
"guiOptions": {
"mandatory": true,
},
Expand Down Expand Up @@ -156,7 +160,7 @@ exports[`API tests getPrompts, i18n is loaded 1`] = `
"when": [Function],
},
{
"default": "",
"default": [Function],
"guiOptions": {
"mandatory": true,
},
Expand Down
Loading
Loading