1use std::cell::RefCell;
5use std::rc::Rc;
6
7use ravenrs_app::{
8 Action, ActionCategory, AuditSettings, AuditTrail, AuditedObjectSpace, AuthError, Authenticator,
9 Credentials, EntityInfo, Identity, Message, MessageKind, ModelError, Module, ObjectSpace, PasswordError,
10 Role, RoleMerge, SECURITY_USER, SecuredObjectSpace, SecurityContext, Selection, SpaceMaker,
11 UserDirectory, Value, ViewController, audit_entity, security_entities,
12};
13use ravenrs_layout::{Align, Justify};
14use ravenrs_reactive::{Binding, State};
15use ravenrs_style::{TextStyle, Weight};
16use ravenrs_widgets::{
17 Alert, AnyView, BuildCx, Button, Card, FormField, HStack, Keyed, Localized, NodeId, Text, TextField,
18 Tone, VStack, View, ViewExt,
19};
20
21use crate::model_editor::ModelEditor;
22use crate::session::Words;
23use crate::{App, Language, Session};
24
25pub(crate) const SET_PASSWORD: &str = "SetPassword";
27pub(crate) const MODEL_EDITOR: &str = "ModelEditor";
29pub(crate) const MODEL_EDITOR_VIEW: &str = "ModelEditor_DashboardView";
30const CARD_WIDTH: f32 = 380.0;
31
32fn message_of(words: &Words, error: &PasswordError) -> String {
33 match error {
34 PasswordError::TooShort => words.password_too_short.clone(),
35 PasswordError::WrongCurrent => words.password_wrong_current.clone(),
36 other => other.to_string(),
37 }
38}
39
40fn text_binding(state: &State<String>) -> Binding<String> {
41 let (read, write) = (state.clone(), state.clone());
42 Binding::new(move || read.get(), move |value| write.set(value))
43}
44
45type Submit = Rc<dyn Fn(Option<&str>, &str) -> Result<(), PasswordError>>;
46
47#[derive(Clone)]
49struct PasswordForm {
50 words: Words,
51 current: Option<State<String>>,
52 new: State<String>,
53 repeated: State<String>,
54 error: State<Option<String>>,
55 submit: Submit,
56 done: Rc<dyn Fn()>,
57}
58
59#[cfg_attr(feature = "live", ravenrs_live::expose)]
60impl PasswordForm {
61 fn new(words: Words, asks_current: bool, submit: Submit, done: Rc<dyn Fn()>) -> Self {
62 Self {
63 words,
64 current: asks_current.then(|| State::new(String::new())),
65 new: State::new(String::new()),
66 repeated: State::new(String::new()),
67 error: State::new(None),
68 submit,
69 done,
70 }
71 }
72
73 fn send(&self) {
74 let (new, repeated) = (self.new.peek().clone(), self.repeated.peek().clone());
75 if new != repeated {
76 self.error.set(Some(self.words.passwords_differ.clone()));
77 return;
78 }
79 let current = self.current.as_ref().map(|state| state.peek().clone());
80 match (self.submit)(current.as_deref(), &new) {
81 Ok(()) => {
82 for state in
83 [Some(&self.new), Some(&self.repeated), self.current.as_ref()].into_iter().flatten()
84 {
85 state.set(String::new());
86 }
87 self.error.set(None);
88 (self.done)();
89 }
90 Err(error) => self.error.set(Some(message_of(&self.words, &error))),
91 }
92 }
93}
94
95impl View for PasswordForm {
96 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
97 let words = &self.words;
98 let sending = self.clone();
99 let current: Option<AnyView> = self.current.as_ref().map(|state| {
100 Box::new(FormField::new(
101 words.current_password.clone(),
102 TextField::new(text_binding(state)).secure().autofocus(),
103 )) as AnyView
104 });
105 let form = self.clone();
106 let content = VStack::new((
107 self.error.get().map(|error| Alert::new(words.change_password.clone(), error).tone(Tone::Danger)),
108 current,
109 FormField::new(words.new_password.clone(), TextField::new(text_binding(&self.new)).secure()),
110 FormField::new(
111 words.repeat_password.clone(),
112 TextField::new(text_binding(&self.repeated)).secure().on_submit(move |_| form.send()),
113 ),
114 Button::new(words.save_password.clone()).on_click(move || sending.send()),
115 ))
116 .spacing(12.0)
117 .align(Align::Stretch);
118 cx.child(&content)
119 }
120}
121
122#[derive(Debug)]
124pub(crate) struct SecurityModule {
125 words: Words,
126 audited: bool,
127}
128
129impl Module for SecurityModule {
130 fn name(&self) -> &str {
131 "Security"
132 }
133
134 fn entities(&self) -> Vec<EntityInfo> {
135 let mut entities = security_entities();
136 if self.audited {
137 entities.push(audit_entity());
138 }
139 entities
140 }
141
142 fn view_controllers(&self) -> Vec<Box<dyn ViewController>> {
143 vec![Box::new(PasswordsController(self.words.clone()))]
144 }
145}
146
147struct PasswordsController(Words);
148
149impl ViewController for PasswordsController {
150 fn id(&self) -> &str {
151 "Passwords"
152 }
153
154 fn actions(&self) -> Vec<Action> {
155 let words = self.0.clone();
156 vec![
157 Action::simple(SET_PASSWORD, words.set_password.clone())
158 .category(ActionCategory::Edit)
159 .target_entity(SECURITY_USER)
160 .selection(Selection::RequireSingle)
161 .icon("key-round")
162 .on_execute(move |cx| {
163 let session = Session::of(cx).ok_or("this action needs the application's screens")?;
164 let user = cx.selected.first().map_or(Value::Null, |user| user.get("id"));
165 show_set_password(&session, user, words.clone());
166 Ok(())
167 }),
168 ]
169 }
170}
171
172fn show_set_password(session: &Session, user: Value, words: Words) {
173 let (context, shown) = (session.frame().security(), session.clone());
174 let submit: Submit = Rc::new(move |_, password| match &context {
175 Some(context) => context.set_password_of(&user, password),
176 None => Err(PasswordError::NotAllowed),
177 });
178 let told = words.password_saved.clone();
179 let done: Rc<dyn Fn()> = Rc::new(move || {
180 shown.close_presented();
181 shown.show_message(Message::new(told.clone(), MessageKind::Success));
182 shown.records_changed();
183 });
184 let form = PasswordForm::new(words.clone(), false, submit, done);
185 session.present(words.set_password, move || Box::new(form.clone()) as AnyView);
186}
187
188pub(crate) fn show_change_password(session: &Session) {
190 let words = session.words().clone();
191 let (context, shown) = (session.frame().security(), session.clone());
192 let submit: Submit = Rc::new(move |current, password| match &context {
193 Some(context) => context.change_own_password(current.unwrap_or(""), password),
194 None => Err(PasswordError::NotAllowed),
195 });
196 let told = words.password_saved.clone();
197 let done: Rc<dyn Fn()> = Rc::new(move || {
198 shown.close_presented();
199 shown.show_message(Message::new(told.clone(), MessageKind::Success));
200 });
201 let form = PasswordForm::new(words.clone(), true, submit, done);
202 session.present(words.change_password, move || Box::new(form.clone()) as AnyView);
203}
204
205type MakeApp = Rc<dyn Fn(&Identity) -> App>;
206type Seed = Box<dyn FnOnce(&mut dyn ObjectSpace) -> Result<(), String>>;
207
208#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
233pub struct SecuredApp {
234 space: SpaceMaker,
235 application: MakeApp,
236 language: Language,
237 texts: crate::app::TextPacks,
238 roles: Vec<Role>,
239 merge: RoleMerge,
240 audit: Option<AuditSettings>,
241 authenticator: Option<Rc<dyn Authenticator>>,
242 seed: Option<Seed>,
243 title: String,
244}
245
246impl std::fmt::Debug for SecuredApp {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 f.debug_struct("SecuredApp").field("title", &self.title).finish_non_exhaustive()
249 }
250}
251
252#[cfg_attr(feature = "live", ravenrs_live::expose)]
253impl SecuredApp {
254 pub fn new<S: ObjectSpace + 'static>(
257 title: impl Into<String>,
258 space: impl Fn() -> S + 'static,
259 application: impl Fn(&Identity) -> App + 'static,
260 ) -> Self {
261 Self {
262 space: Rc::new(move || Box::new(space()) as Box<dyn ObjectSpace>),
263 application: Rc::new(application),
264 language: Language::default(),
265 texts: Vec::new(),
266 roles: Vec::new(),
267 merge: RoleMerge::default(),
268 audit: None,
269 authenticator: None,
270 seed: None,
271 title: title.into(),
272 }
273 }
274
275 pub fn language(mut self, language: Language) -> Self {
277 self.language = language;
278 self
279 }
280
281 pub fn texts(mut self, language: impl Into<String>, json: impl Into<String>) -> Self {
284 self.texts.push((language.into(), json.into()));
285 self
286 }
287
288 pub fn role(mut self, role: Role) -> Self {
290 self.roles.push(role);
291 self
292 }
293
294 pub fn merge(mut self, merge: RoleMerge) -> Self {
296 self.merge = merge;
297 self
298 }
299
300 pub fn audit(mut self, settings: AuditSettings) -> Self {
302 self.audit = Some(settings);
303 self
304 }
305
306 pub fn authenticator(mut self, authenticator: impl Authenticator + 'static) -> Self {
308 self.authenticator = Some(Rc::new(authenticator));
309 self
310 }
311
312 pub fn seed(mut self, seed: impl FnOnce(&mut dyn ObjectSpace) -> Result<(), String> + 'static) -> Self {
314 self.seed = Some(Box::new(seed));
315 self
316 }
317
318 pub fn build(self) -> Result<AnyView, ModelError> {
320 let wrong = |error: ravenrs_app::SpaceError| ModelError(error.to_string());
321 let known = (self.space)().entities();
322 for needed in security_entities().into_iter().chain(self.audit.as_ref().map(|_| audit_entity())) {
323 if !known.iter().any(|entity| entity.name == needed.name) {
324 return Err(ModelError(format!("storage has to know the entity {}", needed.name)));
325 }
326 }
327 let directory = UserDirectory::new(self.space.clone(), self.roles.clone()).map_err(wrong)?;
328 if let Some(seed) = self.seed {
329 let mut space = (self.space)();
330 seed(&mut *space).map_err(ModelError)?;
331 space.commit().map_err(wrong)?;
332 }
333 let stage = if directory.is_empty().map_err(wrong)? { Stage::FirstUser } else { Stage::SignIn };
334 let texts = crate::app::texts_of(self.language, &self.texts)?;
335 let words = Words::from_texts(&texts);
336 let authenticator: Rc<dyn Authenticator> = match self.authenticator {
337 Some(authenticator) => authenticator,
338 None => Rc::new(directory.clone()),
339 };
340 let gate = Gate(Rc::new(GateInner {
341 title: self.title,
342 words,
343 language: self.language,
344 texts: self.texts,
345 labels: ravenrs_widgets::Labels::from_texts(&texts),
346 space: self.space,
347 application: self.application,
348 merge: self.merge,
349 audit: self.audit,
350 directory,
351 authenticator,
352 entities: known,
353 stage: State::new(stage),
354 name: State::new(String::new()),
355 password: State::new(String::new()),
356 repeated: State::new(String::new()),
357 error: State::new(None),
358 entered: RefCell::new(0),
359 }));
360 Ok(Box::new(Localized::new(gate.0.labels.clone(), gate)))
361 }
362
363 pub fn into_view(self) -> AnyView {
365 self.build().unwrap_or_else(|error| Box::new(Alert::new("RavenRS", error.0).tone(Tone::Danger)))
366 }
367}
368
369#[derive(Clone)]
370enum Stage {
371 FirstUser,
373 SignIn,
374 NewPassword(Rc<PasswordForm>),
376 Inside(Rc<AnyView>, u64),
378}
379
380struct GateInner {
381 title: String,
382 words: Words,
383 language: Language,
384 texts: crate::app::TextPacks,
385 labels: ravenrs_widgets::Labels,
386 space: SpaceMaker,
387 application: MakeApp,
388 merge: RoleMerge,
389 audit: Option<AuditSettings>,
390 directory: UserDirectory,
391 authenticator: Rc<dyn Authenticator>,
392 entities: Vec<EntityInfo>,
393 stage: State<Stage>,
394 name: State<String>,
395 password: State<String>,
396 repeated: State<String>,
397 error: State<Option<String>>,
398 entered: RefCell<u64>,
399}
400
401#[derive(Clone)]
403struct Gate(Rc<GateInner>);
404
405#[cfg_attr(feature = "live", ravenrs_live::expose)]
406impl Gate {
407 fn forget_typed(&self) {
408 self.0.password.set(String::new());
409 self.0.repeated.set(String::new());
410 }
411
412 fn sign_in(&self) {
413 let inner = &self.0;
414 let credentials = Credentials::new(inner.name.peek().clone(), inner.password.peek().clone());
415 let result = inner.authenticator.authenticate(&credentials);
416 self.forget_typed();
417 match result {
418 Ok(identity) => self.enter(identity),
419 Err(AuthError::Rejected) => inner.error.set(Some(inner.words.sign_in_failed.clone())),
420 Err(_) => inner.error.set(Some(inner.words.sign_in_unavailable.clone())),
422 }
423 }
424
425 fn create_first(&self) {
426 let inner = &self.0;
427 let (password, repeated) = (inner.password.peek().clone(), inner.repeated.peek().clone());
428 if password != repeated {
429 inner.error.set(Some(inner.words.passwords_differ.clone()));
430 return;
431 }
432 let name = inner.name.peek().clone();
433 match inner.directory.create_first_administrator(&name, &password) {
434 Ok(identity) => {
435 self.forget_typed();
436 self.enter(identity);
437 }
438 Err(error) => inner.error.set(Some(message_of(&inner.words, &error))),
439 }
440 }
441
442 fn enter(&self, identity: Identity) {
443 let inner = &self.0;
444 inner.error.set(None);
445 let must_change = identity.must_change_password;
446 let audit_author = identity.name.clone();
447 let mut context =
448 SecurityContext::new(identity, inner.directory.clone(), inner.entities.clone(), inner.merge)
449 .with_denied_message(inner.words.access_denied.clone());
450 let trail = inner.audit.clone().map(|settings| {
451 let trail = Rc::new(AuditTrail::new(settings, move || audit_author.clone()));
452 trail.write_with(inner.space.clone());
453 trail
454 });
455 if let Some(trail) = &trail {
456 context = context.with_audit_trail(trail.clone());
457 }
458 let context = Rc::new(context);
459 if must_change {
460 let (changing, gate) = (context.clone(), self.clone());
461 let submit: Submit =
462 Rc::new(move |current, new| changing.change_own_password(current.unwrap_or(""), new));
463 let done: Rc<dyn Fn()> = Rc::new(move || gate.open_application(context.clone(), trail.clone()));
464 let form = PasswordForm::new(inner.words.clone(), true, submit, done);
465 inner.stage.set(Stage::NewPassword(Rc::new(form)));
466 } else {
467 self.open_application(context, trail);
468 }
469 }
470
471 fn open_application(&self, context: Rc<SecurityContext>, trail: Option<Rc<AuditTrail>>) {
472 let inner = &self.0;
473 let identity = context.identity();
474 let (raw, secured_context, secured_trail) = (inner.space.clone(), context.clone(), trail.clone());
475 let storage = move || {
476 let _ = secured_context.reload();
479 let under: Box<dyn ObjectSpace> = match &secured_trail {
480 Some(trail) => Box::new(AuditedObjectSpace::new(raw(), trail.clone())),
481 None => raw(),
482 };
483 SecuredObjectSpace::new(under, secured_context.clone())
484 };
485 let (gate, words) = (self.clone(), inner.words.clone());
486 let audited = trail.is_some();
487 let mut app = (inner.application)(&identity).language(inner.language);
488 for (language, json) in &inner.texts {
489 app = app.texts(language.clone(), json.clone());
490 }
491 let app = app
492 .module(SecurityModule { words: words.clone(), audited })
493 .translation("pt-BR", ravenrs_app_i18n::model::SECURITY_PT_BR)
494 .dashboard(
495 ravenrs_app::DashboardInfo::new(MODEL_EDITOR, words.model_editor.clone())
496 .navigation("Administration")
497 .icon("settings")
498 .control(words.model_editor.clone(), MODEL_EDITOR),
499 )
500 .storage(storage)
501 .secured(context, inner.space.clone())
502 .signed_in(identity.name.clone(), move || gate.sign_out());
503 match app.build() {
504 Ok(view) => {
505 *inner.entered.borrow_mut() += 1;
506 inner.stage.set(Stage::Inside(Rc::new(view), *inner.entered.borrow()));
507 }
508 Err(error) => inner.error.set(Some(error.0)),
509 }
510 }
511
512 fn sign_out(&self) {
513 self.forget_typed();
514 self.0.name.set(String::new());
515 self.0.error.set(None);
516 self.0.stage.set(Stage::SignIn);
517 }
518
519 fn title(&self, detail: Option<&str>) -> AnyView {
520 Box::new(
521 VStack::new((
522 Text::new(self.0.title.clone()).style(TextStyle::Title).font_weight(Weight::SEMIBOLD),
523 detail.map(|text| Text::new(text.to_owned()).style(TextStyle::Caption)),
524 ))
525 .spacing(4.0)
526 .align(Align::Stretch),
527 )
528 }
529
530 fn sign_in_form(&self) -> AnyView {
531 let (words, inner) = (&self.0.words, &self.0);
532 let (by_key, by_button) = (self.clone(), self.clone());
533 Box::new(
534 VStack::new((
535 self.title(None),
536 inner.error.get().map(|error| Alert::message(error).tone(Tone::Danger)),
537 FormField::new(
538 words.user_name.clone(),
539 TextField::new(text_binding(&inner.name)).autofocus(),
540 ),
541 FormField::new(
542 words.password.clone(),
543 TextField::new(text_binding(&inner.password))
544 .secure()
545 .on_submit(move |_| by_key.sign_in()),
546 ),
547 Button::new(words.sign_in.clone()).on_click(move || by_button.sign_in()).full_width(),
548 ))
549 .spacing(12.0)
550 .align(Align::Stretch),
551 )
552 }
553
554 fn first_user_form(&self) -> AnyView {
555 let (words, inner) = (&self.0.words, &self.0);
556 let (by_key, by_button) = (self.clone(), self.clone());
557 Box::new(
558 VStack::new((
559 self.title(Some(&words.first_user)),
560 inner
561 .error
562 .get()
563 .map(|error| Alert::new(words.create_user.clone(), error).tone(Tone::Danger)),
564 FormField::new(
565 words.user_name.clone(),
566 TextField::new(text_binding(&inner.name)).autofocus(),
567 ),
568 FormField::new(
569 words.new_password.clone(),
570 TextField::new(text_binding(&inner.password)).secure(),
571 ),
572 FormField::new(
573 words.repeat_password.clone(),
574 TextField::new(text_binding(&inner.repeated))
575 .secure()
576 .on_submit(move |_| by_key.create_first()),
577 ),
578 Button::new(words.create_user.clone()).on_click(move || by_button.create_first()),
579 ))
580 .spacing(12.0)
581 .align(Align::Stretch),
582 )
583 }
584
585 fn new_password_form(&self, form: &PasswordForm) -> AnyView {
586 Box::new(
587 VStack::new((self.title(Some(&self.0.words.must_change_password)), form.clone()))
588 .spacing(12.0)
589 .align(Align::Stretch),
590 )
591 }
592}
593
594impl View for Gate {
595 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
596 let form = match self.0.stage.get() {
597 Stage::Inside(view, entered) => return cx.child(&Keyed::new(entered, SharedView(view))),
598 Stage::FirstUser => self.first_user_form(),
599 Stage::SignIn => self.sign_in_form(),
600 Stage::NewPassword(form) => self.new_password_form(&form),
601 };
602 let centered = HStack::new((Card::new(form).padding(24.0).width(CARD_WIDTH),))
603 .justify(Justify::Center)
604 .align(Align::Center)
605 .grow(1.0);
606 cx.child(¢ered)
607 }
608}
609
610struct SharedView(Rc<AnyView>);
612
613impl View for SharedView {
614 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
615 cx.child(&**self.0)
616 }
617}
618
619pub(crate) fn model_editor(session: &Session) -> impl Fn() -> AnyView + 'static {
621 let editor = Rc::new(ModelEditor::new(session.clone()));
622 move || Box::new(SharedEditor(editor.clone()))
623}
624
625struct SharedEditor(Rc<ModelEditor>);
626
627impl View for SharedEditor {
628 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
629 self.0.build(cx)
630 }
631}