Changes
This commit is contained in:
parent
3a3f899ea2
commit
128b3191e7
68 changed files with 28060 additions and 1152 deletions
235
server-rs/src/pocketbase.rs
Normal file
235
server-rs/src/pocketbase.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CollectionList {
|
||||
items: Vec<CollectionItem>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CollectionItem {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CreateCollection {
|
||||
name: String,
|
||||
r#type: String,
|
||||
fields: Vec<Field>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Field {
|
||||
name: String,
|
||||
r#type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
required: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_select: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
collection_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_size: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
mime_types: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Field {
|
||||
fn text(name: &str, required: bool) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
r#type: "text".to_string(),
|
||||
required: Some(required),
|
||||
max_select: None,
|
||||
collection_id: None,
|
||||
max_size: None,
|
||||
mime_types: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn file(name: &str, mime_types: Vec<&str>) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
r#type: "file".to_string(),
|
||||
required: Some(false),
|
||||
max_select: Some(1),
|
||||
collection_id: None,
|
||||
max_size: Some(10 * 1024 * 1024), // 10 MB
|
||||
mime_types: Some(mime_types.into_iter().map(String::from).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
fn relation(name: &str, collection_id: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
r#type: "relation".to_string(),
|
||||
required: Some(true),
|
||||
max_select: Some(1),
|
||||
collection_id: Some(collection_id.to_string()),
|
||||
max_size: None,
|
||||
mime_types: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn auth_superuser(
|
||||
client: &Client,
|
||||
base_url: &str,
|
||||
email: &str,
|
||||
password: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let url = format!("{base_url}/api/collections/_superusers/auth-with-password");
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&serde_json::json!({
|
||||
"identity": email,
|
||||
"password": password,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
anyhow::bail!("PocketBase superuser auth failed ({status}): {text}");
|
||||
}
|
||||
|
||||
let body: AuthResponse = resp.json().await?;
|
||||
Ok(body.token)
|
||||
}
|
||||
|
||||
async fn list_collections(
|
||||
client: &Client,
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let url = format!("{base_url}/api/collections?perPage=200");
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
anyhow::bail!("Failed to list PocketBase collections ({status}): {text}");
|
||||
}
|
||||
|
||||
let body: CollectionList = resp.json().await?;
|
||||
Ok(body.items.into_iter().map(|c| c.name).collect())
|
||||
}
|
||||
|
||||
async fn create_collection(
|
||||
client: &Client,
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
collection: CreateCollection,
|
||||
) -> anyhow::Result<()> {
|
||||
let name = collection.name.clone();
|
||||
let resp = client
|
||||
.post(&format!("{base_url}/api/collections"))
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.json(&collection)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
anyhow::bail!("Failed to create collection '{name}' ({status}): {text}");
|
||||
}
|
||||
|
||||
info!("Created PocketBase collection: {name}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Look up the internal ID of the `users` auth collection.
|
||||
async fn find_users_collection_id(
|
||||
client: &Client,
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let url = format!("{base_url}/api/collections/users");
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
anyhow::bail!("Failed to fetch users collection ({status}): {text}");
|
||||
}
|
||||
|
||||
let body: serde_json::Value = resp.json().await?;
|
||||
let id = body["id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("users collection has no id field"))?;
|
||||
Ok(id.to_string())
|
||||
}
|
||||
|
||||
/// Ensure the `saved_searches` and `short_urls` collections exist in PocketBase.
|
||||
/// Authenticates as superuser, checks existing collections, and creates any that are missing.
|
||||
pub async fn ensure_collections(
|
||||
client: &Client,
|
||||
base_url: &str,
|
||||
admin_email: &str,
|
||||
admin_password: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let base_url = base_url.trim_end_matches('/');
|
||||
|
||||
let token = auth_superuser(client, base_url, admin_email, admin_password).await?;
|
||||
let existing = list_collections(client, base_url, &token).await?;
|
||||
|
||||
if !existing.iter().any(|n| n == "saved_searches") {
|
||||
let users_id = find_users_collection_id(client, base_url, &token).await?;
|
||||
create_collection(
|
||||
client,
|
||||
base_url,
|
||||
&token,
|
||||
CreateCollection {
|
||||
name: "saved_searches".to_string(),
|
||||
r#type: "base".to_string(),
|
||||
fields: vec![
|
||||
Field::relation("user", &users_id),
|
||||
Field::text("name", true),
|
||||
Field::text("params", true),
|
||||
Field::file("screenshot", vec!["image/png", "image/jpeg", "image/webp"]),
|
||||
],
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
info!("PocketBase collection 'saved_searches' already exists");
|
||||
}
|
||||
|
||||
if !existing.iter().any(|n| n == "short_urls") {
|
||||
create_collection(
|
||||
client,
|
||||
base_url,
|
||||
&token,
|
||||
CreateCollection {
|
||||
name: "short_urls".to_string(),
|
||||
r#type: "base".to_string(),
|
||||
fields: vec![
|
||||
Field::text("code", true),
|
||||
Field::text("params", true),
|
||||
],
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
info!("PocketBase collection 'short_urls' already exists");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue