scroll.server/crates/auth-service/src/service.rs
WiseDev 8a745d634c generate the daily card shop server side
prices come from the csv globals now, a json cost is just an override.
DATABASE_URL defaults to postgres:///scroll when unset.
2026-08-23 08:47:55 +03:00

148 lines
5.3 KiB
Rust

use std::sync::Arc;
use service_rpc::{
AccountRef, AuthApi, AuthRequest, AuthResponse, DeviceInfo, LoginOutcome, RpcResult,
RpcService, Session,
};
use crate::config::AuthConfig;
use crate::store::{unavailable, AccountStore};
use crate::time::{days_between, format_utc, unix_millis, unix_seconds};
fn connect_failed(error: storage::StorageError) -> std::io::Error {
std::io::Error::other(format!(
"{error}\ncreate the database with `createdb scroll`, or point DATABASE_URL somewhere else"
))
}
pub const ERROR_CODE_GENERIC: i32 = 1;
pub const ERROR_CODE_PATCH: i32 = 7;
pub const ERROR_CODE_CLIENT_TOO_OLD: i32 = 8;
pub const ERROR_CODE_BANNED: i32 = 11;
pub struct AuthService {
config: AuthConfig,
store: AccountStore,
}
impl AuthService {
pub async fn bootstrap(config: AuthConfig) -> std::io::Result<Arc<Self>> {
let database = config.database.clone().unwrap_or_default();
let store = AccountStore::open(&database)
.await
.map_err(connect_failed)?;
let accounts = store.count().await?;
tracing::info!(accounts, url = %database.url, "account store ready");
Ok(Arc::new(Self { config, store }))
}
pub fn store(&self) -> &AccountStore {
&self.store
}
fn version_verdict(&self, device: &DeviceInfo) -> Option<(i32, Option<String>)> {
if device.client_build < self.config.min_client_build {
return Some((
ERROR_CODE_CLIENT_TOO_OLD,
Some(format!(
"client build {} is older than the required {}",
device.client_build, self.config.min_client_build
)),
));
}
if device.client_build > self.config.max_client_build {
return Some((
ERROR_CODE_CLIENT_TOO_OLD,
Some(format!(
"client build {} is newer than the supported {}",
device.client_build, self.config.max_client_build
)),
));
}
None
}
}
#[async_trait::async_trait]
impl AuthApi for AuthService {
async fn login(
&self,
account: AccountRef,
pass_token: Option<String>,
device: DeviceInfo,
) -> RpcResult<LoginOutcome> {
if let Some((error_code, message)) = self.version_verdict(&device) {
tracing::warn!(%account, build = device.client_build, "rejecting outdated client");
return Ok(LoginOutcome::Rejected {
error_code,
message,
});
}
let stored = if account.is_zero() || pass_token.is_none() {
None
} else {
self.store.get(account).await.map_err(unavailable)?
};
let stored = match stored {
Some(existing) => {
let presented = pass_token.as_deref().unwrap_or_default();
if existing.pass_token != presented {
tracing::warn!(%account, "pass token mismatch");
return Ok(LoginOutcome::Rejected {
error_code: ERROR_CODE_GENERIC,
message: Some("invalid pass token".to_owned()),
});
}
if existing.banned {
return Ok(LoginOutcome::Rejected {
error_code: ERROR_CODE_BANNED,
message: Some("account is banned".to_owned()),
});
}
existing
}
None => {
let created = self.store.create().await.map_err(unavailable)?;
tracing::info!(account = %created.account_ref(), "created account");
created
}
};
let refreshed = self
.store
.touch_session(stored.account_ref())
.await
.map_err(unavailable)?
.unwrap_or(stored);
let now = unix_seconds();
Ok(LoginOutcome::Accepted(Session {
account: refreshed.account_ref(),
pass_token: refreshed.pass_token.clone(),
session_count: refreshed.session_count,
play_time_seconds: refreshed.play_time_seconds,
days_since_started_playing: days_between(refreshed.created_at, now),
account_created_date: format_utc(refreshed.created_at),
server_time: unix_millis().to_string(),
}))
}
async fn resolve(&self, account: AccountRef) -> RpcResult<bool> {
Ok(self
.store
.get(account)
.await
.map_err(unavailable)?
.is_some())
}
}
#[async_trait::async_trait]
impl RpcService for AuthService {
type Request = AuthRequest;
type Response = AuthResponse;
fn service_name(&self) -> &'static str {
"auth"
}
async fn call(&self, request: AuthRequest) -> RpcResult<AuthResponse> {
match request {
AuthRequest::Login {
account,
pass_token,
device,
} => Ok(AuthResponse::Login(
self.login(account, pass_token, device).await?,
)),
AuthRequest::Resolve { account } => Ok(AuthResponse::Resolved {
known: self.resolve(account).await?,
}),
}
}
}