Skip to main content

rouchdb_adapter_http/
auth.rs

1/// CouchDB authentication helpers.
2///
3/// Supports cookie-based authentication (`_session` endpoint),
4/// session inspection, and user signup.
5use reqwest::Client;
6use serde::{Deserialize, Serialize};
7
8use rouchdb_core::error::{Result, RouchError};
9
10/// A CouchDB session response.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Session {
13    pub ok: bool,
14    #[serde(rename = "userCtx")]
15    pub user_ctx: UserContext,
16}
17
18/// User context from a session response.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct UserContext {
21    pub name: Option<String>,
22    pub roles: Vec<String>,
23}
24
25/// A client that handles CouchDB authentication.
26///
27/// Uses cookie-based auth (`_session` endpoint). The internal `reqwest::Client`
28/// has a cookie store enabled, so after `login()` all subsequent requests
29/// automatically include the auth cookie.
30pub struct AuthClient {
31    client: Client,
32    server_url: String,
33}
34
35impl AuthClient {
36    /// Create a new auth client for the given CouchDB server URL.
37    pub fn new(server_url: &str) -> Self {
38        let client = Client::builder()
39            .cookie_store(true)
40            .build()
41            .unwrap_or_default();
42        Self {
43            client,
44            server_url: server_url.trim_end_matches('/').to_string(),
45        }
46    }
47
48    /// Get the underlying reqwest client (with cookie store).
49    pub fn client(&self) -> &Client {
50        &self.client
51    }
52
53    /// Get the server URL.
54    pub fn server_url(&self) -> &str {
55        &self.server_url
56    }
57
58    /// Log in with username and password (cookie-based auth).
59    pub async fn login(&self, username: &str, password: &str) -> Result<Session> {
60        let resp = self
61            .client
62            .post(format!("{}/_session", self.server_url))
63            .json(&serde_json::json!({"name": username, "password": password}))
64            .send()
65            .await
66            .map_err(|e| RouchError::DatabaseError(e.to_string()))?;
67
68        if !resp.status().is_success() {
69            let status = resp.status();
70            let body = resp.text().await.unwrap_or_default();
71            return Err(RouchError::DatabaseError(format!(
72                "login failed ({}): {}",
73                status, body
74            )));
75        }
76
77        resp.json::<Session>()
78            .await
79            .map_err(|e| RouchError::DatabaseError(e.to_string()))
80    }
81
82    /// Log out (delete session cookie).
83    pub async fn logout(&self) -> Result<()> {
84        self.client
85            .delete(format!("{}/_session", self.server_url))
86            .send()
87            .await
88            .map_err(|e| RouchError::DatabaseError(e.to_string()))?;
89        Ok(())
90    }
91
92    /// Get the current session.
93    pub async fn get_session(&self) -> Result<Session> {
94        let resp = self
95            .client
96            .get(format!("{}/_session", self.server_url))
97            .send()
98            .await
99            .map_err(|e| RouchError::DatabaseError(e.to_string()))?;
100
101        resp.json::<Session>()
102            .await
103            .map_err(|e| RouchError::DatabaseError(e.to_string()))
104    }
105
106    /// Create a new user in the `_users` database.
107    pub async fn sign_up(&self, username: &str, password: &str, roles: Vec<String>) -> Result<()> {
108        let user_doc = serde_json::json!({
109            "_id": format!("org.couchdb.user:{}", username),
110            "name": username,
111            "password": password,
112            "roles": roles,
113            "type": "user"
114        });
115
116        let resp = self
117            .client
118            .put(format!(
119                "{}/_users/org.couchdb.user:{}",
120                self.server_url, username
121            ))
122            .json(&user_doc)
123            .send()
124            .await
125            .map_err(|e| RouchError::DatabaseError(e.to_string()))?;
126
127        if !resp.status().is_success() {
128            let status = resp.status();
129            let body = resp.text().await.unwrap_or_default();
130            return Err(RouchError::DatabaseError(format!(
131                "signup failed ({}): {}",
132                status, body
133            )));
134        }
135
136        Ok(())
137    }
138}