diff --git a/CHANGELOG.md b/CHANGELOG.md index 0045a70fa..51d87869b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added - readonly admin comment on contacts +- credits and privacy policy pages ### Changed - log failed login attempt as warning +### Fixed + +- blocking of email addresses after too many failed login attempts + ## [0.3.3](https://github.com/uzh/pool/tree/0.3.3) - 2023-08-02 ### Added diff --git a/pool/app/i18n/default.ml b/pool/app/i18n/default.ml index 52d8cc684..3c472241a 100644 --- a/pool/app/i18n/default.ml +++ b/pool/app/i18n/default.ml @@ -34,6 +34,9 @@ let default_values = , "Das Passwort muss mindestens 8 Zeichen lang sein, eine Zahl, ein \ Grossbuchstabe und ein Sonderzeichen enthalten." ) ] ) + ; ( "privacy_policy" + , [ "EN", "

Privacy policy

"; "DE", "

Datenschutzerklärung

" ] + ) ] |> CCList.map (fun (key, data) -> let key = key |> Key.create |> get_or_failwith in diff --git a/pool/app/i18n/entity.ml b/pool/app/i18n/entity.ml index 77a5e2941..33be4fd78 100644 --- a/pool/app/i18n/entity.ml +++ b/pool/app/i18n/entity.ml @@ -12,6 +12,7 @@ module Key = struct | GreetingsText [@name "greetings_text"] [@printer print "greetings_text"] | PasswordPolicyText [@name "password_policy_text"] [@printer print "password_policy_text"] + | PrivacyPolicy [@name "privacy_policy"] [@printer print "privacy_policy"] | WelcomeText [@name "welcome_text"] [@printer print "welcome_text"] [@@deriving enum, eq, ord, sexp_of, show { with_path = false }, yojson] end @@ -20,7 +21,7 @@ module Key = struct include Core let is_rich_text = function - | CreditsText | GreetingsText | WelcomeText -> true + | CreditsText | GreetingsText | PrivacyPolicy | WelcomeText -> true | PasswordPolicyText -> false ;; end diff --git a/pool/app/i18n/i18n.ml b/pool/app/i18n/i18n.ml index cd8bfe9b8..8a629dc62 100644 --- a/pool/app/i18n/i18n.ml +++ b/pool/app/i18n/i18n.ml @@ -4,5 +4,39 @@ include Default module Guard = Entity_guard let find = Repo.find +let find_with_default_content = Repo.find_with_default_content let find_by_key = Repo.find_by_key +let find_by_key_opt = Repo.find_by_key_opt let find_all = Repo.find_all + +module I18nPageCache = struct + open Hashtbl + + let tbl + : ( Pool_tenant.Database.Label.t * Entity.Key.t * Pool_common.Language.t + , bool ) t + = + create 5 + ;; + + let find = find_opt tbl + + let add database_label key language value = + replace tbl (database_label, key, language) value + ;; + + let clear () = clear tbl +end + +let i18n_is_set database_label language key = + let open Utils.Lwt_result.Infix in + I18nPageCache.find (database_label, key, language) + |> function + | Some bool -> Lwt.return bool + | None -> + let%lwt existing = + find_by_key_opt database_label key language ||> CCOption.is_some + in + let () = I18nPageCache.add database_label key language existing in + Lwt.return existing +;; diff --git a/pool/app/i18n/i18n.mli b/pool/app/i18n/i18n.mli index e4412d690..97d6a23e8 100644 --- a/pool/app/i18n/i18n.mli +++ b/pool/app/i18n/i18n.mli @@ -3,6 +3,7 @@ module Key : sig | CreditsText | GreetingsText | PasswordPolicyText + | PrivacyPolicy | WelcomeText val create : string -> (t, Pool_common.Message.error) result @@ -54,14 +55,35 @@ val show_event : event -> string val handle_event : Pool_database.Label.t -> event -> unit Lwt.t val find : Pool_database.Label.t -> Pool_common.Id.t -> t Lwt.t +val find_with_default_content + : Pool_database.Label.t + -> Pool_common.Id.t + -> t Lwt.t + val find_by_key : Pool_database.Label.t -> Key.t -> Pool_common.Language.t -> t Lwt.t +val find_by_key_opt + : Pool_database.Label.t + -> Key.t + -> Pool_common.Language.t + -> t option Lwt.t + val find_all : Pool_database.Label.t -> unit -> t list Lwt.t +module I18nPageCache : sig + val clear : unit -> unit +end + +val i18n_is_set + : Pool_database.Label.t + -> Pool_common.Language.t + -> Key.t + -> bool Lwt.t + module Guard : sig module Target : sig val to_authorizable diff --git a/pool/app/i18n/repo/repo.ml b/pool/app/i18n/repo/repo.ml index 53e01c7a5..e1820f556 100644 --- a/pool/app/i18n/repo/repo.ml +++ b/pool/app/i18n/repo/repo.ml @@ -34,10 +34,25 @@ module Sql = struct Utils.Database.find (Pool_database.Label.value pool) find_request ;; + let find_with_default_content_request = + let open Caqti_request.Infix in + {sql| + WHERE uuid = UNHEX(REPLACE(?, '-', '')) + |sql} + |> select_from_i18n_sql + |> Pool_common.Repo.Id.t ->! RepoEntity.t_with_default_content + ;; + + let find_with_default_content pool = + Utils.Database.find + (Pool_database.Label.value pool) + find_with_default_content_request + ;; + let find_by_key_request = let open Caqti_request.Infix in {sql| - WHERE i18n_key = ? AND language = ? + WHERE i18n_key = ? AND language = ? AND content IS NOT NULL AND content != '' |sql} |> select_from_i18n_sql |> Caqti_type.(tup2 RepoEntity.Key.t Pool_common.Repo.Language.t) @@ -51,9 +66,18 @@ module Sql = struct (key, language) ;; + let find_by_key_opt pool key language = + Utils.Database.find_opt + (Pool_database.Label.value pool) + find_by_key_request + (key, language) + ;; + let find_all_request = let open Caqti_request.Infix in - "" |> select_from_i18n_sql |> Caqti_type.unit ->* RepoEntity.t + "" + |> select_from_i18n_sql + |> Caqti_type.unit ->* RepoEntity.t_with_default_content ;; let find_all pool = @@ -118,7 +142,9 @@ module Sql = struct end let find = Sql.find +let find_with_default_content = Sql.find_with_default_content let find_by_key = Sql.find_by_key +let find_by_key_opt = Sql.find_by_key_opt let find_all = Sql.find_all let insert = Sql.insert let update = Sql.update diff --git a/pool/app/i18n/repo/repo_entity.ml b/pool/app/i18n/repo/repo_entity.ml index 0ab31f1ec..6386c82f3 100644 --- a/pool/app/i18n/repo/repo_entity.ml +++ b/pool/app/i18n/repo/repo_entity.ml @@ -21,3 +21,19 @@ let t = Common.Repo.Id.t (tup2 Key.t (tup2 Common.Repo.Language.t Content.t)))) ;; + +let t_with_default_content = + let encode _ = failwith "Decode model only." in + let decode (id, (key, (language, content))) = + let open CCResult in + let content = CCOption.value ~default:"" content |> Content.of_string in + Ok { id; key; language; content } + in + Caqti_type.( + custom + ~encode + ~decode + (tup2 + Common.Repo.Id.t + (tup2 Key.t (tup2 Common.Repo.Language.t (option string))))) +;; diff --git a/pool/app/pool_common/entity_i18n.ml b/pool/app/pool_common/entity_i18n.ml index d1f4034aa..9c967a872 100644 --- a/pool/app/pool_common/entity_i18n.ml +++ b/pool/app/pool_common/entity_i18n.ml @@ -82,6 +82,7 @@ type nav_link = | Assignments | ContactInformation | Contacts + | Credits | CustomFields | Dashboard | Experiments @@ -99,6 +100,7 @@ type nav_link = | Overview | ParticipationTags | PersonalDetails + | PrivacyPolicy | Profile | Queue | Rules diff --git a/pool/app/pool_common/locales/i18n_de.ml b/pool/app/pool_common/locales/i18n_de.ml index a64eef5b0..e8348f1de 100644 --- a/pool/app/pool_common/locales/i18n_de.ml +++ b/pool/app/pool_common/locales/i18n_de.ml @@ -121,6 +121,7 @@ let nav_link_to_string = function | Assignments -> "Anmeldungen" | ContactInformation -> "Kontaktangaben" | Contacts -> "Kontakte" + | Credits -> "Impressum" | CustomFields -> "Felder" | Dashboard -> "Dashboard" | Experiments -> "Experimente" @@ -138,6 +139,7 @@ let nav_link_to_string = function | Overview -> "Übersicht" | ParticipationTags -> "Teilnahmetags" | PersonalDetails -> "Persönliche Angaben" + | PrivacyPolicy -> "Datenschutzerklärung" | Profile -> "Profil" | Queue -> "Hintergrundjobs" | Rules -> "Regeln" diff --git a/pool/app/pool_common/locales/i18n_en.ml b/pool/app/pool_common/locales/i18n_en.ml index fbe32cd59..654f33d65 100644 --- a/pool/app/pool_common/locales/i18n_en.ml +++ b/pool/app/pool_common/locales/i18n_en.ml @@ -119,6 +119,7 @@ let nav_link_to_string = function | Assignments -> "Assignments" | ContactInformation -> "Contact information" | Contacts -> "Contacts" + | Credits -> "Credits" | CustomFields -> "Fields" | Dashboard -> "Dashboard" | Experiments -> "Experiments" @@ -136,6 +137,7 @@ let nav_link_to_string = function | Overview -> "Overview" | ParticipationTags -> "Participation tags" | PersonalDetails -> "Personal details" + | PrivacyPolicy -> "Privacy policy" | Profile -> "Profile" | Queue -> "Queued jobs" | Rules -> "Rules" diff --git a/pool/app/pool_user/pool_user.mli b/pool/app/pool_user/pool_user.mli index 60a01a6c0..c91c0581b 100644 --- a/pool/app/pool_user/pool_user.mli +++ b/pool/app/pool_user/pool_user.mli @@ -246,24 +246,20 @@ module FailedLoginAttempt : sig type t = { id : Id.t - ; email : Entity.EmailAddress.t + ; email : EmailAddress.t ; counter : Counter.t ; blocked_until : BlockedUntil.t option } val create : ?id:Id.t - -> Entity.EmailAddress.t + -> EmailAddress.t -> Counter.t -> BlockedUntil.t option -> t module Repo : sig - val find_opt - : Pool_database.Label.t - -> Entity.EmailAddress.t - -> t option Lwt.t - + val find_opt : Pool_database.Label.t -> EmailAddress.t -> t option Lwt.t val insert : Pool_database.Label.t -> t -> unit Lwt.t val delete : Pool_database.Label.t -> t -> unit Lwt.t end diff --git a/pool/app/system_event/entity.ml b/pool/app/system_event/entity.ml index 4059b9dd6..605721618 100644 --- a/pool/app/system_event/entity.ml +++ b/pool/app/system_event/entity.ml @@ -8,6 +8,8 @@ module Job = struct type t = | GuardianCacheCleared [@name "guardiancachecleared"] [@printer Utils.ppx_printer "guardiancachecleared"] + | I18nPageUpdated [@name "i18npageupdated"] + [@printer Utils.ppx_printer "i18npageupdated"] | SmtpAccountUpdated [@name "smtpaccountupdated"] [@printer Utils.ppx_printer "smtpaccountupdated"] | TenantDatabaseAdded of Pool_database.Label.t [@name "tenantdatabaseadded"] @@ -53,11 +55,11 @@ module EventLog = struct let field = Pool_common.Message.Field.Host let schema () = schema field () - let get () = - Format.asprintf - "%s-%s" - (Unix.gethostname ()) - (() |> Unix.getpid |> CCInt.to_string) + let get ?identifier () = + let hostname = Unix.gethostname () in + match identifier with + | None -> hostname + | Some id -> Format.asprintf "%s-%s" hostname id ;; end diff --git a/pool/app/system_event/event.ml b/pool/app/system_event/event.ml index 0e077b28c..f2c31f9b7 100644 --- a/pool/app/system_event/event.ml +++ b/pool/app/system_event/event.ml @@ -7,12 +7,16 @@ let handle_event : event -> unit Lwt.t = function | Created t -> Repo.insert Pool_database.root t ;; -let handle_system_event system_event = +let handle_system_event ?identifier system_event = let open Utils.Lwt_result.Infix in let open EventLog in let pool = Pool_database.root in let create_event_log ?message status = - create ?message system_event.id (ServiceIdentifier.get ()) status + create + ?message + system_event.id + (ServiceIdentifier.get ?identifier ()) + status |> Repo.EventLog.insert pool in let success_log () = create_event_log Status.Successful in @@ -31,6 +35,9 @@ let handle_system_event system_event = | GuardianCacheCleared -> let () = Guard.Persistence.Cache.clear () in success_log () + | I18nPageUpdated -> + let () = I18n.I18nPageCache.clear () in + success_log () | SmtpAccountUpdated -> let () = Email.Service.Cache.clear () in success_log () diff --git a/pool/app/system_event/service.ml b/pool/app/system_event/service.ml index 89cd9b1f1..7b1116444 100644 --- a/pool/app/system_event/service.ml +++ b/pool/app/system_event/service.ml @@ -3,21 +3,21 @@ open Entity let src = Logs.Src.create "system_event.service" let get_or_failwith = Pool_common.Utils.get_or_failwith -let run () = +let run ?identifier () = let open Utils.Lwt_result.Infix in () - |> EventLog.ServiceIdentifier.get + |> EventLog.ServiceIdentifier.get ?identifier |> Repo.find_pending - >|> Lwt_list.iter_s Event.handle_system_event + >|> Lwt_list.iter_s (Event.handle_system_event ?identifier) ;; -let start_handler () = +let start_handler ?identifier () = let open Schedule in let interval = Ptime.Span.of_int_s 10 in let periodic_fcn () = Logs.debug ~src (fun m -> m ~tags:Pool_database.(Logger.Tags.create root) "Run"); - run () + run ?identifier () in create "system_events" @@ -57,22 +57,24 @@ let schema = config ;; -let start () = +let start ?identifier () = Sihl.Configuration.require schema; - if read_bool Run then start_handler () else Lwt.return_unit + if read_bool Run then start_handler ?identifier () else Lwt.return_unit ;; let stop () = Lwt.return_unit -let lifecycle = +let lifecycle ?identifier () = Sihl.Container.create_lifecycle "System events" ~dependencies:(fun () -> [ Schedule.lifecycle ]) - ~start + ~start:(start ?identifier) ~stop ;; -let register () = +let register ?identifier () = let configuration = Sihl.Configuration.make ~schema () in - Sihl.Container.Service.create ~configuration lifecycle + Sihl.Container.Service.create ~configuration (lifecycle ?identifier ()) ;; + +let register_worker = register ~identifier:"worker" diff --git a/pool/app/system_event/system_event.mli b/pool/app/system_event/system_event.mli index 2391c00f5..8d01198e6 100644 --- a/pool/app/system_event/system_event.mli +++ b/pool/app/system_event/system_event.mli @@ -3,6 +3,7 @@ module Id : module type of Pool_common.Id module Job : sig type t = | GuardianCacheCleared + | I18nPageUpdated | SmtpAccountUpdated | TenantDatabaseAdded of Pool_database.Label.t | TenantDatabaseUpdated of Pool_database.Label.t @@ -34,7 +35,7 @@ module EventLog : sig module ServiceIdentifier : sig include Pool_common.Model.StringSig - val get : unit -> t + val get : ?identifier:string -> unit -> t end module Status : sig @@ -65,9 +66,10 @@ val pp_event : Format.formatter -> event -> unit val show_event : event -> string val find_pending : EventLog.ServiceIdentifier.t -> t list Lwt.t val handle_event : event -> unit Lwt.t -val handle_system_event : t -> unit Lwt.t +val handle_system_event : ?identifier:string -> t -> unit Lwt.t module Service : sig - val run : unit -> unit Lwt.t - val register : unit -> Sihl.Container.Service.t + val run : ?identifier:string -> unit -> unit Lwt.t + val register : ?identifier:string -> unit -> Sihl.Container.Service.t + val register_worker : unit -> Sihl.Container.Service.t end diff --git a/pool/cqrs_command/i18n_command.ml b/pool/cqrs_command/i18n_command.ml index 256e448d8..d45d23b9a 100644 --- a/pool/cqrs_command/i18n_command.ml +++ b/pool/cqrs_command/i18n_command.ml @@ -26,8 +26,18 @@ end = struct let handle ?(tags = Logs.Tag.empty) property (command : t) = Logs.info ~src (fun m -> m "Handle command Update" ~tags); - let edit : I18n.edit = I18n.{ content = command.content } in - Ok [ I18n.Updated (property, edit) |> Pool_event.i18n ] + let open I18n in + let edit : edit = { content = command.content } in + let system_events = + let open Key in + match property |> key with + | CreditsText | GreetingsText | PasswordPolicyText | WelcomeText -> [] + | PrivacyPolicy -> + System_event. + [ Job.I18nPageUpdated |> create |> created |> Pool_event.system_event + ] + in + Ok ((I18n.Updated (property, edit) |> Pool_event.i18n) :: system_events) ;; let decode data = diff --git a/pool/routes/routes.ml b/pool/routes/routes.ml index d9d9b8087..d72e66ec4 100644 --- a/pool/routes/routes.ml +++ b/pool/routes/routes.ml @@ -56,7 +56,11 @@ module Public = struct [ CustomMiddleware.Guardian.require_user_type_of Pool_context.UserType.[ Guest; Contact; Admin ] ] - [ get "/index" index; get "/custom/assets/index.css" index_css ] + [ get "/index" index + ; get "/custom/assets/index.css" index_css + ; get "/credits" credits + ; get "/privacy-policy" privacy_policy + ] ; choose ~middlewares: [ CustomMiddleware.Guardian.require_user_type_of diff --git a/pool/run/run.ml b/pool/run/run.ml index 436081ddd..558383336 100644 --- a/pool/run/run.ml +++ b/pool/run/run.ml @@ -12,7 +12,7 @@ let worker_services = ] () ; Matcher.register () - ; System_event.Service.register () + ; System_event.Service.register_worker () ; User_import.Service.register () ; Assignment.Service.register () ] diff --git a/pool/web/handler/admin_i18n.ml b/pool/web/handler/admin_i18n.ml index 2b091045d..d3536d481 100644 --- a/pool/web/handler/admin_i18n.ml +++ b/pool/web/handler/admin_i18n.ml @@ -50,7 +50,7 @@ let update req = Utils.Lwt_result.map_error (fun err -> err, redirect_path) @@ let tags = Pool_context.Logger.Tags.req req in - let property () = I18n.find database_label id in + let property () = I18n.find_with_default_content database_label id in let%lwt urlencoded = Sihl.Web.Request.to_urlencoded req in let events property = let open CCResult.Infix in diff --git a/pool/web/handler/helpers_login.ml b/pool/web/handler/helpers_login.ml index 4235ed0a8..1588a1d2d 100644 --- a/pool/web/handler/helpers_login.ml +++ b/pool/web/handler/helpers_login.ml @@ -1,5 +1,6 @@ module Label = Pool_database.Label module Message = Pool_common.Message +module EmailAddress = Pool_user.EmailAddress let src = Logs.Src.create "login helper" @@ -16,6 +17,7 @@ let notify_user database_label tags email = | Some (_ : Pool_user.FailedLoginAttempt.BlockedUntil.t) -> let notify () = email + |> EmailAddress.value |> Service.User.find_by_email_opt ~ctx:(Pool_database.to_ctx database_label) >|> function @@ -32,7 +34,7 @@ let notify_user database_label tags email = m ~tags "Could not send account suspension notification to '%s': %s" - email + (email |> EmailAddress.value) Pool_common.(Utils.error_to_string Language.En err)); err in @@ -73,7 +75,11 @@ let login_params urlencoded = |> CCOption.to_result LoginProvideDetails |> Lwt_result.lift in - let email = CCList.assoc ~eq:String.equal Field.(Email |> show) params in + let* email = + CCList.assoc ~eq:String.equal Field.(Email |> show) params + |> EmailAddress.create + |> Lwt_result.lift + in let password = CCList.assoc ~eq:String.equal Field.(Password |> show) params in @@ -87,7 +93,8 @@ let log_request req tags email = Headers.get req.headers "X-Real-IP" |> CCOption.value ~default:"X-Real-IP not found" in - Logs.warn ~src (fun m -> m "Failed login attempt: %s %s" ip email ~tags) + Logs.warn ~src (fun m -> + m "Failed login attempt: %s %s" ip (EmailAddress.value email) ~tags) ;; let login req urlencoded database_label = @@ -140,7 +147,7 @@ let login req urlencoded database_label = let login () = Service.User.login ~ctx:(Pool_database.to_ctx database_label) - email + (EmailAddress.value email) ~password >|> handle_result in diff --git a/pool/web/handler/public.ml b/pool/web/handler/public.ml index 368dc1332..27c2bfde4 100644 --- a/pool/web/handler/public.ml +++ b/pool/web/handler/public.ml @@ -181,3 +181,42 @@ let error req = |> Sihl.Web.Response.of_html |> Lwt.return ;; + +let credits req = + let result + ({ Pool_context.language; query_language; database_label; _ } as context) + = + let error_path = Http_utils.path_with_language query_language "/error" in + let open Utils.Lwt_result.Infix in + let%lwt html = + I18n.find_by_key database_label I18n.Key.CreditsText language + ||> Page.Utils.i18n_page + in + html + |> create_layout req context + >|+ Sihl.Web.Response.of_html + >|- fun err -> err, error_path + in + result |> Http_utils.extract_happy_path ~src req +;; + +let privacy_policy req = + let result + ({ Pool_context.language; query_language; database_label; _ } as context) + = + let redirect_path = Http_utils.path_with_language query_language "/" in + let open Utils.Lwt_result.Infix in + let%lwt policy = + I18n.find_by_key_opt database_label I18n.Key.PrivacyPolicy language + in + match policy with + | None -> Http_utils.redirect_to redirect_path ||> CCResult.return + | Some policy -> + policy + |> Page.Utils.i18n_page + |> create_layout req context + >|+ Sihl.Web.Response.of_html + >|- fun err -> err, redirect_path + in + result |> Http_utils.extract_happy_path ~src req +;; diff --git a/pool/web/view/layout/layout.ml b/pool/web/view/layout/layout.ml index 6db573c91..4ae09d98d 100644 --- a/pool/web/view/layout/layout.ml +++ b/pool/web/view/layout/layout.ml @@ -55,14 +55,53 @@ module Tenant = struct active_language user in + let%lwt footer = + let%lwt privacy_policy_is_set = + I18n.(i18n_is_set database_label active_language Key.PrivacyPolicy) + in + let open Pool_common in + let fragments = + [ txt title_text + ; txt App.version + ; a + ~a:[ a_href (Sihl.Web.externalize_path "/credits") ] + [ txt (Utils.nav_link_to_string active_language I18n.Credits) ] + ] + in + let fragments = + if privacy_policy_is_set + then + fragments + @ [ a + ~a:[ a_href (Sihl.Web.externalize_path "/privacy-policy") ] + [ txt + (Utils.nav_link_to_string + active_language + I18n.PrivacyPolicy) + ] + ] + else fragments + in + footer + ~a: + [ a_class + [ "inset" + ; "flexrow" + ; "flex-gap" + ; "justify-center" + ; "bg-grey-light" + ; "border-top" + ; "push" + ] + ] + (App.combine_footer_fragments fragments) + |> Lwt.return + in html (head page_title head_tags) (body ~a:[ a_class body_tag_classnames ] - ([ App.header ~children query_language title_text - ; content - ; App.footer title_text - ] + ([ App.header ~children query_language title_text; content; footer ] @ scripts)) |> Lwt.return ;; @@ -72,7 +111,7 @@ module Root = struct let create ?active_navigation ?message database_label user content = let open Layout_utils in let language = Language.En in - let title_text = "Z-Pool-Tool" in + let title_text = App.app_name in let page_title = title (txt title_text) in let message = Message.create message language () in let%lwt children = @@ -98,7 +137,7 @@ module Root = struct ~a:[ a_class body_tag_classnames ] [ App.header ~children None title_text ; main_tag [ message; content ] - ; App.footer title_text + ; App.root_footer ; js_script_tag `IndexJs ]) |> Lwt.return @@ -108,7 +147,7 @@ end module Error = struct let create children = let open Layout_utils in - let title_text = "Z-Pool-Tool" in + let title_text = App.app_name in let page_title = title (txt title_text) in let content = main_tag [ children ] in html @@ -119,7 +158,7 @@ module Error = struct ~a:[ a_class body_tag_classnames ] [ App.header None title_text ; content - ; App.footer title_text + ; App.root_footer ; js_script_tag `IndexJs ]) ;; diff --git a/pool/web/view/layout/layout_utils.ml b/pool/web/view/layout/layout_utils.ml index 3e721066e..e1a714de6 100644 --- a/pool/web/view/layout/layout_utils.ml +++ b/pool/web/view/layout/layout_utils.ml @@ -36,6 +36,8 @@ let css_link_tag (file : [ `GlobalStylesheet | `TenantStylesheet ]) = ;; module App = struct + let app_name = "Z-Pool-Tool" + let create_title query_language title = let path = Http_utils.path_with_language query_language "/index" @@ -59,10 +61,25 @@ module App = struct [ create_title query_language title; div children ] ;; - let footer title = - let version = Format.asprintf "Z-Pool-Tool %s" Version.to_string in - let title = span [ txt title ] in - let content = [ title; span [ txt "|" ]; txt version ] in + let version = Format.asprintf "Z-Pool-Tool %s" Version.to_string + + let combine_footer_fragments fragments = + let separator = span [ txt "|" ] in + let rec combine html = function + | [] -> html + | hd :: tl -> + (html + @ + if CCList.length tl > 0 + then [ span [ hd ]; separator ] + else [ span [ hd ] ]) + |> fun html -> combine html tl + in + combine [] fragments + ;; + + let root_footer = + let html = [ txt app_name; txt version ] |> combine_footer_fragments in footer ~a: [ a_class @@ -75,6 +92,6 @@ module App = struct ; "push" ] ] - content + html ;; end diff --git a/pool/web/view/page/page_public.ml b/pool/web/view/page/page_public.ml index f3bda858c..a955b3361 100644 --- a/pool/web/view/page/page_public.ml +++ b/pool/web/view/page/page_public.ml @@ -33,7 +33,7 @@ let login_form [ form ~a:[ a_action action; a_method `Post; a_class [ "stack" ] ] [ csrf_element csrf () - ; input_element ?flash_fetcher language `Text Message.Field.Email + ; input_element ?flash_fetcher language `Email Message.Field.Email ; input_element language `Password Message.Field.Password ; div ~a:[ a_class [ "flexrow"; "align-center"; "flex-gap" ] ] diff --git a/pool/web/view/page/page_utils.ml b/pool/web/view/page/page_utils.ml index e2abf75e0..e30faec74 100644 --- a/pool/web/view/page/page_utils.ml +++ b/pool/web/view/page/page_utils.ml @@ -1,5 +1,13 @@ open Tyxml.Html +let i18n_page ?(narrow = true) i18n = + let classnames = [ "trim"; "safety-margin" ] in + let classnames = if narrow then classnames @ [ "narrow" ] else classnames in + div + ~a:[ a_class classnames ] + I18n.[ i18n |> content |> Content.value |> Unsafe.data ] +;; + let note page_title info = div ~a:[ a_class [ "trim"; "narrow"; "safety-margin" ] ]