187 lines
5.4 KiB
Rust
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()
|
|
}
|
|
}
|