diff --git a/apps/api/src/auth/guard.rs b/apps/api/src/auth/guard.rs index 64f6e28d5..60e81510f 100644 --- a/apps/api/src/auth/guard.rs +++ b/apps/api/src/auth/guard.rs @@ -1,15 +1,19 @@ use crate::responders::ErrorContext; use gem_auth::{AuthClient, verify_auth_signature}; -use primitives::{AuthMessage, AuthenticatedRequest}; +use primitives::WalletSource as PrimitiveWalletSource; +use primitives::WalletType as PrimitiveWalletType; +use primitives::{AuthMessage, AuthenticatedRequest, WalletId}; use rocket::data::{FromData, Outcome, ToByteUnit}; use rocket::http::Status; use rocket::outcome::Outcome::{Error, Success}; use rocket::{Data, Request, State}; use serde::de::DeserializeOwned; use std::sync::Arc; -use storage::Database; use storage::database::devices::DevicesStore; -use storage::models::DeviceRow; +use storage::database::devices_sessions::DeviceSessionsStore; +use storage::models::{DeviceRow, NewDeviceSessionRow, NewWalletRow}; +use storage::repositories::wallets_repository::WalletsRepository; +use storage::{Database, WalletSource, WalletType}; fn error_outcome<'r, T>(req: &'r Request<'_>, status: Status, message: &str) -> Outcome<'r, T, String> { req.local_cache(|| ErrorContext(message.to_string())); @@ -19,6 +23,8 @@ fn error_outcome<'r, T>(req: &'r Request<'_>, status: Status, message: &str) -> pub struct VerifiedAuth { pub device: DeviceRow, pub address: String, + pub wallet_id: i32, + pub wallet_identifier: String, } pub struct Authenticated { @@ -73,10 +79,32 @@ impl<'r, T: DeserializeOwned + Send> FromData<'r> for Authenticated { return error_outcome(req, Status::InternalServerError, "Failed to invalidate nonce"); } + let wallet_identifier = WalletId::Multicoin(body.auth.address.clone()).id(); + let wallet = match db_client.get_or_create_wallet(NewWalletRow { + identifier: wallet_identifier.clone(), + wallet_type: WalletType(PrimitiveWalletType::Multicoin), + source: WalletSource(PrimitiveWalletSource::Import), + }) { + Ok(w) => w, + Err(_) => return error_outcome(req, Status::InternalServerError, "Failed to get or create wallet"), + }; + + let session = NewDeviceSessionRow { + device_id: device.id, + wallet_id: wallet.id, + nonce: body.auth.nonce.clone(), + signature: body.auth.signature.clone(), + }; + if DeviceSessionsStore::add_device_session(&mut db_client, session).is_err() { + return error_outcome(req, Status::InternalServerError, "Failed to store session"); + } + Success(Authenticated { auth: VerifiedAuth { device, address: body.auth.address, + wallet_id: wallet.id, + wallet_identifier, }, data: body.data, }) diff --git a/apps/api/src/referral/client.rs b/apps/api/src/referral/client.rs index 0dc2bbaec..33453347d 100644 --- a/apps/api/src/referral/client.rs +++ b/apps/api/src/referral/client.rs @@ -3,11 +3,8 @@ use std::error::Error; use api_connector::PusherClient; use gem_rewards::{IpSecurityClient, ReferralError, RewardsError, RiskScoreConfig, RiskScoringInput, UsernameError, evaluate_risk}; use primitives::rewards::{RewardRedemptionOption, RewardStatus}; -use primitives::{ConfigKey, IpUsageType, Localize, NaiveDateTimeExt, Platform, ReferralAllowance, ReferralLeaderboard, ReferralQuota, RewardEvent, Rewards, WalletId, now}; -use storage::{ - ConfigCacher, Database, NewWalletRow, ReferralValidationError, RewardsRedemptionsRepository, RewardsRepository, RiskSignalsRepository, WalletSource, WalletType, - WalletsRepository, -}; +use primitives::{ConfigKey, IpUsageType, Localize, NaiveDateTimeExt, Platform, ReferralAllowance, ReferralLeaderboard, ReferralQuota, RewardEvent, Rewards, now}; +use storage::{ConfigCacher, Database, ReferralValidationError, RewardsRedemptionsRepository, RewardsRepository, RiskSignalsRepository, WalletsRepository}; use streamer::{RewardsNotificationPayload, StreamProducer, StreamProducerQueue}; use crate::auth::VerifiedAuth; @@ -171,12 +168,7 @@ impl RewardsClient { pub async fn use_referral_code(&self, auth: &VerifiedAuth, code: &str, ip_address: &str) -> Result, Box> { let locale = auth.device.locale.as_str(); - let wallet_identifier = WalletId::Multicoin(auth.address.clone()).id(); - let wallet = self.db.wallets()?.get_or_create_wallet(NewWalletRow { - identifier: wallet_identifier, - wallet_type: WalletType::Multicoin, - source: WalletSource::Import, - })?; + let wallet = self.db.wallets()?.get_wallet_by_id(auth.wallet_id)?; let referrer_username = self.db.rewards()?.get_referral_code(code)?.ok_or_else(|| { let error = ReferralError::from(ReferralValidationError::CodeDoesNotExist); diff --git a/apps/api/src/referral/mod.rs b/apps/api/src/referral/mod.rs index d0dd1b0c7..60156358d 100644 --- a/apps/api/src/referral/mod.rs +++ b/apps/api/src/referral/mod.rs @@ -8,7 +8,7 @@ use crate::auth::Authenticated; use crate::params::MulticoinParam; use crate::responders::{ApiError, ApiResponse}; use primitives::rewards::{RedemptionRequest, RedemptionResult, RewardRedemptionOption}; -use primitives::{ReferralCode, ReferralLeaderboard, RewardEvent, Rewards, WalletId}; +use primitives::{ReferralCode, ReferralLeaderboard, RewardEvent, Rewards}; use rocket::{State, get, post}; use tokio::sync::Mutex; @@ -34,12 +34,11 @@ pub async fn get_rewards(wallet: MulticoinParam, client: &State, ip: std::net::IpAddr, client: &State>) -> Result, ApiError> { - let wallet_identifier = WalletId::Multicoin(request.auth.address.clone()).id(); Ok(client .lock() .await .create_username( - &wallet_identifier, + &request.auth.wallet_identifier, &request.data.code, request.auth.device.id, &ip.to_string(), @@ -52,8 +51,7 @@ pub async fn create_referral(request: Authenticated, ip: std::net: #[allow(dead_code)] #[post("/rewards/referrals/update", format = "json", data = "")] pub async fn update_referral(request: Authenticated, client: &State>) -> Result, ApiError> { - let wallet_identifier = WalletId::Multicoin(request.auth.address.clone()).id(); - Ok(client.lock().await.change_username(&wallet_identifier, &request.data.code)?.into()) + Ok(client.lock().await.change_username(&request.auth.wallet_identifier, &request.data.code)?.into()) } #[post("/rewards/referrals/use", format = "json", data = "")] diff --git a/crates/storage/src/database/devices_sessions.rs b/crates/storage/src/database/devices_sessions.rs new file mode 100644 index 000000000..734686ba6 --- /dev/null +++ b/crates/storage/src/database/devices_sessions.rs @@ -0,0 +1,16 @@ +use crate::{DatabaseClient, models::*}; +use diesel::prelude::*; + +pub trait DeviceSessionsStore { + fn add_device_session(&mut self, session: NewDeviceSessionRow) -> Result; +} + +impl DeviceSessionsStore for DatabaseClient { + fn add_device_session(&mut self, session: NewDeviceSessionRow) -> Result { + use crate::schema::devices_sessions::dsl::*; + diesel::insert_into(devices_sessions) + .values(&session) + .returning(DeviceSessionRow::as_returning()) + .get_result(&mut self.connection) + } +} diff --git a/crates/storage/src/database/mod.rs b/crates/storage/src/database/mod.rs index 10520e4c8..7a4f5f869 100644 --- a/crates/storage/src/database/mod.rs +++ b/crates/storage/src/database/mod.rs @@ -7,6 +7,7 @@ pub mod chains; pub mod charts; pub mod config; pub mod devices; +pub mod devices_sessions; pub mod fiat; pub mod migrations; pub mod nft; diff --git a/crates/storage/src/migrations/2026-01-28-052000_devices_sessions/down.sql b/crates/storage/src/migrations/2026-01-28-052000_devices_sessions/down.sql new file mode 100644 index 000000000..230cca8d3 --- /dev/null +++ b/crates/storage/src/migrations/2026-01-28-052000_devices_sessions/down.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS idx_devices_sessions_created_at; +DROP INDEX IF EXISTS idx_devices_sessions_wallet_id; +DROP INDEX IF EXISTS idx_devices_sessions_device_id; +DROP TABLE IF EXISTS devices_sessions; diff --git a/crates/storage/src/migrations/2026-01-28-052000_devices_sessions/up.sql b/crates/storage/src/migrations/2026-01-28-052000_devices_sessions/up.sql new file mode 100644 index 000000000..93c34625a --- /dev/null +++ b/crates/storage/src/migrations/2026-01-28-052000_devices_sessions/up.sql @@ -0,0 +1,12 @@ +CREATE TABLE devices_sessions ( + id SERIAL PRIMARY KEY, + device_id INT NOT NULL REFERENCES devices(id) ON DELETE CASCADE, + wallet_id INT NOT NULL REFERENCES wallets(id) ON DELETE CASCADE, + nonce VARCHAR(256) NOT NULL, + signature VARCHAR(256) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp +); + +CREATE INDEX idx_devices_sessions_device_id ON devices_sessions (device_id); +CREATE INDEX idx_devices_sessions_wallet_id ON devices_sessions (wallet_id); +CREATE INDEX idx_devices_sessions_created_at ON devices_sessions (created_at DESC); diff --git a/crates/storage/src/models/device_session.rs b/crates/storage/src/models/device_session.rs new file mode 100644 index 000000000..481f6028d --- /dev/null +++ b/crates/storage/src/models/device_session.rs @@ -0,0 +1,24 @@ +use chrono::NaiveDateTime; +use diesel::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Queryable, Selectable, Serialize, Deserialize, Insertable, Clone)] +#[diesel(table_name = crate::schema::devices_sessions)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct DeviceSessionRow { + pub id: i32, + pub device_id: i32, + pub wallet_id: i32, + pub nonce: String, + pub signature: String, + pub created_at: NaiveDateTime, +} + +#[derive(Debug, Insertable, Clone)] +#[diesel(table_name = crate::schema::devices_sessions)] +pub struct NewDeviceSessionRow { + pub device_id: i32, + pub wallet_id: i32, + pub nonce: String, + pub signature: String, +} diff --git a/crates/storage/src/models/mod.rs b/crates/storage/src/models/mod.rs index 9f1044d94..b6a2211b2 100644 --- a/crates/storage/src/models/mod.rs +++ b/crates/storage/src/models/mod.rs @@ -5,6 +5,7 @@ pub mod chain; pub mod chart; pub mod config; pub mod device; +pub mod device_session; pub mod fiat; pub mod nft_asset; pub mod nft_collection; @@ -34,6 +35,7 @@ pub use self::chain::ChainRow; pub use self::chart::{ChartRow, DailyChartRow, HourlyChartRow}; pub use self::config::ConfigRow; pub use self::device::{DeviceRow, UpdateDeviceRow}; +pub use self::device_session::{DeviceSessionRow, NewDeviceSessionRow}; pub use self::fiat::{ FiatAssetRow, FiatProviderCountryRow, FiatProviderRow, FiatQuoteRequestRow, FiatQuoteRow, FiatRateRow, FiatTransactionRow, FiatTransactionUpdateRow, NewFiatWebhookRow, }; diff --git a/crates/storage/src/schema.rs b/crates/storage/src/schema.rs index cee0aaef2..01e779194 100644 --- a/crates/storage/src/schema.rs +++ b/crates/storage/src/schema.rs @@ -234,6 +234,19 @@ diesel::table! { } } +diesel::table! { + devices_sessions (id) { + id -> Int4, + device_id -> Int4, + wallet_id -> Int4, + #[max_length = 256] + nonce -> Varchar, + #[max_length = 256] + signature -> Varchar, + created_at -> Timestamp, + } +} + diesel::table! { fiat_assets (id) { #[max_length = 128] @@ -952,6 +965,8 @@ diesel::joinable!(charts -> prices (coin_id)); diesel::joinable!(charts_daily -> prices (coin_id)); diesel::joinable!(charts_hourly -> prices (coin_id)); diesel::joinable!(devices -> fiat_rates (currency)); +diesel::joinable!(devices_sessions -> devices (device_id)); +diesel::joinable!(devices_sessions -> wallets (wallet_id)); diesel::joinable!(fiat_assets -> assets (asset_id)); diesel::joinable!(fiat_assets -> fiat_providers (provider)); diesel::joinable!(fiat_providers_countries -> fiat_providers (provider)); @@ -1025,6 +1040,7 @@ diesel::allow_tables_to_appear_in_same_query!( charts_hourly, config, devices, + devices_sessions, fiat_assets, fiat_providers, fiat_providers_countries,