feat: add the ability to resync props
This commit is contained in:
parent
8447fdef5d
commit
e57323ff3f
5 changed files with 180 additions and 36 deletions
|
|
@ -88,6 +88,20 @@ pub enum AccountStatus {
|
|||
Deleted,
|
||||
}
|
||||
|
||||
/// User account tag for feature gating and access control.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "ssr", sqlx(type_name = "user_tag", rename_all = "snake_case"))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UserTag {
|
||||
Guest,
|
||||
Unvalidated,
|
||||
ValidatedEmail,
|
||||
ValidatedSocial,
|
||||
ValidatedOauth2,
|
||||
Premium,
|
||||
}
|
||||
|
||||
/// Authentication provider.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
|
||||
|
|
@ -399,6 +413,7 @@ pub struct User {
|
|||
pub reputation_tier: ReputationTier,
|
||||
pub status: AccountStatus,
|
||||
pub email_verified: bool,
|
||||
pub tags: Vec<UserTag>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -1467,7 +1482,7 @@ impl GuestLoginRequest {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GuestLoginResponse {
|
||||
pub guest_name: String,
|
||||
pub guest_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub redirect_url: String,
|
||||
pub realm: RealmSummary,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,6 +164,99 @@ pub async fn create_server_prop<'e>(
|
|||
Ok(prop)
|
||||
}
|
||||
|
||||
/// Upsert a server prop (insert or update on slug conflict).
|
||||
///
|
||||
/// If a prop with the same slug already exists, it will be updated.
|
||||
/// Otherwise, a new prop will be created.
|
||||
pub async fn upsert_server_prop<'e>(
|
||||
executor: impl PgExecutor<'e>,
|
||||
req: &CreateServerPropRequest,
|
||||
asset_path: &str,
|
||||
created_by: Option<Uuid>,
|
||||
) -> Result<ServerProp, AppError> {
|
||||
let slug = req.slug_or_generate();
|
||||
|
||||
// Positioning: either content layer OR emotion layer OR neither (all NULL)
|
||||
// Database constraint enforces mutual exclusivity
|
||||
let (default_layer, default_emotion, default_position) =
|
||||
if req.default_layer.is_some() {
|
||||
// Content layer prop
|
||||
(
|
||||
req.default_layer.map(|l| l.to_string()),
|
||||
None,
|
||||
Some(req.default_position.unwrap_or(4)), // Default to center position
|
||||
)
|
||||
} else if req.default_emotion.is_some() {
|
||||
// Emotion layer prop
|
||||
(
|
||||
None,
|
||||
req.default_emotion.map(|e| e.to_string()),
|
||||
Some(req.default_position.unwrap_or(4)), // Default to center position
|
||||
)
|
||||
} else {
|
||||
// Non-avatar prop
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
let prop = sqlx::query_as::<_, ServerProp>(
|
||||
r#"
|
||||
INSERT INTO server.props (
|
||||
name, slug, description, tags, asset_path,
|
||||
default_layer, default_emotion, default_position,
|
||||
created_by
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6::props.avatar_layer, $7::props.emotion_state, $8,
|
||||
$9
|
||||
)
|
||||
ON CONFLICT (slug) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
tags = EXCLUDED.tags,
|
||||
asset_path = EXCLUDED.asset_path,
|
||||
default_layer = EXCLUDED.default_layer,
|
||||
default_emotion = EXCLUDED.default_emotion,
|
||||
default_position = EXCLUDED.default_position,
|
||||
updated_at = now()
|
||||
RETURNING
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
tags,
|
||||
asset_path,
|
||||
thumbnail_path,
|
||||
default_layer,
|
||||
default_emotion,
|
||||
default_position,
|
||||
is_unique,
|
||||
is_transferable,
|
||||
is_portable,
|
||||
is_droppable,
|
||||
is_active,
|
||||
available_from,
|
||||
available_until,
|
||||
created_by,
|
||||
created_at,
|
||||
updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(&req.name)
|
||||
.bind(&slug)
|
||||
.bind(&req.description)
|
||||
.bind(&req.tags)
|
||||
.bind(asset_path)
|
||||
.bind(&default_layer)
|
||||
.bind(&default_emotion)
|
||||
.bind(default_position)
|
||||
.bind(created_by)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(prop)
|
||||
}
|
||||
|
||||
/// Delete a server prop.
|
||||
pub async fn delete_server_prop<'e>(
|
||||
executor: impl PgExecutor<'e>,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ pub async fn get_user_by_id(pool: &PgPool, id: Uuid) -> Result<Option<User>, App
|
|||
reputation_tier,
|
||||
status,
|
||||
email_verified,
|
||||
tags,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM auth.users
|
||||
|
|
@ -47,6 +48,7 @@ pub async fn get_user_by_username(pool: &PgPool, username: &str) -> Result<Optio
|
|||
reputation_tier,
|
||||
status,
|
||||
email_verified,
|
||||
tags,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM auth.users
|
||||
|
|
@ -74,6 +76,7 @@ pub async fn get_user_by_email(pool: &PgPool, email: &str) -> Result<Option<User
|
|||
reputation_tier,
|
||||
status,
|
||||
email_verified,
|
||||
tags,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM auth.users
|
||||
|
|
@ -182,6 +185,7 @@ pub async fn get_user_by_session(
|
|||
u.reputation_tier,
|
||||
u.status,
|
||||
u.email_verified,
|
||||
u.tags,
|
||||
u.created_at,
|
||||
u.updated_at
|
||||
FROM auth.users u
|
||||
|
|
@ -491,3 +495,27 @@ pub async fn get_staff_member(pool: &PgPool, user_id: Uuid) -> Result<Option<Sta
|
|||
|
||||
Ok(staff)
|
||||
}
|
||||
|
||||
/// Create a guest user (no password required).
|
||||
///
|
||||
/// Guests are created with the 'guest' tag and no password.
|
||||
/// The database trigger `trg_auth_users_initialize` automatically creates
|
||||
/// their default avatar and inventory.
|
||||
pub async fn create_guest_user(pool: &PgPool, guest_name: &str) -> Result<Uuid, AppError> {
|
||||
// Generate unique username from guest_name (e.g., "guest12345")
|
||||
let username = guest_name.to_lowercase().replace('_', "");
|
||||
|
||||
let (user_id,): (Uuid,) = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO auth.users (username, display_name, tags)
|
||||
VALUES ($1, $2, ARRAY['guest']::auth.user_tag[])
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(&username)
|
||||
.bind(guest_name)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(user_id)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue