mirror of
https://codeberg.org/demostf/frontend.git
synced 2026-06-03 18:24:12 +02:00
steam login
This commit is contained in:
parent
6e456a6596
commit
fc5cd1d24f
12 changed files with 926 additions and 70 deletions
645
Cargo.lock
generated
645
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -14,7 +14,7 @@ tokio = { version = "1.27.0", features = ["full"] }
|
||||||
config = { version = "0.13.3", features = ["toml"] }
|
config = { version = "0.13.3", features = ["toml"] }
|
||||||
time = "0.3.20"
|
time = "0.3.20"
|
||||||
maud = { version = "0.24.0", git = "https://github.com/lambda-fairy/maud", rev = "7233cda35eed7bba91c9c55564d65498067c3822", features = ["axum"] }
|
maud = { version = "0.24.0", git = "https://github.com/lambda-fairy/maud", rev = "7233cda35eed7bba91c9c55564d65498067c3822", features = ["axum"] }
|
||||||
axum = "0.6.12"
|
axum = { version = "0.6.12", features = ["headers"] }
|
||||||
hyper = "0.14.25"
|
hyper = "0.14.25"
|
||||||
hyperlocal = "0.8.0"
|
hyperlocal = "0.8.0"
|
||||||
tower-http = { version = "0.4.0", features = ["trace"] }
|
tower-http = { version = "0.4.0", features = ["trace"] }
|
||||||
|
|
@ -23,6 +23,11 @@ itertools = "0.10.5"
|
||||||
const-fnv1a-hash = "1.1.0"
|
const-fnv1a-hash = "1.1.0"
|
||||||
const_base = "0.2.0"
|
const_base = "0.2.0"
|
||||||
const-str = "0.5.4"
|
const-str = "0.5.4"
|
||||||
|
steam-openid = "0.2.0"
|
||||||
|
async-session = "3.0.0"
|
||||||
|
quick-xml = { version = "0.28.1", features = ["serialize"] }
|
||||||
|
reqwest = "0.11.16"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
lightningcss = { version = "1.0.0-alpha.40", features = ["browserslist", "visitor"] }
|
lightningcss = { version = "1.0.0-alpha.40", features = ["browserslist", "visitor"] }
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@
|
||||||
in rec {
|
in rec {
|
||||||
# `nix develop`
|
# `nix develop`
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
|
OPENSSL_NO_VENDOR = 1;
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
cargo
|
cargo
|
||||||
bacon
|
bacon
|
||||||
|
|
@ -27,6 +29,8 @@
|
||||||
clippy
|
clippy
|
||||||
cargo-audit
|
cargo-audit
|
||||||
cargo-watch
|
cargo-watch
|
||||||
|
pkg-config
|
||||||
|
openssl
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use std::path::PathBuf;
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub listen: Listen,
|
pub listen: Listen,
|
||||||
pub database: DbConfig,
|
pub database: DbConfig,
|
||||||
|
pub site: SiteConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
@ -46,3 +47,8 @@ pub enum Listen {
|
||||||
Socket { path: PathBuf },
|
Socket { path: PathBuf },
|
||||||
Tcp { address: IpAddr, port: u16 },
|
Tcp { address: IpAddr, port: u16 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct SiteConfig {
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ pub mod chat;
|
||||||
pub mod demo;
|
pub mod demo;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod steam_id;
|
pub mod steam_id;
|
||||||
|
pub mod user;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
use maud::Render;
|
use maud::Render;
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::database::HasValueRef;
|
use sqlx::database::HasValueRef;
|
||||||
use sqlx::error::BoxDynError;
|
use sqlx::error::BoxDynError;
|
||||||
use sqlx::{Database, Decode, Type};
|
use sqlx::{Database, Decode, Type};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt::Write;
|
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
|
use std::fmt::{Display, Write};
|
||||||
use steamid_ng::SteamID;
|
use steamid_ng::SteamID;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub enum SteamId {
|
pub enum SteamId {
|
||||||
Id(u64),
|
Id(u64),
|
||||||
Raw(Cow<'static, str>),
|
Raw(Cow<'static, str>),
|
||||||
|
|
@ -51,25 +51,20 @@ impl SteamId {
|
||||||
let id = SteamID::from_steam3(s)?;
|
let id = SteamID::from_steam3(s)?;
|
||||||
Ok(SteamId::Id(id.into()))
|
Ok(SteamId::Id(id.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn steamid64(&self) -> String {
|
||||||
|
match self {
|
||||||
|
SteamId::Id(id) => format!("{}", id),
|
||||||
|
SteamId::Raw(raw) => raw.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for SteamId {
|
impl Debug for SteamId {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
SteamId::Id(id) => SteamID::from(*id).fmt(f),
|
SteamId::Id(id) => Debug::fmt(&SteamID::from(*id), f),
|
||||||
SteamId::Raw(raw) => raw.fmt(f),
|
SteamId::Raw(raw) => Debug::fmt(raw, f),
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for SteamId {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
SteamId::Id(id) => serializer.collect_str(&SteamID::from(*id).steam3()),
|
|
||||||
SteamId::Raw(raw) => serializer.collect_str(raw),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,12 +102,18 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for SteamId {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
SteamId::Id(id) => write!(f, "{id}"),
|
||||||
|
SteamId::Raw(raw) => write!(f, "{raw}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Render for SteamId {
|
impl Render for SteamId {
|
||||||
fn render_to(&self, buffer: &mut String) {
|
fn render_to(&self, buffer: &mut String) {
|
||||||
match self {
|
write!(buffer, "{self}").unwrap()
|
||||||
SteamId::Id(id) => write!(buffer, "{id}").unwrap(),
|
|
||||||
SteamId::Raw(raw) => buffer.push_str(raw),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
80
src/data/user.rs
Normal file
80
src/data/user.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
use crate::data::steam_id::SteamId;
|
||||||
|
use crate::Result;
|
||||||
|
use rand::distributions::Alphanumeric;
|
||||||
|
use rand::Rng;
|
||||||
|
use reqwest::get;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{query, Executor, Postgres};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub steam_id: SteamId,
|
||||||
|
pub name: String,
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub async fn get(
|
||||||
|
connection: impl Executor<'_, Database = Postgres> + Copy,
|
||||||
|
steam_id: SteamId,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let user = query!(
|
||||||
|
r#"SELECT
|
||||||
|
token as "token!", name as "name!"
|
||||||
|
FROM users_named WHERE steamid = $1"#,
|
||||||
|
steam_id.steamid64()
|
||||||
|
)
|
||||||
|
.fetch_optional(connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(user) = user {
|
||||||
|
Ok(User {
|
||||||
|
steam_id,
|
||||||
|
token: user.token,
|
||||||
|
name: user.name,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let profile = Self::fetch(&steam_id).await?;
|
||||||
|
let token: String = rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(64)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
query!(
|
||||||
|
r#"INSERT INTO users(steamid, name, avatar, token)
|
||||||
|
VALUES($1, $2, $3, $4)"#,
|
||||||
|
steam_id.steamid64(),
|
||||||
|
profile.name,
|
||||||
|
profile.avatar,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
.execute(connection)
|
||||||
|
.await?;
|
||||||
|
Ok(User {
|
||||||
|
steam_id,
|
||||||
|
token,
|
||||||
|
name: profile.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch(steam_id: &SteamId) -> Result<Profile> {
|
||||||
|
let response = get(format!(
|
||||||
|
"https://steamcommunity.com/profiles/{steam_id}?xml=1"
|
||||||
|
))
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
Ok(quick_xml::de::from_str(&response)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Profile {
|
||||||
|
#[serde(rename = "steamID")]
|
||||||
|
name: String,
|
||||||
|
#[serde(rename = "avatarMedium")]
|
||||||
|
avatar: String,
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,14 @@ pub enum Error {
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error("page not found")]
|
#[error("page not found")]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
#[error("Failed to validate steam auth")]
|
||||||
|
SteamAuth,
|
||||||
|
#[error(transparent)]
|
||||||
|
Request(#[from] reqwest::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Xml(#[from] quick_xml::de::DeError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Session(#[from] async_session::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
impl IntoResponse for Error {
|
||||||
|
|
|
||||||
122
src/main.rs
122
src/main.rs
|
|
@ -3,19 +3,26 @@ mod config;
|
||||||
mod data;
|
mod data;
|
||||||
mod error;
|
mod error;
|
||||||
mod pages;
|
mod pages;
|
||||||
|
mod session;
|
||||||
|
|
||||||
pub use crate::config::Config;
|
pub use crate::config::Config;
|
||||||
use crate::config::Listen;
|
use crate::config::Listen;
|
||||||
use crate::data::demo::{Demo, ListDemo};
|
use crate::data::demo::{Demo, ListDemo};
|
||||||
|
use crate::data::steam_id::SteamId;
|
||||||
|
use crate::data::user::User;
|
||||||
use crate::pages::about::AboutPage;
|
use crate::pages::about::AboutPage;
|
||||||
use crate::pages::demo::DemoPage;
|
use crate::pages::demo::DemoPage;
|
||||||
use crate::pages::index::Index;
|
use crate::pages::index::Index;
|
||||||
use crate::pages::render;
|
use crate::pages::render;
|
||||||
|
use crate::session::{SessionData, COOKIE_NAME};
|
||||||
use asset::{serve_compiled, serve_static};
|
use asset::{serve_compiled, serve_static};
|
||||||
use axum::extract::{MatchedPath, Path};
|
use async_session::{MemoryStore, Session, SessionStore};
|
||||||
use axum::http::Request;
|
use axum::extract::{MatchedPath, Path, RawQuery};
|
||||||
|
use axum::headers::Cookie;
|
||||||
|
use axum::http::header::{LOCATION, SET_COOKIE};
|
||||||
|
use axum::http::{HeaderValue, Request, StatusCode};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{extract::State, routing::get, Router, Server};
|
use axum::{extract::State, routing::get, Router, Server, TypedHeader};
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
use hyperlocal::UnixServerExt;
|
use hyperlocal::UnixServerExt;
|
||||||
use maud::Markup;
|
use maud::Markup;
|
||||||
|
|
@ -25,8 +32,9 @@ use std::fs::{remove_file, set_permissions, Permissions};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use steam_openid::SteamOpenId;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::{info, info_span};
|
use tracing::{error, info, info_span};
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
fmt::layer, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
|
fmt::layer, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
|
||||||
};
|
};
|
||||||
|
|
@ -35,6 +43,8 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
connection: PgPool,
|
connection: PgPool,
|
||||||
|
openid: SteamOpenId,
|
||||||
|
pub session_store: MemoryStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -50,7 +60,14 @@ async fn main() -> Result<()> {
|
||||||
let config = Config::load(&config)?;
|
let config = Config::load(&config)?;
|
||||||
let connection = config.database.connect().await?;
|
let connection = config.database.connect().await?;
|
||||||
|
|
||||||
let state = Arc::new(App { connection });
|
let session_store = MemoryStore::new();
|
||||||
|
|
||||||
|
let state = Arc::new(App {
|
||||||
|
connection,
|
||||||
|
openid: SteamOpenId::new(&config.site.url, "/login/callback")
|
||||||
|
.expect("invalid steam login url"),
|
||||||
|
session_store: session_store.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
|
|
@ -58,6 +75,9 @@ async fn main() -> Result<()> {
|
||||||
.route("/images/logo.png", get(serve_static!("../images/logo.png")))
|
.route("/images/logo.png", get(serve_static!("../images/logo.png")))
|
||||||
.route("/images/logo.svg", get(serve_static!("../images/logo.svg")))
|
.route("/images/logo.svg", get(serve_static!("../images/logo.svg")))
|
||||||
.route("/about", get(about))
|
.route("/about", get(about))
|
||||||
|
.route("/login/callback", get(login_callback))
|
||||||
|
.route("/login", get(login))
|
||||||
|
.route("/logout", get(logout))
|
||||||
.route("/:id", get(demo))
|
.route("/:id", get(demo))
|
||||||
.layer(
|
.layer(
|
||||||
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
||||||
|
|
@ -99,20 +119,100 @@ async fn main() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn index(State(app): State<Arc<App>>) -> Result<Markup> {
|
async fn index(State(app): State<Arc<App>>, session: SessionData) -> Result<Markup> {
|
||||||
let demos = ListDemo::list(&app.connection, None).await?;
|
let demos = ListDemo::list(&app.connection, None).await?;
|
||||||
Ok(render(Index { demos }))
|
Ok(render(Index { demos }, session))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn about(State(_app): State<Arc<App>>) -> Result<Markup> {
|
async fn about(State(_app): State<Arc<App>>, session: SessionData) -> Result<Markup> {
|
||||||
Ok(render(AboutPage { key: None }))
|
Ok(render(
|
||||||
|
AboutPage {
|
||||||
|
key: session.token(),
|
||||||
|
},
|
||||||
|
session,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn demo(State(app): State<Arc<App>>, Path(id): Path<u32>) -> Result<Markup> {
|
async fn demo(
|
||||||
|
State(app): State<Arc<App>>,
|
||||||
|
Path(id): Path<u32>,
|
||||||
|
session: SessionData,
|
||||||
|
) -> Result<Markup> {
|
||||||
let demo = Demo::by_id(&app.connection, id)
|
let demo = Demo::by_id(&app.connection, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(Error::NotFound)?;
|
.ok_or(Error::NotFound)?;
|
||||||
Ok(render(DemoPage { demo }))
|
Ok(render(DemoPage { demo }, session))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login_callback(
|
||||||
|
State(app): State<Arc<App>>,
|
||||||
|
RawQuery(query): RawQuery,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
let query = query.as_deref().unwrap_or_default();
|
||||||
|
let steam_id = app.openid.verify(query).await.map_err(|e| {
|
||||||
|
error!("{e:?}");
|
||||||
|
Error::SteamAuth
|
||||||
|
})?;
|
||||||
|
let steam_id = SteamId::new(steam_id);
|
||||||
|
let user = User::get(&app.connection, steam_id).await?;
|
||||||
|
let mut session = Session::new();
|
||||||
|
session
|
||||||
|
.insert("user", user)
|
||||||
|
.expect("failed to serialize user");
|
||||||
|
let cookie = app
|
||||||
|
.session_store
|
||||||
|
.store_session(session)
|
||||||
|
.await?
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok((
|
||||||
|
StatusCode::FOUND,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
SET_COOKIE,
|
||||||
|
HeaderValue::from_str(&format!(
|
||||||
|
"{}={}; HttpOnly; SameSite=Lax; Path=/",
|
||||||
|
COOKIE_NAME, cookie
|
||||||
|
))
|
||||||
|
.expect("invalid cookie"),
|
||||||
|
),
|
||||||
|
(LOCATION, HeaderValue::from_static("/")),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login(State(app): State<Arc<App>>) -> impl IntoResponse {
|
||||||
|
(
|
||||||
|
StatusCode::FOUND,
|
||||||
|
[(
|
||||||
|
LOCATION,
|
||||||
|
HeaderValue::from_str(app.openid.get_redirect_url()).unwrap(),
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn logout(
|
||||||
|
State(app): State<Arc<App>>,
|
||||||
|
cookie: Option<TypedHeader<Cookie>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if let Some(session_cookie) = cookie.as_deref().and_then(|cookie| cookie.get(COOKIE_NAME)) {
|
||||||
|
if let Ok(Some(cookie)) = app.session_store.load_session(session_cookie.into()).await {
|
||||||
|
let _ = app.session_store.destroy_session(cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(
|
||||||
|
StatusCode::FOUND,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
SET_COOKIE,
|
||||||
|
HeaderValue::from_str(&format!(
|
||||||
|
"{}=; HttpOnly; SameSite=Lax; expires=Thu, 01 Jan 1970 00:00:00 GMT",
|
||||||
|
COOKIE_NAME
|
||||||
|
))
|
||||||
|
.expect("invalid cookie"),
|
||||||
|
),
|
||||||
|
(LOCATION, HeaderValue::from_str("/").unwrap()),
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handler_404() -> impl IntoResponse {
|
async fn handler_404() -> impl IntoResponse {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ pub mod index;
|
||||||
mod plugin_section;
|
mod plugin_section;
|
||||||
|
|
||||||
use crate::asset::saved_asset_url;
|
use crate::asset::saved_asset_url;
|
||||||
|
use crate::session::SessionData;
|
||||||
use maud::{html, Markup, DOCTYPE};
|
use maud::{html, Markup, DOCTYPE};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
|
@ -12,7 +13,7 @@ pub trait Page {
|
||||||
fn render(&self) -> Markup;
|
fn render(&self) -> Markup;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render<T: Page>(page: T) -> Markup {
|
pub fn render<T: Page>(page: T, session: SessionData) -> Markup {
|
||||||
let style_url = saved_asset_url!("style.css");
|
let style_url = saved_asset_url!("style.css");
|
||||||
html! {
|
html! {
|
||||||
(DOCTYPE)
|
(DOCTYPE)
|
||||||
|
|
@ -30,8 +31,14 @@ pub fn render<T: Page>(page: T) -> Markup {
|
||||||
span { a href = "/about" { "about" } }
|
span { a href = "/about" { "about" } }
|
||||||
span { a href = "/viewer" { "viewer" } }
|
span { a href = "/viewer" { "viewer" } }
|
||||||
span.beta { a href = "/editor" { "editor" } }
|
span.beta { a href = "/editor" { "editor" } }
|
||||||
|
@if let SessionData::Authenticated(user) = session {
|
||||||
|
span.right { a href = "/logout" { "Logout" } }
|
||||||
|
span.right { a href = "/upload" { "Upload" } }
|
||||||
|
span.right { a href = (user.steam_id.profile_link()) { (user.name) } }
|
||||||
|
} @else {
|
||||||
span.right { a.steam-login href = "/login" { "Sign in through Steam" } }
|
span.right { a.steam-login href = "/login" { "Sign in through Steam" } }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.page { (page.render()) }
|
.page { (page.render()) }
|
||||||
}
|
}
|
||||||
footer {
|
footer {
|
||||||
|
|
|
||||||
65
src/session.rs
Normal file
65
src/session.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
use crate::data::user::User;
|
||||||
|
use crate::{App, Result};
|
||||||
|
use async_session::SessionStore as _;
|
||||||
|
use axum::extract::{FromRef, FromRequestParts};
|
||||||
|
use axum::http::request::Parts;
|
||||||
|
use axum::{async_trait, headers::Cookie, RequestPartsExt, TypedHeader};
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
pub const COOKIE_NAME: &str = "tf_session";
|
||||||
|
|
||||||
|
pub enum SessionData {
|
||||||
|
Authenticated(User),
|
||||||
|
UnAuthenticated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionData {
|
||||||
|
pub fn token(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
SessionData::Authenticated(user) => Some(user.token.clone()),
|
||||||
|
SessionData::UnAuthenticated => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for SessionData
|
||||||
|
where
|
||||||
|
Arc<App>: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = Infallible;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let app: Arc<App> = Arc::from_ref(state);
|
||||||
|
let store = &app.session_store;
|
||||||
|
|
||||||
|
let cookie: Option<TypedHeader<Cookie>> = parts.extract().await.unwrap();
|
||||||
|
|
||||||
|
let session_cookie = cookie.as_ref().and_then(|cookie| cookie.get(COOKIE_NAME));
|
||||||
|
|
||||||
|
// return the new created session cookie for client
|
||||||
|
if session_cookie.is_none() {
|
||||||
|
return Ok(Self::UnAuthenticated);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"SessionData: got session cookie from user agent, {}={}",
|
||||||
|
COOKIE_NAME,
|
||||||
|
session_cookie.unwrap()
|
||||||
|
);
|
||||||
|
// continue to decode the session cookie
|
||||||
|
let Ok(Some(session)) = store
|
||||||
|
.load_session(session_cookie.unwrap().to_owned())
|
||||||
|
.await else {
|
||||||
|
return Ok(Self::UnAuthenticated);
|
||||||
|
};
|
||||||
|
let Some(user) = session.get::<User>("user") else {
|
||||||
|
return Ok(Self::UnAuthenticated);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self::Authenticated(user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ header {
|
||||||
padding: .5em 1em;
|
padding: .5em 1em;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: block;
|
display: block;
|
||||||
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main a {
|
.main a {
|
||||||
|
|
@ -68,6 +69,10 @@ header {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .right {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.steam-login:before {
|
a.steam-login:before {
|
||||||
|
|
@ -84,8 +89,7 @@ a.steam-login:before {
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.steam-login {
|
a.steam-login, a.steam-login:visited {
|
||||||
text-transform: none;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 41px;
|
height: 41px;
|
||||||
margin: -1px 0;
|
margin: -1px 0;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue