This commit is contained in:
Andras Schmelczer 2026-02-15 22:39:49 +00:00
parent 03445188ea
commit 524580eb25
102 changed files with 36625 additions and 1295 deletions

View file

@ -1,6 +1,6 @@
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::info;
use tracing::{info, warn};
#[derive(Deserialize)]
struct AuthResponse {
@ -79,7 +79,7 @@ impl Field {
}
}
async fn auth_superuser(
pub async fn auth_superuser(
client: &Client,
base_url: &str,
email: &str,
@ -177,7 +177,82 @@ async fn find_users_collection_id(
Ok(id.to_string())
}
/// Ensure the `saved_searches` and `short_urls` collections exist in PocketBase.
/// Ensure `is_admin` (bool) and `subscription` (text) fields exist on the `users` collection.
/// PocketBase PATCH replaces the entire `fields` array, so we must preserve existing fields.
async fn ensure_user_fields(
client: &Client,
base_url: &str,
token: &str,
) -> anyhow::Result<()> {
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 fields = body["fields"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("users collection has no fields array"))?;
let has_is_admin = fields.iter().any(|f| f["name"] == "is_admin");
let has_subscription = fields.iter().any(|f| f["name"] == "subscription");
let has_newsletter = fields.iter().any(|f| f["name"] == "newsletter");
if has_is_admin && has_subscription && has_newsletter {
info!("PocketBase users collection already has is_admin, subscription, and newsletter fields");
return Ok(());
}
let mut new_fields = fields.clone();
if !has_is_admin {
new_fields.push(serde_json::json!({
"name": "is_admin",
"type": "bool",
}));
}
if !has_subscription {
new_fields.push(serde_json::json!({
"name": "subscription",
"type": "text",
}));
}
if !has_newsletter {
new_fields.push(serde_json::json!({
"name": "newsletter",
"type": "bool",
}));
}
let patch_resp = client
.patch(&url)
.header("Authorization", format!("Bearer {token}"))
.json(&serde_json::json!({ "fields": new_fields }))
.send()
.await?;
if !patch_resp.status().is_success() {
let status = patch_resp.status();
let text = patch_resp.text().await.unwrap_or_default();
anyhow::bail!("Failed to patch users collection ({status}): {text}");
}
info!("Added missing fields to PocketBase users collection");
Ok(())
}
/// Ensure the `saved_searches` and `short_urls` collections exist in PocketBase,
/// and that the `users` collection has `is_admin` and `subscription` fields.
/// Authenticates as superuser, checks existing collections, and creates any that are missing.
pub async fn ensure_collections(
client: &Client,
@ -190,6 +265,8 @@ pub async fn ensure_collections(
let token = auth_superuser(client, base_url, admin_email, admin_password).await?;
let existing = list_collections(client, base_url, &token).await?;
ensure_user_fields(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(
@ -212,6 +289,28 @@ pub async fn ensure_collections(
info!("PocketBase collection 'saved_searches' already exists");
}
if !existing.iter().any(|n| n == "invites") {
create_collection(
client,
base_url,
&token,
CreateCollection {
name: "invites".to_string(),
r#type: "base".to_string(),
fields: vec![
Field::text("code", true),
Field::text("created_by", true),
Field::text("invite_type", true),
Field::text("used_by_id", false),
Field::text("used_at", false),
],
},
)
.await?;
} else {
info!("PocketBase collection 'invites' already exists");
}
if !existing.iter().any(|n| n == "short_urls") {
create_collection(
client,
@ -233,3 +332,94 @@ pub async fn ensure_collections(
Ok(())
}
/// Configure Google and Apple OAuth2 providers in PocketBase settings.
/// Also sets `meta.appUrl` so OAuth callbacks route to `{public_url}/pb`.
pub async fn ensure_oauth_providers(
client: &Client,
base_url: &str,
admin_email: &str,
admin_password: &str,
public_url: &str,
google_client_id: &str,
google_client_secret: &str,
apple_client_id: &str,
apple_client_secret: &str,
) -> anyhow::Result<()> {
let base_url = base_url.trim_end_matches('/');
let token = auth_superuser(client, base_url, admin_email, admin_password).await?;
// GET current settings
let settings_url = format!("{base_url}/api/settings");
let resp = client
.get(&settings_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 PocketBase settings ({status}): {text}");
}
let mut settings: serde_json::Value = resp.json().await?;
// Set meta.appUrl for OAuth redirect
let app_url = format!("{}/pb", public_url.trim_end_matches('/'));
if let Some(meta) = settings.get_mut("meta") {
meta["appUrl"] = serde_json::json!(app_url);
} else {
settings["meta"] = serde_json::json!({ "appUrl": app_url });
}
// Update OAuth2 providers
let providers = settings
.pointer_mut("/oauth2/providers")
.and_then(|v| v.as_array_mut());
if let Some(providers) = providers {
for provider in providers.iter_mut() {
let name = provider
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("");
match name {
"google" => {
provider["clientId"] = serde_json::json!(google_client_id);
provider["clientSecret"] = serde_json::json!(google_client_secret);
provider["enabled"] = serde_json::json!(true);
info!("Configured Google OAuth provider");
}
"apple" => {
provider["clientId"] = serde_json::json!(apple_client_id);
provider["clientSecret"] = serde_json::json!(apple_client_secret);
provider["enabled"] = serde_json::json!(true);
info!("Configured Apple OAuth provider");
}
_ => {}
}
}
} else {
warn!("PocketBase settings missing oauth2.providers array — cannot configure OAuth");
return Ok(());
}
// PATCH settings back
let patch_resp = client
.patch(&settings_url)
.header("Authorization", format!("Bearer {token}"))
.json(&settings)
.send()
.await?;
if !patch_resp.status().is_success() {
let status = patch_resp.status();
let text = patch_resp.text().await.unwrap_or_default();
anyhow::bail!("Failed to update PocketBase settings ({status}): {text}");
}
info!("PocketBase OAuth settings updated (appUrl: {app_url})");
Ok(())
}