155 lines
3.9 KiB
Rust
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())
|
|
}
|