Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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
7 changes: 7 additions & 0 deletions .changeset/itchy-beans-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@sap-ux/odata-service-inquirer': minor
'@sap-ux/fiori-app-sub-generator': patch
---

Adds warning to password input and info to backend system choice, tracks if backend system has stored creds, stores credentials only if both already present.

Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ export async function runPostGenerationTasks(
// 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) {
Copy link
Contributor

@IainSAP IainSAP Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is unnecessary. you cant determine if the user declined here anyway just that the system wont be persisted.

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,30 @@ 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 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 @@ -2,12 +2,13 @@ import { Severity } from '@sap-devx/yeoman-ui-types';
import type { ServiceProvider } from '@sap-ux/axios-extension';
import { isFullUrlDestination, isPartialUrlDestination, type Destination } from '@sap-ux/btp-utils';
import type { InputQuestion, PasswordQuestion } from '@sap-ux/inquirer-common';
import type { BackendSystem } from '@sap-ux/store';
import { BackendSystemKey, type BackendSystem, SystemService } from '@sap-ux/store';
import type { Answers } from 'inquirer';
import { t } from '../../../../i18n';
import { promptNames } from '../../../../types';
import { PromptState, removeCircularFromServiceProvider } from '../../../../utils';
import type { ConnectionValidator } from '../../../connectionValidator';
import LoggerHelper from '../../../logger-helper';
import type { ValidationResult } from '../../../types';
import type { SystemSelectionAnswerType } from '../system-selection/prompt-helpers';
import type { NewSystemAnswers } from '../new-system/types';
Expand Down Expand Up @@ -55,7 +56,21 @@ export function getCredentialsPrompts<T extends Answers>(
guiOptions: {
mandatory: true
},
default: '',
default: async (answers: T) => {
// Prefill username from the selected backend system if available
const selectedSystem = answers?.[promptNames.systemSelection] as SystemSelectionAnswerType;
if (selectedSystem?.type === 'backendSystem') {
const selectedBackendSystem = selectedSystem.system as BackendSystem;
if (selectedBackendSystem?.userDisplayName) {
// Read system with credentials to get the stored username, since we won't assume that displayName = username
const systemWithCredentials = await new SystemService(LoggerHelper.logger).read(
BackendSystemKey.from(selectedBackendSystem) as BackendSystemKey
);
return systemWithCredentials?.username || '';
}
}
return '';
},
validate: (user: string) => user?.length > 0
} as InputQuestion<T>,
{
Expand Down Expand Up @@ -115,6 +130,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.warning
};
}

// 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 @@ -153,13 +175,17 @@ 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;

// Have the credentials changed..
if (backendSystem.username !== username || backendSystem.password !== password) {
// Get the hasStoredCredentials from PromptState to determine if it's temp creds scenario or new/updated
const hasStoredCredentials = PromptState.hasStoredCredentials || false;

PromptState.odataService.connectedSystem.backendSystem = Object.assign(backendSystem, {
username: username,
password,
userDisplayName: username,
newOrUpdated: true
newOrUpdated: hasStoredCredentials
} 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,8 @@ 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 type { BackendSystemKey } from '@sap-ux/store';
import { type BackendSystem, SystemService } from '@sap-ux/store';
import type { ListChoiceOptions } from 'inquirer';
import { t } from '../../../../i18n';
import type { ConnectedSystem, DestinationFilters } from '../../../../types';
Expand Down Expand Up @@ -72,6 +73,8 @@ export async function connectWithBackendSystem(
convertODataVersionType(requiredOdataVersion)
);
} else if (backendSystem.authenticationType === 'basic' || !backendSystem.authenticationType) {
const hasStoredCredentials = !!(backendSystem.username && backendSystem.password);
PromptState.hasStoredCredentials = hasStoredCredentials;
let errorType;
({ valResult: connectValResult, errorType } = await connectionValidator.validateAuth(
backendSystem.url,
Expand All @@ -85,11 +88,7 @@ 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) {
LoggerHelper.logger.error(
t('errors.storedSystemConnectionError', {
systemName: backendSystem.name,
Expand Down Expand Up @@ -246,7 +245,7 @@ export async function createSystemChoices(
return {
name: getBackendSystemDisplayName(system),
value: {
system,
system: system,
type: 'backendSystem'
} as SystemSelectionAnswerType
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,23 +209,33 @@ 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 noCredentials = !PromptState.hasStoredCredentials;
if (noCredentials) {
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 ?? 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."
}
}
6 changes: 6 additions & 0 deletions packages/odata-service-inquirer/src/utils/prompt-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ export class PromptState {
*/
public static backendSystemsCache: BackendSystem[] = [];

/**
* Store whether the selected system has stored credentials
*/
public static hasStoredCredentials?: boolean;

static reset(): void {
// Reset all values in the odataService object, do not reset the object reference itself as it may be used by external consumers
Object.keys(PromptState.odataService).forEach((key) => {
PromptState.odataService[key as keyof OdataServiceAnswers] = undefined;
});
PromptState.hasStoredCredentials = undefined;
}

static resetConnectedSystem(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ exports[`API tests getPrompts, i18n is loaded 1`] = `
"when": [Function],
},
{
"default": "",
"default": [Function],
"guiOptions": {
"mandatory": true,
},
Expand Down Expand Up @@ -156,7 +156,7 @@ exports[`API tests getPrompts, i18n is loaded 1`] = `
"when": [Function],
},
{
"default": "",
"default": [Function],
"guiOptions": {
"mandatory": true,
},
Expand Down
20 changes: 19 additions & 1 deletion packages/odata-service-inquirer/test/unit/index-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,25 @@ jest.mock('@sap-ux/store', () => ({
url: 'http://url2',
systemType: 'BTP'
}
] as BackendSystem[])
] as BackendSystem[]),
read: jest.fn().mockImplementation((key) => {
// Mock read to return systems with credentials
const systems = [
{
name: 'storedSystem1',
url: 'http://url1',
systemType: 'OnPrem',
username: 'user1',
password: 'pass1'
},
{
name: 'storedSystem2',
url: 'http://url2',
systemType: 'BTP'
}
];
return Promise.resolve(systems.find((s) => s.url === key.url));
})
}))
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ exports[`getQuestions getQuestions 1`] = `
"when": [Function],
},
{
"default": "",
"default": [Function],
"guiOptions": {
"mandatory": true,
},
Expand Down Expand Up @@ -164,7 +164,7 @@ exports[`getQuestions getQuestions 1`] = `
"when": [Function],
},
{
"default": "",
"default": [Function],
"guiOptions": {
"mandatory": true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,25 @@ jest.mock('@sap-ux/store', () => ({
url: 'http://url2',
systemType: 'BTP'
}
] as BackendSystem[])
] as BackendSystem[]),
read: jest.fn().mockImplementation((key) => {
// Mock read to return systems with credentials
const systems = [
{
name: 'storedSystem1',
url: 'http://url1',
systemType: 'OnPrem',
username: 'user1',
password: 'pass1'
},
{
name: 'storedSystem2',
url: 'http://url2',
systemType: 'BTP'
}
];
return Promise.resolve(systems.find((s) => s.url === key.url));
})
}))
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe('questions', () => {
"validate": [Function],
},
{
"default": "",
"default": [Function],
"guiOptions": {
"mandatory": true,
},
Expand Down
Loading