mabel/src/auth.rs

155 lines
3.9 KiB
Rust

use anyhow::{anyhow, Result};
use argon2::{
password_hash::{rand_core::OsRng, SaltString},
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
};
use chrono::{
serde::{ts_seconds, ts_seconds_option},
DateTime, Duration, Utc,
};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Pool, Postgres};
use uuid::Uuid;
#[derive(Deserialize, Serialize, FromRow)]
pub struct User {
/// User internal ID
pub id: Uuid,
/// User name (unique per instance, shows up in URLs)
pub name: String,
/// User email (for validation/login)
pub email: Option<String>,
/// Hashed password
pub password: Option<Vec<u8>>,
/// User's chosen displayed name
pub display_name: Option<String>,
/// Biography / User description
pub bio: Option<String>,
/// User roles (as role IDs)
pub roles: Vec<Uuid>,
/// Times
#[serde(with = "ts_seconds")]
pub created_at: DateTime<Utc>,
#[serde(with = "ts_seconds_option")]
pub modified_at: Option<DateTime<Utc>>,
#[serde(with = "ts_seconds_option")]
pub deleted_at: Option<DateTime<Utc>>,
}
#[derive(Deserialize, Serialize, FromRow)]
pub struct Role {
/// Role ID
pub id: Uuid,
/// Role scopes (permissions)
pub scopes: Vec<String>,
}
#[derive(Deserialize, Serialize, FromRow)]
pub struct Session {
/// Role ID
pub id: Uuid,
/// User ID
pub actor: Uuid,
/// Secret
pub secret: String,
/// Times
#[serde(with = "ts_seconds")]
pub created_at: DateTime<Utc>,
#[serde(with = "ts_seconds")]
pub expires_at: DateTime<Utc>,
}
impl Session {
pub async fn create(pool: &Pool<Postgres>, user_id: Uuid, duration: Duration) -> Result<Self> {
let now = Utc::now();
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.naive_utc(),
expires.naive_utc()
)
.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>, id: Uuid) -> Result<Option<Self>> {
Ok(sqlx::query_as("SELECT * FROM sessions WHERE id = $1")
.bind(id)
.fetch_optional(pool)
.await?)
}
pub async fn refresh(self: Self, pool: &Pool<Postgres>, duration: Duration) -> Result<Self> {
let expires_at = Utc::now() + duration;
let secret = random();
sqlx::query!(
"UPDATE sessions SET secret = $1, expires_at = $2 WHERE id = $3 RETURNING id",
secret,
expires_at.naive_utc(),
self.id
)
.fetch_one(pool)
.await?;
Ok(Session {
secret,
expires_at,
..self
})
}
pub fn token(self: &Self) -> String {
format!("{}:{}", self.id.as_u128(), self.secret)
}
pub fn parse_token(token: String) -> Result<(Uuid, String)> {
let (uuid_str, token_str) = token.split_once(":").ok_or(anyhow!("malformed token"))?;
Ok((
Uuid::from_u128(uuid_str.parse::<u128>()?),
token_str.to_string(),
))
}
}
pub fn random() -> String {
SaltString::generate(&mut OsRng).to_string()
}
pub fn hash(plaintext: &String) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
let hashed = Argon2::default()
.hash_password(plaintext.as_bytes(), &salt)
.map_err(|err| anyhow!(err))?
.to_string();
Ok(hashed)
}
pub fn verify(plaintext: &String, hash: &String) -> Result<bool> {
let parsed_hash = PasswordHash::new(hash).map_err(|err| anyhow!(err))?;
Ok(Argon2::default()
.verify_password(plaintext.as_bytes(), &parsed_hash)
.is_ok())
}