backend signup

This commit is contained in:
2025-10-29 15:09:16 +01:00
parent abe3fc777c
commit 1cc0aafc1a
21 changed files with 673 additions and 211 deletions
+37 -149
View File
@@ -2,59 +2,28 @@ use std::env;
use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result};
use argon2::{
password_hash::{PasswordHasher, SaltString},
Argon2,
};
use diesel::{dsl::exists, prelude::*, select};
use once_cell::sync::Lazy;
use reqwest::{Client, Method, StatusCode};
use serde_json::json;
use uuid::Uuid;
use backend::{
auth::password,
config::AppConfig,
db::{self, PgPool},
documents::search::ensure_quickwit_index,
jobs::{enqueue_job, JOB_ANALYZE_DOCUMENT},
models::{DocumentAsset, DocumentAssetObject, NewUser, NewUserMembership, Tenant, User},
models::{
DocumentAsset, DocumentAssetObject, NewUser, NewUserMembership, Tenant, TenantStatus, User,
},
s3,
schema::{
document_asset_objects, document_assets, documents, tenants, user_memberships, users,
},
storage::{ObjectStorage, S3Storage, TenantStorage},
tenants::TenantService,
utils::tracing::init_tracing,
};
use rand::rngs::OsRng;
static QUICKWIT_INDEX_TEMPLATE: Lazy<serde_json::Value> = Lazy::new(|| {
json!({
"version": "0.8",
"index_id": "documents",
"doc_mapping": {
"tokenizers": [
{
"name": "substring",
"type": "ngram",
"min_gram": 2,
"max_gram": 20,
"prefix_only": false
}
],
"field_mappings": [
{ "name": "tenant_id", "type": "text", "stored": true },
{ "name": "document_id", "type": "text", "stored": true },
{ "name": "version_id", "type": "text", "stored": true },
{ "name": "title", "type": "text", "tokenizer": "substring", "stored": true },
{ "name": "text", "type": "text", "tokenizer": "substring", "record": "position" }
]
},
"search_settings": {
"default_search_fields": ["title", "text"]
}
})
});
#[derive(Debug)]
enum Command {
CreateUser {
@@ -218,7 +187,7 @@ fn create_user(pool: &PgPool, username: &str, password: &str) -> Result<()> {
bail!("user '{}' already exists", username);
}
let password_hash = hash_password(password)?;
let password_hash = password::hash_password(password).map_err(|err| anyhow!(err))?;
let new_user = NewUser {
id: Uuid::new_v4(),
username: username.to_string(),
@@ -239,7 +208,7 @@ fn set_password(pool: &PgPool, username: &str, password: &str) -> Result<()> {
}
let mut conn = pool.get().context("failed to get database connection")?;
let password_hash = hash_password(password)?;
let password_hash = password::hash_password(password).map_err(|err| anyhow!(err))?;
let updated = diesel::update(users::table.filter(users::username.eq(username)))
.set(users::password_hash.eq(password_hash))
@@ -253,14 +222,6 @@ fn set_password(pool: &PgPool, username: &str, password: &str) -> Result<()> {
Ok(())
}
fn hash_password(password: &str) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
let hash = Argon2::default()
.hash_password(password.as_bytes(), &salt)
.map_err(|err| anyhow!(err))?;
Ok(hash.to_string())
}
fn list_users(pool: &PgPool) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?;
@@ -315,46 +276,28 @@ fn create_tenant(
storage_root_arg: Option<String>,
quickwit_index_arg: Option<String>,
) -> Result<()> {
if slug.trim().is_empty() {
bail!("tenant slug must not be empty");
}
let service = TenantService::new(pool.clone());
let tenant = service
.create_tenant(
slug,
storage_root_arg.as_deref(),
quickwit_index_arg.as_deref(),
TenantStatus::Creating,
&[],
None,
)
.map_err(|err| anyhow!(format!("{err:?}")))?;
let mut conn = pool.get().context("failed to get database connection")?;
let exists: bool =
select(exists(tenants::table.filter(tenants::slug.eq(slug)))).get_result(&mut conn)?;
if exists {
bail!("tenant '{}' already exists", slug);
}
let id = Uuid::new_v4();
let storage_root = storage_root_arg
.map(|mut s| {
if s.is_empty() {
format!("tenants/{}/", id)
} else {
if !s.ends_with('/') {
s.push('/');
}
s
}
})
.unwrap_or_else(|| format!("tenants/{}/", id));
let quickwit_index = quickwit_index_arg.unwrap_or_else(|| format!("documents-{}", id));
diesel::insert_into(tenants::table)
.values((
tenants::id.eq(id),
tenants::slug.eq(slug),
tenants::storage_root.eq(Some(storage_root.clone())),
tenants::quickwit_index.eq(Some(quickwit_index.clone())),
tenants::status.eq("active"),
tenants::config.eq(serde_json::json!({})),
))
.execute(&mut conn)?;
let storage_root = tenant.storage_root.as_deref().unwrap_or("<none>");
let quickwit_index = tenant.quickwit_index.as_deref().unwrap_or("<none>");
println!(
"created tenant '{}' with id {}, storage_root '{}', quickwit_index '{}'",
slug, id, storage_root, quickwit_index
"created tenant '{}' with id {}, storage_root '{}', quickwit_index '{}', status '{}'",
tenant.slug,
tenant.id,
storage_root,
quickwit_index,
tenant.status.as_str()
);
Ok(())
}
@@ -605,63 +548,19 @@ async fn quickwit_index(
match method {
Method::POST => {
let payload = render_index_template(&index_id);
let response = client
.post(format!("{}/api/v1/indexes", base_endpoint))
.header("content-type", "application/json")
.body(payload)
.send()
ensure_quickwit_index(&client, base_endpoint, &index_id)
.await
.context("failed to send create index request")?;
.context("failed to ensure quickwit index")?;
match response.status() {
status if status.is_success() => {
diesel::update(tenants::table.filter(tenants::id.eq(tenant.id)))
.set(tenants::quickwit_index.eq(Some(index_id.clone())))
.execute(&mut conn)
.context("failed to update tenant quickwit_index")?;
diesel::update(tenants::table.filter(tenants::id.eq(tenant.id)))
.set(tenants::quickwit_index.eq(Some(index_id.clone())))
.execute(&mut conn)
.context("failed to update tenant quickwit_index")?;
println!(
"Tenant '{}' quickwit index set to '{}'.",
tenant.slug, index_id
);
}
StatusCode::CONFLICT => {
let lookup = client
.get(format!("{}/api/v1/indexes/{}", base_endpoint, index_id))
.send()
.await
.context("failed to verify existing quickwit index")?;
let lookup_status = lookup.status();
if !lookup_status.is_success() {
let body = lookup.text().await.unwrap_or_default();
bail!(
"quickwit reported conflict but index lookup failed with status {}: {}",
lookup_status,
body
);
}
diesel::update(tenants::table.filter(tenants::id.eq(tenant.id)))
.set(tenants::quickwit_index.eq(Some(index_id.clone())))
.execute(&mut conn)
.context("failed to update tenant quickwit_index")?;
println!(
"Tenant '{}' quickwit index set to '{}'.",
tenant.slug, index_id
);
}
status => {
let body = response.text().await.unwrap_or_default();
bail!(
"quickwit create index failed with status {}: {}",
status,
body
);
}
}
println!(
"Tenant '{}' quickwit index set to '{}'.",
tenant.slug, index_id
);
}
Method::DELETE => {
let response = client
@@ -694,14 +593,3 @@ async fn quickwit_index(
Ok(())
}
fn render_index_template(index_id: &str) -> String {
let mut template = QUICKWIT_INDEX_TEMPLATE.clone();
if let Some(obj) = template.as_object_mut() {
obj.insert(
"index_id".to_string(),
serde_json::Value::String(index_id.to_string()),
);
}
template.to_string()
}