use chrono::Utc; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use crate::get_pg; use super::{User, UserRole, gen_token}; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Session { /// The unique ID of the session token pub id: uuid::Uuid, /// The generated session token pub token: String, /// The username associated with the session token pub user: String, /// Session creation time pub created: chrono::DateTime, /// Internal CSRF value csrf: uuid::Uuid, /// Named session value pub name: Option, /// Kind of session pub kind: SessionKind, } #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] #[sqlx(type_name = "session_kind", rename_all = "lowercase")] pub enum SessionKind { API, USER, } pub trait Sessions { fn from_session(session: String) -> impl std::future::Future>; fn login( username: &str, password: &str, ) -> impl std::future::Future>; fn api_key(&self, name: &str) -> impl std::future::Future; fn session(&self) -> impl std::future::Future; fn list_sessions(&self) -> impl std::future::Future>; fn end_session(&self) -> impl std::future::Future; } impl Sessions for User { /// Generate a new API Key session async fn api_key(&self, name: &str) -> Session { sqlx::query_as( "INSERT INTO user_session (token, \"user\", kind, name) VALUES ($1, $2, $3, $4) RETURNING *", ) .bind(gen_token(64)) .bind(&self.username) .bind(SessionKind::API) .bind(name) .fetch_one(get_pg!()) .await .unwrap() } /// End a user session async fn end_session(&self) { sqlx::query("DELETE FROM user_session WHERE token = $1") .bind(&self.session) .execute(get_pg!()) .await .unwrap(); } /// Get all sessions for a user async fn list_sessions(&self) -> Vec { sqlx::query_as("SELECT * FROM user_session WHERE \"user\" = $1") .bind(&self.username) .fetch_all(get_pg!()) .await .unwrap() } // Get a user from session ID async fn from_session(session: String) -> Option { let user: Option = sqlx::query_as("SELECT * FROM users WHERE username = (SELECT \"user\" FROM user_session WHERE token = $1)").bind(&session).fetch_optional(get_pg!()).await.unwrap(); if let Some(mut user) = user { user.session = session; return Some(user); } None } /// Login a user with the given username and password async fn login(username: &str, password: &str) -> Option<(Session, UserRole)> { let u = Self::find(username).await?; if !u.verify_pw(password) { return None; } Some((u.session().await, u.user_role)) } /// Generate a new session token for the user /// /// Returns a Session instance containing the generated token and associated user async fn session(&self) -> Session { sqlx::query_as( "INSERT INTO user_session (token, \"user\", kind) VALUES ($1, $2, $3) RETURNING *", ) .bind(gen_token(64)) .bind(&self.username) .bind(SessionKind::USER) .fetch_one(get_pg!()) .await .unwrap() } }