prices come from the csv globals now, a json cost is just an override. DATABASE_URL defaults to postgres:///scroll when unset.
148 lines
5.3 KiB
Rust
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?,
|
|
}),
|
|
}
|
|
}
|
|
}
|