use std::error::Error; use axum::{ Json, extract::{Query, State}, }; use serde::{Deserialize, Serialize}; use serde_json::Value; use sqlx::{PgPool, query, query_as, query_scalar, types::Uuid}; use tower_sessions::Session; use crate::error::AppError; #[derive(Deserialize, Serialize)] pub struct User { id: Uuid, steamid: String, name: String, avatar: String, } impl User { fn new(steamid: T, name: T, avatar: T) -> User where String: From, { User { id: Uuid::nil(), steamid: String::from(steamid), name: String::from(name), avatar: String::from(avatar), } } async fn create(&mut self, pool: &PgPool) -> Result<(), Box> { let uuid: Uuid = query_scalar!( "INSERT INTO users (steamid, name, avatar) VALUES ($1, $2, $3) RETURNING id", self.steamid, self.name, self.avatar ) .fetch_one(pool) .await?; self.id = uuid; Ok(()) } async fn delete(&self, pool: &PgPool) -> Result<(), Box> { query!("DELETE FROM users WHERE id=$1", self.id) .execute(pool) .await?; Ok(()) } async fn update(&self, pool: &PgPool) -> Result<(), Box> { query!( "UPDATE users SET steamid=$1, name=$2, avatar=$3 WHERE id=$4", self.steamid, self.name, self.avatar, self.id ) .execute(pool) .await?; Ok(()) } async fn fetch_id(pool: &PgPool, uuid: Uuid) -> Result> { Ok(query_as!(User, "SELECT * FROM users WHERE id=$1", uuid) .fetch_one(pool) .await?) } async fn steamid_exists(pool: &PgPool, steamid: &String) -> Result> { match query!("SELECT id FROM users WHERE steamid=$1", steamid) .fetch_optional(pool) .await? { Some(_) => Ok(true), None => Ok(false), } } async fn from_steamid(pool: &PgPool, steamid: String) -> Result> { if User::steamid_exists(pool, &steamid).await? { let user = query_as!(User, "SELECT * FROM users WHERE steamid=$1", steamid) .fetch_one(pool) .await?; return Ok(user); } let raw_user = Self::fetch_steam(&steamid).await?; let mut user = User::new( raw_user["steamid"].to_string().trim_matches('"'), raw_user["personaname"].to_string().trim_matches('"'), raw_user["avatarfull"].to_string().trim_matches('"'), ); user.create(pool).await?; Ok(user) } async fn fetch_steam(steamid: &String) -> Result> { let response = reqwest::get(format!("https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=ABBFE30D7BDC1FD582DB1A2CEBD563B4&steamids={steamid}")) .await?.bytes().await?; let mut json: serde_json::Value = serde_json::from_slice(&response)?; Ok(json["response"]["players"][0].take()) } async fn update_from_steam(&mut self, pool: &PgPool) -> Result<(), Box> { let raw_user = Self::fetch_steam(&self.steamid).await?; self.name = raw_user["personaname"].to_string().trim_matches('"').into(); self.avatar = raw_user["avatarfull"].to_string().trim_matches('"').into(); self.update(pool).await } } #[derive(Deserialize, Serialize)] pub struct OpenId { #[serde(rename = "openid.ns")] ns: String, #[serde(rename = "openid.mode")] mode: String, #[serde(rename = "openid.op_endpoint")] op_endpoint: String, #[serde(rename = "openid.claimed_id")] claimed_id: String, #[serde(rename = "openid.identity")] identity: String, #[serde(rename = "openid.return_to")] return_to: String, #[serde(rename = "openid.response_nonce")] response_nonce: String, #[serde(rename = "openid.assoc_handle")] assoc_handle: String, #[serde(rename = "openid.signed")] signed: String, #[serde(rename = "openid.sig")] sig: String, } impl OpenId { pub async fn is_valid(&mut self) -> Result> { self.mode = "check_authentication".into(); let url_encoded = serde_urlencoded::to_string(self)?; let response = reqwest::get(format!( "https://steamcommunity.com/openid/login?{url_encoded}" )) .await? .text() .await?; Ok(response.contains("is_valid:true")) } } #[axum::debug_handler] pub async fn login( State(pool): State, Query(mut info): Query, session: Session, ) -> Result<(), AppError> { if !info.is_valid().await? { return Err(AppError::OpenIdError( "Some query parameters are erronous or lacking.".into(), )); } let steamid = info .identity .replace("https://steamcommunity.com/openid/id/", ""); let user = User::from_steamid(&pool, steamid).await?; session.insert("uuid", user.id).await?; Ok(()) } pub async fn logout(session: Session) -> Result<(), AppError> { session.delete().await?; Ok(()) } #[axum::debug_handler] pub async fn me(State(pool): State, session: Session) -> Result, AppError> { if let Some(uuid) = session.get("uuid").await? { let user = User::fetch_id(&pool, uuid).await?; Ok(Json(user)) } else { Err(AppError::Unauthorised) } } #[derive(Deserialize)] pub struct Settings { username: String, avatar: String, } #[axum::debug_handler] pub async fn update( State(pool): State, session: Session, data: Json, ) -> Result<(), AppError> { if let Some(uuid) = session.get("uuid").await? { let mut user = User::fetch_id(&pool, uuid).await?; if !data.username.is_empty() { user.name = data.username.clone(); } if !data.avatar.is_empty() { user.avatar = data.avatar.clone(); } user.update(&pool).await?; Ok(()) } else { Err(AppError::Unauthorised) } } pub async fn reload(State(pool): State, session: Session) -> Result<(), AppError> { if let Some(uuid) = session.get("uuid").await? { let mut user = User::fetch_id(&pool, uuid).await?; user.update_from_steam(&pool).await?; Ok(()) } else { Err(AppError::Unauthorised) } }