Conversation
📊 Performance Test ResultsComparing 8f06c9d vs trunk site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
e61ae98 to
464cfac
Compare
Implements STU-829 by allowing users to set custom admin username/password either via Blueprints or through new fields in Advanced settings. Blueprint credentials are extracted and pre-filled in the form, with CLI args taking precedence. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
464cfac to
becd91f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 33 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Generate a unique email to avoid conflicts with existing users | ||
| $email = $provided_email ? $provided_email : 'admin@localhost.com'; | ||
| if ( ! $provided_email ) { | ||
| $counter = 1; | ||
| while ( email_exists( $email ) && $counter < 100 ) { | ||
| $email = 'admin' . $counter . '@localhost.com'; | ||
| $counter++; | ||
| } | ||
| } |
There was a problem hiding this comment.
When provided_email is provided but already exists in the database (checked by email_exists), the code still uses it without validation or uniqueness check. This could cause wp_insert_user to fail with a duplicate email error. The email uniqueness check with counter increment only applies when no email is provided. Consider checking if the provided email already exists and handling this case appropriately (either by showing an error to the user or generating a unique variation).
common/lib/passwords.ts
Outdated
| if ( ! email.trim() ) { | ||
| return __( 'Admin email cannot be empty.' ); | ||
| } | ||
| if ( ! /^[^\s@]+@[^\s@]+$/.test( email ) ) { |
There was a problem hiding this comment.
The email validation regex /^[^\s@]+@[^\s@]+$/ is very permissive and allows invalid emails like 'user@domain', 'user@.com', or 'user@@Domain'. While this might be intentional for local development environments, consider using a more robust email validation pattern that requires at least a TLD-like structure (e.g., /^[^\s@]+@[^\s@]+\.[^\s@]+$/) or use a standard email validation library to prevent common typos.
| if ( ! /^[^\s@]+@[^\s@]+$/.test( email ) ) { | |
| if ( ! /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( email ) ) { |
| const hasErrors = | ||
| !! pathError || | ||
| ( useCustomDomain && !! customDomainError ) || | ||
| ! adminUsername.trim() || | ||
| ! adminPassword.trim(); |
There was a problem hiding this comment.
The form validity check for onValidityChange (lines 283-287) doesn't include adminEmail validation, but the errorCount calculation at line 399-406 does include adminEmailError. This creates an inconsistency where the parent component might think the form is valid even when there's an email validation error, potentially allowing form submission with invalid data. Add adminEmail validation to the hasErrors check, similar to how errorCount is calculated.
| if ( adminEmail !== undefined ) { | ||
| const emailError = validateAdminEmail( adminEmail ); | ||
| if ( emailError ) { | ||
| throw new LoggerError( emailError ); | ||
| } | ||
| } |
There was a problem hiding this comment.
When adminEmail is provided but consists only of whitespace (e.g., --admin-email " "), the validateAdminEmail function will return an error message "Admin email cannot be empty." However, this contradicts the fact that email is optional in the UI. Consider checking if adminEmail.trim() is empty and if so, treating it as undefined/not provided rather than calling validateAdminEmail, to maintain consistency with the UI behavior where email is optional.
tools/common/lib/mu-plugins.ts
Outdated
| @@ -416,18 +416,47 @@ function getStandardMuPlugins( options: MuPluginOptions ): MuPlugin[] { | |||
| exit; | |||
| } | |||
There was a problem hiding this comment.
The MU plugin accepts POST data without CSRF protection or authentication. While this is a local-only endpoint (studio-admin-api), it's important to note that any script running on the local machine could potentially call this endpoint. Consider adding a nonce or token-based authentication mechanism to prevent unauthorized admin credential modification, especially since passwords are being changed. Additionally, verify that the endpoint is only accessible from localhost/127.0.0.1.
| $username = ! empty( $_POST['username'] ) ? sanitize_user( $_POST['username'] ) : 'admin'; | ||
| // Fallback to 'admin' if sanitize_user() strips all characters (e.g., !@#$%) | ||
| if ( empty( $username ) ) { |
There was a problem hiding this comment.
The username from POST is sanitized using sanitize_user(), which is correct. However, there's a fallback to 'admin' if sanitization strips all characters (line 421-422). This could mask validation issues - if a user intentionally sets an invalid username (like '!@#$%'), it silently changes to 'admin' instead of returning an error. Consider returning an error response when the sanitized username is empty but a username was provided, to match the client-side validation behavior.
| $username = ! empty( $_POST['username'] ) ? sanitize_user( $_POST['username'] ) : 'admin'; | |
| // Fallback to 'admin' if sanitize_user() strips all characters (e.g., !@#$%) | |
| if ( empty( $username ) ) { | |
| if ( ! empty( $_POST['username'] ) ) { | |
| $username = sanitize_user( $_POST['username'] ); | |
| // If a username was provided but sanitization removed all characters, treat as invalid input. | |
| if ( $username === '' ) { | |
| status_header( 400 ); | |
| header( 'Content-Type: application/json' ); | |
| echo json_encode( [ 'error' => 'Invalid username' ] ); | |
| exit; | |
| } | |
| } else { | |
| // Default to 'admin' when no username is provided at all. |
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [ pathError, customDomainError, useCustomDomain ] ); | ||
| }, [ pathError, customDomainError, useCustomDomain, adminUsername, adminPassword ] ); |
There was a problem hiding this comment.
The useEffect dependencies list is missing adminEmail, even though adminEmail validation should affect form validity. This means changes to adminEmail won't trigger the validity check. Add adminEmail to the dependency array at line 296.
| }, [ pathError, customDomainError, useCustomDomain, adminUsername, adminPassword ] ); | |
| }, [ pathError, customDomainError, useCustomDomain, adminUsername, adminPassword, adminEmail ] ); |
cli/commands/site/create.ts
Outdated
| const adminEmail = options.adminEmail ?? undefined; | ||
| if ( adminEmail ) { | ||
| const emailError = validateAdminEmail( adminEmail ); | ||
| if ( emailError ) { | ||
| throw new LoggerError( emailError ); | ||
| } | ||
| } |
There was a problem hiding this comment.
When adminEmail is provided but consists only of whitespace, the validateAdminEmail function will return an error message "Admin email cannot be empty." However, this contradicts the fact that email is optional in the UI. Consider checking if adminEmail.trim() is empty and if so, treating it as undefined/not provided rather than calling validateAdminEmail, to maintain consistency with the UI behavior where email is optional.
common/lib/blueprint-settings.ts
Outdated
| @@ -4,6 +4,8 @@ type BlueprintSiteSettings = Partial< | |||
| Pick< StoppedSiteDetails, 'phpVersion' | 'customDomain' | 'enableHttps' > | |||
There was a problem hiding this comment.
The type StoppedSiteDetails is used on line 4 but is not imported in this file. This will cause a TypeScript compilation error. Add the import statement for StoppedSiteDetails from the appropriate module (likely from 'src/ipc-types' or similar).
Could you please provide more testing instruction or/and context for this test? |
|
I also tested email changes and everything works well 👍 |
|
Thanks for the review @nightnei
Yup, this is expected! AFAIK WordPress doesn’t support renaming user_login. So when someone changes the username, we create a new admin user and the old one stays. Added a code comment to make this clearer 👍
A site created before this PR, we would always use |
|
Can we group and name fields in 'Add site' and 'Edit site' forms consistently? |
Done |
| if ( ! $provided_email ) { | ||
| $counter = 1; | ||
| while ( email_exists( $email ) && $counter < 100 ) { | ||
| $email = 'admin' . $counter . '@localhost.com'; |
There was a problem hiding this comment.
I am wondering if we should use something like: $email = 'admin-' . uniqid() . '@localhost.com'; for guaranteed uniqueness. It is just a minor thing though so feel free to skip the suggestion
| wp_update_user( array( 'ID' => $user->ID, 'user_email' => $provided_email ) ); | ||
| } | ||
| } else { | ||
| // WordPress doesn't support renaming user_login, so we create a new admin user. |
There was a problem hiding this comment.
I am wondering if this needs to be communicated to users in some way in the UI 🤔 Could be a notice like: Changing the username will create a new admin user. The previous admin user will remain in the database. but then this might be overengineering 🤔
tools/common/lib/passwords.ts
Outdated
| return __( 'Admin username cannot be empty.' ); | ||
| } | ||
| if ( ! /^[a-zA-Z0-9_.@-]+$/.test( username ) ) { | ||
| return __( 'Username can only contain letters, numbers, and _.@- characters' ); |
There was a problem hiding this comment.
As a sidenote, should this end with a period? It looks like all the other strings contain period
| hasUserEditedCredentials.current = true; | ||
| setAdminEmail( value ); | ||
| } } | ||
| placeholder="admin@localhost.com" |
There was a problem hiding this comment.
I am wondering if instead of a placeholder, we should add below a description such as The email defaults to admin@localhost.com` if not added or something along these lines. This could make it clearer to the user what the default is. Not a strong prefernce on my end though
katinthehatsite
left a comment
There was a problem hiding this comment.
Overall, the functionality looked good to me and I did not find anything major 👍 I left some minor comments for consideration but nothing blocking. I tested with the following file (in case someone wants to use it for testing):
|
@katinthehatsite thanks for the review!
I have decided to keep the error in the field column. Would love another review when you get a chance! 🙏 |







Related issues
Proposed Changes
loginstep from unsupported Blueprint featuresTesting Instructions
login: { username: "custom", password: "test123" }— verify credentials prefill in Advanced settings and work in wp-adminsteps: [{ step: "login", username: "studio-admin", password: "Studio-Admin-Password" }]— verify same behavioradmin/random password behavior preserved--admin-usernameand--admin-password— verify credentials workset --admin-username newadmin --admin-password newpass— verify restart and credentials appliedPre-merge Checklist