1use super::client::{Me, PlmClient};
19use super::transport::EhttpTransport;
20use super::PlmTransport;
21use std::cell::RefCell;
22use std::rc::Rc;
23
24#[derive(Clone, Debug, PartialEq)]
26pub enum Status {
27 Connected { url: String, username: String },
29 NotConnected { url: String, reason: Option<String> },
33}
34
35#[derive(Clone, Debug, PartialEq)]
37pub enum Outcome {
38 Done(String),
40 Failed(String),
42}
43
44#[derive(Clone, Debug, PartialEq)]
47pub struct Session {
48 pub status: Status,
49 pub keep: Keep,
50}
51
52pub fn parse_label(label: &str) -> Option<(String, String)> {
55 let rest = label.strip_prefix("PLM ")?;
56 let (url, user) = rest.rsplit_once(" as ")?;
57 Some((url.to_string(), user.to_string()))
58}
59
60#[derive(Clone, Debug, PartialEq)]
63pub enum Keep {
64 Files(std::path::PathBuf),
65 BrowserSession,
66}
67
68pub struct PlmConnection {
69 pub status: Status,
70 keep: Keep,
71 pub token_input: String,
72 pub username: String,
73 pub password: String,
74 pending: Option<Rc<RefCell<Option<Outcome>>>>,
75 pub last: Option<Outcome>,
76 reload: bool,
78 connectable: bool,
81 transport: Rc<dyn Fn(&str) -> Rc<dyn PlmTransport>>,
84}
85
86impl PlmConnection {
87 pub fn new(status: Status, keep: Keep) -> Self {
88 Self {
89 status,
90 keep,
91 token_input: String::new(),
92 username: String::new(),
93 password: String::new(),
94 pending: None,
95 last: None,
96 reload: false,
97 connectable: false,
98 transport: Rc::new(|url: &str| Rc::new(EhttpTransport::new(url.to_string())) as Rc<dyn PlmTransport>),
99 }
100 }
101
102 pub fn url(&self) -> &str {
103 match &self.status {
104 Status::Connected { url, .. } | Status::NotConnected { url, .. } => url,
105 }
106 }
107
108 pub fn keep(&self) -> &Keep {
109 &self.keep
110 }
111
112 pub fn busy(&self) -> bool {
114 self.pending.is_some()
115 }
116
117 pub fn poll(&mut self) -> bool {
120 let Some(slot) = &self.pending else { return false };
121 #[cfg(not(target_arch = "wasm32"))]
122 super::native::run_pending();
123 let Some(outcome) = slot.borrow_mut().take() else { return false };
124 self.pending = None;
125 if let (Keep::BrowserSession, Outcome::Done(_)) = (&self.keep, &outcome) {
126 self.reload = true;
127 }
128 self.connectable = matches!(self.keep, Keep::Files(_)) && matches!(outcome, Outcome::Done(_));
131 if let (Status::NotConnected { url, .. }, Outcome::Failed(why)) = (&self.status, &outcome) {
132 self.status = Status::NotConnected { url: url.clone(), reason: Some(why.clone()) };
133 }
134 self.last = Some(outcome);
135 true
136 }
137
138 pub fn connectable(&self) -> bool {
140 self.connectable
141 }
142
143 pub fn reconnected(&mut self, outcome: Result<String, String>) {
146 self.connectable = outcome.is_err() && self.connectable;
147 self.last = Some(match outcome {
148 Ok(text) => Outcome::Done(text),
149 Err(why) => Outcome::Failed(why),
150 });
151 }
152
153 pub fn set_status(&mut self, status: Status) {
155 if std::mem::discriminant(&status) != std::mem::discriminant(&self.status) {
156 self.status = status;
157 self.connectable = false;
158 }
159 }
160
161 pub fn take_reload(&mut self) -> bool {
163 std::mem::take(&mut self.reload)
164 }
165
166 fn client(&self) -> Rc<PlmClient> {
167 Rc::new(PlmClient::new((self.transport)(self.url())))
168 }
169
170 fn start(&mut self, work: impl std::future::Future<Output = Outcome> + 'static) {
171 let slot: Rc<RefCell<Option<Outcome>>> = Rc::default();
172 let into = slot.clone();
173 self.pending = Some(slot);
174 self.last = None;
175 spawn(async move {
176 let outcome = work.await;
177 *into.borrow_mut() = Some(outcome);
178 });
179 }
180
181 pub fn use_token(&mut self) {
184 let token = self.token_input.trim().to_string();
185 let (client, keep) = (self.client(), self.keep.clone());
186 self.token_input.clear();
187 self.start(async move {
188 match client.sign_in_with_token(&token).await {
189 Ok(me) => keep_token(&keep, &token, &me),
190 Err(e) => Outcome::Failed(e.to_string()),
191 }
192 });
193 }
194
195 pub fn use_password(&mut self) {
198 let (username, password) = (self.username.trim().to_string(), std::mem::take(&mut self.password));
199 let (client, keep) = (self.client(), self.keep.clone());
200 self.start(async move {
201 match keep {
202 Keep::Files(_) => match client.sign_in_for_token(&username, &password, &token_name()).await {
203 Ok((me, token)) => keep_token(&keep, &token, &me),
204 Err(e) => Outcome::Failed(e.to_string()),
205 },
206 Keep::BrowserSession => match client.sign_in_with_password(&username, &password).await {
207 Ok(me) => Outcome::Done(format!("signed in as {}; reloading onto the PLM", me.username)),
208 Err(e) => Outcome::Failed(e.to_string()),
209 },
210 }
211 });
212 }
213
214 pub fn check(&mut self) {
217 let (client, keep) = (self.client(), self.keep.clone());
218 self.start(async move {
219 let signed_in = match &keep {
220 Keep::Files(dir) => match held_token(dir) {
221 Ok(Some(token)) => client.sign_in_with_token(&token).await,
222 Ok(None) => return Outcome::Failed("no token on this machine: paste one, or sign in with a password".into()),
223 Err(e) => return Outcome::Failed(e),
224 },
225 Keep::BrowserSession => client.sign_in_with_browser_session().await,
226 };
227 match signed_in {
228 Ok(me) => Outcome::Done(format!("the server accepts this machine as {} and serves this app", me.username)),
229 Err(e) => Outcome::Failed(e.to_string()),
230 }
231 });
232 }
233
234 pub fn forget(&mut self) {
238 match self.keep.clone() {
239 Keep::Files(dir) => {
240 self.connectable = false;
241 self.last = Some(match forget_files(&dir) {
242 Ok(()) => Outcome::Done("this machine's PLM sign-in is forgotten; the next start is the file app".into()),
243 Err(e) => Outcome::Failed(e),
244 });
245 }
246 Keep::BrowserSession => {
247 let client = self.client();
248 self.start(async move {
249 match client.raw("POST", "/api/logout", Some(b"{}".to_vec())).await {
250 Ok(_) => Outcome::Done("signed out; reloading".into()),
251 Err(e) => Outcome::Failed(e.to_string()),
252 }
253 });
254 }
255 }
256 }
257}
258
259fn token_name() -> String {
261 #[cfg(not(target_arch = "wasm32"))]
262 {
263 let host = std::env::var("HOSTNAME").ok().filter(|h| !h.is_empty());
264 format!("brep-app {}", host.unwrap_or_else(|| "desktop".into()))
265 }
266 #[cfg(target_arch = "wasm32")]
267 {
268 "brep-app web".into()
269 }
270}
271
272fn keep_token(keep: &Keep, token: &str, me: &Me) -> Outcome {
273 match keep {
274 Keep::Files(dir) => match write_token(dir, token) {
275 Ok(()) => Outcome::Done(format!(
276 "signed in as {}; the token is kept on this machine, and the next start connects",
277 me.username
278 )),
279 Err(e) => Outcome::Failed(format!("signed in, but the token could not be kept: {e}")),
280 },
281 Keep::BrowserSession => Outcome::Done(format!("signed in as {}; reloading onto the PLM", me.username)),
282 }
283}
284
285#[cfg(not(target_arch = "wasm32"))]
286fn write_token(dir: &std::path::Path, token: &str) -> Result<(), String> {
287 super::config::write_token(dir, token)
288}
289
290#[cfg(target_arch = "wasm32")]
291fn write_token(_: &std::path::Path, _: &str) -> Result<(), String> {
292 Err("a browser keeps no token file".into())
293}
294
295#[cfg(not(target_arch = "wasm32"))]
296fn held_token(dir: &std::path::Path) -> Result<Option<String>, String> {
297 Ok(super::config::resolve(dir, &|_| None, &Default::default())?.and_then(|c| c.token))
298}
299
300#[cfg(target_arch = "wasm32")]
301fn held_token(_: &std::path::Path) -> Result<Option<String>, String> {
302 Ok(None)
303}
304
305#[cfg(not(target_arch = "wasm32"))]
306fn forget_files(dir: &std::path::Path) -> Result<(), String> {
307 super::config::forget(dir).map(|_| ())
308}
309
310#[cfg(target_arch = "wasm32")]
311fn forget_files(_: &std::path::Path) -> Result<(), String> {
312 Err("a browser keeps no PLM files".into())
313}
314
315fn spawn(task: impl std::future::Future<Output = ()> + 'static) {
316 #[cfg(target_arch = "wasm32")]
317 wasm_bindgen_futures::spawn_local(task);
318 #[cfg(not(target_arch = "wasm32"))]
319 super::native::spawn(task);
320}
321