use ocre::{Ctx, Result, params};
use serde::{Deserialize, Serialize};
use crate::models::user::User;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserSession {
pub id: i64,
pub user_id: i64,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub last_seen_at: String,
pub expires_at: String,
pub created_at: String,
#[serde(deserialize_with = "ocre::bool_from_sql")]
pub current: bool,
}
pub struct NewUserSession {
pub user_id: i64,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub seconds: i64,
}
pub async fn start(ctx: &Ctx, new: NewUserSession) -> Result<String> {
let token = ocre::token::generate();
let db = ctx.db()?;
db.execute(
"DELETE FROM user_sessions WHERE user_id = ?1 AND expires_at <= datetime('now')",
params![new.user_id],
)
.await?;
let user_agent = new.user_agent.map(|agent| agent.chars().take(255).collect::<String>());
db.execute(
"INSERT INTO user_sessions (user_id, digest, ip_address, user_agent, expires_at) \
VALUES (?1, ?2, ?3, ?4, datetime('now', ?5))",
params![new.user_id, ocre::token::digest(&token), new.ip_address, user_agent, format!("+{} seconds", new.seconds)],
)
.await?;
Ok(token)
}
pub async fn authenticate(ctx: &Ctx, token: &str) -> Result<Option<User>> {
#[derive(Deserialize)]
struct Row {
session_id: i64,
#[serde(deserialize_with = "ocre::bool_from_sql")]
stale: bool,
#[serde(flatten)]
user: User,
}
let db = ctx.db()?;
let row: Option<Row> = db
.first(
"SELECT users.*, user_sessions.id AS session_id, \
(user_sessions.last_seen_at < datetime('now', '-1 hour')) AS stale \
FROM user_sessions JOIN users ON users.id = user_sessions.user_id \
WHERE user_sessions.digest = ?1 AND user_sessions.expires_at > datetime('now')",
params![ocre::token::digest(token)],
)
.await?;
let Some(row) = row else { return Ok(None) };
if row.stale {
db.execute("UPDATE user_sessions SET last_seen_at = datetime('now') WHERE id = ?1", params![row.session_id])
.await?;
}
Ok(Some(row.user))
}
pub async fn for_user(ctx: &Ctx, user_id: i64, token: &str) -> Result<Vec<UserSession>> {
ctx.db()?
.all(
"SELECT id, user_id, ip_address, user_agent, last_seen_at, expires_at, created_at, digest = ?2 AS current \
FROM user_sessions WHERE user_id = ?1 AND expires_at > datetime('now') ORDER BY last_seen_at DESC, id DESC",
params![user_id, ocre::token::digest(token)],
)
.await
}
pub async fn end(ctx: &Ctx, token: &str) -> Result<()> {
ctx.db()?.execute("DELETE FROM user_sessions WHERE digest = ?1", params![ocre::token::digest(token)]).await?;
Ok(())
}
pub async fn revoke(ctx: &Ctx, user_id: i64, id: i64) -> Result<bool> {
Ok(ctx.db()?.execute("DELETE FROM user_sessions WHERE id = ?1 AND user_id = ?2", params![id, user_id]).await? > 0)
}
pub async fn revoke_others(ctx: &Ctx, user_id: i64, token: &str) -> Result<usize> {
ctx.db()?
.execute(
"DELETE FROM user_sessions WHERE user_id = ?1 AND digest != ?2",
params![user_id, ocre::token::digest(token)],
)
.await
}