From 9046f032315c5edc7ba6ec3743ef783cb26a84d3 Mon Sep 17 00:00:00 2001 From: Luc Date: Sat, 27 Jul 2024 20:52:01 +0000 Subject: [PATCH] Introduce basic session management --- engine/Cargo.lock | 191 ++++++++++++---------------- engine/Cargo.toml | 12 +- engine/migrations/0001_init.sql | 2 +- engine/migrations/0002_sessions.sql | 8 ++ engine/src/auth/mod.rs | 1 + engine/src/auth/session.rs | 12 ++ engine/src/database/mod.rs | 74 +++++------ engine/src/models/user_data.rs | 1 - engine/src/routes/auth.rs | 23 +++- engine/src/routes/mod.rs | 4 +- 10 files changed, 170 insertions(+), 158 deletions(-) create mode 100644 engine/migrations/0002_sessions.sql create mode 100644 engine/src/auth/session.rs diff --git a/engine/Cargo.lock b/engine/Cargo.lock index 662f488..d9f9967 100644 --- a/engine/Cargo.lock +++ b/engine/Cargo.lock @@ -249,17 +249,6 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" -[[package]] -name = "async-trait" -version = "0.1.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - [[package]] name = "atoi" version = "2.0.0" @@ -281,61 +270,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "axum" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 0.1.2", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "backtrace" version = "0.3.69" @@ -469,8 +403,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -657,6 +593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1006,13 +943,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.3" @@ -1029,7 +972,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown", + "hashbrown 0.14.3", ] [[package]] @@ -1269,6 +1212,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -1276,7 +1230,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", + "serde", ] [[package]] @@ -1401,12 +1356,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "md-5" version = "0.10.6" @@ -1838,7 +1787,7 @@ dependencies = [ "serde_urlencoded", "serde_yaml", "smallvec", - "sync_wrapper 1.0.1", + "sync_wrapper", "tempfile", "thiserror", "time", @@ -1872,7 +1821,7 @@ dependencies = [ "derive_more", "email_address", "futures-util", - "indexmap", + "indexmap 2.2.6", "mime", "num-traits", "poem", @@ -1895,7 +1844,7 @@ checksum = "0812a8f13ac63020d1274d80ea2229e858ed0118a2d9537744465995e0913375" dependencies = [ "darling", "http", - "indexmap", + "indexmap 2.2.6", "mime", "proc-macro-crate", "proc-macro2", @@ -2130,7 +2079,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", @@ -2298,12 +2247,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustversion" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" - [[package]] name = "ryu" version = "1.0.16" @@ -2390,32 +2333,52 @@ version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ - "indexmap", + "indexmap 2.2.6", "itoa", "ryu", "serde", ] [[package]] -name = "serde_path_to_error" -version = "0.1.14" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ + "form_urlencoded", "itoa", + "ryu", "serde", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "serde_with" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.72", ] [[package]] @@ -2424,7 +2387,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -2585,7 +2548,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap", + "indexmap 2.2.6", "log", "memchr", "once_cell", @@ -2798,12 +2761,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.1" @@ -3039,7 +2996,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap", + "indexmap 2.2.6", "toml_datetime", "winnow", ] @@ -3057,7 +3014,6 @@ dependencies = [ "tokio", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -3253,16 +3209,33 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", + "rand", + "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1cd046f83ea2c4e920d6ee9f7c3537ef928d75dce5d84a87c2c5d6b3999a3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] [[package]] name = "v3x-property-engine" version = "0.1.0" dependencies = [ "async-std", - "axum", + "chrono", "dotenv", "dotenvy", "openid", @@ -3271,10 +3244,12 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_with", "sqlx", "terminal-banner", "tracing", "tracing-subscriber", + "uuid", ] [[package]] diff --git a/engine/Cargo.toml b/engine/Cargo.toml index ac50786..b1a53c1 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] async-std = { version = "1.12.0", features = ["attributes", "tokio1"] } -axum = "0.7.3" +chrono = "0.4.38" dotenv = "0.15.0" dotenvy = "0.15.7" openid = "0.14.0" @@ -16,11 +16,21 @@ poem-openapi = { version = "5.0.3", features = ["email", "email_address", "redoc reqwest = "0.12.5" serde = "1.0.204" serde_json = "1.0.120" +serde_with = { version = "3.9.0", features = ["json", "chrono"] } sqlx = { version = "0.7.4", features = ["runtime-async-std", "tls-rustls", "postgres", "uuid", "chrono", "json"] } terminal-banner = "0.4.1" tracing = "0.1.40" tracing-subscriber = "0.3.18" +[dependencies.uuid] +version = "1.10.0" +features = [ + "serde", # Lets you serialize UUIDs + "v4", # Lets you generate random UUIDs + "fast-rng", # Use a faster (but still sufficiently random) RNG + "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs +] + [features] default = ["oauth2"] oauth2 = [] diff --git a/engine/migrations/0001_init.sql b/engine/migrations/0001_init.sql index 3bdf457..d93e848 100644 --- a/engine/migrations/0001_init.sql +++ b/engine/migrations/0001_init.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, - oauth_sub VARCHAR(255) NOT NULL, + oauth_sub VARCHAR(255) NOT NULL UNIQUE, oauth_data JSONB NOT NULL, nickname VARCHAR(255) ); diff --git a/engine/migrations/0002_sessions.sql b/engine/migrations/0002_sessions.sql new file mode 100644 index 0000000..a69f676 --- /dev/null +++ b/engine/migrations/0002_sessions.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS sessions +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id INT NOT NULL, + user_agent VARCHAR(255) NOT NULL, + last_access TIMESTAMPTZ NOT NULL DEFAULT NOW(), + valid BOOLEAN NOT NULL DEFAULT TRUE +); diff --git a/engine/src/auth/mod.rs b/engine/src/auth/mod.rs index 2083fc2..8a75945 100644 --- a/engine/src/auth/mod.rs +++ b/engine/src/auth/mod.rs @@ -1,4 +1,5 @@ pub mod oauth; +pub mod session; pub trait AuthenticationProvider { async fn isValidAuthToken(&self, authToken: &str) -> bool; diff --git a/engine/src/auth/session.rs b/engine/src/auth/session.rs new file mode 100644 index 0000000..dba10ff --- /dev/null +++ b/engine/src/auth/session.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; +use sqlx::types::chrono; +use uuid::Uuid; + +#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)] +pub struct SessionState { + pub id: Uuid, + pub user_id: i32, + pub user_agent: String, + pub last_access: chrono::DateTime, + pub valid: bool, +} diff --git a/engine/src/database/mod.rs b/engine/src/database/mod.rs index 64b10eb..8f0af1f 100644 --- a/engine/src/database/mod.rs +++ b/engine/src/database/mod.rs @@ -10,8 +10,9 @@ use sqlx::{ Execute, Executor, PgPool, }; use tracing::info; +use uuid::Uuid; -use crate::models::user_data::UserData; +use crate::{auth::session::SessionState, models::user_data::UserData}; #[derive(Debug)] pub struct Database { @@ -45,53 +46,38 @@ impl Database { info!("upsert_get_user: sub: {}", sub); - match sqlx::query_as::<_, UserData>("SELECT * FROM users WHERE oauth_sub = $1") + sqlx::query_as::<_, UserData>( + "INSERT INTO users (oauth_sub, oauth_data) VALUES ($1, $2) ON CONFLICT (oauth_sub) DO UPDATE SET oauth_data = $2 RETURNING *" + ) .bind(sub) + .bind(Json(oauth_userinfo)) + .fetch_one(&self.pool).await + } + + pub async fn get_user_from_id(&self, id: i32) -> Result { + let user = sqlx::query_as::<_, UserData>("SELECT * FROM users WHERE id = $1") + .bind(id) .fetch_one(&self.pool) - .await - { - Ok(x) => { - info!("upsert_get_user_found: {:?}", x); - Ok(x) - } - Err(e) => { - info!("upsert_get_user: not found {}", e); - - let mut local_userdata = UserData { - id: 0, - oauth_sub: sub.to_string(), - oauth_data: Json(oauth_userinfo.clone()), - nickname: None, - }; - - let insert_query = sqlx::query!( - "INSERT INTO users (oauth_sub, oauth_data) VALUES ($1, $2) RETURNING id", - local_userdata.oauth_sub, - Json(&oauth_userinfo) as _ - ) - .fetch_one(&self.pool) - .await?; - - info!("upsert_get_user: {:?}", insert_query); - - local_userdata.id = insert_query.id; - - Ok(local_userdata) - } - } + .await?; + + Ok(user) } - // pub async fn insert_user(&self, user: &Userinfo) -> Result { - // let x = self - // .pool - // .execute( - // sqlx::query!( - // "INSERT INTO users (id, email, name, picture, locale, timezone) VALUES ($1, $2, $3, $4, $5, $6)", + pub async fn create_session(&self, user_id: i32, user_agent: &str) -> Result { + let session = sqlx::query_as::<_, SessionState>("INSERT INTO sessions (user_id, user_agent) VALUES ($1, $2) RETURNING *") + .bind(user_id) + .bind(user_agent) + .fetch_one(&self.pool) + .await?; + Ok(session) + } - // ) - // ) - // .await?; + pub async fn get_session_by_id(&self, id: Uuid) -> Result { + let session = sqlx::query_as::<_, SessionState>("SELECT * FROM sessions WHERE id = $1 AND valid = TRUE") + .bind(id) + .fetch_one(&self.pool) + .await?; - // Ok(x.rows_affected()) - // } + Ok(session) + } } diff --git a/engine/src/models/user_data.rs b/engine/src/models/user_data.rs index 4a8c264..db07f62 100644 --- a/engine/src/models/user_data.rs +++ b/engine/src/models/user_data.rs @@ -2,7 +2,6 @@ use openid::Userinfo; use serde::{Deserialize, Serialize}; use sqlx::types::Json; - #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)] pub struct UserData { pub id: i32, diff --git a/engine/src/routes/auth.rs b/engine/src/routes/auth.rs index 92d82fa..f01ee68 100644 --- a/engine/src/routes/auth.rs +++ b/engine/src/routes/auth.rs @@ -1,7 +1,9 @@ use crate::state::AppState; use openid::{Options, Token}; -use poem::{handler, web::{headers::Header, Data, Query, Redirect}, IntoResponse}; +use poem::{handler, web::{cookie::{Cookie, CookieJar}, Data, Json, Query, Redirect}, IntoResponse}; use serde::Deserialize; +use tracing::info; +use uuid::Uuid; use std::sync::Arc; #[handler] @@ -63,9 +65,26 @@ pub async fn callback(query: Query, state: Data<&Arc>) -> imp .await .unwrap(); + let session = state.database.create_session(user.id, "test").await.unwrap(); + + // let session = state.database.get_session_by_id(&user.id).await.unwrap(); + // TODO: return a token - let token = "hello".to_string(); + let token = session.id; Redirect::temporary("http://localhost:3000/hello").with_header("Set-Cookie", format!("property.v3x.token={}", token)) } + +#[handler] +pub async fn me(state: Data<&Arc>, cookies: &CookieJar) -> impl IntoResponse { + + let token = cookies.get("property.v3x.token").unwrap(); + let token = Uuid::parse_str(token.value_str()).unwrap(); + + let session = state.database.get_session_by_id(token).await.unwrap(); + + let user = state.database.get_user_from_id(session.user_id).await.unwrap(); + + Json(user) +} diff --git a/engine/src/routes/mod.rs b/engine/src/routes/mod.rs index 25a2881..4c8134b 100644 --- a/engine/src/routes/mod.rs +++ b/engine/src/routes/mod.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use poem::{get, handler, listener::TcpListener, post, web::{Html, Path}, Endpoint, EndpointExt, Route, Server}; +use poem::{get, handler, listener::TcpListener, middleware::CookieJarManager, post, web::{Html, Path}, Endpoint, EndpointExt, Route, Server}; use poem_openapi::{param::Query, payload::PlainText, OpenApi, OpenApiService}; use crate::state::AppState; @@ -37,10 +37,12 @@ pub async fn serve(state: AppState) -> Result<(), poem::Error> { let app = Route::new() .at("/login", get(auth::login)) + .at("/me", get(auth::me)) .at("/callback", get(auth::callback)) .nest("/api", api_service) .nest("/openapi.json", spec) .at("/", get(ui)) + .with(CookieJarManager::new()) .data(state); // .at("/", get(root)) // .route("/login", get(auth::login))