Skip to content

Add support for Blueprints login#2527

Open
bcotrim wants to merge 29 commits intotrunkfrom
bcotrim/stu-829-blueprints-login
Open

Add support for Blueprints login#2527
bcotrim wants to merge 29 commits intotrunkfrom
bcotrim/stu-829-blueprints-login

Conversation

@bcotrim
Copy link
Contributor

@bcotrim bcotrim commented Feb 3, 2026

Related issues

Proposed Changes

  • Remove login step from unsupported Blueprint features
  • Allow creating a site with custom admin username and password
  • Allow editing a site with custom admin username and password
Create site Edit site
image image

Testing Instructions

  • Create site with Blueprint containing login: { username: "custom", password: "test123" } — verify credentials prefill in Advanced settings and work in wp-admin
  • Create site with Blueprint containing steps: [{ step: "login", username: "studio-admin", password: "Studio-Admin-Password" }] — verify same behavior
  • Create site without Blueprint — verify default admin/random password behavior preserved
  • Modify pre-filled admin credentials in Advanced settings — verify form values override Blueprint
  • Create site via CLI with --admin-username and --admin-password — verify credentials work
  • Edit existing site credentials via Settings modal — verify restart prompt and credentials applied
  • Edit existing site credentials via CLI set --admin-username newadmin --admin-password newpass — verify restart and credentials applied
  • Change username on a site that already has a custom username — verify new user created, old user left intact, auto-login works
  • Try invalid username (special chars, >60 chars, empty) — verify validation errors in both UI and CLI
  • Edit a legacy site (no stored credentials) without changing credentials — verify no unnecessary restart
  • Verify existing sites, import/export, and sync functionality unaffected

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@bcotrim bcotrim marked this pull request as draft February 3, 2026 18:34
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Feb 3, 2026

📊 Performance Test Results

Comparing 8f06c9d vs trunk

site-editor

Metric trunk 8f06c9d Diff Change
load 1397.00 ms 1438.00 ms +41.00 ms ⚪ 0.0%

site-startup

Metric trunk 8f06c9d Diff Change
siteCreation 7099.00 ms 7076.00 ms -23.00 ms ⚪ 0.0%
siteStartup 3948.00 ms 3941.00 ms -7.00 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@bcotrim bcotrim force-pushed the bcotrim/stu-829-blueprints-login branch 3 times, most recently from e61ae98 to 464cfac Compare February 4, 2026 15:02
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>
@bcotrim bcotrim force-pushed the bcotrim/stu-829-blueprints-login branch from 464cfac to becd91f Compare February 4, 2026 15:12
@bcotrim bcotrim self-assigned this Feb 5, 2026
@bcotrim bcotrim marked this pull request as ready for review February 5, 2026 13:40
@bcotrim bcotrim requested a review from a team February 5, 2026 13:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +434 to +442
// 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++;
}
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
if ( ! email.trim() ) {
return __( 'Admin email cannot be empty.' );
}
if ( ! /^[^\s@]+@[^\s@]+$/.test( email ) ) {
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
if ( ! /^[^\s@]+@[^\s@]+$/.test( email ) ) {
if ( ! /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( email ) ) {

Copilot uses AI. Check for mistakes.
Comment on lines 283 to 287
const hasErrors =
!! pathError ||
( useCustomDomain && !! customDomainError ) ||
! adminUsername.trim() ||
! adminPassword.trim();
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines 90 to 95
if ( adminEmail !== undefined ) {
const emailError = validateAdminEmail( adminEmail );
if ( emailError ) {
throw new LoggerError( emailError );
}
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines 399 to 417
@@ -416,18 +416,47 @@ function getStandardMuPlugins( options: MuPluginOptions ): MuPlugin[] {
exit;
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +419 to +421
$username = ! empty( $_POST['username'] ) ? sanitize_user( $_POST['username'] ) : 'admin';
// Fallback to 'admin' if sanitize_user() strips all characters (e.g., !@#$%)
if ( empty( $username ) ) {
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
$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.

Copilot uses AI. Check for mistakes.
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ pathError, customDomainError, useCustomDomain ] );
}, [ pathError, customDomainError, useCustomDomain, adminUsername, adminPassword ] );
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
}, [ pathError, customDomainError, useCustomDomain, adminUsername, adminPassword ] );
}, [ pathError, customDomainError, useCustomDomain, adminUsername, adminPassword, adminEmail ] );

Copilot uses AI. Check for mistakes.
Comment on lines 217 to 223
const adminEmail = options.adminEmail ?? undefined;
if ( adminEmail ) {
const emailError = validateAdminEmail( adminEmail );
if ( emailError ) {
throw new LoggerError( emailError );
}
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 4
@@ -4,6 +4,8 @@ type BlueprintSiteSettings = Partial<
Pick< StoppedSiteDetails, 'phpVersion' | 'customDomain' | 'enableHttps' >
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
@nightnei
Copy link
Contributor

Change username on a site that already has a custom username — verify new user created, old user left intact, auto-login works

I see this
Screenshot 2026-02-12 at 14 30 03
Is it expected?

@nightnei
Copy link
Contributor

I see that updating username creates a new user, is it expected?
I had expectation that the old one will be changed.
Screenshot 2026-02-12 at 14 31 18

Maybe we should add some comment here. WDYT?
Screenshot 2026-02-12 at 14 31 47

@nightnei
Copy link
Contributor

Edit a legacy site (no stored credentials) without changing credentials — verify no unnecessary restart

Could you please provide more testing instruction or/and context for this test?

@nightnei
Copy link
Contributor

I also tested email changes and everything works well 👍
Shallowly reviewed code changes - all LGTM 👍
Just left a few comments

@bcotrim
Copy link
Contributor Author

bcotrim commented Feb 12, 2026

Thanks for the review @nightnei

Change username on a site that already has a custom username — verify new user created, old user left intact, auto-login works

I see this Screenshot 2026-02-12 at 14 30 03 Is it expected?

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 👍

Could you please provide more testing instruction or/and context for this test?

A site created before this PR, we would always use admin admin@localhost.com and a generated password. Unrelated changes to the site settings (like site name for example) should not prompt for a server restart and have no side effects on user login.

@wojtekn
Copy link
Contributor

wojtekn commented Feb 13, 2026

Can we group and name fields in 'Add site' and 'Edit site' forms consistently?

@bcotrim
Copy link
Contributor Author

bcotrim commented Feb 13, 2026

Can we group and name fields in 'Add site' and 'Edit site' forms consistently?

Done

@bcotrim
Copy link
Contributor Author

bcotrim commented Feb 18, 2026

@wojtekn @nightnei could you give this PR another review please?

if ( ! $provided_email ) {
$counter = 1;
while ( email_exists( $email ) && $counter < 100 ) {
$email = 'admin' . $counter . '@localhost.com';
Copy link
Contributor

Choose a reason for hiding this comment

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

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

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 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

The reason why I was asking this is because the user could still see it on their local site which could be potentially confusing:

Image

return __( 'Admin username cannot be empty.' );
}
if ( ! /^[a-zA-Z0-9_.@-]+$/.test( username ) ) {
return __( 'Username can only contain letters, numbers, and _.@- characters' );
Copy link
Contributor

Choose a reason for hiding this comment

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

I am wondering if we should stretch this along one line even if it goes under Password field. I think it looks a bit cut off now and I am not seeing any harm if it streches out. This will make it consistent with other

Image

Copy link
Contributor

Choose a reason for hiding this comment

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

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"
Copy link
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Contributor

@katinthehatsite katinthehatsite left a comment

Choose a reason for hiding this comment

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

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):

blueprints.json

Copy link
Contributor

@nightnei nightnei left a comment

Choose a reason for hiding this comment

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

Thanks @bcotrim, everything LGTM 👍
Also, I like the comment from Kat about adding a description to the UI, that the new user will be created. I would add it.

@bcotrim
Copy link
Contributor Author

bcotrim commented Feb 20, 2026

@katinthehatsite thanks for the review!
I've addressed most of the feedback. Regarding the username validation error I tried it but it looks strange if both username and password have validation errors:

image

I have decided to keep the error in the field column.

Would love another review when you get a chance! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants