mabel/src/auth/session.rs

187 lines
5.4 KiB
Rust

use anyhow::{anyhow, Result};
use axum::http::StatusCode;
use chrono::{Duration, NaiveDateTime, Utc};
use cookie::Cookie;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Pool, Postgres};
use uuid::Uuid;
use crate::error::AppError;
use super::{hash::random, user::User};
pub const USER_NOT_FOUND: AppError = AppError::ClientError {
status: StatusCode::UNAUTHORIZED,
code: "user-not-found",
message: "The logged-in user was not found",
};
#[derive(Deserialize, Serialize, Clone, FromRow)]
pub struct Session {
/// Role ID
pub id: Uuid,
/// User ID
pub actor: Uuid,
/// Secret
pub secret: String,
/// Times
pub created_at: NaiveDateTime,
pub expires_at: NaiveDateTime,
}
impl Session {
pub async fn create(pool: &Pool<Postgres>, user_id: Uuid, duration: Duration) -> Result<Self> {
let now = Utc::now().naive_utc();
let expires = now + duration;
let secret = random();
let result = sqlx::query!(
"INSERT INTO sessions (actor, secret, created_at, expires_at) VALUES ($1, $2, $3, $4) RETURNING id",
user_id,
secret,
now,
expires
)
.fetch_one(pool)
.await?;
Ok(Self {
id: result.id,
actor: user_id,
secret: secret,
created_at: now,
expires_at: now + duration,
})
}
pub async fn find(pool: &Pool<Postgres>, session_id: Uuid) -> Result<Option<(Self, User)>> {
let record = sqlx::query!(
"SELECT
sessions.id AS session_id,
sessions.actor AS session_actor,
sessions.secret,
sessions.created_at AS session_created_at,
sessions.expires_at,
users.id AS user_id,
users.name,
users.email,
users.display_name,
users.bio,
users.roles,
users.created_at AS user_created_at,
users.modified_at,
users.deleted_at
FROM
sessions
JOIN
users ON sessions.actor = users.id
WHERE
sessions.id = $1",
session_id
)
.fetch_optional(pool)
.await?;
match record {
None => Ok(None),
Some(record) => Ok(Some((
Self {
id: record.session_id,
actor: record.session_actor,
secret: record.secret,
created_at: record.session_created_at,
expires_at: record.expires_at,
},
User {
id: record.user_id,
name: record.name,
email: record.email,
password: None,
display_name: record.display_name,
bio: record.bio,
roles: record.roles,
created_at: record.user_created_at,
modified_at: record.modified_at,
deleted_at: record.deleted_at,
},
))),
}
}
pub async fn refresh(self: Self, pool: &Pool<Postgres>, duration: Duration) -> Result<Self> {
let expires_at = (Utc::now() + duration).naive_utc();
sqlx::query!(
"UPDATE sessions SET expires_at = $1 WHERE id = $2 RETURNING id",
expires_at,
self.id
)
.fetch_one(pool)
.await?;
Ok(Session { expires_at, ..self })
}
pub fn token(self: &Self) -> String {
format!("{}:{}", self.id.as_u128(), self.secret)
}
pub fn parse_token(token: &str) -> Result<(Uuid, String)> {
let (uuid_str, token_str) = token
.split_once(":")
.ok_or_else(|| anyhow!("malformed token"))?;
Ok((
Uuid::from_u128(uuid_str.parse::<u128>()?),
token_str.to_string(),
))
}
pub async fn user(self: &Self, pool: &Pool<Postgres>) -> Result<User, AppError<'static>> {
match User::get_id(pool, self.actor).await? {
Some(user) => Ok(user),
None => Err(USER_NOT_FOUND),
}
}
pub async fn destroy(self: Self, pool: &Pool<Postgres>) -> Result<()> {
sqlx::query!("DELETE FROM sessions WHERE id = $1", self.id)
.execute(pool)
.await?;
Ok(())
}
pub async fn prune_dead(pool: &Pool<Postgres>) -> Result<u64> {
let now = Utc::now().naive_utc();
let result = sqlx::query!("DELETE FROM sessions WHERE expires_at < $1", now)
.execute(pool)
.await?;
Ok(result.rows_affected())
}
pub fn cookie(self: &Self, domain: &str, secure: bool) -> String {
Cookie::build("session", self.token())
.domain(domain)
.secure(secure)
.http_only(!secure)
.path("/")
.expires(
cookie::time::OffsetDateTime::from_unix_timestamp(self.expires_at.timestamp())
.unwrap(),
)
.finish()
.to_string()
}
pub fn cookie_for_delete(domain: &str, secure: bool) -> String {
Cookie::build("session", "")
.domain(domain)
.secure(secure)
.http_only(!secure)
.path("/")
.max_age(cookie::time::Duration::seconds(0))
.finish()
.to_string()
}
}