1
0
mirror of https://github.com/quay/quay.git synced 2026-01-26 06:21:37 +03:00

[redhat-3.16] uibug: updated new UI to ask user to verify the mail when creating new account with email enabled (PROJQUAY-9655) (#4450)

* updated new UI to ask user to verify the mail when creating new account with email enabled

* fixed failing cypress test

---------

Co-authored-by: shudeshp <shudeshp@redhat.com>
This commit is contained in:
OpenShift Cherrypick Robot
2025-11-19 13:13:08 +01:00
committed by GitHub
parent 6eeb06dbfe
commit ef66ec6920
4 changed files with 271 additions and 163 deletions

View File

@@ -195,4 +195,69 @@ describe('Create Account Page', () => {
cy.contains('Already have an account?');
cy.contains('Sign in');
});
it('Shows email verification message when awaiting_verification is true', () => {
const testUser = {
username: `testuser${Date.now()}`,
email: `test${Date.now()}@example.com`,
password: 'validpassword123',
};
// Setup account creation with awaiting_verification response
cy.intercept('POST', '/api/v1/user/', {
statusCode: 200,
body: {awaiting_verification: true},
}).as('createUserAwaitingVerification');
// Setup signin intercept to verify it's NOT called
cy.intercept('POST', '/api/v1/signin', (req) => {
throw new Error('Signin should not be called when awaiting verification');
}).as('signinShouldNotBeCalled');
cy.visit('/createaccount');
// Fill form with valid data
cy.get('#username').type(testUser.username);
cy.get('#email').type(testUser.email);
cy.get('#password').type(testUser.password);
cy.get('#confirm-password').type(testUser.password);
// Submit form
cy.get('button[type=submit]').click();
// Wait for create user API call
cy.wait('@createUserAwaitingVerification').then((interception) => {
expect(interception.request.body).to.deep.equal({
username: testUser.username,
email: testUser.email,
password: testUser.password,
});
});
// Should show verification message
cy.get('[data-testid="awaiting-verification-alert"]').should('be.visible');
cy.contains(
'Thank you for registering! We have sent you an activation email.',
);
cy.contains('verify your email address').should('be.visible');
// Form should be hidden (check visibility, not existence since display:none keeps elements in DOM)
cy.get('#username').should('not.be.visible');
cy.get('#email').should('not.be.visible');
cy.get('#password').should('not.be.visible');
// Should not redirect to organization page
cy.url().should('include', '/createaccount');
cy.url().should('not.include', '/organization');
// Should show sign in link (it's outside the form when awaiting verification)
// Find the visible link that's not inside a hidden form
cy.get('a[href="/signin"]').should('be.visible');
// The text should also be visible (rendered outside the hidden form)
cy.contains('body', 'Already have an account?').should('be.visible');
// Verify no auto-login was attempted - signin API should not be called
// If signin was attempted, it would have thrown an error from the intercept
cy.wait(500); // Small wait to ensure no signin API call is made
});
});

View File

