Build the Synapsis Tauri onboarding and authentication shell with the official wordmark and a fluid stable-topology signal mesh

Hop-State: A_06FPFZF1AB46ARJ4HD8ZQH8
Hop-Proposal: R_06FPFZEBFD47ZTA2XKJ2Q0G
Hop-Task: T_06FPFXR242APZTT77HV934R
Hop-Attempt: AT_06FPFXR241F9H1V2J0RCGGG
This commit is contained in:
2026-07-15 15:55:19 -07:00
committed by Hop
parent 85d5b6f7d2
commit 0695655ccf
74 changed files with 6433 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
use reqwest::{Client, Method, StatusCode, Url};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::time::Duration;
use tauri::State;
struct AppState {
client: Client,
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct NodeInfo {
name: Option<String>,
description: Option<String>,
accent_color: Option<String>,
domain: Option<String>,
logo_url: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct User {
id: String,
handle: String,
display_name: Option<String>,
avatar_url: Option<String>,
}
#[derive(Deserialize)]
struct SessionPayload {
user: Option<User>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ConnectionPayload {
node_url: String,
node: NodeInfo,
user: Option<User>,
}
#[derive(Serialize)]
struct AuthPayload {
user: User,
}
#[derive(Deserialize)]
struct ErrorPayload {
error: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RegistrationRequest<'a> {
handle: &'a str,
display_name: &'a str,
email: &'a str,
password: &'a str,
}
fn normalize_node(input: &str) -> Result<Url, String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err("Enter a valid Synapsis node URL.".into());
}
let candidate = if trimmed.contains("://") {
trimmed.to_owned()
} else {
format!("https://{trimmed}")
};
let mut url = Url::parse(&candidate).map_err(|_| "Enter a valid Synapsis node URL.")?;
if url.scheme() != "http" && url.scheme() != "https" {
return Err("Synapsis nodes must use an http or https URL.".into());
}
if url.host_str().is_none() {
return Err("Enter a valid Synapsis node URL.".into());
}
if url.host_str() == Some("www.synapsis.social") {
url.set_host(Some("synapsis.social"))
.map_err(|_| "Enter a valid Synapsis node URL.")?;
}
url.set_username("").ok();
url.set_password(None).ok();
url.set_path("/");
url.set_query(None);
url.set_fragment(None);
Ok(url)
}
fn endpoint(base: &Url, path: &str) -> Result<Url, String> {
base.join(path)
.map_err(|_| "The selected node address is invalid.".into())
}
async fn decode_response<T: DeserializeOwned>(response: reqwest::Response) -> Result<T, String> {
let status = response.status();
let bytes = response.bytes().await.map_err(|error| error.to_string())?;
if !status.is_success() {
let message = serde_json::from_slice::<ErrorPayload>(&bytes)
.ok()
.and_then(|payload| payload.error)
.unwrap_or_else(|| format!("Request failed ({status})."));
return Err(message);
}
serde_json::from_slice(&bytes).map_err(|_| "The node returned an unexpected response.".into())
}
async fn get<T: DeserializeOwned>(client: &Client, base: &Url, path: &str) -> Result<T, String> {
let response = client
.get(endpoint(base, path)?)
.header("Accept", "application/json")
.send()
.await
.map_err(|error| format!("Couldnt reach this node: {error}"))?;
decode_response(response).await
}
async fn post<T: DeserializeOwned, B: Serialize>(
client: &Client,
base: &Url,
path: &str,
body: &B,
) -> Result<T, String> {
let response = client
.request(Method::POST, endpoint(base, path)?)
.header("Accept", "application/json")
.json(body)
.send()
.await
.map_err(|error| format!("Couldnt reach this node: {error}"))?;
decode_response(response).await
}
#[tauri::command]
async fn connect(
state: State<'_, AppState>,
node_url: String,
) -> Result<ConnectionPayload, String> {
let base = normalize_node(&node_url)?;
let node = get::<NodeInfo>(&state.client, &base, "/api/node").await?;
let session = get::<SessionPayload>(&state.client, &base, "/api/auth/me")
.await
.unwrap_or(SessionPayload { user: None });
Ok(ConnectionPayload {
node_url: base.to_string(),
node,
user: session.user,
})
}
#[tauri::command]
async fn login(
state: State<'_, AppState>,
node_url: String,
email: String,
password: String,
) -> Result<AuthPayload, String> {
let base = normalize_node(&node_url)?;
let payload = post::<SessionPayload, _>(
&state.client,
&base,
"/api/auth/login",
&serde_json::json!({ "email": email.trim(), "password": password }),
)
.await?;
payload
.user
.map(|user| AuthPayload { user })
.ok_or_else(|| "The node returned an unexpected response.".into())
}
#[tauri::command]
async fn register(
state: State<'_, AppState>,
node_url: String,
handle: String,
display_name: String,
email: String,
password: String,
) -> Result<AuthPayload, String> {
let base = normalize_node(&node_url)?;
let body = RegistrationRequest {
handle: handle.trim().trim_start_matches('@'),
display_name: display_name.trim(),
email: email.trim(),
password: &password,
};
let payload =
post::<SessionPayload, _>(&state.client, &base, "/api/auth/register", &body).await?;
payload
.user
.map(|user| AuthPayload { user })
.ok_or_else(|| "The node returned an unexpected response.".into())
}
#[tauri::command]
async fn logout(state: State<'_, AppState>, node_url: String) -> Result<(), String> {
let base = normalize_node(&node_url)?;
let response = state
.client
.post(endpoint(&base, "/api/auth/logout")?)
.json(&serde_json::json!({}))
.send()
.await
.map_err(|error| format!("Couldnt reach this node: {error}"))?;
if response.status().is_success() || response.status() == StatusCode::UNAUTHORIZED {
Ok(())
} else {
Err(format!("Log out failed ({}).", response.status()))
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let client = Client::builder()
.cookie_store(true)
.timeout(Duration::from_secs(20))
.user_agent("Synapsis Desktop/0.1")
.build()
.expect("failed to build Synapsis HTTP client");
tauri::Builder::default()
.manage(AppState { client })
.invoke_handler(tauri::generate_handler![connect, login, register, logout])
.run(tauri::generate_context!())
.expect("error while running Synapsis");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_bare_node_names() {
assert_eq!(
normalize_node("synapsis.social").unwrap().as_str(),
"https://synapsis.social/"
);
}
#[test]
fn strips_paths_credentials_and_fragments() {
assert_eq!(
normalize_node("https://person:secret@example.com/somewhere?q=1#fragment")
.unwrap()
.as_str(),
"https://example.com/"
);
}
#[test]
fn rejects_non_web_schemes() {
assert!(normalize_node("file:///tmp/node").is_err());
}
}