if the client presents an id we don't know, create it under that id and bump the sequence past it. also chest offers match through BaseChest, so one silver row covers every arena.
157 lines
5.6 KiB
Rust
157 lines
5.6 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() {
|
|
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 if !account.is_zero() => {
|
|
let adopted = self
|
|
.store
|
|
.adopt(account, pass_token.as_deref())
|
|
.await
|
|
.map_err(unavailable)?;
|
|
tracing::info!(account = %adopted.account_ref(), "adopted the account the client asked for");
|
|
adopted
|
|
}
|
|
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?,
|
|
}),
|
|
}
|
|
}
|
|
}
|