@@ -24,11 +24,17 @@ export function useCreateAccount() {
try {
// Create the user account
await createUser(username, password, email);
const response = await createUser(username, password, email);
// Clear CSRF token after account creation (session state changed)
GlobalAuthState.csrfToken = null;
// Check if email verification is required
if (response.awaiting_verification === true) {
// Email verification required, return success but indicate verification is needed
return {success: true, awaitingVerification: true};
}
// Auto-login after successful account creation
try {
const loginResponse = await loginUser(username, password);

View File

@@ -122,11 +122,16 @@ export interface CreateUserRequest {
email: string;
}
export interface CreateUserResponse {
awaiting_verification?: boolean;
[key: string]: any; // Allow other fields from the API response
}
export async function createUser(
username: string,
password: string,
email: string,
) {
): Promise<CreateUserResponse> {
const response = await axios.post('/api/v1/user/', {
username,
password,

View File

@@ -22,6 +22,7 @@ export function CreateAccount() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [awaitingVerification, setAwaitingVerification] = useState(false);
const {createAccountWithAutoLogin, isLoading, error, setError} =
useCreateAccount();
@@ -75,7 +76,10 @@ export function CreateAccount() {
return;
}
await createAccountWithAutoLogin(username, password, email);
const result = await createAccountWithAutoLogin(username, password, email);
if (result.success && result.awaitingVerification) {
setAwaitingVerification(true);
}
};
const errMessage = (
@@ -89,166 +93,194 @@ export function CreateAccount() {
);
const createAccountForm = (
<Form>
<FormGroup
label="Username"
isRequired
fieldId="username"
validated={validateUsername(username)}
>
<TextInput
isRequired
type="text"
id="username"
name="username"
value={username}
onChange={(_event, v) => setUsername(v)}
validated={validateUsername(username)}
/>
<FormHelperText>
<HelperText>
<HelperTextItem
variant={
validateUsername(username) === ValidatedOptions.error
? 'error'
: 'default'
}
icon={
validateUsername(username) === ValidatedOptions.error ? (
<ExclamationCircleIcon />
) : undefined
}
>
Username must be at least 3 characters and contain only letters,
numbers, hyphens, underscores, and periods
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
<FormGroup
label="Email"
isRequired
fieldId="email"
validated={validateEmail(email)}
>
<TextInput
isRequired
type="email"
id="email"
name="email"
value={email}
onChange={(_event, v) => setEmail(v)}
validated={validateEmail(email)}
/>
<FormHelperText>
<HelperText>
<HelperTextItem
variant={
validateEmail(email) === ValidatedOptions.error
? 'error'
: 'default'
}
icon={
validateEmail(email) === ValidatedOptions.error ? (
<ExclamationCircleIcon />
) : undefined
}
>
Please enter a valid email address
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
<FormGroup
label="Password"
isRequired
fieldId="password"
validated={validatePassword(password)}
>
<TextInput
isRequired
type="password"
id="password"
name="password"
value={password}
onChange={(_event, v) => setPassword(v)}
validated={validatePassword(password)}
/>
<FormHelperText>
<HelperText>
<HelperTextItem
variant={
validatePassword(password) === ValidatedOptions.error
? 'error'
: 'default'
}
icon={
validatePassword(password) === ValidatedOptions.error ? (
<ExclamationCircleIcon />
) : undefined
}
>
Password must be at least 8 characters long
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
<FormGroup
label="Confirm Password"
isRequired
fieldId="confirm-password"
validated={validateConfirmPassword(confirmPassword)}
>
<TextInput
isRequired
type="password"
id="confirm-password"
name="confirm-password"
value={confirmPassword}
onChange={(_event, v) => setConfirmPassword(v)}
validated={validateConfirmPassword(confirmPassword)}
/>
<FormHelperText>
<HelperText>
<HelperTextItem
variant={
validateConfirmPassword(confirmPassword) ===
ValidatedOptions.error
? 'error'
: 'default'
}
icon={
validateConfirmPassword(confirmPassword) ===
ValidatedOptions.error ? (
<ExclamationCircleIcon />
) : undefined
}
>
Passwords must match
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
{error && errMessage}
<FormGroup>
<Button
variant="primary"
type="submit"
isBlock
isDisabled={!isFormValid() || isLoading}
isLoading={isLoading}
onClick={onCreateAccountClick}
<>
{awaitingVerification && (
<Alert
variant="info"
isInline
title=""
style={{marginBottom: '20px'}}
data-testid="awaiting-verification-alert"
>
Create Account
</Button>
</FormGroup>
Thank you for registering! We have sent you an activation email. You
must <strong>verify your email address</strong> before you can
continue.
</Alert>
)}
<FormGroup>
<Form style={{display: awaitingVerification ? 'none' : 'block'}}>
<FormGroup
label="Username"
isRequired
fieldId="username"
validated={validateUsername(username)}
>
<TextInput
isRequired
type="text"
id="username"
name="username"
value={username}
onChange={(_event, v) => setUsername(v)}
validated={validateUsername(username)}
/>
<FormHelperText>
<HelperText>
<HelperTextItem
variant={
validateUsername(username) === ValidatedOptions.error
? 'error'
: 'default'
}
icon={
validateUsername(username) === ValidatedOptions.error ? (
<ExclamationCircleIcon />
) : undefined
}
>
Username must be at least 3 characters and contain only letters,
numbers, hyphens, underscores, and periods
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
<FormGroup
label="Email"
isRequired
fieldId="email"
validated={validateEmail(email)}
>
<TextInput
isRequired
type="email"
id="email"
name="email"
value={email}
onChange={(_event, v) => setEmail(v)}
validated={validateEmail(email)}
/>
<FormHelperText>
<HelperText>
<HelperTextItem
variant={
validateEmail(email) === ValidatedOptions.error
? 'error'
: 'default'
}
icon={
validateEmail(email) === ValidatedOptions.error ? (
<ExclamationCircleIcon />
) : undefined
}
>
Please enter a valid email address
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
<FormGroup
label="Password"
isRequired
fieldId="password"
validated={validatePassword(password)}
>
<TextInput
isRequired
type="password"
id="password"
name="password"
value={password}
onChange={(_event, v) => setPassword(v)}
validated={validatePassword(password)}
/>
<FormHelperText>
<HelperText>
<HelperTextItem
variant={
validatePassword(password) === ValidatedOptions.error
? 'error'
: 'default'
}
icon={
validatePassword(password) === ValidatedOptions.error ? (
<ExclamationCircleIcon />
) : undefined
}
>
Password must be at least 8 characters long
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
<FormGroup
label="Confirm Password"
isRequired
fieldId="confirm-password"
validated={validateConfirmPassword(confirmPassword)}
>
<TextInput
isRequired
type="password"
id="confirm-password"
name="confirm-password"
value={confirmPassword}
onChange={(_event, v) => setConfirmPassword(v)}
validated={validateConfirmPassword(confirmPassword)}
/>
<FormHelperText>
<HelperText>
<HelperTextItem
variant={
validateConfirmPassword(confirmPassword) ===
ValidatedOptions.error
? 'error'
: 'default'
}
icon={
validateConfirmPassword(confirmPassword) ===
ValidatedOptions.error ? (
<ExclamationCircleIcon />
) : undefined
}
>
Passwords must match
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
{error && errMessage}
<FormGroup>
<Button
variant="primary"
type="submit"
isBlock
isDisabled={!isFormValid() || isLoading}
isLoading={isLoading}
onClick={onCreateAccountClick}
>
Create Account
</Button>
</FormGroup>
<FormGroup>
<div style={{textAlign: 'center', marginTop: '16px'}}>
Already have an account?{' '}
<Link
to="/signin"
style={{color: 'var(--pf-v5-global--link--Color)'}}
>
Sign in
</Link>
</div>
</FormGroup>
</Form>
{awaitingVerification && (
<div style={{textAlign: 'center', marginTop: '16px'}}>
Already have an account?{' '}
<Link
@@ -258,8 +290,8 @@ export function CreateAccount() {
Sign in
</Link>
</div>
</FormGroup>
</Form>
)}
</>
);
return (