add the ability to register from inside the user-ui

This commit is contained in:
Evan Carroll 2026-01-21 00:11:50 -06:00
parent 31e01292f9
commit ed1a1f10f9
12 changed files with 655 additions and 5 deletions

View file

@ -1619,6 +1619,43 @@ pub struct GuestLoginResponse {
pub realm: RealmSummary,
}
/// Request to register a guest as a full user.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterGuestRequest {
pub username: String,
pub email: Option<String>,
pub password: String,
pub confirm_password: String,
}
#[cfg(feature = "ssr")]
impl RegisterGuestRequest {
/// Validate the registration request.
pub fn validate(&self) -> Result<(), AppError> {
validation::validate_username(&self.username)?;
// Email: basic format if provided and non-empty
if let Some(ref email) = self.email {
let email_trimmed = email.trim();
if !email_trimmed.is_empty() && !validation::is_valid_email(email_trimmed) {
return Err(AppError::Validation("Invalid email address".to_string()));
}
}
validation::validate_password(&self.password)?;
validation::validate_passwords_match(&self.password, &self.confirm_password)?;
Ok(())
}
}
/// Response after successful guest registration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterGuestResponse {
pub success: bool,
pub username: String,
}
/// Request to join a realm.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JoinRealmRequest {

View file

@ -1,6 +1,6 @@
//! User-related database queries.
use sqlx::PgPool;
use sqlx::{PgConnection, PgPool};
use uuid::Uuid;
use crate::models::{StaffMember, User, UserWithAuth};
@ -499,6 +499,74 @@ pub async fn get_staff_member(
Ok(staff)
}
/// Upgrade a guest user to a full user account.
///
/// Updates the user's username, password, and optionally email,
/// and removes the 'guest' tag. Returns error if the user is not a guest.
pub async fn upgrade_guest_to_user(
pool: &PgPool,
user_id: Uuid,
username: &str,
password: &str,
email: Option<&str>,
) -> Result<(), AppError> {
let mut conn = pool.acquire().await?;
upgrade_guest_to_user_conn(&mut *conn, user_id, username, password, email).await
}
/// Upgrade a guest user to a full user account using an existing connection.
///
/// Updates the user's username, password, and optionally email,
/// and removes the 'guest' tag. Returns error if the user is not a guest.
/// Use this version when you need RLS context set on the connection.
pub async fn upgrade_guest_to_user_conn(
conn: &mut PgConnection,
user_id: Uuid,
username: &str,
password: &str,
email: Option<&str>,
) -> Result<(), AppError> {
use argon2::{
Argon2, PasswordHasher,
password_hash::{SaltString, rand_core::OsRng},
};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| AppError::Internal(format!("Failed to hash password: {}", e)))?
.to_string();
let result: sqlx::postgres::PgQueryResult = sqlx::query(
r#"
UPDATE auth.users
SET username = $2,
password_hash = $3,
email = $4,
display_name = $2,
tags = array_remove(tags, 'guest'),
auth_provider = 'local',
updated_at = now()
WHERE id = $1 AND 'guest' = ANY(tags)
"#,
)
.bind(user_id)
.bind(username)
.bind(&password_hash)
.bind(email)
.execute(&mut *conn)
.await?;
if result.rows_affected() == 0 {
return Err(AppError::Forbidden(
"User is not a guest or does not exist".to_string(),
));
}
Ok(())
}
/// Create a guest user (no password required).
///
/// Guests are created with the 'guest' tag and no password.

View file

@ -98,6 +98,10 @@ pub enum ClientMessage {
/// Arguments for the subcommand.
args: Vec<String>,
},
/// Request to refresh identity after registration (guest → user conversion).
/// Server will fetch updated user data and broadcast to all members.
RefreshIdentity,
}
/// Server-to-client WebSocket messages.
@ -260,4 +264,14 @@ pub enum ServerMessage {
/// Human-readable result message.
message: String,
},
/// A member's identity was updated (e.g., guest registered as user).
MemberIdentityUpdated {
/// User ID of the member.
user_id: Uuid,
/// New display name.
display_name: String,
/// Whether the member is still a guest.
is_guest: bool,
},
}