reshape app state endpoint

This commit is contained in:
Brandon Thomas committed 2024-09-10 23:13:51 -04:00
1 parent 87f70d507f
commit 89ff405318
5 files changed
+211 -57

No files matched your search

@@ -0,0 +1,107 @@
use crate::http_server::session::lookup::user_session_extended::UserSessionExtended;
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
use std::collections::BTreeSet;
use utoipa::ToSchema;
#[derive(Serialize, ToSchema, Clone)]
pub struct AppStatePermissions {
/// Moderation flag.
/// Use this to determine if an account can act on other accounts.
pub is_moderator: bool,
/// Collection of feature / rollout flags
/// This is the proper place to detect if a user has access to some rollout (non-paywall) feature.
/// NB: The BTreeSet maintains order so React doesn't introduce re-render state bugs when order changes
pub feature_flags: BTreeSet<UserFeatureFlag>,
/// Legacy feature flags. Do not use.
#[deprecated(note = "DO NOT USE. This is only to port old logic.")]
pub legacy_permission_flags: AppStateLegacyPermissionFlags,
}
#[derive(Serialize, ToSchema, Clone)]
pub struct AppStateLegacyPermissionFlags {
// Usage permissions:
pub can_use_tts: bool,
pub can_use_w2l: bool,
pub can_delete_own_tts_results: bool,
pub can_delete_own_w2l_results: bool,
pub can_delete_own_account: bool,
// Contribution permissions:
pub can_upload_tts_models: bool,
pub can_upload_w2l_templates: bool,
pub can_delete_own_tts_models: bool,
pub can_delete_own_w2l_templates: bool,
// Moderation permissions:
pub can_approve_w2l_templates: bool,
pub can_edit_other_users_profiles: bool,
pub can_edit_other_users_tts_models: bool,
pub can_edit_other_users_w2l_templates: bool,
pub can_delete_other_users_tts_models: bool,
pub can_delete_other_users_tts_results: bool,
pub can_delete_other_users_w2l_templates: bool,
pub can_delete_other_users_w2l_results: bool,
pub can_ban_users: bool,
pub can_delete_users: bool,
}
pub fn get_permissions(
maybe_user: Option<&UserSessionExtended>
) -> AppStatePermissions {
match maybe_user {
None => LOGGED_OUT_PERMISSIONS.clone(),
Some(user) => AppStatePermissions {
is_moderator: user.role.user_role_slug == "moderator",
feature_flags: user.feature_flags.clone_flags(),
legacy_permission_flags: AppStateLegacyPermissionFlags {
can_use_tts: user.role.can_use_tts,
can_use_w2l: user.role.can_use_w2l,
can_delete_own_tts_results: user.role.can_delete_own_tts_results,
can_delete_own_w2l_results: user.role.can_delete_own_w2l_results,
can_delete_own_account: user.role.can_delete_own_account,
can_upload_tts_models: user.role.can_upload_tts_models,
can_upload_w2l_templates: user.role.can_upload_w2l_templates,
can_delete_own_tts_models: user.role.can_delete_own_tts_models,
can_delete_own_w2l_templates: user.role.can_delete_own_w2l_templates,
can_approve_w2l_templates: user.role.can_approve_w2l_templates,
can_edit_other_users_profiles: user.role.can_edit_other_users_profiles,
can_edit_other_users_tts_models: user.role.can_edit_other_users_tts_models,
can_edit_other_users_w2l_templates: user.role.can_edit_other_users_w2l_templates,
can_delete_other_users_tts_models: user.role.can_delete_other_users_tts_models,
can_delete_other_users_tts_results: user.role.can_delete_other_users_tts_results,
can_delete_other_users_w2l_templates: user.role.can_delete_other_users_w2l_templates,
can_delete_other_users_w2l_results: user.role.can_delete_other_users_w2l_results,
can_ban_users: user.role.can_ban_users,
can_delete_users: user.role.can_delete_users,
},
},
}
}
const LOGGED_OUT_PERMISSIONS : AppStatePermissions = AppStatePermissions {
is_moderator: false,
feature_flags: BTreeSet::new(),
legacy_permission_flags: AppStateLegacyPermissionFlags {
can_use_tts: false,
can_use_w2l: false,
can_delete_own_tts_results: false,
can_delete_own_w2l_results: false,
can_delete_own_account: false,
can_upload_tts_models: false,
can_upload_w2l_templates: false,
can_delete_own_tts_models: false,
can_delete_own_w2l_templates: false,
can_approve_w2l_templates: false,
can_edit_other_users_profiles: false,
can_edit_other_users_tts_models: false,
can_edit_other_users_w2l_templates: false,
can_delete_other_users_tts_models: false,
can_delete_other_users_tts_results: false,
can_delete_other_users_w2l_templates: false,
can_delete_other_users_w2l_results: false,
can_ban_users: false,
can_delete_users: false,
},
};
@@ -1,5 +1,7 @@
use billing_component::stripe::traits::internal_user_lookup::UserMetadata;
use utoipa::ToSchema;
use mysql_queries::queries::users::user_sessions::get_user_session_by_token::SessionUserRecord;
use crate::http_server::session::lookup::user_session_extended::UserSessionExtended;
#[derive(Serialize, ToSchema)]
pub struct AppStatePremiumInfo {
@@ -7,6 +9,10 @@ pub struct AppStatePremiumInfo {
/// having *either* a loyalty perk or a paid subscription.
pub user_has_premium: bool,
/// If the user has free premium perks.
/// This is from free loyalty program perks.
pub user_has_free_premium: bool,
/// If the user has paid for a premium subscription, this will
/// be true. Loyalty perks are not considered paid.
pub user_has_paid_premium: bool,
@@ -29,28 +35,33 @@ pub struct AppStateSubscriptionProductKey {
}
pub fn get_premium_info(
user_metadata: &UserMetadata,
user_metadata: &UserSessionExtended,
) -> AppStatePremiumInfo {
let maybe_loyalty_program = user_metadata
.premium
.maybe_loyalty_program_key
.as_deref()
.map(|lp| lp.to_string());
let active_subscriptions = user_metadata
.existing_subscription_keys
.premium
.subscription_plans
.iter()
.map(|sub| AppStateSubscriptionProductKey {
namespace: sub.internal_subscription_namespace.to_string(),
product_slug: sub.internal_subscription_product_slug.to_string(),
// TODO: Is this correct? Should it be externally facing?
namespace: sub.subscription_namespace.to_string(),
product_slug: sub.subscription_product_slug.to_string(),
})
.collect::<Vec<_>>();
let user_has_paid_premium = !active_subscriptions.is_empty();
let user_has_premium = user_has_paid_premium || maybe_loyalty_program.is_some();
let user_has_free_premium = maybe_loyalty_program.is_some();
let user_has_premium = user_has_paid_premium || user_has_free_premium;
AppStatePremiumInfo {
user_has_premium,
user_has_free_premium,
user_has_paid_premium,
maybe_loyalty_program,
active_subscriptions,
@@ -0,0 +1,42 @@
use crate::http_server::common_responses::user_details_lite::UserDefaultAvatarInfo;
use crate::http_server::session::lookup::user_session_extended::UserSessionExtended;
use tokens::tokens::users::UserToken;
use utoipa::ToSchema;
/// Information on the logged-in user's account.
/// (This is a copy of the `UserDetailsLight` struct.)
#[derive(Serialize, ToSchema)]
pub struct AppStateUserInfo {
/// The token for the user
pub user_token: UserToken,
/// The unique username someone logs in with
/// As of 2023-08-23, this is always lowercase
pub username: String,
/// As of 2023-08-23, this is the username with capitalization
/// (In the future, a display name can be customized by the user.)
pub display_name: String,
/// Email hash for Gravatar
/// Always set for now since login is email/username+password.
/// In the future this will need to become an optional *OR* be filled with a bogus hash.
pub gravatar_hash: String,
/// For users without a gravatar, we show one of our own avatars.
pub default_avatar: UserDefaultAvatarInfo,
// In the future, we'll also support user-uploaded avatars that we store on our servers.
}
pub fn get_user_info(
user: &UserSessionExtended
) -> AppStateUserInfo {
AppStateUserInfo {
default_avatar: UserDefaultAvatarInfo::from_username(&user.user.username),
user_token: user.user_token_typed.clone(),
username: user.user.username.to_string(),
display_name: user.user.display_name.to_string(),
gravatar_hash: user.user.email_gravatar_hash.to_string(),
}
}
@@ -1,4 +1,6 @@
pub mod get_permissions;
pub mod get_premium_info;
pub mod get_server_info;
pub mod get_status_alert;
pub mod get_user_info;
pub mod get_user_locale;
@@ -1,6 +1,8 @@
use crate::http_server::endpoints::app_state::components::get_permissions::{get_permissions, AppStatePermissions};
use crate::http_server::endpoints::app_state::components::get_premium_info::{get_premium_info, AppStatePremiumInfo};
use crate::http_server::endpoints::app_state::components::get_server_info::{get_server_info, AppStateServerInfo};
use crate::http_server::endpoints::app_state::components::get_status_alert::{get_status_alert, AppStateStatusAlertInfo};
use crate::http_server::endpoints::app_state::components::get_user_info::{get_user_info, AppStateUserInfo};
use crate::http_server::endpoints::app_state::components::get_user_locale::{get_user_locale, AppStateUserLocale};
use crate::state::server_state::ServerState;
use actix_web::http::StatusCode;
@@ -10,6 +12,7 @@ use billing_component::stripe::traits::internal_user_lookup::InternalUserLookup;
use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::ListActiveUserSubscriptionsError;
use enums::by_table::user_ratings::entity_type::UserRatingEntityType;
use enums::by_table::user_ratings::rating_value::UserRatingValue;
use hostname::get;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use log::{error, info};
use mysql_queries::composite_keys::by_table::user_ratings::user_rating_entity::UserRatingEntity;
@@ -32,11 +35,12 @@ const REFRESH_INTERVAL: Duration = Duration::from_secs(60);
// - server_info (once)
// - detect_locale (once)
// - status_alert_check (60 seconds)
// TODO:
// - active_subscriptions
// TODO:
// - session
#[derive(Serialize, ToSchema)]
pub struct AppStateResponse {
/// All endpoints return `success = true` on 200.
pub success: bool,
/// Tell the frontend client how fast to refresh their view of this state.
@@ -57,35 +61,43 @@ pub struct AppStateResponse {
pub maybe_alert: Option<AppStateStatusAlertInfo>,
/// Information on user locale (language codes, etc.)
pub user_locale: AppStateUserLocale,
pub locale: AppStateUserLocale,
/// Whether the user is logged in.
pub logged_in: bool,
/// If the user is logged into an account with a valid session, this will
/// contain the user's account info.
pub maybe_user: Option<AppStateUserAccountInfo>,
pub maybe_user_info: Option<AppStateUserInfo>,
/// Information on user permissions.
pub permissions: AppStatePermissions,
/// Contains details oof the user's premium subscription status.
pub maybe_premium: Option<AppStatePremiumInfo>,
}
/// User account information.
/// This is only for valid logged-in users.
#[derive(Serialize, ToSchema)]
pub struct AppStateUserAccountInfo {
/// Details on the user's premium account status.
pub premium: AppStatePremiumInfo,
}
// /// User account information.
// /// This is only for valid logged-in users.
// #[derive(Serialize, ToSchema)]
// pub struct AppStateUserAccountInfo {
// /// Information on the user's account
// pub account: String,
//
// /// Details on the user's premium subscription status.
// pub premium: AppStatePremiumInfo,
// }
// =============== Error Response ===============
#[derive(Debug, Serialize, ToSchema)]
pub enum AppStateError {
BadInput(String),
NotAuthorized,
ServerError,
}
impl ResponseError for AppStateError {
fn status_code(&self) -> StatusCode {
match *self {
AppStateError::BadInput(_) => StatusCode::BAD_REQUEST,
AppStateError::NotAuthorized => StatusCode::UNAUTHORIZED,
AppStateError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
@@ -113,33 +125,25 @@ impl std::fmt::Display for AppStateError {
/// This single endpoint can replace the following endpoints:
/// - `GET /detect_locale`
/// - `GET /server_info`
/// - `GET /v1/billing/active_subscriptions` (TODO: port new logic to old endpoint)
/// - `GET /v1/status_alert_check`
///
/// TODO: - `GET /v1/billing/active_subscriptions`
/// - `GET /v1/session`
///
/// This endpoint will probably grow new functionality in the future as well.
#[utoipa::path(
get,
tag = "App State",
path = "/v1/app_state",
params(
("entity_type", description = "The type of the entity being rated."),
("entity_token", description = "Entity token"),
),
responses(
(status = 200, description = "List User Bookmarks", body = AppStateResponse),
(status = 400, description = "Bad input", body = AppStateError),
(status = 401, description = "Not authorized", body = AppStateError),
(status = 200, description = "Success response", body = AppStateResponse),
(status = 500, description = "Server error", body = AppStateError),
),
)]
pub async fn get_app_state_handler(
http_request: HttpRequest,
internal_user_lookup: web::Data<dyn InternalUserLookup>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<AppStateResponse>, AppStateError>
{
/*
let mut mysql_connection = server_state.mysql_pool.acquire()
.await
.map_err(|e| {
@@ -149,48 +153,36 @@ pub async fn get_app_state_handler(
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
error!("Session checker error: {:?}", e);
AppStateError::ServerError
})?;
let user_session = match maybe_user_session {
Some(session) => session,
None => {
info!("not logged in");
return Err(AppStateError::NotAuthorized);
}
};
*/
let maybe_user_metadata = internal_user_lookup
.lookup_user_from_http_request(&http_request)
.await
.map_err(|err| {
error!("Error looking up user: {:?}", err);
AppStateError::ServerError // NB: This was probably *our* fault.
})?;
let server_info = get_server_info(&server_state);
let user_locale = get_user_locale(&http_request);
let maybe_alert = get_status_alert(&server_state);
let locale = get_user_locale(&http_request);
let mut maybe_user = None;
let mut maybe_user_info = maybe_user_session
.as_ref()
.map(|session| get_user_info(session));
if let Some(user_metadata) = maybe_user_metadata {
maybe_user = Some(AppStateUserAccountInfo {
premium: get_premium_info(&user_metadata),
});
}
let permissions = get_permissions(maybe_user_session.as_ref());
let maybe_premium = maybe_user_session
.as_ref()
.map(|session| get_premium_info(session));
Ok(Json(AppStateResponse {
success: true,
refresh_interval_millis: REFRESH_INTERVAL.as_millis(),
server_info,
user_locale,
locale,
maybe_alert,
maybe_user,
maybe_user_info,
permissions,
maybe_premium,
logged_in: false,
}))
}