feat(manager-core,picloud): accept email on admin create + patch
The /admins create/patch endpoints now plumb email through to the repo so the dashboard's invite + edit forms aren't silently dropping it on the floor. Discovered during smoke testing — the database column existed and was exposed in the response DTO, but neither the request DTO nor the repo's create() accepted it. CreateAdminRequest gains optional email; PatchAdminRequest gains email with JSON Merge Patch semantics: absent → don't change null → clear (write NULL) "<string>" → set to that value The tri-state needs Option<Option<String>> with a tiny custom deserializer; serde collapses absent and null otherwise. normalize_email() trims, treats blanks as None, and rejects obviously bogus values (no '@', >254 chars) with a 422. Real email verification is a future concern. Repo trait gains an email parameter on create() and a new update_email() method. The unique-violation branch in create now inspects constraint() to distinguish duplicate username from duplicate email. Integration test exercises create-with-email, PATCH null clears, PATCH value sets, PATCH without email key no-ops on email. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,7 @@ pub async fn bootstrap_first_admin_with<R: AdminUserRepository + ?Sized>(
|
||||
&username,
|
||||
&password_hash,
|
||||
picloud_shared::InstanceRole::Owner,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
info!(username = %username, "bootstrapped initial admin user");
|
||||
@@ -176,13 +177,14 @@ mod tests {
|
||||
username: &str,
|
||||
_password_hash: &str,
|
||||
instance_role: InstanceRole,
|
||||
email: Option<&str>,
|
||||
) -> Result<AdminUserRow, AdminUserRepositoryError> {
|
||||
let row = AdminUserRow {
|
||||
id: AdminUserId::new(),
|
||||
username: username.to_string(),
|
||||
is_active: true,
|
||||
instance_role,
|
||||
email: None,
|
||||
email: email.map(str::to_string),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
last_login_at: None,
|
||||
@@ -204,6 +206,13 @@ mod tests {
|
||||
) -> Result<AdminUserRow, AdminUserRepositoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn update_email(
|
||||
&self,
|
||||
_i: AdminUserId,
|
||||
_e: Option<&str>,
|
||||
) -> Result<AdminUserRow, AdminUserRepositoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn update_instance_role(
|
||||
&self,
|
||||
_i: AdminUserId,
|
||||
@@ -272,7 +281,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn populated_db_is_noop() {
|
||||
let repo = InMemoryRepo::default();
|
||||
repo.create("seeded", "x", InstanceRole::Owner)
|
||||
repo.create("seeded", "x", InstanceRole::Owner, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let env = BootstrapEnv {
|
||||
|
||||
Reference in New Issue
Block a user