fix: guests
* make guest status a flag on users * add logout handlers * add logout notification for other users
This commit is contained in:
parent
23630b19b2
commit
60a6680eaf
21 changed files with 523 additions and 601 deletions
|
|
@ -3041,13 +3041,13 @@ pub struct SpotListResponse {
|
|||
// =============================================================================
|
||||
|
||||
/// A user's presence in a channel.
|
||||
/// Note: Guests are regular users with the 'guest' tag, so all members have a user_id.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct ChannelMember {
|
||||
pub id: Uuid,
|
||||
pub channel_id: Uuid,
|
||||
pub user_id: Option<Uuid>,
|
||||
pub guest_session_id: Option<Uuid>,
|
||||
pub user_id: Uuid,
|
||||
/// X coordinate in scene space
|
||||
pub position_x: f64,
|
||||
/// Y coordinate in scene space
|
||||
|
|
@ -3061,14 +3061,14 @@ pub struct ChannelMember {
|
|||
}
|
||||
|
||||
/// Channel member with user info for display.
|
||||
/// Note: Guests are regular users with the 'guest' tag, identified via is_guest field.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct ChannelMemberInfo {
|
||||
pub id: Uuid,
|
||||
pub channel_id: Uuid,
|
||||
pub user_id: Option<Uuid>,
|
||||
pub guest_session_id: Option<Uuid>,
|
||||
/// Display name (user's display_name or guest's guest_name)
|
||||
pub user_id: Uuid,
|
||||
/// Display name (user's display_name)
|
||||
pub display_name: String,
|
||||
/// X coordinate in scene space
|
||||
pub position_x: f64,
|
||||
|
|
|
|||
|
|
@ -62,23 +62,3 @@ pub async fn clear_user_context(pool: &PgPool) -> Result<(), AppError> {
|
|||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the current guest session context for Row-Level Security.
|
||||
///
|
||||
/// This should be called for guest users to enable RLS policies
|
||||
/// that depend on the current guest session ID.
|
||||
pub async fn set_guest_context(pool: &PgPool, guest_session_id: Uuid) -> Result<(), AppError> {
|
||||
sqlx::query("SELECT public.set_current_guest_session_id($1)")
|
||||
.bind(guest_session_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the current guest session context.
|
||||
pub async fn clear_guest_context(pool: &PgPool) -> Result<(), AppError> {
|
||||
sqlx::query("SELECT public.set_current_guest_session_id(NULL)")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
pub mod avatars;
|
||||
pub mod channel_members;
|
||||
pub mod channels;
|
||||
pub mod guests;
|
||||
pub mod inventory;
|
||||
pub mod loose_props;
|
||||
pub mod memberships;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ pub async fn join_channel<'e>(
|
|||
id,
|
||||
instance_id as channel_id,
|
||||
user_id,
|
||||
guest_session_id,
|
||||
ST_X(position) as position_x,
|
||||
ST_Y(position) as position_y,
|
||||
facing_direction,
|
||||
|
|
@ -169,8 +168,7 @@ pub async fn get_channel_members<'e>(
|
|||
cm.id,
|
||||
cm.instance_id as channel_id,
|
||||
cm.user_id,
|
||||
cm.guest_session_id,
|
||||
COALESCE(u.display_name, gs.guest_name, 'Anonymous') as display_name,
|
||||
COALESCE(u.display_name, 'Anonymous') as display_name,
|
||||
ST_X(cm.position) as position_x,
|
||||
ST_Y(cm.position) as position_y,
|
||||
cm.facing_direction,
|
||||
|
|
@ -181,7 +179,6 @@ pub async fn get_channel_members<'e>(
|
|||
COALESCE('guest' = ANY(u.tags), false) as is_guest
|
||||
FROM scene.instance_members cm
|
||||
LEFT JOIN auth.users u ON cm.user_id = u.id
|
||||
LEFT JOIN auth.guest_sessions gs ON cm.guest_session_id = gs.id
|
||||
LEFT JOIN auth.active_avatars aa ON cm.user_id = aa.user_id AND aa.realm_id = $2
|
||||
WHERE cm.instance_id = $1
|
||||
ORDER BY cm.joined_at ASC
|
||||
|
|
@ -208,7 +205,6 @@ pub async fn get_channel_member<'e>(
|
|||
cm.id,
|
||||
cm.instance_id as channel_id,
|
||||
cm.user_id,
|
||||
cm.guest_session_id,
|
||||
COALESCE(u.display_name, 'Anonymous') as display_name,
|
||||
ST_X(cm.position) as position_x,
|
||||
ST_Y(cm.position) as position_y,
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
//! Guest session database queries.
|
||||
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use chattyness_error::AppError;
|
||||
|
||||
/// Guest session record.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct GuestSession {
|
||||
pub id: Uuid,
|
||||
pub guest_name: String,
|
||||
pub current_realm_id: Option<Uuid>,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Create a new guest session.
|
||||
///
|
||||
/// Returns the guest session ID.
|
||||
pub async fn create_guest_session(
|
||||
pool: &PgPool,
|
||||
guest_name: &str,
|
||||
realm_id: Uuid,
|
||||
token_hash: &str,
|
||||
user_agent: Option<&str>,
|
||||
ip_address: Option<&str>,
|
||||
expires_at: DateTime<Utc>,
|
||||
) -> Result<Uuid, AppError> {
|
||||
let (session_id,): (Uuid,) = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO auth.guest_sessions (guest_name, token_hash, user_agent, ip_address, current_realm_id, expires_at)
|
||||
VALUES ($1, $2, $3, $4::inet, $5, $6)
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(guest_name)
|
||||
.bind(token_hash)
|
||||
.bind(user_agent)
|
||||
.bind(ip_address)
|
||||
.bind(realm_id)
|
||||
.bind(expires_at)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(session_id)
|
||||
}
|
||||
|
||||
/// Get a guest session by ID.
|
||||
pub async fn get_guest_session(
|
||||
pool: &PgPool,
|
||||
session_id: Uuid,
|
||||
) -> Result<Option<GuestSession>, AppError> {
|
||||
let session = sqlx::query_as::<_, GuestSession>(
|
||||
r#"
|
||||
SELECT id, guest_name, current_realm_id, expires_at, created_at
|
||||
FROM auth.guest_sessions
|
||||
WHERE id = $1 AND expires_at > now()
|
||||
"#,
|
||||
)
|
||||
.bind(session_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
// Update last activity if session exists
|
||||
if session.is_some() {
|
||||
sqlx::query("UPDATE auth.guest_sessions SET last_activity_at = now() WHERE id = $1")
|
||||
.bind(session_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Delete a guest session.
|
||||
pub async fn delete_guest_session(pool: &PgPool, session_id: Uuid) -> Result<(), AppError> {
|
||||
sqlx::query("DELETE FROM auth.guest_sessions WHERE id = $1")
|
||||
.bind(session_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a random guest name like "Guest_12345".
|
||||
pub fn generate_guest_name() -> String {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let number: u32 = rng.gen_range(10000..100000);
|
||||
format!("Guest_{}", number)
|
||||
}
|
||||
|
||||
/// Calculate guest session expiry (24 hours from now).
|
||||
pub fn guest_session_expiry() -> DateTime<Utc> {
|
||||
Utc::now() + TimeDelta::hours(24)
|
||||
}
|
||||
|
|
@ -716,3 +716,11 @@ pub async fn update_user_preferences_conn(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a random guest name like "Guest_12345".
|
||||
pub fn generate_guest_name() -> String {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let number: u32 = rng.gen_range(10000..100000);
|
||||
format!("Guest_{}", number)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ pub mod close_codes {
|
|||
pub const SCENE_CHANGE: u16 = 4000;
|
||||
/// Server timeout (no message received within timeout period).
|
||||
pub const SERVER_TIMEOUT: u16 = 4001;
|
||||
/// User explicitly logged out.
|
||||
pub const LOGOUT: u16 = 4003;
|
||||
}
|
||||
|
||||
/// Reason for member disconnect.
|
||||
|
|
@ -126,20 +128,16 @@ pub enum ServerMessage {
|
|||
|
||||
/// A member left the channel.
|
||||
MemberLeft {
|
||||
/// User ID (if authenticated user).
|
||||
user_id: Option<Uuid>,
|
||||
/// Guest session ID (if guest).
|
||||
guest_session_id: Option<Uuid>,
|
||||
/// User ID of the member who left.
|
||||
user_id: Uuid,
|
||||
/// Reason for disconnect.
|
||||
reason: DisconnectReason,
|
||||
},
|
||||
|
||||
/// A member updated their position.
|
||||
PositionUpdated {
|
||||
/// User ID (if authenticated user).
|
||||
user_id: Option<Uuid>,
|
||||
/// Guest session ID (if guest).
|
||||
guest_session_id: Option<Uuid>,
|
||||
/// User ID of the member.
|
||||
user_id: Uuid,
|
||||
/// New X coordinate.
|
||||
x: f64,
|
||||
/// New Y coordinate.
|
||||
|
|
@ -148,10 +146,8 @@ pub enum ServerMessage {
|
|||
|
||||
/// A member changed their emotion.
|
||||
EmotionUpdated {
|
||||
/// User ID (if authenticated user).
|
||||
user_id: Option<Uuid>,
|
||||
/// Guest session ID (if guest).
|
||||
guest_session_id: Option<Uuid>,
|
||||
/// User ID of the member.
|
||||
user_id: Uuid,
|
||||
/// Emotion name (e.g., "happy", "sad", "neutral").
|
||||
emotion: String,
|
||||
/// Asset paths for all 9 positions of the new emotion layer.
|
||||
|
|
@ -173,10 +169,8 @@ pub enum ServerMessage {
|
|||
ChatMessageReceived {
|
||||
/// Unique message ID.
|
||||
message_id: Uuid,
|
||||
/// User ID of sender (if authenticated user).
|
||||
user_id: Option<Uuid>,
|
||||
/// Guest session ID (if guest).
|
||||
guest_session_id: Option<Uuid>,
|
||||
/// User ID of sender.
|
||||
user_id: Uuid,
|
||||
/// Display name of sender.
|
||||
display_name: String,
|
||||
/// Message content.
|
||||
|
|
@ -217,10 +211,8 @@ pub enum ServerMessage {
|
|||
PropPickedUp {
|
||||
/// ID of the prop that was picked up.
|
||||
prop_id: Uuid,
|
||||
/// User ID who picked it up (if authenticated).
|
||||
picked_up_by_user_id: Option<Uuid>,
|
||||
/// Guest session ID who picked it up (if guest).
|
||||
picked_up_by_guest_id: Option<Uuid>,
|
||||
/// User ID who picked it up.
|
||||
picked_up_by_user_id: Uuid,
|
||||
},
|
||||
|
||||
/// A prop expired and was removed.
|
||||
|
|
@ -231,10 +223,8 @@ pub enum ServerMessage {
|
|||
|
||||
/// A member updated their avatar appearance.
|
||||
AvatarUpdated {
|
||||
/// User ID (if authenticated user).
|
||||
user_id: Option<Uuid>,
|
||||
/// Guest session ID (if guest).
|
||||
guest_session_id: Option<Uuid>,
|
||||
/// User ID of the member.
|
||||
user_id: Uuid,
|
||||
/// Updated avatar render data.
|
||||
avatar: AvatarRenderData,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue