/* * authservice db/session.rs * - pgsql session functions * * Copyright (C) 2025 Real Microsoft, LLC * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use chrono::{DateTime, Duration, TimeDelta, Utc}; use log::error; use rand::Rng; use sea_orm::ActiveValue::Set; use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter}; use serde::{Deserialize, Serialize}; use entity::user_session; use crate::db::{genid, user}; use crate::db::user::FetchUserError; // todo: make this configurable // 10 days pub const SESSION_TTL_SECONDS: u64 = 10 * 24 * 60 * 60; fn generate_session_token() -> String { rand::thread_rng() .sample_iter(&rand::distributions::Alphanumeric) .take(64) .map(char::from) .collect() } #[derive(Debug)] pub enum CreateSessionError { UserDoesntExist, DatabaseError, } pub async fn create_session(db: &DatabaseConnection, user_id: String, ip_address: String) -> Result { user::check_if_user_exists(db, user_id.to_string()).await.map_err(|e| match e { FetchUserError::UserNotFound => { CreateSessionError::UserDoesntExist } FetchUserError::DatabaseError => { CreateSessionError::DatabaseError } })?; let display_id = genid(); let token = generate_session_token(); let now = Utc::now(); let death = now + Duration::try_seconds(SESSION_TTL_SECONDS as i64).expect("BAD SESSION TTL SECONDS"); let record = user_session::ActiveModel { id: Set(display_id.clone()), user: Set(user_id), display_id: Set(display_id), token: Set(token.clone()), flags: Set(0), ip_address: Set(Some(ip_address)), created_on: Set(now.naive_utc()), death_date: Set(death.naive_utc()), }; user_session::Entity::insert(record).exec(db).await.map_err(|e| { error!("DATABASE ERROR WHILE CREATESESSION: {e}"); CreateSessionError::DatabaseError })?; Ok(token) } #[derive(Debug)] pub enum DeleteSessionError { /// session doesn't exist SessionDoesntExist, DatabaseError, } /// deletes the given session from the session table, logging out the user /// returns an error if the user or session doesn't exist, will delete the session regardless of /// if it is expired pub async fn delete_session(db: &DatabaseConnection, token: String) -> Result<(), DeleteSessionError> { match check_if_session_died(db, token.clone(), false, false).await { Ok(_) => {} Err(e) => match e { FetchSessionError::SessionInvalid => {} FetchSessionError::SessionDoesntExist => { return Err(DeleteSessionError::SessionDoesntExist); } FetchSessionError::DatabaseError => { return Err(DeleteSessionError::DatabaseError); } } } user_session::Entity::delete(user_session::Entity::find().filter(user_session::Column::Token.eq(&token)).one(db).await.map_err(|e| { error!("DATABASE ERROR WHILE DELETESESSION (query): {e}"); DeleteSessionError::DatabaseError })?.ok_or(DeleteSessionError::SessionDoesntExist)?.into_active_model()).exec(db).await.map_err(|e| { error!("DATABASE ERROR WHILE DELETESESSION (delete): {e}"); DeleteSessionError::DatabaseError })?; Ok(()) } #[derive(Debug)] pub enum FetchSessionError { /// death date reached SessionInvalid, /// session doesn't exist SessionDoesntExist, DatabaseError, } pub async fn check_if_session_died(db: &DatabaseConnection, token: String, extend_life: bool, rm_session: bool) -> Result<(), FetchSessionError> { let record = user_session::Entity::find().filter(user_session::Column::Token.eq(&token)).one(db).await.map_err(|e| { error!("DATABASE ERROR WHILE CHECKSESSIONDIED (query): {e}"); FetchSessionError::DatabaseError })?.ok_or(FetchSessionError::SessionDoesntExist)?; let now = Utc::now(); // is now bigger than death date? if record.death_date.signed_duration_since(now.naive_utc()) <= TimeDelta::nanoseconds(0) { // died if rm_session { user_session::Entity::delete(record.into_active_model()).exec(db).await.map_err(|e| { error!("DATABASE ERROR WHILE CHECKSESSIONDIED (delete): {e}"); FetchSessionError::DatabaseError })?; } return Err(FetchSessionError::SessionInvalid); } if extend_life { let death = now + Duration::try_seconds(SESSION_TTL_SECONDS as i64).expect("BAD SESSION TTL SECONDS"); let mut record = record.into_active_model(); record.death_date = Set(death.naive_utc()); record.update(db).await.map_err(|e| { error!("DATABASE ERROR WHILE CHECKSESSIONDIED (update): {e}"); FetchSessionError::DatabaseError })?; } Ok(()) } pub async fn get_session_death_date(db: &DatabaseConnection, token: String) -> Result, FetchSessionError> { check_if_session_died(db, token.clone(), false, true).await?; Ok(user_session::Entity::find().filter(user_session::Column::Token.eq(token)).one(db).await.map_err(|e| { error!("DATABASE ERROR WHILE GETSESSIONDEATHDATE: {e}"); FetchSessionError::DatabaseError })?.ok_or(FetchSessionError::SessionDoesntExist)?.death_date.and_utc()) } pub async fn get_userid_from_session(db: &DatabaseConnection, token: String, extend_life: bool) -> Result { check_if_session_died(db, token.clone(), extend_life, true).await?; Ok(user_session::Entity::find().filter(user_session::Column::Token.eq(&token)).one(db).await.map_err(|e| { error!("DATABASE ERROR WHILE SESSION2USERID: {e}"); FetchSessionError::DatabaseError })?.ok_or(FetchSessionError::SessionDoesntExist)?.user) }