spacetimedb_sdk/
credentials.rs1#[cfg(not(feature = "browser"))]
12mod native_mod {
13 use home::home_dir;
14 use spacetimedb_lib::{bsatn, de::Deserialize, ser::Serialize};
15 use std::path::PathBuf;
16 use thiserror::Error;
17
18 const CREDENTIALS_DIR: &str = ".spacetimedb_client_credentials";
19
20 #[derive(Error, Debug)]
21 pub enum CredentialFileError {
22 #[error("Failed to determine user home directory as root for credentials storage")]
23 DetermineHomeDir,
24 #[error("Error creating credential storage directory {path}")]
25 CreateDir {
26 path: PathBuf,
27 #[source]
28 source: std::io::Error,
29 },
30 #[error("Error serializing credentials for storage in file")]
31 Serialize {
32 #[source]
33 source: bsatn::EncodeError,
34 },
35 #[error("Error writing BSATN-serialized credentials to file {path}")]
36 Write {
37 path: PathBuf,
38 #[source]
39 source: std::io::Error,
40 },
41 #[error("Error reading BSATN-serialized credentials from file {path}")]
42 Read {
43 path: PathBuf,
44 #[source]
45 source: std::io::Error,
46 },
47 #[error("Error deserializing credentials from bytes stored in file {path}")]
48 Deserialize {
49 path: PathBuf,
50 #[source]
51 source: bsatn::DecodeError,
52 },
53 }
54
55 pub struct File {
61 filename: String,
62 }
63
64 #[derive(Serialize, Deserialize)]
65 struct Credentials {
66 token: String,
67 }
68
69 impl File {
70 pub fn new(key: impl Into<String>) -> Self {
84 Self { filename: key.into() }
85 }
86
87 fn determine_home_dir() -> Result<PathBuf, CredentialFileError> {
88 home_dir().ok_or(CredentialFileError::DetermineHomeDir)
89 }
90
91 fn ensure_credentials_dir() -> Result<(), CredentialFileError> {
92 let mut path = Self::determine_home_dir()?;
93 path.push(CREDENTIALS_DIR);
94
95 std::fs::create_dir_all(&path).map_err(|source| CredentialFileError::CreateDir { path, source })
96 }
97
98 fn path(&self) -> Result<PathBuf, CredentialFileError> {
99 let mut path = Self::determine_home_dir()?;
100 path.push(CREDENTIALS_DIR);
101 path.push(&self.filename);
102 Ok(path)
103 }
104
105 pub fn save(self, token: impl Into<String>) -> Result<(), CredentialFileError> {
118 Self::ensure_credentials_dir()?;
119
120 let creds = bsatn::to_vec(&Credentials { token: token.into() })
121 .map_err(|source| CredentialFileError::Serialize { source })?;
122 let path = self.path()?;
123 std::fs::write(&path, creds).map_err(|source| CredentialFileError::Write { path, source })?;
124 Ok(())
125 }
126
127 pub fn load(self) -> Result<Option<String>, CredentialFileError> {
141 let path = self.path()?;
142
143 let bytes = match std::fs::read(&path) {
144 Ok(bytes) => bytes,
145 Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => return Ok(None),
146 Err(source) => return Err(CredentialFileError::Read { path, source }),
147 };
148
149 let creds = bsatn::from_slice::<Credentials>(&bytes)
150 .map_err(|source| CredentialFileError::Deserialize { path, source })?;
151 Ok(Some(creds.token))
152 }
153 }
154}
155
156#[cfg(feature = "browser")]
157mod web_mod {
158 pub use gloo_storage::{LocalStorage, SessionStorage, Storage};
159
160 pub mod cookies {
161 use thiserror::Error;
162 use wasm_bindgen::{JsCast, JsValue};
163 use web_sys::HtmlDocument;
164
165 #[derive(Error, Debug)]
166 pub enum CookieError {
167 #[error("Error reading cookies: {0:?}")]
168 Get(JsValue),
169
170 #[error("Error setting cookie `{key}`: {js_value:?}")]
171 Set { key: String, js_value: JsValue },
172 }
173
174 pub struct Cookie {
176 name: String,
177 value: String,
178 path: Option<String>,
179 domain: Option<String>,
180 max_age: Option<i32>,
181 secure: bool,
182 same_site: Option<SameSite>,
183 }
184
185 impl Cookie {
186 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
188 Self {
189 name: name.into(),
190 value: value.into(),
191 path: None,
192 domain: None,
193 max_age: None,
194 secure: false,
195 same_site: None,
196 }
197 }
198
199 pub fn get(name: &str) -> Result<Option<String>, CookieError> {
201 let doc = get_html_document();
202 let all = doc.cookie().map_err(CookieError::Get)?;
203 for cookie in all.split(';') {
204 let cookie = cookie.trim();
205 if let Some((k, v)) = cookie.split_once('=')
206 && k == name
207 {
208 return Ok(Some(v.to_string()));
209 }
210 }
211
212 Ok(None)
213 }
214
215 pub fn path(mut self, path: impl Into<String>) -> Self {
217 self.path = Some(path.into());
218 self
219 }
220
221 pub fn domain(mut self, domain: impl Into<String>) -> Self {
223 self.domain = Some(domain.into());
224 self
225 }
226
227 pub fn max_age(mut self, seconds: i32) -> Self {
229 self.max_age = Some(seconds);
230 self
231 }
232
233 pub fn secure(mut self, enabled: bool) -> Self {
236 self.secure = enabled;
237 self
238 }
239
240 pub fn same_site(mut self, same_site: SameSite) -> Self {
242 self.same_site = Some(same_site);
243 self
244 }
245
246 pub fn set(self) -> Result<(), CookieError> {
247 let doc = get_html_document();
248 let mut parts = vec![format!("{}={}", self.name, self.value)];
249
250 if let Some(path) = self.path {
251 parts.push(format!("Path={path}"));
252 }
253 if let Some(domain) = self.domain {
254 parts.push(format!("Domain={domain}"));
255 }
256 if let Some(age) = self.max_age {
257 parts.push(format!("Max-Age={age}"));
258 }
259 if self.secure {
260 parts.push("Secure".into());
261 }
262 if let Some(same) = self.same_site {
263 parts.push(format!("SameSite={same}"));
264 }
265
266 let cookie_str = parts.join("; ");
267 doc.set_cookie(&cookie_str).map_err(|e| CookieError::Set {
268 key: self.name.clone(),
269 js_value: e,
270 })
271 }
272
273 pub fn delete(self) -> Result<(), CookieError> {
275 self.value("").max_age(0).set()
276 }
277
278 fn value(mut self, value: impl Into<String>) -> Self {
280 self.value = value.into();
281 self
282 }
283 }
284
285 pub enum SameSite {
287 Strict,
288 Lax,
289 None,
290 }
291
292 impl std::fmt::Display for SameSite {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 match self {
295 SameSite::Strict => f.write_str("Strict"),
296 SameSite::Lax => f.write_str("Lax"),
297 SameSite::None => f.write_str("None"),
298 }
299 }
300 }
301
302 fn get_html_document() -> HtmlDocument {
303 gloo_utils::document().unchecked_into::<HtmlDocument>()
304 }
305 }
306}
307
308#[cfg(not(feature = "browser"))]
309pub use native_mod::*;
310
311#[cfg(feature = "browser")]
312pub use web_mod::*;