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" ] ]