From 063f94cbbeb9b52be5fa7ff5711ec63b277cb010 Mon Sep 17 00:00:00 2001 From: rupansh Date: Fri, 16 Feb 2024 22:05:19 +0530 Subject: [PATCH] feat: integrate auth metadata api --- did/individual_user_template.did | 4 +- did/platform_orchestrator.did | 5 +- did/post_cache.did | 2 +- did/user_index.did | 9 ++- src/app.rs | 3 +- src/component/auth_provider.rs | 5 +- src/component/connect.rs | 2 +- src/page/profile/mod.rs | 26 +++--- src/state/auth/mod.rs | 113 +++++++++++++++++++++++++++ src/state/{auth.rs => auth/types.rs} | 68 ++-------------- src/state/canisters.rs | 63 +++++++++++---- 11 files changed, 197 insertions(+), 103 deletions(-) create mode 100644 src/state/auth/mod.rs rename src/state/{auth.rs => auth/types.rs} (57%) diff --git a/did/individual_user_template.did b/did/individual_user_template.did index 7dc31af8..4a6d7a3a 100644 --- a/did/individual_user_template.did +++ b/did/individual_user_template.did @@ -275,7 +275,7 @@ type UserProfileUpdateDetailsFromFrontend = record { profile_picture_url : opt text; display_name : opt text; }; -service : { +service : (IndividualUserTemplateInitArgs) -> { add_post_v2 : (PostDetailsFromFrontend) -> (Result); backup_data_to_backup_canister : (principal, principal) -> (); bet_on_currently_viewing_post : (PlaceBetArg) -> (Result_1); @@ -344,4 +344,4 @@ service : { update_profiles_that_follow_me_toggle_list_with_specified_profile : ( FollowerArg, ) -> (Result_2); -} \ No newline at end of file +} diff --git a/did/platform_orchestrator.did b/did/platform_orchestrator.did index 546b5b3c..94f1f307 100644 --- a/did/platform_orchestrator.did +++ b/did/platform_orchestrator.did @@ -17,11 +17,12 @@ type WasmType = variant { SubnetOrchestratorWasm; }; service : (PlatformOrchestratorInitArgs) -> { - get_next_available_subnet : () -> (principal) query; + get_all_available_subnet_orchestrators : () -> (vec principal) query; + get_all_subnet_orchestrators : () -> (vec principal) query; get_subnet_last_upgrade_status : () -> (CanisterUpgradeStatus) query; get_version : () -> (text) query; provision_subnet_orchestrator_canister : (principal) -> (Result); subnet_orchestrator_maxed_out : () -> (); upgrade_canister : (UpgradeCanisterArg) -> (Result_1); upload_wasms : (WasmType, vec nat8) -> (Result_1); -} \ No newline at end of file +} diff --git a/did/post_cache.did b/did/post_cache.did index 865601fa..a5b093e6 100644 --- a/did/post_cache.did +++ b/did/post_cache.did @@ -52,7 +52,7 @@ type TopPostsFetchError = variant { InvalidBoundsPassed; ExceededMaxNumberOfItemsAllowedInOneRequest; }; -service : { +service : (PostCacheInitArgs) -> { get_cycle_balance : () -> (nat) query; get_top_posts_aggregated_from_canisters_on_this_network_for_home_feed : ( nat64, diff --git a/did/user_index.did b/did/user_index.did index 59c444a7..e3c5fcb9 100644 --- a/did/user_index.did +++ b/did/user_index.did @@ -4,6 +4,7 @@ type CanisterStatusResponse = record { memory_size : nat; cycles : nat; settings : DefiniteCanisterSettings; + query_stats : QueryStats; idle_cycles_burned_per_day : nat; module_hash : opt vec nat8; }; @@ -27,6 +28,12 @@ type KnownPrincipalType = variant { CanisterIdSnsGovernance; UserIdGlobalSuperAdmin; }; +type QueryStats = record { + response_payload_bytes_total : nat; + num_instructions_total : nat; + num_calls_total : nat; + request_payload_bytes_total : nat; +}; type RejectionCode = variant { NoError; CanisterError; @@ -70,7 +77,7 @@ type UserIndexInitArgs = record { version : text; access_control_map : opt vec record { principal; vec UserAccessRole }; }; -service : { +service : (UserIndexInitArgs) -> { are_signups_enabled : () -> (bool) query; backup_all_individual_user_canisters : () -> (); create_pool_of_individual_user_available_canisters : (text, vec nat8) -> ( diff --git a/src/app.rs b/src/app.rs index 2f94cdb5..93822b21 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,7 +16,7 @@ use crate::{ upload::UploadPostPage, }, state::{ - auth::AuthState, + auth::{AuthClient, AuthState}, canisters::{do_canister_auth, Canisters}, }, utils::MockPartialEq, @@ -33,6 +33,7 @@ pub fn App() -> impl IntoView { provide_context(PostViewCtx::default()); let auth_state = AuthState::default(); provide_context(auth_state.clone()); + provide_context(AuthClient::default()); provide_context(Resource::local( move || MockPartialEq(auth_state.identity.get()), |auth| do_canister_auth(auth.0), diff --git a/src/component/auth_provider.rs b/src/component/auth_provider.rs index 879c5e42..77c8f10c 100644 --- a/src/component/auth_provider.rs +++ b/src/component/auth_provider.rs @@ -1,6 +1,9 @@ use crate::{ consts::AUTH_URL, - state::auth::{auth_state, DelegationIdentity, SessionResponse}, + state::auth::{ + auth_state, + types::{DelegationIdentity, SessionResponse}, + }, }; use leptos::*; use leptos_use::{use_event_listener, use_window}; diff --git a/src/component/connect.rs b/src/component/connect.rs index 0b7b5dcd..76f70a87 100644 --- a/src/component/connect.rs +++ b/src/component/connect.rs @@ -9,7 +9,7 @@ use reqwest::Url; use crate::{ consts::{self, ACCOUNT_CONNECTED_STORE}, - state::auth::{auth_state, SessionResponse}, + state::auth::{auth_state, types::SessionResponse}, }; #[component] diff --git a/src/page/profile/mod.rs b/src/page/profile/mod.rs index 87f7d18e..a7d3cb12 100644 --- a/src/page/profile/mod.rs +++ b/src/page/profile/mod.rs @@ -8,7 +8,8 @@ use leptos_icons::*; use leptos_router::*; use crate::{ - component::spinner::FullScreenSpinner, state::canisters::unauth_canisters, + component::spinner::FullScreenSpinner, + state::{auth::auth_client, canisters::unauth_canisters}, utils::profile::ProfileDetails, }; @@ -109,28 +110,21 @@ fn ProfileViewInner(user: ProfileDetails, user_canister: Principal) -> impl Into #[component] pub fn ProfileView() -> impl IntoView { let params = use_params::(); - let principal_or_username = move || { + let principal = move || { params.with(|p| { let ProfileParams { id } = p.as_ref().ok()?; - let res = Principal::from_text(id).map_err(|_| id.clone()); - Some(res) + Principal::from_text(id).ok() }) }; - let user_details = create_resource(principal_or_username, |principal_or_username| async move { + let user_details = create_resource(principal, |principal| async move { let canisters = unauth_canisters(); - let user_index = canisters.user_index(); - let user_canister = match principal_or_username? { - Ok(p) => user_index - .get_user_canister_id_from_user_principal_id(p) - .await - .ok()??, - Err(u) => user_index - .get_user_canister_id_from_unique_user_name(u) - .await - .ok()??, - }; + let auth = auth_client(); + let user_canister = auth + .get_individual_canister_by_user_principal(principal?) + .await + .ok()??; let user = canisters.individual_user(user_canister); let user_details = user.get_profile_details().await.ok()?; Some((user_details.into(), user_canister)) diff --git a/src/state/auth/mod.rs b/src/state/auth/mod.rs new file mode 100644 index 00000000..415191a5 --- /dev/null +++ b/src/state/auth/mod.rs @@ -0,0 +1,113 @@ +pub mod types; + +use std::num::ParseIntError; + +use ic_agent::{export::Principal, identity::DelegatedIdentity}; + +use leptos::{create_effect, create_signal, expect_context, Effect, ReadSignal, RwSignal}; +use leptos_use::storage::{use_local_storage, StringCodec}; +use thiserror::Error; + +use crate::consts::{ACCOUNT_CONNECTED_STORE, AUTH_URL}; +use types::{DelegationIdentity, SessionResponse, UserDetails}; + +#[derive(Error, Debug, Clone)] +pub enum AuthError { + #[error("Invalid Secret Key")] + InvalidSecretKey(#[from] k256::elliptic_curve::Error), + #[error("Invalid expiry")] + InvalidExpiry(#[from] ParseIntError), + #[error("reqwest error: {0}")] + Reqwest(String), +} + +impl From for AuthError { + fn from(e: reqwest::Error) -> Self { + AuthError::Reqwest(e.to_string()) + } +} + +#[derive(Default, Clone)] +pub struct AuthClient { + client: reqwest::Client, +} + +impl AuthClient { + pub async fn generate_session(&self) -> Result { + let resp: SessionResponse = self + .client + .post(AUTH_URL.join("api/generate_session").unwrap()) + .send() + .await? + .json() + .await?; + resp.delegation_identity.try_into() + } + + pub async fn update_user_metadata( + &self, + id: DelegationIdentity, + user_canister: Principal, + username: String, + ) -> Result<(), AuthError> { + let details = UserDetails { + delegation_identity: id, + user_canister_id: user_canister.to_text(), + user_name: username, + }; + let res = self + .client + .post(AUTH_URL.join("rest_api/update_user_metadata").unwrap()) + .json(&details) + .send() + .await?; + if res.status().is_success() { + Ok(()) + } else { + Err(AuthError::Reqwest(res.text().await?)) + } + } + + pub async fn get_individual_canister_by_user_principal( + &self, + user_principal: Principal, + ) -> Result, AuthError> { + let res = self + .client + .post(AUTH_URL.join("rest_api/get_user_canister").unwrap()) + .json(&user_principal.to_text()) + .send() + .await? + .text() + .await?; + Ok(Principal::from_text(res).ok()) + } +} + +pub fn auth_client() -> AuthClient { + expect_context() +} + +#[derive(Default, Clone)] +pub struct AuthState { + pub identity: RwSignal>, +} + +pub fn auth_state() -> AuthState { + expect_context() +} + +/// Prevents hydration bugs if the value in store is used to conditionally show views +/// this is because the server will always get a `false` value and do rendering based on that +pub fn account_connected_reader() -> (ReadSignal, Effect<()>) { + let (read_account_connected, _, _) = + use_local_storage::(ACCOUNT_CONNECTED_STORE); + let (is_connected, set_is_connected) = create_signal(false); + + ( + is_connected, + create_effect(move |_| { + set_is_connected(read_account_connected()); + }), + ) +} diff --git a/src/state/auth.rs b/src/state/auth/types.rs similarity index 57% rename from src/state/auth.rs rename to src/state/auth/types.rs index 5df1c19d..97e802d5 100644 --- a/src/state/auth.rs +++ b/src/state/auth/types.rs @@ -1,15 +1,10 @@ -use std::num::ParseIntError; - use candid::Principal; use ic_agent::identity::{DelegatedIdentity, Secp256k1Identity}; use k256::SecretKey; -use leptos::{create_effect, create_signal, expect_context, Effect, ReadSignal, RwSignal}; -use leptos_use::storage::{use_local_storage, StringCodec}; use serde::{Deserialize, Serialize}; -use thiserror::Error; -use crate::consts::{ACCOUNT_CONNECTED_STORE, AUTH_URL}; +use super::AuthError; #[derive(Debug, Serialize, Clone)] struct PrincipalId { @@ -90,60 +85,9 @@ pub struct SessionResponse { pub delegation_identity: DelegationIdentity, } -#[derive(Error, Debug, Clone)] -pub enum AuthError { - #[error("Invalid Secret Key")] - InvalidSecretKey(#[from] k256::elliptic_curve::Error), - #[error("Invalid expiry")] - InvalidExpiry(#[from] ParseIntError), - #[error("reqwest error: {0}")] - Reqwest(String), -} - -impl From for AuthError { - fn from(e: reqwest::Error) -> Self { - AuthError::Reqwest(e.to_string()) - } -} - -#[derive(Default, Clone)] -pub struct AuthClient { - client: reqwest::Client, -} - -impl AuthClient { - pub async fn generate_session(&self) -> Result { - let resp: SessionResponse = self - .client - .post(AUTH_URL.join("api/generate_session").unwrap()) - .send() - .await? - .json() - .await?; - resp.delegation_identity.try_into() - } -} - -#[derive(Default, Clone)] -pub struct AuthState { - pub identity: RwSignal>, -} - -pub fn auth_state() -> AuthState { - expect_context() -} - -/// Prevents hydration bugs if the value in store is used to conditionally show views -/// this is because the server will always get a `false` value and do rendering based on that -pub fn account_connected_reader() -> (ReadSignal, Effect<()>) { - let (read_account_connected, _, _) = - use_local_storage::(ACCOUNT_CONNECTED_STORE); - let (is_connected, set_is_connected) = create_signal(false); - - ( - is_connected, - create_effect(move |_| { - set_is_connected(read_account_connected()); - }), - ) +#[derive(Serialize)] +pub struct UserDetails { + pub delegation_identity: DelegationIdentity, + pub user_canister_id: String, + pub user_name: String, } diff --git a/src/state/canisters.rs b/src/state/canisters.rs index 10359b0c..8089b3b2 100644 --- a/src/state/canisters.rs +++ b/src/state/canisters.rs @@ -9,17 +9,18 @@ use crate::{ individual_user_template::IndividualUserTemplate, platform_orchestrator::{self, PlatformOrchestrator}, post_cache::{self, PostCache}, - user_index::{self, UserIndex}, + user_index::UserIndex, AGENT_URL, }, utils::MockPartialEq, }; -use super::auth::{AuthError, DelegationIdentity}; +use super::auth::{types::DelegationIdentity, AuthClient, AuthError}; #[derive(Clone)] pub struct Canisters { agent: ic_agent::Agent, + auth_client: AuthClient, id: Option>, user_canister: Principal, expiry: u64, @@ -33,6 +34,7 @@ impl Default for Canisters { .build() .unwrap(), id: None, + auth_client: AuthClient::default(), user_canister: Principal::anonymous(), expiry: 0, } @@ -56,6 +58,7 @@ impl Canisters { .build() .unwrap(), id: Some(id), + auth_client: AuthClient::default(), user_canister: Principal::anonymous(), expiry, } @@ -89,10 +92,6 @@ impl Canisters { IndividualUserTemplate(user_canister, &self.agent) } - pub fn user_index(&self) -> UserIndex<'_> { - UserIndex(user_index::CANISTER_ID, &self.agent) - } - pub fn user_index_with(&self, subnet_principal: Principal) -> UserIndex<'_> { UserIndex(subnet_principal, &self.agent) } @@ -109,17 +108,25 @@ pub fn unauth_canisters() -> Canisters { pub type AuthCanistersResource = Resource>, Result>, AuthError>>; -pub async fn do_canister_auth( - auth: Option, -) -> Result>, AuthError> { - let Some(auth) = auth else { - return Ok(None); - }; - let auth: DelegatedIdentity = auth.try_into()?; - let mut canisters = Canisters::::authenticated(auth); +async fn create_individual_canister( + canisters: &Canisters, + delegation_id: DelegationIdentity, +) -> Result { let orchestrator = canisters.orchestrator(); // TODO: error handling - let subnet_idx = orchestrator.get_next_available_subnet().await.unwrap(); + let subnet_idxs = orchestrator + .get_all_available_subnet_orchestrators() + .await + .unwrap(); + + let mut by = [0u8; 16]; + let principal = canisters.identity().sender().unwrap(); + let principal_by = principal.as_slice(); + let cnt = by.len().min(principal_by.len()); + by[..cnt].copy_from_slice(&principal_by[..cnt]); + + let discrim = u128::from_be_bytes(by); + let subnet_idx = subnet_idxs[(discrim % subnet_idxs.len() as u128) as usize]; let idx = canisters.user_index_with(subnet_idx); // TOOD: referrer // TODO: error handling @@ -129,7 +136,31 @@ pub async fn do_canister_auth( ) .await .unwrap(); - canisters.user_canister = user_canister; + canisters + .auth_client + .update_user_metadata(delegation_id, user_canister, "".into()) + .await?; + Ok(user_canister) +} + +pub async fn do_canister_auth( + auth: Option, +) -> Result>, AuthError> { + let Some(delegation_identity) = auth else { + return Ok(None); + }; + let auth: DelegatedIdentity = delegation_identity.clone().try_into()?; + let mut canisters = Canisters::::authenticated(auth); + canisters.user_canister = if let Some(user_canister) = canisters + .auth_client + .get_individual_canister_by_user_principal(canisters.identity().sender().unwrap()) + .await? + { + user_canister + } else { + create_individual_canister(&canisters, delegation_identity).await? + }; + Ok(Some(canisters)) }