add the ability to register from inside the user-ui
This commit is contained in:
parent
31e01292f9
commit
ed1a1f10f9
12 changed files with 655 additions and 5 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue