use anyhow::{Context, Result};
use chrono::{Duration, Utc};
use sea_orm::*;
use uuid::Uuid;
use crate::entities::{registration_tokens, user_sessions, users};
/// Generate an opaque session token.
pub fn generate_session_token() -> String {
Uuid::new_v4().to_string()
}
/// Generate an opaque registration (invite) token.
pub fn generate_registration_token() -> String {
Uuid::new_v4().to_string()
}
/// Create a new session for `user_id`, valid for `session_duration_hours`.
pub async fn create_session(
db: &DatabaseConnection,
user_id: Uuid,
session_duration_hours: i64,
) -> Result<user_sessions::Model> {
let now = Utc::now();
let session = user_sessions::ActiveModel {
id: Set(Uuid::now_v7()),
user_id: Set(user_id),
session_token: Set(generate_session_token()),
expires_at: Set((now + Duration::hours(session_duration_hours)).into()),
created_at: Set(now.into()),
last_used_at: Set(now.into()),
};
session.insert(db).await.context("failed to create session")
}
/// Validate a session token; on success bumps `last_used_at` and returns the
/// owning user alongside the session.
pub async fn validate_session(
db: &DatabaseConnection,
token: &str,
) -> Result<Option<(users::Model, user_sessions::Model)>> {
let found = user_sessions::Entity::find()
.filter(user_sessions::Column::SessionToken.eq(token))
.filter(user_sessions::Column::ExpiresAt.gt(Utc::now()))
.find_also_related(users::Entity)
.one(db)
.await
.context("failed to query session")?;
if let Some((session, Some(user))) = found {
let mut update: user_sessions::ActiveModel = session.clone().into();
update.last_used_at = Set(Utc::now().into());
update.update(db).await.context("failed to touch session")?;
Ok(Some((user, session)))
} else {
Ok(None)
}
}
/// Delete a session (logout). Returns whether a row was removed.
pub async fn delete_session(db: &DatabaseConnection, token: &str) -> Result<bool> {
let result = user_sessions::Entity::delete_many()
.filter(user_sessions::Column::SessionToken.eq(token))
.exec(db)
.await
.context("failed to delete session")?;
Ok(result.rows_affected > 0)
}
/// Purge expired sessions. Handy to call from a periodic task.
pub async fn cleanup_expired_sessions(db: &DatabaseConnection) -> Result<u64> {
let result = user_sessions::Entity::delete_many()
.filter(user_sessions::Column::ExpiresAt.lt(Utc::now()))
.exec(db)
.await
.context("failed to clean up sessions")?;
Ok(result.rows_affected)
}
/// Create an invite token, optionally pinned to an email address.
pub async fn create_registration_token(
db: &DatabaseConnection,
created_by_user_id: Option<Uuid>,
email: Option<String>,
expires_in_days: i64,
) -> Result<registration_tokens::Model> {
let now = Utc::now();
let token = registration_tokens::ActiveModel {
id: Set(Uuid::now_v7()),
token: Set(generate_registration_token()),
email: Set(email),
created_by_user_id: Set(created_by_user_id),
expires_at: Set((now + Duration::days(expires_in_days)).into()),
is_used: Set(false),
used_by_user_id: Set(None),
used_at: Set(None),
created_at: Set(now.into()),
};
token.insert(db).await.context("failed to create registration token")
}
/// Validate an invite token for the given email. An email-pinned token only
/// matches when the email matches.
pub async fn validate_registration_token(
db: &DatabaseConnection,
token: &str,
email: &str,
) -> Result<Option<registration_tokens::Model>> {
let found = registration_tokens::Entity::find()
.filter(registration_tokens::Column::Token.eq(token))
.filter(registration_tokens::Column::IsUsed.eq(false))
.filter(registration_tokens::Column::ExpiresAt.gt(Utc::now()))
.one(db)
.await
.context("failed to query registration token")?;
match found {
Some(t) => {
if let Some(pinned) = &t.email {
if pinned != email {
return Ok(None);
}
}
Ok(Some(t))
}
None => Ok(None),
}
}
/// Mark an invite token as consumed by `used_by_user_id`.
pub async fn mark_registration_token_used(
db: &DatabaseConnection,
token_id: Uuid,
used_by_user_id: Uuid,
) -> Result<()> {
let mut token: registration_tokens::ActiveModel = registration_tokens::Entity::find_by_id(token_id)
.one(db)
.await
.context("failed to find registration token")?
.context("registration token not found")?
.into();
token.is_used = Set(true);
token.used_by_user_id = Set(Some(used_by_user_id));
token.used_at = Set(Some(Utc::now().into()));
token.update(db).await.context("failed to mark token used")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tokens_are_distinct_uuids() {
let a = generate_session_token();
let b = generate_registration_token();
assert_ne!(a, b);
assert!(Uuid::parse_str(&a).is_ok());
assert!(Uuid::parse_str(&b).is_ok());
}
}
neiam
/fieri-forever
the flavortown lunch engine
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
5.2 KiB · text
History
25c4237