tailwindcss + wip auth

This commit is contained in:
Per Stark
2024-12-12 20:59:27 +01:00
parent 1c4b3284bf
commit 1d8e19c88f
26 changed files with 2496 additions and 300 deletions

152
src/auth.rs Normal file
View File

@@ -0,0 +1,152 @@
// use crate::{error::ApiError, server::routes::auth::SignupParams, storage::db::SurrealDbClient};
// use axum::async_trait;
// use axum_session_auth::Authentication;
// use serde::{Deserialize, Serialize};
// use surrealdb::{
// engine::any::Any,
// opt::auth::{Database, Namespace, Record},
// Object, Surreal,
// };
// use tracing::info;
// use uuid::Uuid;
// #[derive(Deserialize, Serialize)]
// pub struct AuthParams {
// email: String,
// password: String,
// user_id: String,
// }
// #[derive(Debug, Clone, Serialize, Deserialize)]
// pub struct User {
// pub user_id: String,
// pub email: String,
// #[serde(default)]
// pub anonymous: bool,
// }
// impl Default for User {
// fn default() -> Self {
// Self {
// user_id: "user:guest".into(),
// email: "guest@example.com".into(),
// anonymous: true,
// }
// }
// }
// #[async_trait]
// impl Authentication<User, i64, Surreal<Any>> for User {
// async fn load_user(userid: i64, pool: Option<&Surreal<Any>>) -> Result<User, anyhow::Error> {
// let pool = pool.unwrap();
// User::get_user(userid, pool)
// .await
// .ok_or_else(|| anyhow::anyhow!("Could not load user"))
// }
// fn is_authenticated(&self) -> bool {
// !self.anonymous
// }
// fn is_active(&self) -> bool {
// !self.anonymous
// }
// fn is_anonymous(&self) -> bool {
// self.anonymous
// }
// }
// impl User {
// // pub async fn get_user_by_email(
// // email: &str,
// // db: &SurrealDbClient,
// // ) -> Result<Option<Self>, ApiError> {
// // info!("First, let's see what records exist");
// // let debug_query: Vec<User> = db.select("users").await?;
// // // let debug_query: Vec<User> = db.client.query("SELECT * FROM user").await?.take(0)?;
// // info!("All users in database: {:?}", debug_query);
// // // let tables: Vec<String> = db.client.query("INFO FOR DB").await?.take(0)?;
// // // info!("Available tables: {:?}", tables);
// // // Modified query to match exactly how the record is stored
// // let user: Option<User> = db
// // .client
// // .query("SELECT * FROM user WHERE email = $email LIMIT 1")
// // .bind(("email", email.to_string()))
// // .await?
// // .take(0)?;
// // info!("Found user: {:?}", user);
// // Ok(user)
// // }
// pub async fn get_user(id: i64, pool: &Surreal<Any>) -> Option<Self> {
// let user: Option<User> = pool
// .query("SELECT * FROM user WHERE user_id = $user_id")
// .bind(("user_id", format!("user:{}", id)))
// .await
// .ok()?
// .take(0)
// .ok()?;
// user
// }
// pub async fn signin(params: SignupParams, db: &SurrealDbClient) -> Result<(), ApiError> {
// info!("Trying to sign in");
// let result = db
// .client
// .signin(Record {
// access: "account",
// namespace: "test",
// database: "test",
// params: SignupParams {
// email: params.email,
// password: params.password,
// },
// })
// .await?;
// info!("{:?}", result.into_insecure_token());
// Ok(())
// }
// pub async fn signup(params: SignupParams, db: &SurrealDbClient) -> Result<Self, ApiError> {
// // First check if user already exists
// if let Some(_) = Self::get_user_by_email(&params.email, db).await? {
// return Err(ApiError::UserAlreadyExists);
// }
// // Use SurrealDB's built-in signup
// let signup_response = db
// .client
// .signup(Record {
// access: "account",
// namespace: "test",
// database: "test",
// params: AuthParams {
// email: params.email.clone(),
// password: params.password.clone(),
// user_id: Uuid::new_v4().to_string(),
// },
// })
// .await?;
// info!("Signup response: {:?}", signup_response);
// // Wait a moment to ensure the record is created
// tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
// Self::signin(params, db).await?;
// // Fetch the created user
// // let user = Self::get_user_by_email(&params.email, db)
// // .await?
// // .ok_or(ApiError::UserNotFound)?;
// Ok(User::default())
// }
// }

View File

@@ -1,23 +1,33 @@
use axum::{
extract::DefaultBodyLimit,
http::Method,
routing::{get, post},
Router,
};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{Auth, AuthConfig, AuthSession, AuthSessionLayer, Rights};
use axum_session_surreal::SessionSurrealPool;
use std::sync::Arc;
use surrealdb::{engine::any::Any, Surreal};
use tera::Tera;
use tower_http::services::ServeDir;
use tracing::info;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use zettle_db::{
rabbitmq::{consumer::RabbitMQConsumer, publisher::RabbitMQProducer, RabbitMQConfig},
server::{
routes::{
file::upload_handler, index::index_handler, ingress::ingress_handler,
query::query_handler, queue_length::queue_length_handler,
auth::{show_signup_form, signup_handler},
file::upload_handler,
index::index_handler,
ingress::ingress_handler,
query::query_handler,
queue_length::queue_length_handler,
search_result::search_result_handler,
},
AppState,
},
storage::db::SurrealDbClient,
storage::{db::SurrealDbClient, types::user::User},
};
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
@@ -44,11 +54,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tera: Arc::new(Tera::new("src/server/templates/**/*.html").unwrap()),
openai_client: Arc::new(async_openai::Client::new()),
};
// app_state.surreal_db_client.query("DELETE user").await?;
// setup_auth(&app_state.surreal_db_client).await?;
let session_config = SessionConfig::default()
.with_table_name("test_session_table")
.with_secure(false);
let auth_config = AuthConfig::<String>::default();
let session_store: SessionStore<SessionSurrealPool<Any>> = SessionStore::new(
Some(app_state.surreal_db_client.client.clone().into()),
session_config,
)
.await?;
// Create Axum router
let app = Router::new()
.nest("/api/v1", api_routes_v1())
.nest("", html_routes())
.nest(
"/",
html_routes(
session_store,
auth_config,
app_state.surreal_db_client.client.clone(),
),
)
.with_state(app_state);
tracing::info!("Listening on 0.0.0.0:3000");
@@ -72,9 +103,34 @@ fn api_routes_v1() -> Router<AppState> {
}
/// Router for HTML endpoints
fn html_routes() -> Router<AppState> {
///
fn html_routes(
session_store: SessionStore<SessionSurrealPool<Any>>,
auth_config: AuthConfig<String>,
db_client: Surreal<Any>,
) -> Router<AppState> {
Router::new()
.route("/", get(index_handler))
.route("/search", get(search_result_handler))
.route("/signup", get(show_signup_form).post(signup_handler))
.nest_service("/assets", ServeDir::new("src/server/assets"))
.layer(
AuthSessionLayer::<User, String, SessionSurrealPool<Any>, Surreal<Any>>::new(Some(
db_client,
))
.with_config(auth_config),
)
.layer(SessionLayer::new(session_store))
}
// async fn setup_auth(db: &SurrealDbClient) -> Result<(), Box<dyn std::error::Error>> {
// db.query(
// "DEFINE TABLE user SCHEMALESS;
// DEFINE INDEX unique_name ON TABLE user FIELDS email UNIQUE;
// DEFINE ACCESS account ON DATABASE TYPE RECORD
// SIGNUP ( CREATE user SET email = $email, password = crypto::argon2::generate($password), anonymous = false, user_id = $user_id)
// SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) );",
// )
// .await?;
// Ok(())
// }

View File

@@ -60,16 +60,25 @@ pub enum ApiError {
OpenAIerror(#[from] OpenAIError),
#[error("File error: {0}")]
FileError(#[from] FileError),
#[error("SurrealDb error: {0}")]
SurrealDbError(#[from] surrealdb::Error),
#[error("User already exists")]
UserAlreadyExists,
#[error("User was not found")]
UserNotFound,
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
let (status, error_message) = match &self {
ApiError::ProcessingError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
ApiError::SurrealDbError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
ApiError::PublishingError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
ApiError::DatabaseError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
ApiError::OpenAIerror(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
ApiError::QueryError(_) => (StatusCode::BAD_REQUEST, self.to_string()),
ApiError::UserAlreadyExists => (StatusCode::BAD_REQUEST, self.to_string()),
ApiError::UserNotFound => (StatusCode::BAD_REQUEST, self.to_string()),
ApiError::IngressContentError(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
}

View File

@@ -10,20 +10,20 @@ use async_openai::types::{
ResponseFormatJsonSchema,
};
use serde_json::json;
use surrealdb::engine::remote::ws::Client;
use surrealdb::engine::any::Any;
use surrealdb::Surreal;
use tracing::debug;
use super::types::llm_analysis_result::LLMGraphAnalysisResult;
pub struct IngressAnalyzer<'a> {
db_client: &'a Surreal<Client>,
db_client: &'a Surreal<Any>,
openai_client: &'a async_openai::Client<async_openai::config::OpenAIConfig>,
}
impl<'a> IngressAnalyzer<'a> {
pub fn new(
db_client: &'a Surreal<Client>,
db_client: &'a Surreal<Any>,
openai_client: &'a async_openai::Client<async_openai::config::OpenAIConfig>,
) -> Self {
Self {

View File

@@ -1,3 +1,4 @@
pub mod auth;
pub mod error;
pub mod ingress;
pub mod rabbitmq;

View File

@@ -1,4 +1,4 @@
use surrealdb::{engine::remote::ws::Client, Error, Surreal};
use surrealdb::{engine::any::Any, Error, Surreal};
use tracing::debug;
use crate::storage::types::{knowledge_entity::KnowledgeEntity, StoredObject};
@@ -33,7 +33,7 @@ use crate::storage::types::{knowledge_entity::KnowledgeEntity, StoredObject};
pub async fn find_entities_by_source_ids<T>(
source_id: Vec<String>,
table_name: String,
db_client: &Surreal<Client>,
db_client: &Surreal<Any>,
) -> Result<Vec<T>, Error>
where
T: for<'de> serde::Deserialize<'de>,
@@ -50,7 +50,7 @@ where
/// Find entities by their relationship to the id
pub async fn find_entities_by_relationship_by_id(
db_client: &Surreal<Client>,
db_client: &Surreal<Any>,
entity_id: String,
) -> Result<Vec<KnowledgeEntity>, Error> {
let query = format!(
@@ -65,7 +65,7 @@ pub async fn find_entities_by_relationship_by_id(
/// Get a specific KnowledgeEntity by its id
pub async fn get_entity_by_id(
db_client: &Surreal<Client>,
db_client: &Surreal<Any>,
entity_id: &str,
) -> Result<Option<KnowledgeEntity>, Error> {
db_client

View File

@@ -11,7 +11,7 @@ use crate::{
};
use futures::future::{try_join, try_join_all};
use std::collections::HashMap;
use surrealdb::{engine::remote::ws::Client, Surreal};
use surrealdb::{engine::any::Any, Surreal};
/// Performs a comprehensive knowledge entity retrieval using multiple search strategies
/// to find the most relevant entities for a given query.
@@ -34,7 +34,7 @@ use surrealdb::{engine::remote::ws::Client, Surreal};
/// * `Result<Vec<KnowledgeEntity>, ProcessingError>` - A deduplicated vector of relevant
/// knowledge entities, or an error if the retrieval process fails
pub async fn combined_knowledge_entity_retrieval(
db_client: &Surreal<Client>,
db_client: &Surreal<Any>,
openai_client: &async_openai::Client<async_openai::config::OpenAIConfig>,
query: &str,
) -> Result<Vec<KnowledgeEntity>, ProcessingError> {

View File

@@ -1,4 +1,4 @@
use surrealdb::{engine::remote::ws::Client, Surreal};
use surrealdb::{engine::any::Any, Surreal};
use crate::{error::ProcessingError, utils::embedding::generate_embedding};
@@ -25,7 +25,7 @@ use crate::{error::ProcessingError, utils::embedding::generate_embedding};
pub async fn find_items_by_vector_similarity<T>(
take: u8,
input_text: &str,
db_client: &Surreal<Client>,
db_client: &Surreal<Any>,
table: String,
openai_client: &async_openai::Client<async_openai::config::OpenAIConfig>,
) -> Result<Vec<T>, ProcessingError>

File diff suppressed because it is too large Load Diff

36
src/server/routes/auth.rs Normal file
View File

@@ -0,0 +1,36 @@
use axum::{
extract::State,
response::{Html, IntoResponse},
Form,
};
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use serde::{Deserialize, Serialize};
use surrealdb::{engine::any::Any, Surreal};
use crate::{error::ApiError, server::AppState, storage::types::user::User};
#[derive(Deserialize, Serialize)]
pub struct SignupParams {
pub email: String,
pub password: String,
}
pub async fn show_signup_form(State(state): State<AppState>) -> Html<String> {
let context = tera::Context::new();
let html = state
.tera
.render("auth/signup.html", &context)
.unwrap_or_else(|_| "<h1>Error rendering template</h1>".to_string());
Html(html)
}
pub async fn signup_handler(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<SignupParams>,
) -> Result<impl IntoResponse, ApiError> {
let user = User::create_new(form.email, form.password, &state.surreal_db_client).await?;
auth.login_user(user.id);
Ok(())
}

View File

@@ -1,13 +1,21 @@
use axum::{extract::State, response::Html};
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use serde_json::json;
use surrealdb::{engine::any::Any, Surreal};
use tera::Context;
use tracing::info;
use crate::{error::ApiError, server::AppState};
use crate::{error::ApiError, server::AppState, storage::types::user::User};
pub async fn index_handler(State(state): State<AppState>) -> Result<Html<String>, ApiError> {
pub async fn index_handler(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<Html<String>, ApiError> {
info!("Displaying index page");
info!("{:?}", auth.current_user);
let queue_length = state.rabbitmq_consumer.get_queue_length().await?;
let output = state

View File

@@ -1,3 +1,4 @@
pub mod auth;
pub mod file;
pub mod index;
pub mod ingress;

View File

@@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block content %}
<div class="min-h-screen bg-base-200 flex items-center justify-center">
<div class="card w-96 bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl font-bold text-center mb-4">Sign Up</h2>
<form hx-post="/signup" hx-target="#signup-result" class="space-y-4">
<div class="form-control w-full">
<label class="label">
<span class="label-text">Email</span>
</label>
<input type="text" name="email" placeholder="Enter username" class="input input-bordered w-full" required />
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Password</span>
</label>
<input type="password" name="password" placeholder="Enter password" class="input input-bordered w-full"
required />
</div>
<div class="form-control mt-6">
<button class="btn btn-primary">
Sign Up
<span class="loading loading-spinner hidden"></span>
</button>
</div>
<div id="signup-result"></div>
<div class="divider">OR</div>
<div class="text-center text-sm">
Already have an account?
<a href="/login" class="link link-primary">Login</a>
</div>
</form>
</div>
</div>
</div>
<!-- Add loading indicator when form is submitting -->
<script>
document.body.addEventListener('htmx:beforeRequest', function (evt) {
if (evt.target.tagName === 'FORM') {
evt.target.querySelector('.loading-spinner').classList.remove('hidden');
evt.target.querySelector('button').disabled = true;
}
});
document.body.addEventListener('htmx:afterRequest', function (evt) {
if (evt.target.tagName === 'FORM') {
evt.target.querySelector('.loading-spinner').classList.add('hidden');
evt.target.querySelector('button').disabled = false;
}
});
</script>
{% endblock %}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
@@ -9,30 +9,25 @@
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
</head>
<body class="min-h-screen bg-gradient-to-br from-slate-50 to-purple-200">
<body class="min-h-screen">
<!-- Navbar -->
<nav class="bg-black/30 backdrop-blur-sm border-b border-white/10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center">
<a href="/" class="flex items-center">
<!-- You can add your logo here -->
<span class="text-2xl hover:text-white font-bold text-gray-300 bg-clip-text">
radien
</span>
</a>
</div>
<div class="flex items-center space-x-4">
<a href="/upload"
class="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors">
Upload
</a>
<a href="/files"
class="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors">
Files
</a>
</div>
</div>
<nav class="navbar bg-base-200">
<div class="flex-1">
<a class="btn btn-ghost text-xl">radien</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1">
<li><a>Link</a></li>
<li>
<details>
<summary>Parent</summary>
<ul class="bg-base-100 rounded-t-none p-2">
<li><a>Link 1</a></li>
<li><a>Link 2</a></li>
</ul>
</details>
</li>
</ul>
</div>
</nav>

View File

@@ -18,26 +18,13 @@
<!-- Search Bar -->
<div class="w-full max-w-2xl">
<input type="text" placeholder="Search..." name="query"
class="w-full px-6 py-4 bg-black/30 backdrop-blur-md text-white placeholder-gray-400 outline-none rounded-xl"
hx-get="/search" hx-target="#search-results">
<input type="text" placeholder="Enter your search query" class="input input-bordered w-full" name="query"
hx-get="/search" hx-target="#search-results" />
</div>
<!-- Search Results -->
<div id="search-results" class="w-full max-w-2xl mt-4">
<!-- Results will be populated here by HTMX -->
</div>
<!-- Quick Actions -->
<div class="flex gap-4 mt-8">
<a href="/upload"
class="px-6 py-3 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 rounded-lg text-blue-400 transition-all hover:scale-105">
Upload File
</a>
<a href="/files"
class="px-6 py-3 bg-purple-600/20 hover:bg-purple-600/30 border border-purple-500/30 rounded-lg text-purple-400 transition-all hover:scale-105">
Browse Files
</a>
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,9 @@
<div class="h-auto min-h-36 w-full rounded-md bg-gradient-to-r from-pink-500 via-red-500 to-yellow-500 p-0.5">
<div class="flex flex-col h-full w-full items-center justify-center bg-gray-800 rounded-md p-4 space-y-3">
<p class="font-black text-white text-center break-words">{{result}}</p>
<hr class="w-full border-gray-600" />
<p class="font-black text-white text-center text-sm">{{references}}</p>
<div class="border-">
<div class="chat chat-start">
<div class="chat-bubble">
{{result}}
<hr />
{{references}}
</div>
</div>
</div>

View File

@@ -1,14 +1,14 @@
use super::types::StoredObject;
use std::ops::Deref;
use surrealdb::{
engine::remote::ws::{Client, Ws},
engine::any::{connect, Any},
opt::auth::Root,
Error, Surreal,
};
#[derive(Clone)]
pub struct SurrealDbClient {
pub client: Surreal<Client>,
pub client: Surreal<Any>,
}
impl SurrealDbClient {
@@ -19,7 +19,7 @@ impl SurrealDbClient {
/// # Returns
/// * `SurrealDbClient` initialized
pub async fn new() -> Result<Self, Error> {
let db = Surreal::new::<Ws>("127.0.0.1:8000").await?;
let db = connect("ws://127.0.0.1:8000").await?;
// Sign in to database
db.signin(Root {
@@ -60,7 +60,7 @@ impl SurrealDbClient {
}
impl Deref for SurrealDbClient {
type Target = Surreal<Client>;
type Target = Surreal<Any>;
fn deref(&self) -> &Self::Target {
&self.client
@@ -75,7 +75,7 @@ impl Deref for SurrealDbClient {
///
/// # Returns
/// * `Result` - Item or Error
pub async fn store_item<T>(db_client: &Surreal<Client>, item: T) -> Result<Option<T>, Error>
pub async fn store_item<T>(db_client: &Surreal<Any>, item: T) -> Result<Option<T>, Error>
where
T: StoredObject + Send + Sync + 'static,
{
@@ -92,7 +92,7 @@ where
///
/// # Returns
/// * `Result` - Vec<T> or Error
pub async fn get_all_stored_items<T>(db_client: &Surreal<Client>) -> Result<Vec<T>, Error>
pub async fn get_all_stored_items<T>(db_client: &Surreal<Any>) -> Result<Vec<T>, Error>
where
T: for<'de> StoredObject,
{
@@ -107,7 +107,7 @@ where
///
/// # Returns
/// * `Result<Option<T>, Error>` - The found item or Error
pub async fn get_item<T>(db_client: &Surreal<Client>, id: &str) -> Result<Option<T>, Error>
pub async fn get_item<T>(db_client: &Surreal<Any>, id: &str) -> Result<Option<T>, Error>
where
T: for<'de> StoredObject,
{

View File

@@ -1,5 +1,5 @@
use crate::{error::ProcessingError, stored_object};
use surrealdb::{engine::remote::ws::Client, Surreal};
use surrealdb::{engine::any::Any, Surreal};
use tracing::debug;
use uuid::Uuid;
@@ -28,7 +28,7 @@ impl KnowledgeRelationship {
}
pub async fn store_relationship(
&self,
db_client: &Surreal<Client>,
db_client: &Surreal<Any>,
) -> Result<(), ProcessingError> {
let query = format!(
"RELATE knowledge_entity:`{}` -> relates_to -> knowledge_entity:`{}`",

View File

@@ -5,6 +5,7 @@ pub mod knowledge_entity;
pub mod knowledge_relationship;
pub mod text_chunk;
pub mod text_content;
pub mod user;
#[async_trait]
pub trait StoredObject: Serialize + for<'de> Deserialize<'de> {

100
src/storage/types/user.rs Normal file
View File

@@ -0,0 +1,100 @@
use crate::{
error::ApiError,
storage::db::{get_item, SurrealDbClient},
stored_object,
};
use axum_session_auth::Authentication;
use surrealdb::{engine::any::Any, Surreal};
use uuid::Uuid;
stored_object!(User, "user", {
email: String,
password: String,
anonymous: bool
});
#[async_trait]
impl Authentication<User, String, Surreal<Any>> for User {
async fn load_user(userid: String, pool: Option<&Surreal<Any>>) -> Result<User, anyhow::Error> {
let pool = pool.unwrap();
Ok(get_item::<Self>(&pool, userid.as_str()).await?.unwrap())
// User::get_user(userid, pool)
// .await
// .ok_or_else(|| anyhow::anyhow!("Could not load user"))
}
fn is_authenticated(&self) -> bool {
!self.anonymous
}
fn is_active(&self) -> bool {
!self.anonymous
}
fn is_anonymous(&self) -> bool {
self.anonymous
}
}
impl User {
pub async fn create_new(
email: String,
password: String,
db: &SurrealDbClient,
) -> Result<Self, ApiError> {
// Check if user exists
if let Some(_) = Self::find_by_email(&email, db).await? {
return Err(ApiError::UserAlreadyExists);
}
let id = Uuid::new_v4().to_string();
let user: Option<User> = db
.client
.query(
"CREATE type::thing('user', $id) SET
email = $email,
password = crypto::argon2::generate($password),
anonymous = false",
)
.bind(("id", id))
.bind(("email", email))
.bind(("password", password))
.await?
.take(0)?;
user.ok_or(ApiError::UserAlreadyExists)
}
pub async fn authenticate(
email: String,
password: String,
db: &SurrealDbClient,
) -> Result<Self, ApiError> {
let user: Option<User> = db
.client
.query(
"SELECT * FROM user
WHERE email = $email
AND crypto::argon2::compare(password, $password)",
)
.bind(("email", email))
.bind(("password", password))
.await?
.take(0)?;
user.ok_or(ApiError::UserAlreadyExists)
}
pub async fn find_by_email(
email: &str,
db: &SurrealDbClient,
) -> Result<Option<Self>, ApiError> {
let user: Option<User> = db
.client
.query("SELECT * FROM user WHERE email = $email LIMIT 1")
.bind(("email", email.to_string()))
.await?
.take(0)?;
Ok(user)
}
}