use std::{collections::HashSet, str::FromStr};
use axum::async_trait;
use axum_login::{AuthUser, AuthnBackend, AuthzBackend, UserId};
use password_auth::{generate_hash, verify_password, VerifyError};
use secrecy::{CloneableSecret, DebugSecret, ExposeSecret, Secret, SerializableSecret, Zeroize};
use serde::{Deserialize, Serialize};
use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal};
use ulid::Ulid;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Password(String);
impl Password {
pub fn new<S: AsRef<str>>(password: S) -> Self {
Self(generate_hash(password.as_ref()))
}
pub fn verify<S: AsRef<str>>(&self, attempt: S) -> bool {
match verify_password(attempt.as_ref(), &self.0) {
Ok(()) => true,
Err(VerifyError::PasswordInvalid) => false,
Err(VerifyError::Parse(_)) => false,
}
}
}
impl Zeroize for Password {
fn zeroize(&mut self) {
self.0.zeroize();
}
}
impl SerializableSecret for Password {}
impl CloneableSecret for Password {}
impl DebugSecret for Password {}
pub type SecretPassword = Secret<Password>;
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct User {
id: Thing,
email: String,
pub username: String,
password: SecretPassword,
}
impl User {
pub fn code(&self) -> u64 {
const DEFAULT_CODE: u64 = 3033132;
const MAX_CODE: u64 = 999999;
let uuid: uuid::Uuid = match uuid::Uuid::from_str(&self.id.id.to_raw()) {
Ok(uuid) => uuid,
Err(_) => return DEFAULT_CODE,
};
let ulid: Ulid = uuid.into();
ulid.timestamp_ms() % MAX_CODE
}
}
impl AuthUser for User {
type Id = Thing;
fn id(&self) -> Self::Id {
self.id.clone()
}
fn session_auth_hash(&self) -> &[u8] {
self.password.expose_secret().0.as_bytes()
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Credentials {
pub email: String,
pub password: String,
pub next: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Backend {
db: Surreal<Client>,
}
impl Backend {
pub fn new(db: Surreal<Client>) -> Self {
Self { db }
}
}
#[async_trait]
impl AuthnBackend for Backend {
type User = User;
type Credentials = Credentials;
type Error = surrealdb::Error;
async fn authenticate(
&self,
creds: Self::Credentials,
) -> Result<Option<Self::User>, Self::Error> {
let user: Option<User> = self
.db
.query("SELECT email, username, password, id, code FROM user WHERE email = <string> $email")
.bind(("email", creds.email.to_ascii_lowercase()))
.await?
.take(0)?;
Ok(user.filter(|user| user.password.expose_secret().verify(creds.password)))
}
async fn get_user(&self, user_id: &UserId<Backend>) -> Result<Option<Self::User>, Self::Error> {
let user: Option<User> = self
.db
.query("SELECT username, password, email, code, id FROM user WHERE id = $id")
.bind(("id", user_id))
.await?
.take(0)?;
Ok(user)
}
}
#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone, Serialize)]
struct Record {
#[allow(dead_code)]
id: Thing,
}
#[derive(Hash, PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub struct Permission {
id: Thing,
aspect: String,
subject: Option<Record>,
}
#[async_trait]
impl AuthzBackend for Backend {
type Permission = Permission;
async fn get_user_permissions(
&self,
user: &Self::User,
) -> Result<HashSet<Permission>, Self::Error> {
let user_permissions: Vec<Permission> = self
.db
.query(r#"SELECT id, aspect, ->granted->user FROM permission WHERE user == $id"#)
.bind(("id", user.id.clone()))
.await?
.take(0)?;
Ok(user_permissions.into_iter().collect())
}
async fn get_group_permissions(
&self,
user: &Self::User,
) -> Result<HashSet<Permission>, Self::Error> {
let group_permissions: Vec<Permission> = self.db.query(r#"SELECT id, aspect, ->granted->group<-member_of<-user FROM permission WHERE user == $id"#).bind(("id", user.id.clone())).await?.take(0)?;
Ok(group_permissions.into_iter().collect())
}
}
pub type AuthSession = axum_login::AuthSession<Backend>;