diff --git a/web/cypress/e2e/account-settings.cy.ts b/web/cypress/e2e/account-settings.cy.ts
index d1ad54721..21882bf5b 100644
--- a/web/cypress/e2e/account-settings.cy.ts
+++ b/web/cypress/e2e/account-settings.cy.ts
@@ -3,8 +3,11 @@
import {humanizeTimeForExpiry, parseTimeDuration} from 'src/libs/utils';
describe('Account Settings Page', () => {
- beforeEach(() => {
+ before(() => {
cy.exec('npm run quay:seed');
+ });
+
+ beforeEach(() => {
cy.visit('/signin');
cy.request('GET', `${Cypress.env('REACT_QUAY_APP_API_URL')}/csrf_token`)
.then((response) => response.body.csrf_token)
@@ -196,7 +199,27 @@ describe('Account Settings Page', () => {
cy.get('#checkbox').should('be.checked');
});
- it('CLI Token', () => {
+ it('CLI Token - Generate Encrypted Password with All Credential Formats', () => {
+ // Mock the encrypted password API call - wrong password
+ cy.intercept('POST', '/api/v1/user/clientkey', (req) => {
+ if (req.body.password === 'wrongpassword') {
+ req.reply({
+ statusCode: 400,
+ body: {
+ error_message: 'Invalid Username or Password',
+ error_type: 'invalid_auth',
+ },
+ });
+ } else if (req.body.password === 'password') {
+ req.reply({
+ statusCode: 200,
+ body: {
+ key: 'fake-encrypted-password-12345',
+ },
+ });
+ }
+ }).as('createClientKey');
+
cy.visit('/organization/user1?tab=Settings');
// navigate to CLI Tab
@@ -208,13 +231,75 @@ describe('Account Settings Page', () => {
// Wrong password
cy.get('#delete-confirmation-input').type('wrongpassword');
cy.get('#submit').click();
+ cy.wait('@createClientKey');
cy.contains('Invalid Username or Password');
// Correct password
cy.get('#delete-confirmation-input').clear();
cy.get('#delete-confirmation-input').type('password');
cy.get('#submit').click();
- cy.contains('Your encrypted password is');
+ cy.wait('@createClientKey');
+
+ // Should show credentials modal with all tabs
+ cy.get('[data-testid="credentials-modal"]').should('be.visible');
+ cy.contains('Credentials for user1').should('exist');
+
+ // Verify alert message for encrypted password
+ cy.contains('Encrypted Password').should('exist');
+ cy.contains('This encrypted password can be used for').should('exist');
+ cy.contains('docker login').should('exist');
+
+ // Verify all tabs exist
+ cy.contains('Encrypted Password').should('exist');
+ cy.contains('Kubernetes Secret').should('exist');
+ cy.contains('rkt Configuration').should('exist');
+ cy.contains('Podman Login').should('exist');
+ cy.contains('Docker Login').should('exist');
+ cy.contains('Docker Configuration').should('exist');
+
+ // Verify Encrypted Password tab (default) shows username and password
+ cy.get('[data-testid="credentials-modal-copy-username"]').should('exist');
+ cy.get('[data-testid="credentials-modal-copy-username"]')
+ .find('input[readonly]')
+ .should('have.value', 'user1'); // Encrypted password uses actual username
+ cy.get('[data-testid="credentials-modal-copy-password"]').should('exist');
+
+ // Test Kubernetes Secret tab
+ cy.contains('Kubernetes Secret').click();
+ cy.contains('Step 1: Create secret YAML file').should('exist');
+ cy.contains('apiVersion: v1').should('exist');
+ cy.contains('kind: Secret').should('exist');
+ cy.contains('user1-pull-secret').should('exist');
+
+ // Test Docker Login tab
+ cy.contains('Docker Login').click();
+ cy.contains('Run docker login command').should('exist');
+ cy.contains('docker login').should('exist');
+ // Verify username is user1 (not $app like application tokens)
+ cy.contains('user1').should('exist'); // Username in command
+ cy.contains('localhost').should('exist'); // Server hostname in command
+
+ // Test Podman Login tab
+ cy.contains('Podman Login').click();
+ cy.contains('Run podman login command').should('exist');
+ cy.contains('podman login').should('exist');
+ // Verify username is user1
+ cy.contains('user1').should('exist'); // Username in command
+
+ // Test rkt Configuration tab
+ cy.contains('rkt Configuration').click();
+ cy.contains('Step 1: Create rkt configuration file').should('exist');
+ cy.contains('rktKind').should('exist');
+
+ // Test Docker Configuration tab
+ cy.contains('Docker Configuration').click();
+ cy.contains('Step 1: Create Docker config file').should('exist');
+ cy.contains('This will').should('exist');
+ cy.contains('overwrite').should('exist');
+
+ // Close modal
+ cy.get('[data-testid="credentials-modal-close"]').click();
+ cy.get('[data-testid="credentials-modal"]').should('not.exist');
});
it('Avatar Display', () => {
@@ -351,7 +436,9 @@ describe('Account Settings Page', () => {
const mockNotification = function () {
// Empty constructor for mocking purposes
} as unknown as typeof Notification;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockNotification as any).permission = 'default';
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockNotification as any).requestPermission = cy
.stub()
.resolves('granted');
@@ -489,7 +576,8 @@ describe('Account Settings Page', () => {
// Check section title
cy.contains('Docker CLI and other Application Tokens').should('exist');
- // Check tokens table
+ // Check tokens table - wait for table to be populated
+ cy.get('table').last().should('be.visible');
cy.get('table')
.last()
.within(() => {
@@ -555,13 +643,13 @@ describe('Account Settings Page', () => {
cy.wait('@createToken');
// Should show success step with token
- cy.get('[data-testid="token-credentials-modal"]').within(() => {
+ cy.get('[data-testid="credentials-modal"]').within(() => {
cy.contains('Token Created Successfully').should('exist');
- cy.get('[data-testid="copy-token-button"]').should('exist');
+ cy.get('[data-testid="credentials-modal-copy-password"]').should('exist');
});
- cy.get('[data-testid="token-credentials-close"]').click();
- cy.get('[data-testid="token-credentials-modal"]').should('not.exist');
+ cy.get('[data-testid="credentials-modal-close"]').click();
+ cy.get('[data-testid="credentials-modal"]').should('not.exist');
cy.wait('@getTokensAfterCreate');
// Verify new token appears in table
@@ -724,7 +812,7 @@ describe('Account Settings Page', () => {
cy.wait('@createToken');
// Should show success with tabs
- cy.get('[data-testid="token-credentials-modal"]').within(() => {
+ cy.get('[data-testid="credentials-modal"]').within(() => {
cy.contains('Token Created Successfully').should('exist');
// Check all tabs exist
@@ -736,9 +824,9 @@ describe('Account Settings Page', () => {
cy.contains('Docker Configuration').should('exist');
// Verify default tab (Application Token) shows username and token
- cy.get('[data-testid="copy-username"]').should('exist');
- cy.get('[data-testid="copy-token-button"]').should('exist');
- cy.get('[data-testid="copy-token-button"]')
+ cy.get('[data-testid="credentials-modal-copy-username"]').should('exist');
+ cy.get('[data-testid="credentials-modal-copy-password"]').should('exist');
+ cy.get('[data-testid="credentials-modal-copy-password"]')
.find('input')
.should('have.value', 'fake-token-code-12345');
@@ -757,11 +845,17 @@ describe('Account Settings Page', () => {
cy.contains('Podman Login').click();
cy.contains('Run podman login command').should('exist');
cy.contains('podman login').should('exist');
+ // Verify command contains username $app
+ cy.contains('$app').should('exist'); // Username in command
+ cy.contains('localhost').should('exist'); // Server hostname in command
// Test Docker Login tab
cy.contains('Docker Login').click();
cy.contains('Run docker login command').should('exist');
cy.contains('docker login').should('exist');
+ // Verify command contains username $app
+ cy.contains('$app').should('exist'); // Username in command
+ cy.contains('localhost').should('exist'); // Server hostname in command
// Test Docker Configuration tab
cy.contains('Docker Configuration').click();
@@ -770,8 +864,8 @@ describe('Account Settings Page', () => {
cy.contains('overwrite').should('exist');
});
- cy.get('[data-testid="token-credentials-close"]').click();
- cy.get('[data-testid="token-credentials-modal"]').should('not.exist');
+ cy.get('[data-testid="credentials-modal-close"]').click();
+ cy.get('[data-testid="credentials-modal"]').should('not.exist');
});
it('Clickable Token Titles - View Token Details', () => {
@@ -825,12 +919,12 @@ describe('Account Settings Page', () => {
// Verify modal shows token details with tabs
cy.contains('Application Token').should('exist');
- cy.get('[data-testid="copy-username"]').should('exist');
- cy.get('[data-testid="copy-token-button"]').should('exist');
+ cy.get('[data-testid="credentials-modal-copy-username"]').should('exist');
+ cy.get('[data-testid="credentials-modal-copy-password"]').should('exist');
// Close modal
cy.contains('button', 'Done').click();
- cy.get('[data-testid="token-credentials-modal"]').should('not.exist');
+ cy.get('[data-testid="credentials-modal"]').should('not.exist');
});
it('View Token Modal - Token Never Accessed', () => {
@@ -876,13 +970,13 @@ describe('Account Settings Page', () => {
cy.contains('Credentials for Unused Token').should('be.visible');
// Verify modal shows token credentials
- cy.get('[data-testid="token-credentials-modal"]').should('be.visible');
- cy.get('[data-testid="copy-username"]').should('exist');
- cy.get('[data-testid="copy-token-button"]').should('exist');
+ cy.get('[data-testid="credentials-modal"]').should('be.visible');
+ cy.get('[data-testid="credentials-modal-copy-username"]').should('exist');
+ cy.get('[data-testid="credentials-modal-copy-password"]').should('exist');
// Close modal
cy.contains('button', 'Done').click();
- cy.get('[data-testid="token-credentials-modal"]').should('not.exist');
+ cy.get('[data-testid="credentials-modal"]').should('not.exist');
});
it('Settings Tab Hidden in Read-Only Mode', () => {
diff --git a/web/src/components/modals/CreateApplicationTokenModal.tsx b/web/src/components/modals/CreateApplicationTokenModal.tsx
index e8e36c529..e0d4814d1 100644
--- a/web/src/components/modals/CreateApplicationTokenModal.tsx
+++ b/web/src/components/modals/CreateApplicationTokenModal.tsx
@@ -11,7 +11,7 @@ import {
} from '@patternfly/react-core';
import {useCreateApplicationToken} from 'src/hooks/UseApplicationTokens';
import {IApplicationToken} from 'src/resources/UserResource';
-import ApplicationTokenCredentials from './ApplicationTokenCredentials';
+import CredentialsModal from './CredentialsModal';
interface CreateApplicationTokenModalProps {
isOpen: boolean;
@@ -64,10 +64,15 @@ export default function CreateApplicationTokenModal({
// If token was created successfully, show credentials
if (createdToken) {
return (
-
);
diff --git a/web/src/components/modals/ApplicationTokenCredentials.tsx b/web/src/components/modals/CredentialsModal.tsx
similarity index 73%
rename from web/src/components/modals/ApplicationTokenCredentials.tsx
rename to web/src/components/modals/CredentialsModal.tsx
index 4fc917ce0..d9bad1f90 100644
--- a/web/src/components/modals/ApplicationTokenCredentials.tsx
+++ b/web/src/components/modals/CredentialsModal.tsx
@@ -14,22 +14,31 @@ import {
CodeBlockCode,
Text,
} from '@patternfly/react-core';
-import {IApplicationToken} from 'src/resources/UserResource';
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
-interface ApplicationTokenCredentialsProps {
+export interface Credentials {
+ username: string;
+ password: string;
+ title: string;
+}
+
+export type CredentialsType = 'token' | 'encrypted-password';
+
+interface CredentialsModalProps {
isOpen: boolean;
onClose: () => void;
- token: IApplicationToken;
+ credentials: Credentials;
+ type: CredentialsType;
isNewlyCreated?: boolean;
}
-export default function ApplicationTokenCredentials({
+export default function CredentialsModal({
isOpen,
onClose,
- token,
+ credentials,
+ type,
isNewlyCreated = false,
-}: ApplicationTokenCredentialsProps) {
+}: CredentialsModalProps) {
const [activeTabKey, setActiveTabKey] = useState(0);
const quayConfig = useQuayConfig();
@@ -38,7 +47,9 @@ export default function ApplicationTokenCredentials({
};
const getContainerLoginCommand = (runtime: 'docker' | 'podman') => {
- return `${runtime} login -u='$app' -p='${token?.token_code}' ${getServerHostname()}`;
+ return `${runtime} login -u='${credentials.username}' -p='${
+ credentials.password
+ }' ${getServerHostname()}`;
};
const kubernetesYaml = useMemo(() => {
@@ -46,7 +57,7 @@ export default function ApplicationTokenCredentials({
const dockerConfigJson = {
auths: {
[hostname]: {
- auth: btoa(`$app:${token?.token_code}`),
+ auth: btoa(`${credentials.username}:${credentials.password}`),
email: '',
},
},
@@ -55,11 +66,16 @@ export default function ApplicationTokenCredentials({
return `apiVersion: v1
kind: Secret
metadata:
- name: ${token?.title}-pull-secret
+ name: ${credentials.title}-pull-secret
data:
.dockerconfigjson: ${btoa(JSON.stringify(dockerConfigJson))}
type: kubernetes.io/dockerconfigjson`;
- }, [token?.title, token?.token_code, quayConfig?.config?.SERVER_HOSTNAME]);
+ }, [
+ credentials.title,
+ credentials.username,
+ credentials.password,
+ quayConfig?.config?.SERVER_HOSTNAME,
+ ]);
const rktConfig = useMemo(() => {
const hostname = getServerHostname();
@@ -69,43 +85,55 @@ type: kubernetes.io/dockerconfigjson`;
"domains": ["${hostname}"],
"type": "basic",
"credentials": {
- "user": "$app",
- "password": "${token?.token_code}"
+ "user": "${credentials.username}",
+ "password": "${credentials.password}"
}
}`;
- }, [token?.token_code, quayConfig?.config?.SERVER_HOSTNAME]);
+ }, [
+ credentials.username,
+ credentials.password,
+ quayConfig?.config?.SERVER_HOSTNAME,
+ ]);
const dockerConfig = useMemo(() => {
const hostname = getServerHostname();
return `{
"auths": {
"${hostname}": {
- "auth": "${btoa(`$app:${token?.token_code}`)}",
+ "auth": "${btoa(`${credentials.username}:${credentials.password}`)}",
"email": ""
}
}
}`;
- }, [token?.token_code, quayConfig?.config?.SERVER_HOSTNAME]);
+ }, [
+ credentials.username,
+ credentials.password,
+ quayConfig?.config?.SERVER_HOSTNAME,
+ ]);
+
+ const isToken = type === 'token';
+ const firstTabTitle = isToken ? 'Application Token' : 'Encrypted Password';
+ const passwordFieldLabel = isToken ? 'Token' : 'Encrypted Password';
return (
Done
,
]}
>
- {isNewlyCreated ? (
+ {isNewlyCreated && isToken && (
- ) : (
+ )}
+ {!isNewlyCreated && isToken && (
)}
+ {!isToken && (
+
+ This encrypted password can be used for docker login and
+ other CLI commands. It is recommended to use this instead of your
+ plaintext password.
+
+ )}
setActiveTabKey(tabIndex)}
>
- Application Token}
- >
+ {firstTabTitle}}>
@@ -183,7 +221,7 @@ type: kubernetes.io/dockerconfigjson`;
className="pf-v5-u-mb-md"
>
- {`kubectl create -f ${token?.title}-pull-secret.yaml --namespace=NAMESPACE`}
+ {`kubectl create -f ${credentials.title}-pull-secret.yaml --namespace=NAMESPACE`}
@@ -192,7 +230,7 @@ type: kubernetes.io/dockerconfigjson`;
- {`imagePullSecrets:\n - name: ${token?.title}-pull-secret`}
+ {`imagePullSecrets:\n - name: ${credentials.title}-pull-secret`}
diff --git a/web/src/components/modals/GenerateEncryptedPasswordModal.tsx b/web/src/components/modals/GenerateEncryptedPasswordModal.tsx
index 36430132a..42de8cbbb 100644
--- a/web/src/components/modals/GenerateEncryptedPasswordModal.tsx
+++ b/web/src/components/modals/GenerateEncryptedPasswordModal.tsx
@@ -1,11 +1,14 @@
import {Modal, ModalVariant, Button, TextInput} from '@patternfly/react-core';
-import {useState} from 'react';
+import {useState, useEffect} from 'react';
import FormError from 'src/components/errors/FormError';
import {addDisplayError} from 'src/resources/ErrorHandling';
import {useCreateClientKey} from 'src/hooks/UseCreateClientKey';
+import {useCurrentUser} from 'src/hooks/UseCurrentUser';
+import CredentialsModal, {Credentials} from './CredentialsModal';
export function GenerateEncryptedPassword(props: ConfirmationModalProps) {
const [err, setErr] = useState();
+ const {user} = useCurrentUser();
const [password, setPassword] = useState('');
const [step, setStep] = useState(1);
@@ -15,6 +18,7 @@ export function GenerateEncryptedPassword(props: ConfirmationModalProps) {
setErr(addDisplayError('Error', error));
},
onSuccess: () => {
+ setErr(undefined); // Clear any previous errors
setStep(step + 1);
},
});
@@ -23,53 +27,69 @@ export function GenerateEncryptedPassword(props: ConfirmationModalProps) {
createClientKey(password);
};
+ const handleClose = () => {
+ props.toggleModal();
+ };
+
+ // Cleanup state when modal closes
+ useEffect(() => {
+ if (!props.modalOpen) {
+ setStep(1);
+ setPassword('');
+ setErr(undefined);
+ }
+ }, [props.modalOpen]);
+
+ // Step 2: Show credentials modal with all credential formats
+ // Only transition to step 2 if we have clientKey, user, and no error
+ if (step === 2 && clientKey && user && !err) {
+ const credentials: Credentials = {
+ username: user.username,
+ password: clientKey,
+ title: user.username,
+ };
+
+ return (
+
+ );
+ }
+
+ // Step 1: Password input
return (
- {props.buttonText}
- ,
- ,
- ]
- : [
- ,
- ]
- }
+ onClose={handleClose}
+ actions={[
+ ,
+ ,
+ ]}
>
- {step == 1 && (
- <>
-
- setPassword(value)}
- aria-label="text input example"
- label="Password"
- />
- Please enter your password in order to generate
- >
- )}
- {step == 2 && (
- <>
- Your encrypted password is:
{clientKey}
- >
- )}
+
+ setPassword(value)}
+ aria-label="text input example"
+ label="Password"
+ />
+ Please enter your password in order to generate
);
}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/Settings/CLIConfiguration.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/Settings/CLIConfiguration.tsx
index ad31e7850..8059cf6f0 100644
--- a/web/src/routes/OrganizationsList/Organization/Tabs/Settings/CLIConfiguration.tsx
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/Settings/CLIConfiguration.tsx
@@ -22,7 +22,7 @@ import {EllipsisVIcon, KeyIcon} from '@patternfly/react-icons';
import {GenerateEncryptedPassword} from 'src/components/modals/GenerateEncryptedPasswordModal';
import CreateApplicationTokenModal from 'src/components/modals/CreateApplicationTokenModal';
import RevokeTokenModal from 'src/components/modals/RevokeTokenModal';
-import ApplicationTokenCredentials from 'src/components/modals/ApplicationTokenCredentials';
+import CredentialsModal from 'src/components/modals/CredentialsModal';
import {
useApplicationTokens,
useApplicationToken,
@@ -353,10 +353,15 @@ export const CliConfiguration = () => {
)}
{fetchedToken && !isFetchingToken && (
-
)}
>