From 57e212420c97172319ee0d3d1d57c6b71637d836 Mon Sep 17 00:00:00 2001 From: Philippe Pittoli Date: Mon, 5 May 2025 03:47:01 +0200 Subject: [PATCH] Bulma: s/Style/Web/ + modals in a module --- src/App/Container.purs | 32 +- src/App/DisplayErrors.purs | 60 +-- src/App/Page/Administration.purs | 60 +-- src/App/Page/Authentication.purs | 58 +-- src/App/Page/DomainList.purs | 58 +-- src/App/Page/Home.purs | 44 +- src/App/Page/MailValidation.purs | 16 +- src/App/Page/Migration.purs | 20 +- src/App/Page/Navigation.purs | 6 +- src/App/Page/Registration.purs | 24 +- src/App/Page/Setup.purs | 44 +- src/App/Page/Zone.purs | 689 ++++++------------------------- src/App/Templates/Modal.purs | 375 +++++++++++++++++ src/App/Templates/Table.purs | 47 ++- src/App/Text/Explanations.purs | 186 ++++----- src/App/Type/Field.purs | 18 + src/App/Type/RRForm.purs | 149 +++++++ src/App/Type/RRId.purs | 3 + src/App/Type/RRModal.purs | 16 + src/Bulma.purs | 1 - src/Web.purs | 14 + src/{Style => Web}/Button.purs | 2 +- src/{Style => Web}/Input.purs | 2 +- src/{Style.purs => Web/Tag.purs} | 14 +- 24 files changed, 1022 insertions(+), 916 deletions(-) create mode 100644 src/App/Templates/Modal.purs create mode 100644 src/App/Type/Field.purs create mode 100644 src/App/Type/RRForm.purs create mode 100644 src/App/Type/RRId.purs create mode 100644 src/App/Type/RRModal.purs create mode 100644 src/Web.purs rename src/{Style => Web}/Button.purs (99%) rename src/{Style => Web}/Input.purs (99%) rename src/{Style.purs => Web/Tag.purs} (72%) diff --git a/src/App/Container.purs b/src/App/Container.purs index 882e276..74f3142 100644 --- a/src/App/Container.purs +++ b/src/App/Container.purs @@ -45,7 +45,7 @@ module App.Container where import Prelude (Unit, bind, discard, unit, ($), (=<<), (<>), show, pure, (+), (&&), (>)) -import Style as Style +import Web as Web import Data.Array as A import Data.ArrayBuffer.Types (ArrayBuffer) @@ -299,9 +299,9 @@ render state Migration -> render_migration LegalNotice -> render_legal_notice -- The footer includes logs and both the WS child components. - , Style.hr - , Style.columns_ [ Style.column_ [ Style.h3 "Logs (watch this if something fails 😅)", render_logs ] - , Style.column_ [ Style.level + , Web.hr + , Web.columns_ [ Web.column_ [ Web.h3 "Logs (watch this if something fails 😅)", render_logs ] + , Web.column_ [ Web.level [ render_auth_WS , render_dnsmanager_WS , legal_notice_btn @@ -317,12 +317,12 @@ render state migration_warning :: forall w. HH.HTML w Action migration_warning = HH.div [HP.classes [C.notification, C.is_warning]] - [ Style.p """ + [ Web.p """ ⚠️​ (FR) le service a été migré d'une ancienne base de code récemment. Le développement se poursuit mais le service devrait être stable, mis à part quelques redémarrages de temps à autre. Merci de nous contacter si vous voyez une erreur. """ - , Style.p """ + , Web.p """ ⚠️​ (EN) migration from old codebase was performed. Development is still on-going but the service should be fairly stable. Reboots will happen on occasion. @@ -335,23 +335,23 @@ render state case state.user_data of Just (Tuple Nothing _) -> HH.div [HP.classes [C.notification, C.is_warning]] - [ Style.p """ + [ Web.p """ ⚠️​ MIGRATION (FR): veuillez indiquer une adresse email pour votre compte. Tout compte sans adresse email sera supprimé sous 6 mois. """ - , Style.p """ + , Web.p """ ⚠️​ MIGRATION (EN): please associate an email address to your account. Accounts without a validated email address will be discarded within 6 months. """ ] _ -> HH.text "" - legal_notice_btn = Style.btn_ [] "Legal notice" (Routing LegalNotice) + legal_notice_btn = Web.btn_ [] "Legal notice" (Routing LegalNotice) reconnection_bar :: forall w. HH.HTML w Action reconnection_bar = if (state.are_we_connected_to_authd && state.are_we_connected_to_dnsmanagerd) then HH.div_ [] - else Style.btn_ [C.is_large, C.is_danger] "You have been disconnected. Click here to reconnect." Reconnection + else Web.btn_ [C.is_large, C.is_danger] "You have been disconnected. Click here to reconnect." Reconnection render_auth_WS :: forall monad. MonadAff monad => H.ComponentHTML Action ChildSlots monad render_auth_WS = HH.slot _ws_auth unit WS.component (Tuple wsURLauthd "authd") AuthenticationDaemonEvent @@ -361,8 +361,8 @@ render state render_notifications = case state.notif of NoNotification -> HH.div_ [] - GoodNotification v -> Style.box [Style.notification_success v CloseNotif] - BadNotification v -> Style.box [Style.notification_danger v CloseNotif] + GoodNotification v -> Web.box [Web.notification_success v CloseNotif] + BadNotification v -> Web.box [Web.notification_danger v CloseNotif] render_home :: forall monad. MonadAff monad => H.ComponentHTML Action ChildSlots monad render_home = HH.slot_ _ho unit HomeInterface.component unit @@ -375,7 +375,7 @@ render state render_setup :: forall monad. MonadAff monad => H.ComponentHTML Action ChildSlots monad render_setup = case state.user_data of Just user_data -> HH.slot _setupi unit SetupInterface.component user_data SetupInterfaceEvent - Nothing -> Style.p "You shouldn't see this page. Please, reconnect." + Nothing -> Web.p "You shouldn't see this page. Please, reconnect." render_mail_validation :: forall monad. MonadAff monad => H.ComponentHTML Action ChildSlots monad render_mail_validation = HH.slot _mvi unit MVI.component unit MailValidationInterfaceEvent render_zone :: forall monad. String -> MonadAff monad => H.ComponentHTML Action ChildSlots monad @@ -388,8 +388,8 @@ render state render_legal_notice :: forall monad. MonadAff monad => H.ComponentHTML Action ChildSlots monad render_legal_notice - = Style.section_small [ Explanations.legal_notice - , Style.btn_ [C.is_large, C.margin_top 3, C.is_info] "Home page" (Routing Home) + = Web.section_small [ Explanations.legal_notice + , Web.btn_ [C.is_large, C.margin_top 3, C.is_info] "Home page" (Routing Home) ] render_nav :: forall monad. MonadAff monad => H.ComponentHTML Action ChildSlots monad @@ -409,7 +409,7 @@ render state ] render_logs :: forall monad. MonadAff monad => H.ComponentHTML Action ChildSlots monad - render_logs = Style.container [ HH.slot_ _log unit AppLog.component unit ] + render_logs = Web.container [ HH.slot_ _log unit AppLog.component unit ] ref_paypal_div :: H.RefLabel ref_paypal_div = H.RefLabel "paypal-div" diff --git a/src/App/DisplayErrors.purs b/src/App/DisplayErrors.purs index 2b007c5..ec3c7ac 100644 --- a/src/App/DisplayErrors.purs +++ b/src/App/DisplayErrors.purs @@ -15,37 +15,37 @@ import App.Validation.Label as ValidationLabel import GenericParser.DomainParser.Common (DomainError(..)) as DomainParser import GenericParser.IPAddress as IPAddress -import Style as Style +import Web as Web error_to_paragraph :: forall w i. ValidationDNS.Error -> HH.HTML w i -error_to_paragraph v = Style.error_message (Style.p $ show_error_title v) +error_to_paragraph v = Web.error_message (Web.p $ show_error_title v) (case v of - ValidationDNS.UNKNOWN -> Style.p "An internal error happened." + ValidationDNS.UNKNOWN -> Web.p "An internal error happened." ValidationDNS.VEIPv4 err -> maybe default_error show_error_ip4 err.error ValidationDNS.VEIPv6 err -> maybe default_error show_error_ip6 err.error ValidationDNS.VEName err -> maybe default_error show_error_domain err.error ValidationDNS.VETTL min max n -> - Style.p $ "TTL should have a value between " + Web.p $ "TTL should have a value between " <> show min <> " and " <> show max <> ", current value: " <> show n <> "." ValidationDNS.VEDMARCpct min max n -> - Style.p $ "DMARC sample rate should have a value between " + Web.p $ "DMARC sample rate should have a value between " <> show min <> " and " <> show max <> ", current value: " <> show n <> "." ValidationDNS.VEDMARCri min max n -> - Style.p $ "DMARC report interval should have a value between " + Web.p $ "DMARC report interval should have a value between " <> show min <> " and " <> show max <> ", current value: " <> show n <> "." ValidationDNS.VETXT err -> maybe default_error show_error_txt err.error ValidationDNS.VECNAME err -> maybe default_error show_error_domain err.error ValidationDNS.VENS err -> maybe default_error show_error_domain err.error ValidationDNS.VEMX err -> maybe default_error show_error_domain err.error - ValidationDNS.VEPriority min max n -> Style.p $ "Priority should have a value between " <> show min <> " and " <> show max + ValidationDNS.VEPriority min max n -> Web.p $ "Priority should have a value between " <> show min <> " and " <> show max <> ", current value: " <> show n <> "." ValidationDNS.VESRV err -> maybe default_error show_error_domain err.error - ValidationDNS.VEPort min max n -> Style.p $ "Port should have a value between " <> show min <> " and " <> show max + ValidationDNS.VEPort min max n -> Web.p $ "Port should have a value between " <> show min <> " and " <> show max <> ", current value: " <> show n <> "." - ValidationDNS.VEWeight min max n -> Style.p $ "Weight should have a value between " <> show min <> " and " <> show max + ValidationDNS.VEWeight min max n -> Web.p $ "Weight should have a value between " <> show min <> " and " <> show max <> ", current value: " <> show n <> "." - ValidationDNS.VECAAflag min max n -> Style.p $ "CAA flag should have a value between " <> show min <> " and " <> show max + ValidationDNS.VECAAflag min max n -> Web.p $ "CAA flag should have a value between " <> show min <> " and " <> show max <> ", current value: " <> show n <> "." -- SPF dedicated RR @@ -56,13 +56,13 @@ error_to_paragraph v = Style.error_message (Style.p $ show_error_title v) ValidationDNS.DKIMInvalidKeySize min max -> show_error_key_sizes min max ) - where default_error = Style.p "" + where default_error = Web.p "" show_error_key_sizes :: forall w i. Int -> Int -> HH.HTML w i show_error_key_sizes min max = if min == max - then Style.p $ "Chosen signature algorithm only accepts public key input of " <> show min <> " characters." - else Style.p $ "Chosen signature algorithm only accepts public key input between " + then Web.p $ "Chosen signature algorithm only accepts public key input of " <> show min <> " characters." + else Web.p $ "Chosen signature algorithm only accepts public key input between " <> show min <> " and " <> show max <> " characters." -- | `show_error_title` provide a simple title string to display to the user in case of an error with an entry. @@ -96,12 +96,12 @@ show_error_title v = case v of show_error_domain :: forall w i. DomainParser.DomainError -> HH.HTML w i show_error_domain e = case e of DomainParser.LabelTooLarge size -> - Style.p $ "The label contains too many characters (" <> show size <> ")." + Web.p $ "The label contains too many characters (" <> show size <> ")." DomainParser.DomainTooLarge size -> - Style.p $ "The domain contains too many characters (" <> show size <> ")." + Web.p $ "The domain contains too many characters (" <> show size <> ")." -- DomainParser.InvalidCharacter -- DomainParser.EOFExpected - _ -> Style.p """ + _ -> Web.p """ The domain (or label) contains invalid characters. A domain label should start with a letter, then possibly a series of letters, digits and hyphenations ("-"), @@ -111,31 +111,31 @@ show_error_domain e = case e of show_error_ip6 :: forall w i. IPAddress.IPv6Error -> HH.HTML w i show_error_ip6 e = case e of IPAddress.IP6TooManyHexaDecimalCharacters -> - Style.p "IP6TooManyHexaDecimalCharacters" + Web.p "IP6TooManyHexaDecimalCharacters" IPAddress.IP6NotEnoughChunks -> - Style.p """ + Web.p """ The IPv6 representation is erroneous, it should contain 8 groups of hexadecimal characters or being shortened with a double ':' character, such as "2000::1". """ IPAddress.IP6TooManyChunks -> - Style.p "The IPv6 representation is erroneous. It should contain only up to 8 groups of hexadecimal characters." + Web.p "The IPv6 representation is erroneous. It should contain only up to 8 groups of hexadecimal characters." IPAddress.IP6IrrelevantShortRepresentation -> - Style.p "IPv6 address has been unnecessarily shortened (with two ':')." - IPAddress.IP6InvalidRange -> Style.p "IPv6 address or range isn't valid." + Web.p "IPv6 address has been unnecessarily shortened (with two ':')." + IPAddress.IP6InvalidRange -> Web.p "IPv6 address or range isn't valid." show_error_ip4 :: forall w i. IPAddress.IPv4Error -> HH.HTML w i show_error_ip4 e = case e of IPAddress.IP4NumberTooBig n -> - Style.p $ "IPv4 address contains a number too big (should be between 0 and 255). Current entered number: " <> show n + Web.p $ "IPv4 address contains a number too big (should be between 0 and 255). Current entered number: " <> show n IPAddress.IP4IrrelevantShortRepresentation -> - Style.p "IPv4 address has been unnecessarily shortened (with two '.')." - IPAddress.IP4InvalidRange -> Style.p "IPv4 address or range isn't valid." + Web.p "IPv4 address has been unnecessarily shortened (with two '.')." + IPAddress.IP4InvalidRange -> Web.p "IPv4 address or range isn't valid." show_error_txt :: forall w i. ValidationDNS.TXTError -> HH.HTML w i show_error_txt e = case e of - ValidationDNS.TXTInvalidCharacter -> Style.p "The TXT field contains some invalid characters." + ValidationDNS.TXTInvalidCharacter -> Web.p "The TXT field contains some invalid characters." ValidationDNS.TXTTooLong max n -> - Style.p $ "An TXT field is limited to " <> show max <> " characters (currently there are " + Web.p $ "An TXT field is limited to " <> show max <> " characters (currently there are " <> show n <> " characters)." domainerror_string :: DomainParser.DomainError -> String @@ -146,14 +146,14 @@ domainerror_string (DomainParser.EOFExpected) = "EOFExpected" -- | This `error_to_paragraph` is designed to go along the `Validation.Label` module. error_to_paragraph_label :: forall w i. ValidationLabel.Error -> HH.HTML w i -error_to_paragraph_label v = Style.error_message (Style.p $ show_error_title_label v) +error_to_paragraph_label v = Web.error_message (Web.p $ show_error_title_label v) (case v of ValidationLabel.ParsingError x -> case x.error of - Nothing -> Style.p "" + Nothing -> Web.p "" Just (ValidationLabel.CannotParse err) -> show_error_domain err - Just (ValidationLabel.CannotEntirelyParse) -> Style.p "Cannot entirely parse the label." + Just (ValidationLabel.CannotEntirelyParse) -> Web.p "Cannot entirely parse the label." Just (ValidationLabel.Size min max n) -> - Style.p $ "Label size should be between " <> show min <> " and " <> show max + Web.p $ "Label size should be between " <> show min <> " and " <> show max <> " (current size: " <> show n <> ")." ) diff --git a/src/App/Page/Administration.purs b/src/App/Page/Administration.purs index fab68d4..a11e8eb 100644 --- a/src/App/Page/Administration.purs +++ b/src/App/Page/Administration.purs @@ -13,7 +13,7 @@ module App.Page.Administration where import Prelude (Unit, bind, discard, not, pure, show, ($), (<<<), (<>), (=<<), map, (/=), (==), unit) import Data.Eq (class Eq) -import Style as Style +import Web as Web import Data.Maybe (Maybe(..)) import Data.Array as A @@ -128,64 +128,64 @@ initialState _ = { addUserForm: { login: "", admin: false, email: "", pass: " render :: forall m. State -> H.ComponentHTML Action () m render { addUserForm, searchUserForm, matching_users, current_tab, orphan_domains } - = Style.section_small + = Web.section_small [ fancy_tab_bar , case current_tab of - Home -> Style.h3 "Select an action" - Search -> Style.columns_ - [ Style.column [C.is 3] [Style.article (Style.p "Search users") render_searchuser_form] - , Style.column_ [ Style.h3 "Result", show_found_users ] + Home -> Web.h3 "Select an action" + Search -> Web.columns_ + [ Web.column [C.is 3] [Web.article (Web.p "Search users") render_searchuser_form] + , Web.column_ [ Web.h3 "Result", show_found_users ] ] - Add -> Style.columns_ - [ Style.column [C.is 5] [Style.article (Style.p "Add a new user") render_adduser_form] ] + Add -> Web.columns_ + [ Web.column [C.is 5] [Web.article (Web.p "Add a new user") render_adduser_form] ] OrphanDomains -> HH.div_ - [ Style.btn_ [C.is_small] "Get orphan domains" ShowOrphanDomains + [ Web.btn_ [C.is_small] "Get orphan domains" ShowOrphanDomains , show_orphan_domains ] ] where fancy_tab_bar = - Style.fancy_tabs - [ Style.tab_entry (is_tab_active Home) "Home" (ChangeTab Home) - , Style.tab_entry (is_tab_active Search) "Search" (ChangeTab Search) - , Style.tab_entry (is_tab_active Add) "Add" (ChangeTab Add) - , Style.tab_entry (is_tab_active OrphanDomains) "OrphanDomains" (ChangeTab OrphanDomains) + Web.fancy_tabs + [ Web.tab_entry (is_tab_active Home) "Home" (ChangeTab Home) + , Web.tab_entry (is_tab_active Search) "Search" (ChangeTab Search) + , Web.tab_entry (is_tab_active Add) "Add" (ChangeTab Add) + , Web.tab_entry (is_tab_active OrphanDomains) "OrphanDomains" (ChangeTab OrphanDomains) ] is_tab_active tab = current_tab == tab - show_found_users = Style.box [ HH.ul_ $ map user_card matching_users ] - user_card user = HH.li_ [ Style.btn_delete (RemoveUser user.uid) - , Style.btn_ [C.is_small] user.login (ShowUser user.uid) + show_found_users = Web.box [ HH.ul_ $ map user_card matching_users ] + user_card user = HH.li_ [ Web.btn_delete (RemoveUser user.uid) + , Web.btn_ [C.is_small] user.login (ShowUser user.uid) ] - show_orphan_domains = Style.box [ HH.ul_ $ map domain_entry orphan_domains ] - domain_entry domain = HH.li_ [ Style.btn_delete (RemoveDomain domain) - , Style.btn_ [C.is_small] domain (ShowDomain domain) + show_orphan_domains = Web.box [ HH.ul_ $ map domain_entry orphan_domains ] + domain_entry domain = HH.li_ [ Web.btn_delete (RemoveDomain domain) + , Web.btn_ [C.is_small] domain (ShowDomain domain) ] up x = HandleAddUserInput <<< x render_adduser_form = HH.form [ HE.onSubmit PreventSubmit ] - [ Style.box_input "login" "User login" "login" (up ADDUSER_INP_login) addUserForm.login - , Style.btn_labeled "adminbtn" "Admin" (show addUserForm.admin) (HandleAddUserInput ADDUSER_toggle_admin) - , Style.box_input "email" "User email" "email" (up ADDUSER_INP_email) addUserForm.email - , Style.box_password "password" "User password" "password" (up ADDUSER_INP_pass) addUserForm.pass - , Style.btn "Send" AddUserAttempt + [ Web.box_input "login" "User login" "login" (up ADDUSER_INP_login) addUserForm.login + , Web.btn_labeled "adminbtn" "Admin" (show addUserForm.admin) (HandleAddUserInput ADDUSER_toggle_admin) + , Web.box_input "email" "User email" "email" (up ADDUSER_INP_email) addUserForm.email + , Web.box_password "password" "User password" "password" (up ADDUSER_INP_pass) addUserForm.pass + , Web.btn "Send" AddUserAttempt ] render_searchuser_form = HH.form [ HE.onSubmit PreventSubmit ] - [ Style.p """ + [ Web.p """ Following input accepts any regex. This is used to search for a user based on their login, full name or email address. """ - , Style.box_input "regex" "Regex" "regex" (up SEARCHUSER_INP_regex) searchUserForm.regex - --, Style.btn_labeled "adminbtn" "Admin" (show searchUserForm.admin) + , Web.box_input "regex" "Regex" "regex" (up SEARCHUSER_INP_regex) searchUserForm.regex + --, Web.btn_labeled "adminbtn" "Admin" (show searchUserForm.admin) -- (HandleAddUserInput SEARCHUSER_toggle_admin) - --, Style.box_input "domain" "Domain owned" "blah.netlib.re." + --, Web.box_input "domain" "Domain owned" "blah.netlib.re." -- (up SEARCHUSER_INP_domain) searchUserForm.domain - , Style.btn "Send" SearchUserAttempt + , Web.btn "Send" SearchUserAttempt ] handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit diff --git a/src/App/Page/Authentication.purs b/src/App/Page/Authentication.purs index 4bfbe79..9f79bc6 100644 --- a/src/App/Page/Authentication.purs +++ b/src/App/Page/Authentication.purs @@ -17,7 +17,7 @@ import Halogen.HTML.Events as HE import Web.Event.Event as Event import Web.Event.Event (Event) -import Style as Style +import Web as Web import Web.HTML (window) as HTML import Web.HTML.Window (sessionStorage) as Window @@ -135,24 +135,24 @@ component = render :: forall m. State -> H.ComponentHTML Action () m render { current_tab, authenticationForm, passwordRecoveryForm, newPasswordForm, errors } = - Style.section_small + Web.section_small [ fancy_tab_bar , if A.length errors > 0 - -- then HH.div_ [ Style.box [ HH.text (A.fold $ map show_error errors) ] ] - then HH.div_ [ Style.box [Style.notification_danger (A.fold $ map show_error errors) CloseErrorStuff]] + -- then HH.div_ [ Web.box [ HH.text (A.fold $ map show_error errors) ] ] + then HH.div_ [ Web.box [Web.notification_danger (A.fold $ map show_error errors) CloseErrorStuff]] else HH.div_ [] , case current_tab of - Auth -> Style.box auth_form - ILostMyPassword -> Style.box passrecovery_form - Recovery -> Style.box newpass_form + Auth -> Web.box auth_form + ILostMyPassword -> Web.box passrecovery_form + Recovery -> Web.box newpass_form ] where fancy_tab_bar = - Style.fancy_tabs - [ Style.tab_entry (is_tab_active Auth) "Authentication" (ChangeTab Auth) - , Style.tab_entry (is_tab_active ILostMyPassword) "I lost my password! 😟" (ChangeTab ILostMyPassword) - , Style.tab_entry (is_tab_active Recovery) "Recover with a token" (ChangeTab Recovery) + Web.fancy_tabs + [ Web.tab_entry (is_tab_active Auth) "Authentication" (ChangeTab Auth) + , Web.tab_entry (is_tab_active ILostMyPassword) "I lost my password! 😟" (ChangeTab ILostMyPassword) + , Web.tab_entry (is_tab_active Recovery) "Recover with a token" (ChangeTab Recovery) ] is_tab_active tab = current_tab == tab @@ -203,45 +203,45 @@ render { current_tab, authenticationForm, passwordRecoveryForm, newPasswordForm, <> show min <> " and " <> show max <> " (currently: " <> show n <> ")" - auth_form = [ Style.h3 "Authentication", render_auth_form ] + auth_form = [ Web.h3 "Authentication", render_auth_form ] passrecovery_form = - [ Style.h3 "You forgot your password (or your login)" - , Style.div_content [] - [ Style.p "Enter either your login or email and you'll receive a recovery token." + [ Web.h3 "You forgot your password (or your login)" + , Web.div_content [] + [ Web.p "Enter either your login or email and you'll receive a recovery token." ] , render_password_recovery_form ] newpass_form = - [ Style.h3 "You got the password recovery mail" - , Style.div_content [] - [ Style.p "Nice! You get to choose your new password." + [ Web.h3 "You got the password recovery mail" + , Web.div_content [] + [ Web.p "Nice! You get to choose your new password." ] , render_new_password_form ] render_auth_form = HH.form [ HE.onSubmit AuthenticationAttempt ] - [ Style.username_input "Username" authenticationForm.login (HandleAuthenticationInput <<< AUTH_INP_login) - , Style.password_input "Password" authenticationForm.pass (HandleAuthenticationInput <<< AUTH_INP_pass) - , Style.btn_validation + [ Web.username_input "Username" authenticationForm.login (HandleAuthenticationInput <<< AUTH_INP_login) + , Web.password_input "Password" authenticationForm.pass (HandleAuthenticationInput <<< AUTH_INP_pass) + , Web.btn_validation ] render_password_recovery_form = HH.form [ HE.onSubmit PasswordRecoveryAttempt ] - [ Style.username_input "Username" passwordRecoveryForm.login (HandlePasswordRecovery <<< PASSR_INP_login) - , Style.email_input "Email" passwordRecoveryForm.email (HandlePasswordRecovery <<< PASSR_INP_email) - , Style.btn_validation + [ Web.username_input "Username" passwordRecoveryForm.login (HandlePasswordRecovery <<< PASSR_INP_login) + , Web.email_input "Email" passwordRecoveryForm.email (HandlePasswordRecovery <<< PASSR_INP_email) + , Web.btn_validation ] render_new_password_form = HH.form [ HE.onSubmit NewPasswordAttempt ] - [ Style.username_input "Username" newPasswordForm.login (HandleNewPassword <<< NEWPASS_INP_login) - , Style.token_input "Token" newPasswordForm.token (HandleNewPassword <<< NEWPASS_INP_token) - , Style.password_input_new "Password" newPasswordForm.password (HandleNewPassword <<< NEWPASS_INP_password) + [ Web.username_input "Username" newPasswordForm.login (HandleNewPassword <<< NEWPASS_INP_login) + , Web.token_input "Token" newPasswordForm.token (HandleNewPassword <<< NEWPASS_INP_token) + , Web.password_input_new "Password" newPasswordForm.password (HandleNewPassword <<< NEWPASS_INP_password) - , Style.password_input_confirmation "Confirmation" newPasswordForm.confirmation + , Web.password_input_confirmation "Confirmation" newPasswordForm.confirmation (HandleNewPassword <<< NEWPASS_INP_confirmation) - , Style.btn_validation + , Web.btn_validation ] diff --git a/src/App/Page/DomainList.purs b/src/App/Page/DomainList.purs index 1668921..bcc4362 100644 --- a/src/App/Page/DomainList.purs +++ b/src/App/Page/DomainList.purs @@ -27,7 +27,7 @@ import Halogen.HTML.Events as HE import Halogen.HTML.Events as HHE import Web.Event.Event as Event import Web.Event.Event (Event) -import Style as Style +import Web as Web import App.Templates.Table (owned_domains, shared_domains) as Table import App.DisplayErrors (error_to_paragraph_label) @@ -179,41 +179,41 @@ initialState _ = render :: forall m. State -> H.ComponentHTML Action () m render { accepted_domains, my_domains, newDomainForm, askDomainTransferForm, deletion_modal } - = Style.section_small + = Web.section_small [ case deletion_modal of - Nothing -> HH.div_ [ Style.columns_ domain_line - , Style.hr - , Style.columns_ new_domain_line - , Style.hr - , Style.columns_ explanations_line + Nothing -> HH.div_ [ Web.columns_ domain_line + , Web.hr + , Web.columns_ new_domain_line + , Web.hr + , Web.columns_ explanations_line ] - Just domain -> Style.modal "Deleting a domain" + Just domain -> Web.modal "Deleting a domain" [warning_message domain] [modal_delete_button domain, modal_cancel_button] ] where - c = Style.column_ + c = Web.column_ domain_line = [ c render_my_domains, c render_my_shared_domains ] new_domain_line = [ c render_new_domain, c render_gain_ownership ] explanations_line = [ c render_share_ownership_explanation, c render_transfer_ownership_explanation ] render_my_domains = - [ Style.h3 "My domains" - , Style.simple_quote "You are the exclusive owner of the following domains." + [ Web.h3 "My domains" + , Web.simple_quote "You are the exclusive owner of the following domains." , Table.owned_domains domains_i_exclusively_own EnterDomain TransferDomain ShareDomain DeleteDomainModal ] render_my_shared_domains = - [ Style.h3 "Shared domains" - , Style.simple_quote """ + [ Web.h3 "Shared domains" + , Web.simple_quote """ The following domains are shared with other users. In case you are the last owner, you can "unshare" it and gain exclusive ownership. """ , Table.shared_domains domains_i_share EnterDomain UnShareDomain DeleteDomainModal ] render_new_domain = - [ Style.h3 "New domain" - , Style.quote [ Style.p "The heart of dnsmanager! 🎉" - , Style.p "You can reserve a domain name, right here." + [ Web.h3 "New domain" + , Web.quote [ Web.p "The heart of dnsmanager! 🎉" + , Web.p "You can reserve a domain name, right here." , HH.text """ Later you will be able to change the content, share, transfer or even delete the domain. """ @@ -222,16 +222,16 @@ render { accepted_domains, my_domains, newDomainForm, askDomainTransferForm, del ] render_gain_ownership = - [ Style.h3 "Get the ownership of a domain" - , Style.simple_quote """ + [ Web.h3 "Get the ownership of a domain" + , Web.simple_quote """ Someone wants to give you (or share with you) the ownership of a domain. Please enter the UUID of the transfer. """ , render_ask_domain_transfer_form ] render_share_ownership_explanation = - [ Style.h3 "Share the ownership of a domain" - , Style.simple_quote """ + [ Web.h3 "Share the ownership of a domain" + , Web.simple_quote """ Ask for a "share token" for your domain and give it to other users. All the owners can make modifications to the domain. Don't let the administration of a domain be the burden of a single person! @@ -239,14 +239,14 @@ render { accepted_domains, my_domains, newDomainForm, askDomainTransferForm, del ] render_transfer_ownership_explanation = - [ Style.h3 "Transfer the ownership of a domain" - , Style.simple_quote """ + [ Web.h3 "Transfer the ownership of a domain" + , Web.simple_quote """ Ask for a transfer token for your domain and give it to the new owner. """ ] - modal_delete_button domain = Style.alert_btn "Delete the domain" (RemoveDomain domain) - modal_cancel_button = Style.cancel_button CancelModal + modal_delete_button domain = Web.alert_btn "Delete the domain" (RemoveDomain domain) + modal_cancel_button = Web.cancel_button CancelModal -- I own all domain without a "share key". domains_i_exclusively_own = A.sort $ A.filter (\domain -> domain.share_key == Nothing) my_domains @@ -259,19 +259,19 @@ render { accepted_domains, my_domains, newDomainForm, askDomainTransferForm, del <> "\". Are you sure you want to do this? This is " , HH.strong_ [ HH.text "irreversible" ] , HH.text "." - , Style.notification_warning' """ + , Web.notification_warning' """ In case this domain is shared, it will just be removed from your domains. """ ] render_add_domain_form = HH.form [ HE.onSubmit NewDomainAttempt ] - [ Style.new_domain_field + [ Web.new_domain_field (HandleNewDomainInput <<< INP_newdomain) newDomainForm.new_domain [ HHE.onSelectedIndexChange domain_choice ] (map (\v -> "." <> v) accepted_domains) - , Style.btn_validation_ "add a new domain" + , Web.btn_validation_ "add a new domain" , if A.length newDomainForm._errors > 0 then HH.div_ $ map error_to_paragraph_label newDomainForm._errors else HH.div_ [ ] @@ -279,10 +279,10 @@ render { accepted_domains, my_domains, newDomainForm, askDomainTransferForm, del render_ask_domain_transfer_form = HH.form [ HE.onSubmit AskDomainTransferAttempt ] - [ Style.box_input "idTransferToken" "Token" "UUID of the domain" + [ Web.box_input "idTransferToken" "Token" "UUID of the domain" AskDomainTransferUUIDInput askDomainTransferForm.uuid - , Style.btn_validation_ "ask for a domain transfer" + , Web.btn_validation_ "ask for a domain transfer" , if A.length askDomainTransferForm._errors > 0 then HH.div_ $ map error_to_paragraph_label askDomainTransferForm._errors else HH.div_ [ ] diff --git a/src/App/Page/Home.purs b/src/App/Page/Home.purs index d4b3056..97c288c 100644 --- a/src/App/Page/Home.purs +++ b/src/App/Page/Home.purs @@ -13,7 +13,7 @@ import Halogen.HTML as HH import Halogen.HTML.Properties as HP import CSSClasses as C -import Style as Style +import Web as Web type Input = Unit type Action = Unit @@ -41,38 +41,38 @@ initialState _ = unit render :: forall m. State -> H.ComponentHTML Action () m render _ = HH.div_ - [ Style.hero_danger + [ Web.hero_danger -- "THIS IS A BETA RELEASE" -- "You can register, login and play a bit with the tool. Feel free to report errors and suggestions." [ HH.text "MESSAGE DE MIGRATION" ] - [ Style.p "En bref : le site a été refait à neuf, en anglais (une traduction arrivera… un jour), et les comptes ont été migrés." - , Style.p "Les utilisateurs peuvent se connecter avec leurs identifiants habituels et leurs domaines sont toujours attribués, mais le contenu n'a pas été préservé (il faut re-configurer les zones)." - , Style.p """ + [ Web.p "En bref : le site a été refait à neuf, en anglais (une traduction arrivera… un jour), et les comptes ont été migrés." + , Web.p "Les utilisateurs peuvent se connecter avec leurs identifiants habituels et leurs domaines sont toujours attribués, mais le contenu n'a pas été préservé (il faut re-configurer les zones)." + , Web.p """ Les comptes migrés sont conservés 6 mois, puis supprimés si aucune connexion n'est faite, afin de purger un certain nombre de vieux comptes de robots. """ , HH.p [ HP.classes [C.margin_top 3] ] - [ Style.outside_link [C.button, C.is_info] url_linuxfr "Article linuxfr.org présentant la nouvelle version de netlibre" ] + [ Web.outside_link [C.button, C.is_info] url_linuxfr "Article linuxfr.org présentant la nouvelle version de netlibre" ] ] - , Style.section_small - [ Style.h1 "Welcome to netlib.re" - , Style.subtitle "Free domain names for the common folks" - , Style.hr + , Web.section_small + [ Web.h1 "Welcome to netlib.re" + , Web.subtitle "Free domain names for the common folks" + , Web.hr , render_description , render_update_why_and_contact - , Style.hr + , Web.hr , render_how_and_code ] ] where url_linuxfr = "https://linuxfr.org/news/netlibre-un-service-libre-et-un-nom-de-domaine-gratuit" - title = Style.h3 - expl content = Style.div_content [] [ Style.explanation content ] - p = Style.p - b x = Style.column_ [ Style.box [ Style.div_content [] x ] ] + title = Web.h3 + expl content = Web.div_content [] [ Web.explanation content ] + p = Web.p + b x = Web.column_ [ Web.box [ Web.div_content [] x ] ] - render_description = Style.columns_ [ render_basics, render_no_expert, render_no_housing ] - render_update_why_and_contact = Style.columns_ [ render_updates, render_why, render_contact ] + render_description = Web.columns_ [ render_basics, render_no_expert, render_no_housing ] + render_update_why_and_contact = Web.columns_ [ render_updates, render_why, render_contact ] render_basics = b [ title "What is provided?" @@ -96,7 +96,7 @@ render _ = HH.div_ render_updates = b [ title "Automatic updates" , p "Update your records with a single, stupidly simple command. For example:" - , expl [ Style.strong "wget https://www.netlib.re/token-update/" ] + , expl [ Web.strong "wget https://www.netlib.re/token-update/" ] , p "Every A and AAAA records have tokens for easy updates." ] @@ -112,7 +112,7 @@ render _ = HH.div_ , p "For legal matter: abuse@netlib.re" ] - render_how_and_code = Style.columns_ [ render_how, render_code ] + render_how_and_code = Web.columns_ [ render_how, render_code ] render_how = b [ title "How does this work?" , p "We pay for the domain names (netlib.re and codelib.re) and let you have a subdomain." @@ -137,8 +137,8 @@ render _ = HH.div_ this user-friendly website, so you can manage your zones. 🥰 """ ] - , Style.hr - , Style.p "But of course, there are a few more technical parts:" + , Web.hr + , Web.p "But of course, there are a few more technical parts:" , HH.ul_ [ link "https://git.baguette.netlib.re/Baguette/libipc" "libIPC" """ @@ -154,4 +154,4 @@ render _ = HH.div_ ] ] link url link_title content - = HH.li_ [ Style.outside_link [] url link_title, HH.text ", ", HH.text content ] + = HH.li_ [ Web.outside_link [] url link_title, HH.text ", ", HH.text content ] diff --git a/src/App/Page/MailValidation.purs b/src/App/Page/MailValidation.purs index cd3402c..485c83e 100644 --- a/src/App/Page/MailValidation.purs +++ b/src/App/Page/MailValidation.purs @@ -16,7 +16,7 @@ import Halogen.HTML.Events as HE import Web.Event.Event as Event import Web.Event.Event (Event) -import Style as Style +import Web as Web import App.Type.LogMessage import App.Message.AuthenticationDaemon as AuthD @@ -82,21 +82,21 @@ initialState _ = render :: forall m. State -> H.ComponentHTML Action () m render { mailValidationForm } - = Style.section_small [ Style.columns_ [ b mail_validation_form ] ] + = Web.section_small [ Web.columns_ [ b mail_validation_form ] ] where - b e = Style.column_ [ Style.box e ] + b e = Web.column_ [ Web.box e ] mail_validation_form - = [ Style.h3 "Verify your account" - , Style.div_content [] [Style.explanation [Style.p "Email addresses must be validated within 30 minutes."]] + = [ Web.h3 "Verify your account" + , Web.div_content [] [Web.explanation [Web.p "Email addresses must be validated within 30 minutes."]] , render_register_form ] render_register_form = HH.form [ HE.onSubmit ValidateInputs ] - [ Style.username_input "Username" mailValidationForm.login (HandleValidationInput <<< VALIDATION_INP_login) - , Style.token_input "Token" mailValidationForm.token (HandleValidationInput <<< VALIDATION_INP_token) - , Style.btn_validation + [ Web.username_input "Username" mailValidationForm.login (HandleValidationInput <<< VALIDATION_INP_login) + , Web.token_input "Token" mailValidationForm.token (HandleValidationInput <<< VALIDATION_INP_token) + , Web.btn_validation ] handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit diff --git a/src/App/Page/Migration.purs b/src/App/Page/Migration.purs index 823f9b5..11569a0 100644 --- a/src/App/Page/Migration.purs +++ b/src/App/Page/Migration.purs @@ -36,7 +36,7 @@ import Web.Event.Event (Event) -- import Data.Generic.Rep (class Generic) -- import Data.Show.Generic (genericShow) -import Style as Style +import Web as Web import Scroll (scrollToTop) import App.Type.LogMessage @@ -113,39 +113,39 @@ component = render :: forall m. State -> H.ComponentHTML Action () m render state - = Style.section_small [Style.columns_ + = Web.section_small [Web.columns_ [ b email_form , b token_form ]] where - b e = Style.column_ [ Style.box e ] + b e = Web.column_ [ Web.box e ] email_form - = [ Style.h3 "New Email address" + = [ Web.h3 "New Email address" -- TODO: put some text here , HH.form [ HE.onSubmit (Verify EmailAddress) ] - [ email_input, email_error, Style.btn_validation ] + [ email_input, email_error, Web.btn_validation ] ] - email_input = Style.email_input "Email" state.email (UserInput EmailAddress) + email_input = Web.email_input "Email" state.email (UserInput EmailAddress) email_error = case between 0 5 (S.length state.email), E.email state.email of true, _ -> HH.text "" - _, Left errors -> Style.error_box "newAddress" "Email error" (show_error $ Email errors) + _, Left errors -> Web.error_box "newAddress" "Email error" (show_error $ Email errors) _, Right _ -> HH.text "" token_form - = [ Style.h3 "Email validation token" + = [ Web.h3 "Email validation token" -- TODO: put some text here , HH.form [ HE.onSubmit (Verify Token) ] - [ token_input {-, token_error -}, Style.btn_validation ] + [ token_input {-, token_error -}, Web.btn_validation ] ] - token_input = Style.token_input "Token" state.token (UserInput Token) + token_input = Web.token_input "Token" state.token (UserInput Token) handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit handleAction = case _ of diff --git a/src/App/Page/Navigation.purs b/src/App/Page/Navigation.purs index 2c68489..e9e7c1c 100644 --- a/src/App/Page/Navigation.purs +++ b/src/App/Page/Navigation.purs @@ -18,7 +18,7 @@ import Halogen.HTML.Properties as HP import Halogen.HTML.Properties.ARIA as ARIA import CSSClasses as C -import Style as Style +import Web as Web import App.Type.Pages (Page(..)) import App.Type.LogMessage (LogMessage) @@ -142,7 +142,7 @@ render { logged, active, admin, login } = HH.a [ HP.classes $ [C.navbar_burger] <> if active then [C.is_active] else [] , ARIA.label "menu" , ARIA.expanded "false" - , Style.data_target "navbar-netlibre" + , Web.data_target "navbar-netlibre" , HE.onClick (\_ -> ToggleMenu) ] [ HH.span [ARIA.hidden "true"] [] , HH.span [ARIA.hidden "true"] [] @@ -192,7 +192,7 @@ render { logged, active, admin, login } = , HE.onClick (\_ -> action) ] [ (HH.text str) ] - dropdown_element classes link str = Style.outside_link ([C.navbar_item] <> classes) link str + dropdown_element classes link str = Web.outside_link ([C.navbar_item] <> classes) link str dropdown_element_primary link str = dropdown_element [C.has_background_info_light] link str dropdown_element_secondary link str = dropdown_element [C.has_background_warning_light] link str diff --git a/src/App/Page/Registration.purs b/src/App/Page/Registration.purs index 16cea78..7ffc6dc 100644 --- a/src/App/Page/Registration.purs +++ b/src/App/Page/Registration.purs @@ -18,7 +18,7 @@ import Web.Event.Event (Event) import App.Text.Explanations as Explanations -import Style as Style +import Web as Web import CSSClasses as C import Data.String as S @@ -96,11 +96,11 @@ component = render :: forall m. State -> H.ComponentHTML Action () m render { registrationForm } - = Style.section_small [Style.columns_ [ b registration_form ]] + = Web.section_small [Web.columns_ [ b registration_form ]] where - b e = Style.column_ [ Style.box e ] - registration_form = [ Style.h3 "Register", render_register_form ] + b e = Web.column_ [ Web.box e ] + registration_form = [ Web.h3 "Register", render_register_form ] render_register_form = HH.form [ HE.onSubmit ValidateInputs ] @@ -109,39 +109,39 @@ render { registrationForm } password_input <> password_error <> legal_mentions <> validation_btn) - username_input = [ Style.username_input "Username" registrationForm.login (HandleRegisterInput <<< REG_INP_login) ] + username_input = [ Web.username_input "Username" registrationForm.login (HandleRegisterInput <<< REG_INP_login) ] username_error = case between 0 1 (S.length registrationForm.login), L.login registrationForm.login of true, _ -> [] - _, Left errors -> [ Style.error_box "loginREGISTER" "Login error" (show_error $ Login errors) ] + _, Left errors -> [ Web.error_box "loginREGISTER" "Login error" (show_error $ Login errors) ] _, Right _ -> [] - email_input = [ Style.email_input "Email" registrationForm.email (HandleRegisterInput <<< REG_INP_email) ] + email_input = [ Web.email_input "Email" registrationForm.email (HandleRegisterInput <<< REG_INP_email) ] email_error = case between 0 5 (S.length registrationForm.email), E.email registrationForm.email of true, _ -> [] - _, Left errors -> [ Style.error_box "emailREGISTER" "Email error" (show_error $ Email errors) ] + _, Left errors -> [ Web.error_box "emailREGISTER" "Email error" (show_error $ Email errors) ] _, Right _ -> [] - password_input = [ Style.password_input "Password" registrationForm.pass (HandleRegisterInput <<< REG_INP_pass) ] + password_input = [ Web.password_input "Password" registrationForm.pass (HandleRegisterInput <<< REG_INP_pass) ] password_error = case between 0 15 (S.length registrationForm.pass), P.password registrationForm.pass of true, _ -> [] - _, Left errors -> [ Style.error_box "passwordREGISTER" "Password error" (show_error $ Password errors) ] + _, Left errors -> [ Web.error_box "passwordREGISTER" "Password error" (show_error $ Password errors) ] _, Right _ -> [] legal_mentions = [ Explanations.legal_notice , HH.div [HP.classes [C.margin_top 3, C.margin_bottom 3]] - [ Style.checkbox + [ Web.checkbox [HH.span [HP.classes [C.margin_left 3]] [HH.text "I have read and accept the terms of service and privacy policy."]] LegalCheckboxToggle ] ] - validation_btn = [ Style.btn_validation ] + validation_btn = [ Web.btn_validation ] handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit handleAction = case _ of diff --git a/src/App/Page/Setup.purs b/src/App/Page/Setup.purs index 413c7ee..319f07f 100644 --- a/src/App/Page/Setup.purs +++ b/src/App/Page/Setup.purs @@ -15,7 +15,7 @@ import Halogen.HTML.Events as HE import Web.Event.Event as Event import Web.Event.Event (Event) -import Style as Style +import Web as Web import CSSClasses as C import App.Type.Email as Email @@ -89,50 +89,50 @@ initialState emails = render :: forall m. State -> H.ComponentHTML Action () m render { modal, newPasswordForm, emails } = - Style.section_small + Web.section_small [ render_emails emails - , Style.hr + , Web.hr , case modal of DeleteAccountModal -> render_delete_account_modal - NoModal -> Style.columns_ - [ b [ Style.btn_ [C.is_large, C.is_info] "Change email address" RouteChangeEmailAddressPage ] - , b [ Style.h3 "Change password", render_new_password_form ] - , b [ Style.h3 "Delete account", render_delete_account ] + NoModal -> Web.columns_ + [ b [ Web.btn_ [C.is_large, C.is_info] "Change email address" RouteChangeEmailAddressPage ] + , b [ Web.h3 "Change password", render_new_password_form ] + , b [ Web.h3 "Delete account", render_delete_account ] ] ] where - b e = Style.column_ e + b e = Web.column_ e render_emails (Tuple current pending) = HH.div [] $ render_current current <> render_pending pending where - render_current (Just (Email.Email e)) = [ Style.p $ "Current email address: " ] <> - [ Style.btn_ro [C.is_small, C.is_warning] e] - render_current Nothing = [ Style.p "You do not currently have a validated email address." ] + render_current (Just (Email.Email e)) = [ Web.p $ "Current email address: " ] <> + [ Web.btn_ro [C.is_small, C.is_warning] e] + render_current Nothing = [ Web.p "You do not currently have a validated email address." ] - render_pending (Just (Email.Email e)) = [ Style.p $ "Pending email address: " ] <> - [ Style.btn_ro [C.is_small, C.is_warning] e] + render_pending (Just (Email.Email e)) = [ Web.p $ "Pending email address: " ] <> + [ Web.btn_ro [C.is_small, C.is_warning] e] render_pending Nothing = [] - render_delete_account = Style.alert_btn "Delete my account" DeleteAccountPopup + render_delete_account = Web.alert_btn "Delete my account" DeleteAccountPopup render_new_password_form = HH.form [ HE.onSubmit ChangePasswordAttempt ] - [ Style.box_password "passwordNEWPASS" "New Password" "password" + [ Web.box_password "passwordNEWPASS" "New Password" "password" (HandleNewPassword <<< NEWPASS_INP_password) newPasswordForm.password - , Style.box_password "confirmationNEWPASS" "Confirmation" "confirmation" + , Web.box_password "confirmationNEWPASS" "Confirmation" "confirmation" (HandleNewPassword <<< NEWPASS_INP_confirmation) newPasswordForm.confirmation - , Style.btn_validation + , Web.btn_validation ] - render_delete_account_modal = Style.modal "Delete your account" - [ Style.p "Your account and domains will be removed." - , Style.strong "⚠ You won't be able to recover your data." + render_delete_account_modal = Web.modal "Delete your account" + [ Web.p "Your account and domains will be removed." + , Web.strong "⚠ You won't be able to recover your data." ] - [ Style.alert_btn "GO AHEAD LOL" DeleteAccount - , Style.cancel_button CancelModal + [ Web.alert_btn "GO AHEAD LOL" DeleteAccount + , Web.cancel_button CancelModal ] handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit diff --git a/src/App/Page/Zone.purs b/src/App/Page/Zone.purs index 84b30c6..ae38811 100644 --- a/src/App/Page/Zone.purs +++ b/src/App/Page/Zone.purs @@ -17,6 +17,7 @@ import Prelude (class Show, Unit, bind, comparing, discard, map, pure, show, uni import Data.Generic.Rep (class Generic) import Data.Show.Generic (genericShow) +import App.Templates.Modal as Modal import Web.HTML (window) as HTML import Web.HTML.Window (sessionStorage) as Window import Web.Storage.Storage as Storage @@ -42,11 +43,14 @@ import Halogen.HTML as HH import Halogen.HTML.Properties as HP import App.Templates.Table as Table -import Style as Style +import Web as Web import CSSClasses as C import App.Text.Explanations as Explanations +import App.Type.RRId +import App.Type.Field as Field +import App.Type.RRModal import App.Type.AcceptedRRTypes (AcceptedRRTypes(..)) import App.Type.ResourceRecord (ResourceRecord , emptyRR, mechanism_types, modifier_types, qualifier_types @@ -56,14 +60,10 @@ import App.Type.ResourceRecord (Mechanism, Modifier, Qualifier(..), SRVProtocol( import App.Type.DKIM as DKIM import App.Type.DMARC as DMARC -import App.DisplayErrors (error_to_paragraph, show_error_email) - import App.Type.LogMessage (LogMessage(..)) import App.Message.DNSManagerDaemon as DNSManager import App.Validation.DNS as Validation -type RRId = Int - -- | `App.Page.Zone` can send messages through websocket interface -- | connected to dnsmanagerd. See `App.WS`. -- | @@ -86,21 +86,6 @@ type Slot = H.Slot Query Output type Input = String -data Field - = Field_Domain String - | Field_TTL String - | Field_Target String - | Field_Priority String - | Field_Weight String - | Field_Port String - | Field_SPF_v String - | Field_SPF_mechanisms (Array RR.Mechanism) - | Field_SPF_modifiers (Array RR.Modifier) - | Field_SPF_q RR.Qualifier - - | Field_CAA_flag String - | Field_CAA_value String - -- | Steps to create a new RR: -- | 1. `CreateNewRRModal AcceptedRRTypes`: create a modal with default values based on selected accepted type. -- | 2. `UpdateCurrentRR Field`: modify the fields of the future new RR. @@ -137,7 +122,7 @@ data Action | ReturnToDomainList -- | Update new entry form (in the `rr_modal` modal). - | UpdateCurrentRR Field + | UpdateCurrentRR Field.Field -- | Validate a new resource record before adding it. | ValidateRR AcceptedRRTypes @@ -159,71 +144,11 @@ data Action -- | Automatically closes the modal. | RemoveRR RRId - -- | Ask a (new) token for a RR. - | NewToken RRId - -- | Ask `dnsmanagerd` for the generated zone file. | AskZoneFile - | CAA_tag Int - - | SRV_Protocol Int - - | SPF_Mechanism_q Int - | SPF_Mechanism_t Int - | SPF_Mechanism_v String - | SPF_Modifier_t Int - | SPF_Modifier_v String - | SPF_Qualifier Int - - -- | Remove a SPF mechanism of the currently modified (SPF) entry (see `_currentRR`). - | SPF_remove_mechanism Int - -- | Remove a SPF modifier of the currently modified (SPF) entry (see `_currentRR`). - | SPF_remove_modifier Int - - -- | Add a SPF mechanism to the currently modified (SPF) entry (see `_currentRR`). - | SPF_Mechanism_Add - -- | Add a SPF modifier to the currently modified (SPF) entry (see `_currentRR`). - | SPF_Modifier_Add - - -- | Change the temporary mail address for DMARC. - | DMARC_mail String - - -- | Change the temporary report size limit for DMARC. - | DMARC_mail_limit String - - -- | Change the requested report interval. - | DMARC_ri String - - -- | Add a new mail address to the DMARC rua list. - | DMARC_rua_Add - - -- | Add a new mail address to the DMARC ruf list. - | DMARC_ruf_Add - - -- | Remove a mail address of the DMARC rua list. - | DMARC_remove_rua Int - - -- | Remove a mail address of the DMARC ruf list. - | DMARC_remove_ruf Int - - | DMARC_policy Int - | DMARC_sp_policy Int - | DMARC_adkim Int - | DMARC_aspf Int - | DMARC_pct String - | DMARC_fo Int - - | DKIM_hash_algo Int - | DKIM_sign_algo Int - | DKIM_pubkey String - | DKIM_note String - -data RRModal - = NoModal - | NewRRModal AcceptedRRTypes - | UpdateRRModal - | RemoveRRModal RRId + -- | Modification of any attribute of the current RR. + | RRUpdate RRUpdateValue data Tab = Zone | TheBasics | TokenExplanation derive instance eqTab :: Eq Tab @@ -231,6 +156,9 @@ derive instance genericTab :: Generic Tab _ instance showTab :: Show Tab where show = genericShow +import App.Type.RRForm + +-- FIXME: this state is a mess. type State = { _domain :: String @@ -242,24 +170,7 @@ type State = --, _local_errors :: Hash.HashMap RRId (Array Validation.Error) -- Unique RR form. - , _currentRR :: ResourceRecord - , _currentRR_errors :: Array Validation.Error - , _dmarc_mail_errors :: Array Email.Error - - -- SPF details. - , spf_mechanism_q :: String - , spf_mechanism_t :: String - , spf_mechanism_v :: String - , spf_modifier_t :: String - , spf_modifier_v :: String - - , dmarc_mail :: String - , dmarc_mail_limit :: Maybe Int - - , dkim :: DKIM.DKIM - , dmarc :: DMARC.DMARC - - , _zonefile :: Maybe String + , _rr_form :: RRForm , current_tab :: Tab } @@ -281,15 +192,6 @@ component = default_domain :: String default_domain = "netlib.re" -default_rr_A :: ResourceRecord -default_rr_A = emptyRR { rrtype = "A", name = "server1", target = "192.0.2.1" } - -default_empty_rr :: ResourceRecord -default_empty_rr = default_rr_A - -default_qualifier_str = "hard_fail" :: String -default_caa = { flag: 0, tag: CAA.Issue, value: "letsencrypt.org" } :: CAA.CAA - initialState :: Input -> State initialState domain = { rr_modal: NoModal @@ -299,24 +201,7 @@ initialState domain = , _resources: [] --, _local_errors: Hash.empty - -- This is the state for the new RR modal. - , _currentRR: default_empty_rr - -- List of errors within the form in new RR modal. - , _currentRR_errors: [] - , _dmarc_mail_errors: [] - , _zonefile: Nothing - - , spf_mechanism_q: "pass" - , spf_mechanism_t: "a" - , spf_mechanism_v: "" - , spf_modifier_t: "redirect" - , spf_modifier_v: "" - - , dkim: DKIM.emptyDKIMRR - , dmarc: DMARC.emptyDMARCRR - - , dmarc_mail: "" - , dmarc_mail_limit: Nothing + , _rr_form: mkEmptyRRForm , current_tab: Zone } @@ -325,7 +210,7 @@ type SortableRecord l = Record (rrtype :: String, rrid :: Int | l) render :: forall m. State -> H.ComponentHTML Action () m render state - = Style.section_small + = Web.section_small [ fancy_tab , case state.current_tab of Zone -> render_zone @@ -334,25 +219,29 @@ render state ] where fancy_tab = - Style.fancy_tabs - [ Style.tab_entry (is_tab_active Zone) "Zone" (ChangeTab Zone) - , Style.tab_entry (is_tab_active TheBasics) "The basics 🧠" (ChangeTab TheBasics) - , Style.tab_entry (is_tab_active TokenExplanation) "Tokens? 🤨" (ChangeTab TokenExplanation) + Web.fancy_tabs + [ Web.tab_entry (is_tab_active Zone) "Zone" (ChangeTab Zone) + , Web.tab_entry (is_tab_active TheBasics) "The basics 🧠" (ChangeTab TheBasics) + , Web.tab_entry (is_tab_active TokenExplanation) "Tokens? 🤨" (ChangeTab TokenExplanation) ] is_tab_active tab = state.current_tab == tab + call_to_current_rr_modal + = Modal.current_rr_modal state._domain state._currentRR state.rr_modal + UpdateCurrentRR NewToken RRUpdate ValidateRR ValidateLocal CancelModal + render_zone = case state.rr_modal of - RemoveRRModal rr_id -> modal_rr_delete rr_id - NewRRModal _ -> render_current_rr_modal - UpdateRRModal -> render_current_rr_modal + RemoveRRModal rr_id -> Modal.modal_rr_delete rr_id RemoveRR CancelModal + NewRRModal _ -> call_to_current_rr_modal + UpdateRRModal -> call_to_current_rr_modal NoModal -> HH.div_ - [ Style.level [ Style.btn_ [C.is_large, C.is_info] "Back to the domain list" ReturnToDomainList - , Style.h1 state._domain + [ Web.level [ Web.btn_ [C.is_large, C.is_info] "Back to the domain list" ReturnToDomainList + , Web.h1 state._domain ] [] - , Style.hr + , Web.hr , Table.resource_records (sorted state._resources) CreateUpdateRRModal DeleteRRModal NewToken - , Style.hr + , Web.hr , render_new_records state , render_zonefile state._zonefile ] @@ -364,319 +253,6 @@ render state # map (A.sortBy (comparing (_.rrid))) -- -> [[x1x2][y][z1z2z3]] # A.concat -- -> [x1 x2 y z1 z2 z3] - modal_rr_delete :: forall w. Int -> HH.HTML w Action - modal_rr_delete rr_id = Style.modal "Deleting a resource record" - [warning_message] [modal_delete_button, Style.cancel_button CancelModal] - where - modal_delete_button = Style.alert_btn "Delete the resource record" (RemoveRR rr_id) - warning_message - = HH.p [] [ HH.text "You are about to delete a resource record, this action is " - , Style.strong "irreversible" - , HH.text "." - ] - - render_current_rr_modal :: forall w. HH.HTML w Action - render_current_rr_modal = - case state._currentRR.rrtype of - "A" -> template (modal_content_simple A) (foot_content A) - "AAAA" -> template (modal_content_simple AAAA) (foot_content AAAA) - "TXT" -> template (modal_content_simple TXT) (foot_content TXT) - "CNAME" -> template (modal_content_simple CNAME) (foot_content CNAME) - "NS" -> template (modal_content_simple NS) (foot_content NS) - "MX" -> template modal_content_mx (foot_content MX) - "CAA" -> template modal_content_caa (foot_content CAA) - "SRV" -> template modal_content_srv (foot_content SRV) - "SPF" -> template modal_content_spf (foot_content SPF) - "DKIM" -> template modal_content_dkim (foot_content DKIM) - "DMARC" -> template modal_content_dmarc (foot_content DMARC) - _ -> Style.p $ "Invalid type: " <> state._currentRR.rrtype - where - side_text_for_name_input name_id = Style.side_text_above_input name_id "Name" (HH.text $ "Empty name = root domain (" <> state._domain <> ".)") - -- DRY - updateForm x = UpdateCurrentRR <<< x - render_errors = if A.length state._currentRR_errors > 0 - then HH.div_ $ [ Style.h3 "Errors: " ] <> map error_to_paragraph state._currentRR_errors - else HH.div_ [ ] - modal_content_simple :: AcceptedRRTypes -> Array (HH.HTML w Action) - modal_content_simple x = - [ render_errors - , render_introduction_text x - , side_text_for_name_input ("domain" <> state._currentRR.rrtype) - , Style.input_with_side_text ("domain" <> state._currentRR.rrtype) "" "www" - (updateForm Field_Domain) - state._currentRR.name - display_domain_side - , Style.box_input ("ttl" <> state._currentRR.rrtype) "TTL" "1800" - (updateForm Field_TTL) - (show state._currentRR.ttl) - , case state._currentRR.rrtype of - "AAAA" -> Style.box_input ("target" <> state._currentRR.rrtype) "Target" "2001:db8::1" (updateForm Field_Target) state._currentRR.target - "TXT" -> Style.box_input ("target" <> state._currentRR.rrtype) "Your text" "blah blah" (updateForm Field_Target) state._currentRR.target - "CNAME" -> Style.box_input ("target" <> state._currentRR.rrtype) "Target" "www" (updateForm Field_Target) state._currentRR.target - "NS" -> Style.box_input ("target" <> state._currentRR.rrtype) "Target" "ns0.example.com." (updateForm Field_Target) state._currentRR.target - _ -> Style.box_input ("target" <> state._currentRR.rrtype) "Target" "198.51.100.5" (updateForm Field_Target) state._currentRR.target - ] <> case state.rr_modal of - UpdateRRModal -> - if A.elem state._currentRR.rrtype ["A", "AAAA"] - then [ Style.field_entry ("token" <> state._currentRR.rrtype) "Token" - (maybe (Style.text "❌​") Style.p state._currentRR.token) - ] - else [] - _ -> [] - - render_introduction_text :: AcceptedRRTypes -> HH.HTML w Action - render_introduction_text = case _ of - A -> Style.div_content [] [Style.explanation Explanations.a_introduction] - AAAA -> Style.div_content [] [Style.explanation Explanations.aaaa_introduction] - TXT -> Style.div_content [] [Style.explanation Explanations.txt_introduction] - CNAME -> Style.div_content [] [Style.explanation Explanations.cname_introduction] - NS -> Style.div_content [] [Style.explanation Explanations.ns_introduction] - _ -> HH.p_ [] - modal_content_mx :: Array (HH.HTML w Action) - modal_content_mx = - [ render_errors - , Style.div_content [] [Style.explanation Explanations.mx_introduction] - , side_text_for_name_input "domainMX" - , Style.input_with_side_text "domainMX" "" "www" - (updateForm Field_Domain) - state._currentRR.name - display_domain_side - , Style.box_input ("ttlMX") "TTL" "1800" - (updateForm Field_TTL) - (show state._currentRR.ttl) - , Style.box_input ("targetMX") "Target" "www" - (updateForm Field_Target) - state._currentRR.target - , Style.box_input ("priorityMX") "Priority" "10" - (updateForm Field_Priority) - (maybe "" show state._currentRR.priority) - ] - modal_content_caa :: Array (HH.HTML w Action) - modal_content_caa = - [ render_errors - , Style.div_content [] [Style.explanation Explanations.caa_introduction] - , side_text_for_name_input "domainCAA" - , Style.input_with_side_text "domainCAA" "" "www" - (updateForm Field_Domain) - state._currentRR.name - display_domain_side - , Style.box_input ("ttlCAA") "TTL" "1800" - (updateForm Field_TTL) - (show state._currentRR.ttl) - , Style.hr - , Style.box_input ("flagCAA") "Flag" "" - (updateForm Field_CAA_flag) - (show (fromMaybe default_caa state._currentRR.caa).flag) - , Style.selection_field'' "tagCAA" "Tag" CAA_tag (A.zip CAA.tags_txt CAA.tags_raw) - CAA.Issue - (Just (fromMaybe default_caa state._currentRR.caa).tag) - , HH.div [HP.classes [C.notification, C.is_warning]] - [ Style.p "⚠️​ CAA entries aren't thoroughly verified, yet. Also, do not put quotes." - ] - , Style.box_input "valueCAA" "Value" "" (updateForm Field_CAA_value) - (fromMaybe default_caa state._currentRR.caa).value - ] - modal_content_srv :: Array (HH.HTML w Action) - modal_content_srv = - [ Style.div_content [] [Style.explanation Explanations.srv_introduction] - , render_errors - , Style.box_input ("ttlSRV") "TTL" "1800" - (updateForm Field_TTL) - (show state._currentRR.ttl) - , Style.box_input "domainSRV" "Service name" "service name" - (updateForm Field_Domain) - state._currentRR.name - , Style.selection_field "protocolSRV" "Protocol" SRV_Protocol RR.srv_protocols_txt - (maybe "udp" (toLower <<< show) state._currentRR.protocol) - , Style.box_input ("targetSRV") "Where the server is" "www" - (updateForm Field_Target) - state._currentRR.target - , Style.box_input ("portSRV") "Port of the service" "5061" - (updateForm Field_Port) - (maybe "" show state._currentRR.port) - , Style.div_content [] [Style.explanation [Style.p "The priority field is a numeric value that indicates the preference of the server (lower values indicate higher priority)."]] - , Style.box_input ("prioritySRV") "Priority" "10" - (updateForm Field_Priority) - (maybe "" show state._currentRR.priority) - -- Style.div_content [] [Style.explanation Explanations.spf_introduction], Style.p "Modifiers provide additional instructions, such as explanations for SPF failures or redirecting SPF checks to another domain." - , Style.div_content [] [Style.explanation [Style.p "The weight field is a relative weight used when multiple servers have the same priority, determining how often they should be used."]] - , Style.box_input ("weightSRV") "Weight" "100" - (updateForm Field_Weight) - (maybe "" show state._currentRR.weight) - ] - modal_content_spf :: Array (HH.HTML w Action) - modal_content_spf = - [ Style.div_content [] [Style.explanation Explanations.spf_introduction] - , render_errors - , side_text_for_name_input "domainSPF" - , Style.input_with_side_text "domainSPF" "" "Let this alone." - (updateForm Field_Domain) - state._currentRR.name - display_domain_side - , Style.box_input "ttlSPF" "TTL" "1800" - (updateForm Field_TTL) - (show state._currentRR.ttl) - --, case state._currentRR.v of - -- Nothing -> Style.p "default value for the version (spf1)" - -- Just v -> Style.box_input "vSPF" "Version" "spf1" (updateForm Field_SPF_v) v - , Style.hr - , Style.box_with_tag [C.has_background_info_light] tag_mechanisms - [ Style.div_content [] [Style.explanation [Style.p "Mechanisms specify which mail servers are allowed to send mail for the domain and how to evaluate the sending mail server’s IP address."] ] - , maybe (Style.p "You don't have any mechanism.") (Table.display_mechanisms SPF_remove_mechanism) state._currentRR.mechanisms - , Style.hr - , Style.h4 "New mechanism" - , Style.selection_field "idMechanismQ" "Policy" SPF_Mechanism_q qualifier_types state.spf_mechanism_q - , Style.selection_field "idMechanismT" "Type" SPF_Mechanism_t mechanism_types state.spf_mechanism_t - , Style.box_input "valueNewMechanismSPF" "Value" "" - SPF_Mechanism_v - state.spf_mechanism_v - , Style.btn "Add a mechanism" SPF_Mechanism_Add - ] - , Style.hr - , Style.box_with_tag [C.has_background_success_light] tag_modifiers - [ Style.div_content [] [Style.explanation [Style.p "Modifiers provide additional instructions, such as explanations for SPF failures or redirecting SPF checks to another domain."] ] - , maybe (Style.p "You don't have any modifier.") (Table.display_modifiers SPF_remove_modifier) state._currentRR.modifiers - , Style.hr - , Style.h4 "New modifier" - , Style.selection_field "idModifierT" "Modifier" SPF_Modifier_t modifier_types state.spf_modifier_t - , Style.box_input "valueNewModifierSPF" "Value" "" - SPF_Modifier_v - state.spf_modifier_v - , Style.btn "Add a modifier" SPF_Modifier_Add - ] - , Style.hr - , Style.box - [ Style.h3 "Default behavior" - , Style.div_content [] [Style.explanation Explanations.spf_default_behavior] - , Style.selection SPF_Qualifier qualifier_types (maybe default_qualifier_str show_qualifier state._currentRR.q) - ] - ] - - tag_mechanisms = Style.tags [Style.tag "Mechanisms"] - tag_modifiers = Style.tags [Style.tag "Modifiers"] - - tag_aggregated_reports = Style.tags [Style.tag "Addresses to contact for aggregated reports"] - tag_detailed_reports = Style.tags [Style.tag "Addresses to contact for detailed reports"] - - modal_content_dkim :: Array (HH.HTML w Action) - modal_content_dkim = - [ Style.div_content [] [Style.explanation Explanations.dkim_introduction] - , render_errors - , side_text_for_name_input "domainDKIM" - , Style.input_with_side_text "domainDKIM" "" "default._domainkey" - (updateForm Field_Domain) - state._currentRR.name - display_domain_side - , Style.box_input "ttlDKIM" "TTL" "1800" - (updateForm Field_TTL) - (show state._currentRR.ttl) - , Style.hr - , Style.div_content [] [Style.explanation Explanations.dkim_default_algorithms] - , Style.selection_field "idDKIMSignature" "Signature algo" - DKIM_sign_algo - (map show DKIM.sign_algos) - (show $ fromMaybe DKIM.RSA state.dkim.k) - , Style.selection_field "idDKIMHash" "Hash algo" - DKIM_hash_algo - (map show DKIM.hash_algos) - (show $ fromMaybe DKIM.SHA256 state.dkim.h) - , Style.box_input "pkDKIM" "Public Key" "Your public key, such as \"MIIBIjANBgqh...\"" DKIM_pubkey state.dkim.p - , Style.box_input "noteDKIM" "Note" "Note for fellow administrators." DKIM_note (fromMaybe "" state.dkim.n) - ] - - modal_content_dmarc :: Array (HH.HTML w Action) - modal_content_dmarc = - [ Style.div_content [] [Style.explanation Explanations.dmarc_introduction] - , render_errors - , side_text_for_name_input "domainDMARC" - , Style.input_with_side_text "domainDMARC" "" "_dmarc" - (updateForm Field_Domain) - state._currentRR.name - display_domain_side - , Style.box_input "ttlDMARC" "TTL" "1800" (updateForm Field_TTL) (show state._currentRR.ttl) - - , Style.hr - , Style.div_content [] [Style.explanation Explanations.dmarc_policy] - , Style.selection_field' "idDMARCPolicy" "Policy" DMARC_policy - (A.zip DMARC.policies_txt DMARC.policies_raw) - (show state.dmarc.p) - , Style.div_content [] [Style.explanation Explanations.dmarc_sp_policy] - , Style.selection_field' "idDMARCPolicy_sp" "Policy for subdomains" DMARC_sp_policy - (zip_nullable DMARC.policies_txt_with_null DMARC.policies_raw) - (maybe "-" show state.dmarc.sp) - - , Style.hr - , Style.div_content [] [Style.explanation Explanations.dmarc_adkim] - , Style.selection_field' "idDMARCadkim" "Consistency Policy for DKIM" DMARC_adkim - (zip_nullable DMARC.consistency_policies_txt DMARC.consistency_policies_raw) - (maybe "-" show state.dmarc.adkim) - , Style.div_content [] [Style.explanation Explanations.dmarc_aspf] - , Style.selection_field' "idDMARCaspf" "Consistency Policy for SPF" DMARC_aspf - (zip_nullable DMARC.consistency_policies_txt DMARC.consistency_policies_raw) - (maybe "-" show state.dmarc.aspf) - - , Style.hr - , Style.div_content [] [Style.explanation Explanations.dmarc_pct] - , Style.box_input "idDMARCpct" "Sample rate (between 0 and 100)" "100" DMARC_pct (maybe "100" show state.dmarc.pct) - - , Style.hr - , Style.selection_field' "idDMARCfo" "When to send a report" DMARC_fo - (zip_nullable DMARC.report_occasions_txt DMARC.report_occasions_raw) - (maybe "-" show state.dmarc.fo) - - , Style.hr - , Style.div_content [] [Style.explanation Explanations.dmarc_contact] - , Style.box_with_tag [C.has_background_info_light] tag_aggregated_reports - [ maybe (Style.p "There is no address to send aggregated reports to.") - (Table.display_dmarc_mail_addresses DMARC_remove_rua) - state.dmarc.rua - ] - , Style.box_with_tag [C.has_background_success_light] tag_detailed_reports - [ maybe (Style.p "There is no address to send detailed reports to.") - (Table.display_dmarc_mail_addresses DMARC_remove_ruf) - state.dmarc.ruf - ] - - , Style.hr - , render_dmarc_mail_errors - , Style.box_input "idDMARCmail" "Address to contact" "admin@example.com" DMARC_mail state.dmarc_mail - , Style.box_input "idDMARCmaillimit" "Report size limit (in KB)" "2000" DMARC_mail_limit (maybe "0" show state.dmarc_mail_limit) - , Style.level [ Style.btn_ [C.has_background_info_light] "New address for aggregated report" DMARC_rua_Add - , Style.btn_ [C.has_background_success_light] "New address for specific report" DMARC_ruf_Add - ] [] - - , Style.hr - , Style.div_content [] [Style.explanation Explanations.dmarc_ri] - , Style.box_input "idDMARCri" "Report interval (in seconds)" "86400" DMARC_ri (maybe "0" show state.dmarc.ri) - ] - - render_dmarc_mail_errors - = if A.length state._dmarc_mail_errors > 0 - then Style.notification_danger_block' - $ [ Style.h3 "Invalid mail 😥" ] <> map (Style.p <<< show_error_email) state._dmarc_mail_errors - else HH.div_ [ ] - - display_domain_side = (if state._currentRR.name == (state._domain <> ".") then "" else "." <> state._domain) - newtokenbtn = Style.btn (maybe "🏁​ Ask for a token" (\_ -> "🏁​ Ask for a new token") state._currentRR.token) (NewToken state._currentRR.rrid) - foot_content x = - case state.rr_modal of - NewRRModal _ -> [Style.btn_add (ValidateRR x)] - UpdateRRModal -> [Style.btn_save ValidateLocal] <> case x of - A -> [newtokenbtn] - AAAA -> [newtokenbtn] - _ -> [] - _ -> [Style.p "state.rr_modal should either be NewRRModal or UpdateRRModal."] - template content foot_ = Style.modal title content foot - where - title = case state.rr_modal of - NoModal -> "Error: no modal should be displayed" - NewRRModal t_ -> "New " <> show t_ <> " resource record" - UpdateRRModal -> "Update " <> state._currentRR.rrtype <> " Resource Record" - RemoveRRModal rr_id -> "Error: should display removal modal instead (for resource record " <> show rr_id <> ")" - foot = foot_ <> [Style.cancel_button CancelModal] - -zip_nullable :: forall a. Array a -> Array String -> Array (Tuple a String) -zip_nullable txt raw = A.zip txt ([""] <> raw) handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit handleAction = case _ of @@ -684,7 +260,7 @@ handleAction = case _ of -- | Works for both "new RR", "update RR" and "remove RR" modals. CancelModal -> do H.modify_ _ { rr_modal = NoModal } - H.modify_ _ { _currentRR_errors = [] } + H.modify_ _ { _errors = [] } H.modify_ _ { _dmarc_mail_errors = [] } handleAction $ ResetTemporaryValues @@ -709,7 +285,7 @@ handleAction = case _ of case first (\rr -> rr.rrid == rr_id) state._resources of Nothing -> H.raise $ Log $ ErrorLog $ "Resource Record " <> show rr_id <> " not found" Just rr -> do - H.modify_ _ { _currentRR = rr } + H.modify_ _ { _rr_form { _rr = rr } } _ <- case rr.rrtype of "DKIM" -> H.modify_ _ { dkim = fromMaybe DKIM.emptyDKIMRR rr.dkim } "DMARC" -> H.modify_ _ { dmarc = fromMaybe DMARC.emptyDMARCRR rr.dmarc } @@ -719,36 +295,7 @@ handleAction = case _ of -- | Each time a "new RR" button is clicked, the form resets. CreateNewRRModal t -> do state <- H.get - H.modify_ _ { rr_modal = NewRRModal t } - let default_rr_AAAA = emptyRR { rrtype = "AAAA", name = "server1", target = "2001:db8::1" } - default_rr_TXT = emptyRR { rrtype = "TXT", name = "txt", target = "some text" } - default_rr_CNAME = emptyRR { rrtype = "CNAME", name = "www", target = "server1" } - default_rr_NS = emptyRR { rrtype = "NS", name = (state._domain <> "."), target = "ns0.example.com." } - default_rr_MX = emptyRR { rrtype = "MX", name = "mail", target = "server1", priority = Just 10 } - default_rr_CAA = emptyRR { rrtype = "CAA", name = "", caa = Just default_caa } - default_rr_SRV = emptyRR { rrtype = "SRV", name = "voip", target = "server1" - , port = Just 5061, weight = Just 100, priority = Just 10 - , protocol = Just RR.TCP } - default_mechanisms = maybe [] (\x -> [x]) $ to_mechanism "pass" "mx" "" - default_rr_SPF = emptyRR { rrtype = "SPF", name = "", target = "" - , mechanisms = Just default_mechanisms - , q = Just RR.HardFail - } - default_rr_DKIM = emptyRR { rrtype = "DKIM", name = "default._domainkey", target = "" } - default_rr_DMARC = emptyRR { rrtype = "DMARC", name = "_dmarc", target = "" } - - case t of - A -> H.modify_ _ { _currentRR = default_rr_A } - AAAA -> H.modify_ _ { _currentRR = default_rr_AAAA } - TXT -> H.modify_ _ { _currentRR = default_rr_TXT } - CNAME -> H.modify_ _ { _currentRR = default_rr_CNAME } - NS -> H.modify_ _ { _currentRR = default_rr_NS } - MX -> H.modify_ _ { _currentRR = default_rr_MX } - CAA -> H.modify_ _ { _currentRR = default_rr_CAA } - SRV -> H.modify_ _ { _currentRR = default_rr_SRV } - SPF -> H.modify_ _ { _currentRR = default_rr_SPF } - DKIM -> H.modify_ _ { _currentRR = default_rr_DKIM } - DMARC -> H.modify_ _ { _currentRR = default_rr_DMARC } + H.modify_ _ { rr_modal = NewRRModal t, _rr_form { _rr = default_rr t state._domain } } -- | Initialize the Zone component: ask for the domain zone to `dnsmanagerd`. Initialize -> do @@ -771,23 +318,23 @@ handleAction = case _ of -- | Else, the different errors are added to the state. ValidateRR t -> do -- In case the `name` part of the resource record is empty, consider the name to be the domain itself. - H.modify_ \s -> s { _currentRR = replace_name s._domain s._currentRR } + H.modify_ \s -> s { _rr_form { _rr = replace_name s._domain s._rr_form._rr } } - -- TODO: should the code design change? Would the code be simplified by working only on _currentRR.dkim? - -- Since _currentRR.dkim isn't modified directly, it is copied from `State`. + -- TODO: should the code design change? Would the code be simplified by working only on _rr_form._rr.dkim? + -- Since _rr_form._rr.dkim isn't modified directly, it is copied from `State`. _ <- case t of - DKIM -> H.modify_ \state -> state { _currentRR { dkim = Just state.dkim } } - DMARC -> H.modify_ \state -> state { _currentRR { dmarc = Just state.dmarc } } + DKIM -> H.modify_ \state -> state { _rr_form { _rr { dkim = Just state.dkim } } } + DMARC -> H.modify_ \state -> state { _rr_form { _rr { dmarc = Just state.dmarc } } } _ -> pure unit state <- H.get - case Validation.validation state._currentRR of + case Validation.validation state._rr_form._rr of Left actual_errors -> do -- H.raise $ Log $ ErrorLog $ "Cannot add this " <> show t <> " resource record, some errors occured in the record:" -- loopE (\v -> H.raise $ Log $ ErrorLog $ "==> " <> show_error v) actual_errors - H.modify_ _ { _currentRR_errors = actual_errors } + H.modify_ _ { _errors = actual_errors } Right newrr -> do - H.modify_ _ { _currentRR_errors = [] + H.modify_ _ { _errors = [] , _dmarc_mail_errors = [] , dkim = DKIM.emptyDKIMRR , dmarc = DMARC.emptyDMARCRR @@ -809,35 +356,35 @@ handleAction = case _ of -- | Update the currently displayed RR form (new or update RR). UpdateCurrentRR field -> do state <- H.get - let newRR = update_field state._currentRR field - H.modify_ _ { _currentRR = newRR } + let newRR = update_field state._rr_form._rr field + H.modify_ _ { _rr_form { _rr = newRR } } -- | Validate any local RR with the new `_resources` and `_local_errors`. ValidateLocal -> do -- In case the `name` part of the resource record is empty, consider the name to be the domain itself. - H.modify_ \s -> s { _currentRR = replace_name s._domain s._currentRR } + H.modify_ \s -> s { _rr_form { _rr = replace_name s._domain s._rr_form._rr } } - -- Since _currentRR.dkim isn't modified directly, it is copied from `State`. + -- Since _rr_form._rr.dkim isn't modified directly, it is copied from `State`. state0 <- H.get - _ <- case state0._currentRR.rrtype of - "DKIM" -> H.modify_ _ { _currentRR { dkim = Just state0.dkim } } - "DMARC" -> H.modify_ _ { _currentRR { dmarc = Just state0.dmarc } } + _ <- case state0._rr_form._rr.rrtype of + "DKIM" -> H.modify_ _ { _rr_form { _rr { dkim = Just state0.dkim } } } + "DMARC" -> H.modify_ _ { _rr_form { _rr { dmarc = Just state0.dmarc } } } _ -> pure unit state <- H.get - case Validation.validation state._currentRR of + case Validation.validation state._rr_form._rr of Left actual_errors -> do - H.modify_ _ { _currentRR_errors = actual_errors } + H.modify_ _ { _errors = actual_errors } Right rr -> do - H.modify_ _ { _currentRR_errors = [], _dmarc_mail_errors = [] } + H.modify_ _ { _errors = [], _dmarc_mail_errors = [] } handleAction $ SaveRR rr ResetTemporaryValues -> do - H.modify_ _ { spf_mechanism_q = "pass" - , spf_mechanism_t = "a" - , spf_mechanism_v = "" - , spf_modifier_t = "redirect" - , spf_modifier_v = "" + H.modify_ _ { _rr_form { tmp { spf { mechanism_q = "pass" } } } + , _rr_form { tmp { spf { mechanism_t = "a" } } } + , _rr_form { tmp { spf { mechanism_v = "" } } } + , _rr_form { tmp { spf { modifier_t = "redirect" } } } + , _rr_form { tmp { spf { modifier_v = "" } } } , dmarc_mail = "" , dmarc_mail_limit = Nothing , _dmarc_mail_errors = [] @@ -890,53 +437,51 @@ handleAction = case _ of CAA.ContactEmail -> "contact@example.com" CAA.ContactPhone -> "0203040506" _ -> "" - new_caa = (fromMaybe default_caa state._currentRR.caa) { tag = new_tag, value = new_value } - H.modify_ _ { _currentRR { caa = Just new_caa } } + new_caa = (fromMaybe default_caa state._rr_form._rr.caa) { tag = new_tag, value = new_value } + H.modify_ _ { _rr_form { _rr { caa = Just new_caa } } } - SRV_Protocol v -> H.modify_ _ { _currentRR { protocol = RR.srv_protocols A.!! v } } + SRV_Protocol v -> H.modify_ _ { _rr_form { _rr { protocol = RR.srv_protocols A.!! v } } } - SPF_Mechanism_q v -> H.modify_ _ { spf_mechanism_q = maybe "pass" id $ qualifier_types A.!! v } - SPF_Mechanism_t v -> H.modify_ _ { spf_mechanism_t = maybe "a" id $ mechanism_types A.!! v } - SPF_Mechanism_v v -> H.modify_ _ { spf_mechanism_v = v } - SPF_Modifier_t v -> H.modify_ _ { spf_modifier_t = maybe "redirect" id $ modifier_types A.!! v } - SPF_Modifier_v v -> H.modify_ _ { spf_modifier_v = v } - SPF_Qualifier v -> H.modify_ _ { _currentRR { q = qualifiers A.!! v } } + SPF_Mechanism_q v -> H.modify_ _ { _rr_form { tmp { spf { mechanism_q = maybe "pass" id $ qualifier_types A.!! v }}} + SPF_Mechanism_t v -> H.modify_ _ { _rr_form { tmp { spf { mechanism_t = maybe "a" id $ mechanism_types A.!! v }}} + SPF_Mechanism_v v -> H.modify_ _ { _rr_form { tmp { spf { mechanism_v = v }}} + SPF_Modifier_t v -> H.modify_ _ { _rr_form { tmp { spf { modifier_t = maybe "redirect" id $ modifier_types A.!! v }}} + SPF_Modifier_v v -> H.modify_ _ { _rr_form { tmp { spf { modifier_v = v }}} + SPF_Qualifier v -> H.modify_ _ { _rr_form { _rr { q = qualifiers A.!! v } } SPF_remove_mechanism i -> - H.modify_ \s -> s { _currentRR { mechanisms = case s._currentRR.mechanisms of - Just ms -> Just (remove_id i $ attach_id 0 ms) - Nothing -> Nothing - } - } + H.modify_ \s -> s { _rr_form { _rr { mechanisms = case s._rr_form._rr.mechanisms of + Just ms -> Just (remove_id i $ attach_id 0 ms) + Nothing -> Nothing + } } } SPF_remove_modifier i -> - H.modify_ \s -> s { _currentRR { modifiers = case s._currentRR.modifiers of - Just ms -> Just (remove_id i $ attach_id 0 ms) - Nothing -> Nothing - } - } + H.modify_ \s -> s { _rr_form { _rr { modifiers = case s._rr_form._rr.modifiers of + Just ms -> Just (remove_id i $ attach_id 0 ms) + Nothing -> Nothing + } } } SPF_Mechanism_Add -> do state <- H.get - let m = state._currentRR.mechanisms - m_q = state.spf_mechanism_q - m_t = state.spf_mechanism_t - m_v = state.spf_mechanism_v + let m = state._rr_form._rr.mechanisms + m_q = state._rr_form.tmp.spf.mechanism_q + m_t = state._rr_form.tmp.spf.mechanism_t + m_v = state._rr_form.tmp.spf.mechanism_v new_list_of_mechanisms = maybe [] id m <> maybe [] (\x -> [x]) (to_mechanism m_q m_t m_v) new_value = case new_list_of_mechanisms of [] -> Nothing v -> Just v - H.modify_ _ { _currentRR { mechanisms = new_value }} + H.modify_ _ { _rr_form { _rr { mechanisms = new_value }}} handleAction $ ResetTemporaryValues SPF_Modifier_Add -> do state <- H.get - let m = state._currentRR.modifiers - m_t = state.spf_modifier_t - m_v = state.spf_modifier_v + let m = state._rr_form._rr.modifiers + m_t = state._rr_form.tmp.spf.modifier_t + m_v = state._rr_form.tmp.spf.modifier_v new_list_of_modifiers = maybe [] id m <> maybe [] (\x -> [x]) (to_modifier m_t m_v) new_value = case new_list_of_modifiers of [] -> Nothing v -> Just v - H.modify_ _ { _currentRR { modifiers = new_value }} + H.modify_ _ { _rr_form._rr { modifiers = new_value }} handleAction $ ResetTemporaryValues DMARC_mail v -> H.modify_ _ { dmarc_mail = v } @@ -1046,41 +591,37 @@ handleQuery = case _ of add_RR :: State -> ResourceRecord -> State add_RR state new_rr = state { _resources = (state._resources <> [ new_rr ]) } --- Rendering - --- Component definition and initial state - render_new_records :: forall (w :: Type). State -> HH.HTML w Action render_new_records _ - = Style.hdiv - [ Style.h1 "Adding new records" + = Web.hdiv + [ Web.h1 "Adding new records" -- use "level" to get horizontal buttons next to each other (probably vertical on mobile) - , Style.level [ - Style.btn "A" (CreateNewRRModal A) - , Style.btn "AAAA" (CreateNewRRModal AAAA) - , Style.btn "TXT" (CreateNewRRModal TXT) - , Style.btn "CNAME" (CreateNewRRModal CNAME) - , Style.btn "NS" (CreateNewRRModal NS) - , Style.btn "MX" (CreateNewRRModal MX) - , Style.btn "SRV" (CreateNewRRModal SRV) + , Web.level [ + Web.btn "A" (CreateNewRRModal A) + , Web.btn "AAAA" (CreateNewRRModal AAAA) + , Web.btn "TXT" (CreateNewRRModal TXT) + , Web.btn "CNAME" (CreateNewRRModal CNAME) + , Web.btn "NS" (CreateNewRRModal NS) + , Web.btn "MX" (CreateNewRRModal MX) + , Web.btn "SRV" (CreateNewRRModal SRV) ] [] - , Style.hr - , Style.h1 "Special records about certifications and the mail system" + , Web.hr + , Web.h1 "Special records about certifications and the mail system" -- use "level" to get horizontal buttons next to each other (probably vertical on mobile) - , Style.level [ - Style.btn "CAA" (CreateNewRRModal CAA) - , Style.btn "SPF" (CreateNewRRModal SPF) - , Style.btn "DKIM" (CreateNewRRModal DKIM) - , Style.btn "DMARC" (CreateNewRRModal DMARC) + , Web.level [ + Web.btn "CAA" (CreateNewRRModal CAA) + , Web.btn "SPF" (CreateNewRRModal SPF) + , Web.btn "DKIM" (CreateNewRRModal DKIM) + , Web.btn "DMARC" (CreateNewRRModal DMARC) ] [] - , Style.hr - , Style.level [ - Style.btn "Get the final zone file" AskZoneFile + , Web.hr + , Web.level [ + Web.btn "Get the final zone file" AskZoneFile ] [HH.text "For debug purposes. ⚠"] ] render_zonefile :: forall (w :: Type). Maybe String -> HH.HTML w Action -render_zonefile zonefile = Style.box [ maybe (HH.text "") (\x -> HH.pre_ [HH.text x]) zonefile ] +render_zonefile zonefile = Web.box [ maybe (HH.text "") (\x -> HH.pre_ [HH.text x]) zonefile ] -- ACTIONS @@ -1098,23 +639,23 @@ loopE f a = case (A.head a) of Nothing -> pure unit Just xs -> loopE f xs -update_field :: ResourceRecord -> Field -> ResourceRecord +update_field :: ResourceRecord -> Field.Field -> ResourceRecord update_field rr updated_field = case updated_field of - Field_Domain val -> rr { name = toLower val } - Field_Target val -> rr { target = val } - Field_TTL val -> rr { ttl = fromMaybe 0 (fromString val) } - Field_Priority val -> rr { priority = fromString val } - Field_Weight val -> rr { weight = fromString val } - Field_Port val -> rr { port = fromString val } - Field_SPF_v val -> rr { v = Just val } - Field_SPF_mechanisms val -> rr { mechanisms = Just val } - Field_SPF_modifiers val -> rr { modifiers = Just val } - Field_SPF_q val -> rr { q = Just val } + Field.Domain val -> rr { name = toLower val } + Field.Target val -> rr { target = val } + Field.TTL val -> rr { ttl = fromMaybe 0 (fromString val) } + Field.Priority val -> rr { priority = fromString val } + Field.Weight val -> rr { weight = fromString val } + Field.Port val -> rr { port = fromString val } + Field.SPF_v val -> rr { v = Just val } + Field.SPF_mechanisms val -> rr { mechanisms = Just val } + Field.SPF_modifiers val -> rr { modifiers = Just val } + Field.SPF_q val -> rr { q = Just val } - Field_CAA_flag val -> + Field.CAA_flag val -> let new_caa = (fromMaybe default_caa rr.caa) { flag = fromMaybe 0 $ fromString val } in rr { caa = Just new_caa } - Field_CAA_value val -> + Field.CAA_value val -> let new_caa = (fromMaybe default_caa rr.caa) { value = val } in rr { caa = Just new_caa } diff --git a/src/App/Templates/Modal.purs b/src/App/Templates/Modal.purs new file mode 100644 index 0000000..b3f09b7 --- /dev/null +++ b/src/App/Templates/Modal.purs @@ -0,0 +1,375 @@ +-- | `App.Templates.Modal` gathers all the website's modals, providing +-- | an easy way to duplicate modals in different pages and to display +-- | content in a consistent manner. +module App.Templates.Modal where + +import Prelude (map, show, ($), (<<<), (<>), (==), (>)) + +import CSSClasses as C + +import Data.Array as A +import Data.Maybe (Maybe(..), fromMaybe, maybe) + +import Data.Tuple (Tuple) + +import App.Type.CAA as CAA +import App.Text.Explanations as Explanations +import Web as Web +import Halogen.HTML as HH +import Halogen.HTML.Properties as HP +import App.Type.RRId (RRId) +import App.Type.DMARC as DMARC +import App.Type.DKIM as DKIM +import App.Type.Field as Field +import App.Templates.Table as Table +import Data.String (toLower) + +-- FIXME: this import is related to messy types. A replacement should be found. +import App.Type.RRForm + +import App.Type.RRModal (RRModal(..)) +import App.Type.AcceptedRRTypes (AcceptedRRTypes(..)) + +import App.Type.ResourceRecord (mechanism_types, modifier_types, qualifier_types, show_qualifier) +import App.Type.ResourceRecord as RR + +import App.DisplayErrors (error_to_paragraph, show_error_email) + +type ActionCancelModal = forall i. i +modal_rr_delete :: forall w i. Int -> (Int -> i) -> ActionCancelModal -> HH.HTML w i +modal_rr_delete rr_id action_remove_rr action_cancel_modal = Web.modal "Deleting a resource record" + [warning_message] [modal_delete_button, Web.cancel_button action_cancel_modal] + where + modal_delete_button = Web.alert_btn "Delete the resource record" (action_remove_rr rr_id) + warning_message + = HH.p [] [ HH.text "You are about to delete a resource record, this action is " + , Web.strong "irreversible" + , HH.text "." + ] + +zip_nullable :: forall a. Array a -> Array String -> Array (Tuple a String) +zip_nullable txt raw = A.zip txt ([""] <> raw) + +type Domain = String +type ActionUpdateForm = forall i. (Field.Field -> i) +type ActionNewToken = forall i. (RRId -> i) +type ActionUpdateRR = forall i. (RRUpdateValue -> i) +type ActionValidateNewRR = forall i. (AcceptedRRTypes -> i) +type ActionValidateLocalRR = forall i. i +current_rr_modal :: forall w i. + Domain -> RRForm -> RRModal + -> ActionUpdateForm -> ActionNewToken + -> ActionUpdateRR -> ActionValidateNewRR -> ActionValidateLocalRR -> ActionCancelModal + -> HH.HTML w i +current_rr_modal selected_domain form rr_modal + action_update_form action_new_token + action_update_rr action_validate_rr action_validate_local_rr action_cancel_modal = + case form._rr.rrtype of + "A" -> template (modal_content_simple A) (foot_content A) + "AAAA" -> template (modal_content_simple AAAA) (foot_content AAAA) + "TXT" -> template (modal_content_simple TXT) (foot_content TXT) + "CNAME" -> template (modal_content_simple CNAME) (foot_content CNAME) + "NS" -> template (modal_content_simple NS) (foot_content NS) + "MX" -> template modal_content_mx (foot_content MX) + "CAA" -> template modal_content_caa (foot_content CAA) + "SRV" -> template modal_content_srv (foot_content SRV) + "SPF" -> template modal_content_spf (foot_content SPF) + "DKIM" -> template modal_content_dkim (foot_content DKIM) + "DMARC" -> template modal_content_dmarc (foot_content DMARC) + _ -> Web.p $ "Invalid type: " <> form._rr.rrtype + where + side_text_for_name_input name_id + = Web.side_text_above_input name_id "Name" (HH.text $ "Empty name = root domain (" <> selected_domain <> ".)") + + -- DRY + render_errors = if A.length form._errors > 0 + then HH.div_ $ [ Web.h3 "Errors: " ] <> map error_to_paragraph form._errors + else HH.div_ [ ] + + modal_content_simple :: AcceptedRRTypes -> Array (HH.HTML w i) + modal_content_simple x = + [ render_errors + , render_introduction_text x + , side_text_for_name_input ("domain" <> form._rr.rrtype) + , Web.input_with_side_text ("domain" <> form._rr.rrtype) "" "www" + (action_update_form <<< Field.Domain) + form._rr.name + display_domain_side + , Web.box_input ("ttl" <> form._rr.rrtype) "TTL" "1800" + (action_update_form <<< Field.TTL) + (show form._rr.ttl) + , case form._rr.rrtype of + "AAAA" -> Web.box_input ("target" <> form._rr.rrtype) "Target" "2001:db8::1" (action_update_form <<< Field.Target) form._rr.target + "TXT" -> Web.box_input ("target" <> form._rr.rrtype) "Your text" "blah blah" (action_update_form <<< Field.Target) form._rr.target + "CNAME" -> Web.box_input ("target" <> form._rr.rrtype) "Target" "www" (action_update_form <<< Field.Target) form._rr.target + "NS" -> Web.box_input ("target" <> form._rr.rrtype) "Target" "ns0.example.com." (action_update_form <<< Field.Target) form._rr.target + _ -> Web.box_input ("target" <> form._rr.rrtype) "Target" "198.51.100.5" (action_update_form <<< Field.Target) form._rr.target + ] <> case rr_modal of + UpdateRRModal -> + if A.elem form._rr.rrtype ["A", "AAAA"] + then [ Web.field_entry ("token" <> form._rr.rrtype) "Token" + (maybe (Web.text "❌​") Web.p form._rr.token) + ] + else [] + _ -> [] + + render_introduction_text :: AcceptedRRTypes -> HH.HTML w i + render_introduction_text = case _ of + A -> Web.div_content [] [Web.explanation Explanations.a_introduction] + AAAA -> Web.div_content [] [Web.explanation Explanations.aaaa_introduction] + TXT -> Web.div_content [] [Web.explanation Explanations.txt_introduction] + CNAME -> Web.div_content [] [Web.explanation Explanations.cname_introduction] + NS -> Web.div_content [] [Web.explanation Explanations.ns_introduction] + _ -> HH.p_ [] + + modal_content_mx :: Array (HH.HTML w i) + modal_content_mx = + [ render_errors + , Web.div_content [] [Web.explanation Explanations.mx_introduction] + , side_text_for_name_input "domainMX" + , Web.input_with_side_text "domainMX" "" "www" + (action_update_form <<< Field.Domain) + form._rr.name + display_domain_side + , Web.box_input ("ttlMX") "TTL" "1800" + (action_update_form <<< Field.TTL) + (show form._rr.ttl) + , Web.box_input ("targetMX") "Target" "www" + (action_update_form <<< Field.Target) + form._rr.target + , Web.box_input ("priorityMX") "Priority" "10" + (action_update_form <<< Field.Priority) + (maybe "" show form._rr.priority) + ] + + modal_content_caa :: Array (HH.HTML w i) + modal_content_caa = + [ render_errors + , Web.div_content [] [Web.explanation Explanations.caa_introduction] + , side_text_for_name_input "domainCAA" + , Web.input_with_side_text "domainCAA" "" "www" + (action_update_form <<< Field.Domain) + form._rr.name + display_domain_side + , Web.box_input ("ttlCAA") "TTL" "1800" + (action_update_form <<< Field.TTL) + (show form._rr.ttl) + , Web.hr + , Web.box_input ("flagCAA") "Flag" "" + (action_update_form <<< Field.CAA_flag) + (show (fromMaybe default_caa form._rr.caa).flag) + , Web.selection_field'' "tagCAA" "Tag" (action_update_rr <<< CAA_tag) (A.zip CAA.tags_txt CAA.tags_raw) + CAA.Issue + (Just (fromMaybe default_caa form._rr.caa).tag) + , HH.div [HP.classes [C.notification, C.is_warning]] + [ Web.p "⚠️​ CAA entries aren't thoroughly verified, yet. Also, do not put quotes." + ] + , Web.box_input "valueCAA" "Value" "" (action_update_form <<< Field.CAA_value) + (fromMaybe default_caa form._rr.caa).value + ] + + modal_content_srv :: Array (HH.HTML w i) + modal_content_srv = + [ Web.div_content [] [Web.explanation Explanations.srv_introduction] + , render_errors + , Web.box_input ("ttlSRV") "TTL" "1800" + (action_update_form <<< Field.TTL) + (show form._rr.ttl) + , Web.box_input "domainSRV" "Service name" "service name" + (action_update_form <<< Field.Domain) + form._rr.name + , Web.selection_field "protocolSRV" "Protocol" (action_update_rr <<< SRV_Protocol) RR.srv_protocols_txt + (maybe "udp" (toLower <<< show) form._rr.protocol) + , Web.box_input ("targetSRV") "Where the server is" "www" + (action_update_form <<< Field.Target) + form._rr.target + , Web.box_input ("portSRV") "Port of the service" "5061" + (action_update_form <<< Field.Port) + (maybe "" show form._rr.port) + , Web.div_content [] [Web.explanation [Web.p "The priority field is a numeric value that indicates the preference of the server (lower values indicate higher priority)."]] + , Web.box_input ("prioritySRV") "Priority" "10" + (action_update_form <<< Field.Priority) + (maybe "" show form._rr.priority) + -- Web.div_content [] [Web.explanation Explanations.spf_introduction], Web.p "Modifiers provide additional instructions, such as explanations for SPF failures or redirecting SPF checks to another domain." + , Web.div_content [] [Web.explanation [Web.p "The weight field is a relative weight used when multiple servers have the same priority, determining how often they should be used."]] + , Web.box_input ("weightSRV") "Weight" "100" + (action_update_form <<< Field.Weight) + (maybe "" show form._rr.weight) + ] + + modal_content_spf :: Array (HH.HTML w i) + modal_content_spf = + [ Web.div_content [] [Web.explanation Explanations.spf_introduction] + , render_errors + , side_text_for_name_input "domainSPF" + , Web.input_with_side_text "domainSPF" "" "Let this alone." + (action_update_form <<< Field.Domain) + form._rr.name + display_domain_side + , Web.box_input "ttlSPF" "TTL" "1800" + (action_update_form <<< Field.TTL) + (show form._rr.ttl) + --, case form._rr.v of + -- Nothing -> Web.p "default value for the version (spf1)" + -- Just v -> Web.box_input "vSPF" "Version" "spf1" (action_update_form <<< Field.SPF_v) v + , Web.hr + , Web.box_with_tag [C.has_background_info_light] tag_mechanisms + [ Web.div_content [] [Web.explanation [Web.p "Mechanisms specify which mail servers are allowed to send mail for the domain and how to evaluate the sending mail server’s IP address."] ] + , maybe (Web.p "You don't have any mechanism.") (Table.display_mechanisms (action_update_rr <<< SPF_remove_mechanism)) form._rr.mechanisms + , Web.hr + , Web.h4 "New mechanism" + , Web.selection_field "idMechanismQ" "Policy" (action_update_rr <<< SPF_Mechanism_q) qualifier_types form.tmp.spf.mechanism_q + , Web.selection_field "idMechanismT" "Type" (action_update_rr <<< SPF_Mechanism_t) mechanism_types form.tmp.spf.mechanism_t + , Web.box_input "valueNewMechanismSPF" "Value" "" + (action_update_rr <<< SPF_Mechanism_v) + form.tmp.spf.mechanism_v + , Web.btn "Add a mechanism" (action_update_rr SPF_Mechanism_Add) + ] + , Web.hr + , Web.box_with_tag [C.has_background_success_light] tag_modifiers + [ Web.div_content [] [Web.explanation [Web.p "Modifiers provide additional instructions, such as explanations for SPF failures or redirecting SPF checks to another domain."] ] + , maybe (Web.p "You don't have any modifier.") (Table.display_modifiers (action_update_rr <<< SPF_remove_modifier)) form._rr.modifiers + , Web.hr + , Web.h4 "New modifier" + , Web.selection_field "idModifierT" "Modifier" (action_update_rr <<< SPF_Modifier_t) modifier_types form.tmp.spf.modifier_t + , Web.box_input "valueNewModifierSPF" "Value" "" + (action_update_rr <<< SPF_Modifier_v) + form.tmp.spf.modifier_v + , Web.btn "Add a modifier" (action_update_rr SPF_Modifier_Add) + ] + , Web.hr + , Web.box + [ Web.h3 "Default behavior" + , Web.div_content [] [Web.explanation Explanations.spf_default_behavior] + , Web.selection (action_update_rr <<< SPF_Qualifier) qualifier_types (maybe default_qualifier_str show_qualifier form._rr.q) + ] + ] + + tag_mechanisms = Web.tags [Web.tag "Mechanisms"] + tag_modifiers = Web.tags [Web.tag "Modifiers"] + + tag_aggregated_reports = Web.tags [Web.tag "Addresses to contact for aggregated reports"] + tag_detailed_reports = Web.tags [Web.tag "Addresses to contact for detailed reports"] + + modal_content_dkim :: Array (HH.HTML w i) + modal_content_dkim = + [ Web.div_content [] [Web.explanation Explanations.dkim_introduction] + , render_errors + , side_text_for_name_input "domainDKIM" + , Web.input_with_side_text "domainDKIM" "" "default._domainkey" + (action_update_form <<< Field.Domain) + form._rr.name + display_domain_side + , Web.box_input "ttlDKIM" "TTL" "1800" + (action_update_form <<< Field.TTL) + (show form._rr.ttl) + , Web.hr + , Web.div_content [] [Web.explanation Explanations.dkim_default_algorithms] + , Web.selection_field "idDKIMSignature" "Signature algo" + (action_update_rr <<< DKIM_sign_algo) + (map show DKIM.sign_algos) + (show $ fromMaybe DKIM.RSA form.tmp.dkim.k) + , Web.selection_field "idDKIMHash" "Hash algo" + (action_update_rr <<< DKIM_hash_algo) + (map show DKIM.hash_algos) + (show $ fromMaybe DKIM.SHA256 form.tmp.dkim.h) + , Web.box_input "pkDKIM" "Public Key" "Your public key, such as \"MIIBIjANBgqh...\"" (action_update_rr <<< DKIM_pubkey) form.tmp.dkim.p + , Web.box_input "noteDKIM" "Note" "Note for fellow administrators." (action_update_rr <<< DKIM_note) (fromMaybe "" form.tmp.dkim.n) + ] + + modal_content_dmarc :: Array (HH.HTML w i) + modal_content_dmarc = + [ Web.div_content [] [Web.explanation Explanations.dmarc_introduction] + , render_errors + , side_text_for_name_input "domainDMARC" + , Web.input_with_side_text "domainDMARC" "" "_dmarc" + (action_update_form <<< Field.Domain) + form._rr.name + display_domain_side + , Web.box_input "ttlDMARC" "TTL" "1800" (action_update_form <<< Field.TTL) (show form._rr.ttl) + + , Web.hr + , Web.div_content [] [Web.explanation Explanations.dmarc_policy] + , Web.selection_field' "idDMARCPolicy" "Policy" (action_update_rr <<< DMARC_policy) + (A.zip DMARC.policies_txt DMARC.policies_raw) + (show form.tmp.dmarc.p) + , Web.div_content [] [Web.explanation Explanations.dmarc_sp_policy] + , Web.selection_field' "idDMARCPolicy_sp" "Policy for subdomains" (action_update_rr <<< DMARC_sp_policy) + (zip_nullable DMARC.policies_txt_with_null DMARC.policies_raw) + (maybe "-" show form.tmp.dmarc.sp) + + , Web.hr + , Web.div_content [] [Web.explanation Explanations.dmarc_adkim] + , Web.selection_field' "idDMARCadkim" "Consistency Policy for DKIM" (action_update_rr <<< DMARC_adkim) + (zip_nullable DMARC.consistency_policies_txt DMARC.consistency_policies_raw) + (maybe "-" show form.tmp.dmarc.adkim) + , Web.div_content [] [Web.explanation Explanations.dmarc_aspf] + , Web.selection_field' "idDMARCaspf" "Consistency Policy for SPF" (action_update_rr <<< DMARC_aspf) + (zip_nullable DMARC.consistency_policies_txt DMARC.consistency_policies_raw) + (maybe "-" show form.tmp.dmarc.aspf) + + , Web.hr + , Web.div_content [] [Web.explanation Explanations.dmarc_pct] + , Web.box_input "idDMARCpct" "Sample rate (between 0 and 100)" "100" (action_update_rr <<< DMARC_pct) (maybe "100" show form.tmp.dmarc.pct) + + , Web.hr + , Web.selection_field' "idDMARCfo" "When to send a report" (action_update_rr <<< DMARC_fo) + (zip_nullable DMARC.report_occasions_txt DMARC.report_occasions_raw) + (maybe "-" show form.tmp.dmarc.fo) + + , Web.hr + , Web.div_content [] [Web.explanation Explanations.dmarc_contact] + , Web.box_with_tag [C.has_background_info_light] tag_aggregated_reports + [ maybe (Web.p "There is no address to send aggregated reports to.") + (Table.display_dmarc_mail_addresses (action_update_rr <<< DMARC_remove_rua)) + form.tmp.dmarc.rua + ] + , Web.box_with_tag [C.has_background_success_light] tag_detailed_reports + [ maybe (Web.p "There is no address to send detailed reports to.") + (Table.display_dmarc_mail_addresses (action_update_rr <<< DMARC_remove_ruf)) + form.tmp.dmarc.ruf + ] + + , Web.hr + , render_dmarc_mail_errors + , Web.box_input "idDMARCmail" "Address to contact" "admin@example.com" (action_update_rr <<< DMARC_mail) form.tmp.dmarc_mail + , Web.box_input "idDMARCmaillimit" "Report size limit (in KB)" "2000" (action_update_rr <<< DMARC_mail_limit) (maybe "0" show form.tmp.dmarc_mail_limit) + , Web.level [ Web.btn_ [C.has_background_info_light] "New address for aggregated report" (action_update_rr DMARC_rua_Add) + , Web.btn_ [C.has_background_success_light] "New address for specific report" (action_update_rr DMARC_ruf_Add) + ] [] + + , Web.hr + , Web.div_content [] [Web.explanation Explanations.dmarc_ri] + , Web.box_input "idDMARCri" "Report interval (in seconds)" "86400" (action_update_rr <<< DMARC_ri) (maybe "0" show form.tmp.dmarc.ri) + ] + + render_dmarc_mail_errors + = if A.length form._dmarc_mail_errors > 0 + then Web.notification_danger_block' + $ [ Web.h3 "Invalid mail 😥" ] <> map (Web.p <<< show_error_email) form._dmarc_mail_errors + else HH.div_ [ ] + + display_domain_side = (if form._rr.name == (selected_domain <> ".") then "" else "." <> selected_domain) + + newtokenbtn :: HH.HTML w i + newtokenbtn = Web.btn (maybe "🏁​ Ask for a token" (\_ -> "🏁​ Ask for a new token") form._rr.token) (action_new_token form._rr.rrid) + + foot_content :: AcceptedRRTypes -> Array (HH.HTML w i) + foot_content x = + case rr_modal of + NewRRModal _ -> [Web.btn_add (action_validate_rr x)] + UpdateRRModal -> [Web.btn_save action_validate_local_rr ] <> case x of + A -> [newtokenbtn] + AAAA -> [newtokenbtn] + _ -> [] + _ -> [Web.p "rr_modal should either be NewRRModal or UpdateRRModal."] + + template :: Array (HH.HTML w i) -> Array (HH.HTML w i) -> HH.HTML w i + template content foot_ = Web.modal title content foot + where + title = case rr_modal of + NoModal -> "Error: no modal should be displayed" + NewRRModal t_ -> "New " <> show t_ <> " resource record" + UpdateRRModal -> "Update " <> form._rr.rrtype <> " Resource Record" + RemoveRRModal rr_id -> "Error: should display removal modal instead (for resource record " <> show rr_id <> ")" + foot = foot_ <> [Web.cancel_button action_cancel_modal] diff --git a/src/App/Templates/Table.purs b/src/App/Templates/Table.purs index aa8cf5b..6351cc4 100644 --- a/src/App/Templates/Table.purs +++ b/src/App/Templates/Table.purs @@ -1,3 +1,6 @@ +-- | `App.Templates.Table` gathers all the website's tables, providing +-- | an easy way to duplicate tables in different pages and to display +-- | content in a consistent manner. module App.Templates.Table ( owned_domains , shared_domains @@ -17,8 +20,8 @@ import Data.Maybe (Maybe(..), fromMaybe, maybe) import Data.Tuple (Tuple(..)) -import Style as Style -import Style.Button as Button +import Web as Web +import Web.Button as Button import Bulma as Bulma import Halogen.HTML as HH import Halogen.HTML.Properties as HP @@ -122,15 +125,15 @@ resource_records records action_create_or_update_rr action_delete_rr action_new_ all_dkim_rr = all_XX_rr "DKIM" all_dmarc_rr = all_XX_rr "DMARC" - tag_soa = Style.tags [Style.tag_ro "SOA", Style.tag_ro "read only"] - tag_basic = Style.tags [Style.tag "Basic Resource Records (A, AAAA, PTR, NS, TXT)"] - tag_mx = Style.tags [Style.tag "MX"] - tag_caa = Style.tags [Style.tag "CAA"] - tag_srv = Style.tags [Style.tag "SRV"] - tag_spf = Style.tags [Style.tag "SPF"] - tag_dkim = Style.tags [Style.tag "DKIM"] - tag_dmarc = Style.tags [Style.tag "DMARC"] - tag_basic_ro = Style.tags [Style.tag_ro "Basic Resource Records", Style.tag_ro "read only"] + tag_soa = Web.tags [Web.tag_ro "SOA", Web.tag_ro "read only"] + tag_basic = Web.tags [Web.tag "Basic Resource Records (A, AAAA, PTR, NS, TXT)"] + tag_mx = Web.tags [Web.tag "MX"] + tag_caa = Web.tags [Web.tag "CAA"] + tag_srv = Web.tags [Web.tag "SRV"] + tag_spf = Web.tags [Web.tag "SPF"] + tag_dkim = Web.tags [Web.tag "DKIM"] + tag_dmarc = Web.tags [Web.tag "DMARC"] + tag_basic_ro = Web.tags [Web.tag_ro "Basic Resource Records", Web.tag_ro "read only"] rr_box :: Array HH.ClassName -- css classes (such as colors) -> HH.HTML w i -- box title (type of data) @@ -509,15 +512,15 @@ port_header = HH.abbr [ HH.text "Port" ] display_mechanisms :: forall w i. (Int -> i) -> Array RR.Mechanism -> HH.HTML w i -display_mechanisms _ [] = Style.p "You don't have any mechanism." +display_mechanisms _ [] = Web.p "You don't have any mechanism." display_mechanisms action_remove_mechanism ms = - Style.table [] [ mechanism_table_header, HH.tbody_ $ map render_mechanism_row $ attach_id 0 ms] + Web.table [] [ mechanism_table_header, HH.tbody_ $ map render_mechanism_row $ attach_id 0 ms] where render_mechanism_row :: (Tuple Int RR.Mechanism) -> HH.HTML w i render_mechanism_row (Tuple i m) = HH.tr_ [ txt_name $ maybe "" show_qualifier m.q - , HH.td_ [ Style.p $ show_mechanism_type m.t ] - , HH.td_ [ Style.p m.v ] + , HH.td_ [ Web.p $ show_mechanism_type m.t ] + , HH.td_ [ Web.p m.v ] , HH.td_ [ Button.alert_btn "x" (action_remove_mechanism i) ] ] mechanism_table_header :: HH.HTML w i @@ -530,14 +533,14 @@ display_mechanisms action_remove_mechanism ms = ] display_modifiers :: forall w i. (Int -> i) -> Array RR.Modifier -> HH.HTML w i -display_modifiers _ [] = Style.p "You don't have any modifier." +display_modifiers _ [] = Web.p "You don't have any modifier." display_modifiers action_remove_modifier ms = - Style.table [] [ modifier_table_header, HH.tbody_ $ map render_modifier_row $ attach_id 0 ms] + Web.table [] [ modifier_table_header, HH.tbody_ $ map render_modifier_row $ attach_id 0 ms] where render_modifier_row :: (Tuple Int RR.Modifier) -> HH.HTML w i render_modifier_row (Tuple i m) = HH.tr_ - [ HH.td_ [ Style.p $ show_modifier_type m.t ] - , HH.td_ [ Style.p m.v ] + [ HH.td_ [ Web.p $ show_modifier_type m.t ] + , HH.td_ [ Web.p m.v ] , HH.td_ [ Button.alert_btn "x" (action_remove_modifier i) ] ] modifier_table_header :: HH.HTML w i @@ -550,12 +553,12 @@ display_modifiers action_remove_modifier ms = display_dmarc_mail_addresses :: forall w i. (Int -> i) -> Array DMARC.DMARCURI -> HH.HTML w i display_dmarc_mail_addresses f ms = - Style.table [] [ dmarc_dmarcuri_table_header, HH.tbody_ $ map render_dmarcuri_row $ attach_id 0 ms] + Web.table [] [ dmarc_dmarcuri_table_header, HH.tbody_ $ map render_dmarcuri_row $ attach_id 0 ms] where render_dmarcuri_row :: (Tuple Int DMARC.DMARCURI) -> HH.HTML w i render_dmarcuri_row (Tuple i m) = HH.tr_ - [ HH.td_ [ Style.p m.mail ] - , HH.td_ [ Style.p $ maybe "(no size limit)" show m.limit ] + [ HH.td_ [ Web.p m.mail ] + , HH.td_ [ Web.p $ maybe "(no size limit)" show m.limit ] , HH.td_ [ Button.alert_btn "x" (f i) ] ] dmarc_dmarcuri_table_header :: HH.HTML w i diff --git a/src/App/Text/Explanations.purs b/src/App/Text/Explanations.purs index 30d1683..d9752c8 100644 --- a/src/App/Text/Explanations.purs +++ b/src/App/Text/Explanations.purs @@ -1,22 +1,22 @@ module App.Text.Explanations where import Halogen.HTML as HH import Halogen.HTML.Properties as HP -import Style as Style +import Web as Web import CSSClasses as C expl' :: forall w i. String -> HH.HTML w i -expl' text = expl [Style.p text] +expl' text = expl [Web.p text] expl :: forall w i. Array (HH.HTML w i) -> HH.HTML w i -expl content = Style.div_content [] [ Style.explanation content ] +expl content = Web.div_content [] [ Web.explanation content ] expl_txt :: forall w i. String -> HH.HTML w i -expl_txt content = Style.explanation [ Style.p content ] +expl_txt content = Web.explanation [ Web.p content ] col :: forall w i. Array (HH.HTML w i) -> HH.HTML w i -col arr = Style.column_ [ Style.box arr ] +col arr = Web.column_ [ Web.box arr ] tokens :: forall w i. HH.HTML w i tokens = HH.div_ - [ Style.h3 "What are tokens?" + [ Web.h3 "What are tokens?" , expl' """ Tokens are a simple way to update a resource record (A or AAAA) with your current IP address. """ @@ -30,61 +30,61 @@ tokens = HH.div_ , HH.u_ [HH.text ""] ] ] - , Style.p "For example: https://www.netlib.re/token-update/53be0c45-61c4-4d29-8ae9-c2cc8767603d" - , Style.hr - , Style.h3 "How to automate the update of my IP address?" - , Style.p "On Linux, you can make your computer access the update link with the following command." - , expl [ Style.strong "wget https://www.netlib.re/token-update/" ] - , Style.p """ + , Web.p "For example: https://www.netlib.re/token-update/53be0c45-61c4-4d29-8ae9-c2cc8767603d" + , Web.hr + , Web.h3 "How to automate the update of my IP address?" + , Web.p "On Linux, you can make your computer access the update link with the following command." + , expl [ Web.strong "wget https://www.netlib.re/token-update/" ] + , Web.p """ No need for a more complex program. This works just fine. And you can run this command every hour. For example, in your crontab (Linux and Unix related): """ - , expl [ Style.strong "0 * * * * wget " ] - , Style.p """ + , expl [ Web.strong "0 * * * * wget " ] + , Web.p """ Commands for other operating systems may differ, but you get the idea. """ - , Style.hr - , Style.h3 "The obvious trap ⚠" - , Style.p """ + , Web.hr + , Web.h3 "The obvious trap ⚠" + , Web.p """ Make sure to access the website using the related IP address. To update an IPv6 address (AAAA), force your application to access the URL using an IPv6 address. """ - , expl [ HH.p_ [ Style.strong "wget -6 " ] + , expl [ HH.p_ [ Web.strong "wget -6 " ] , HH.p_ [ HH.text "To force the use of an IPv6 address." ] - , HH.p_ [ Style.strong "wget -4 " ] + , HH.p_ [ Web.strong "wget -4 " ] , HH.p_ [ HH.text "To force the use of an IPv4 address." ] ] ] basics :: forall w i. HH.HTML w i basics = HH.div_ - [ Style.h3 "Basics of DNS" - , Style.p """ + [ Web.h3 "Basics of DNS" + , Web.p """ The domain name system (DNS) enables people share a name instead of an address to find a website or service. """ - , Style.p """ + , Web.p """ To configure a zone, the first steps are trivial. """ - , Style.hr - , Style.h3 "I have something to host (A and AAAA records)." + , Web.hr + , Web.h3 "I have something to host (A and AAAA records)." , expl' "Let's assume you have a web server and you host your website somewhere." - , Style.p """ + , Web.p """ You want an A (IPv4) or AAAA (IPv6) record pointing to your server, named "enigma" for example. """ - , Style.hr - , Style.h3 "You need other names pointing to your server (CNAME records)." - , Style.p """ + , Web.hr + , Web.h3 "You need other names pointing to your server (CNAME records)." + , Web.p """ You may not want to use the name of your server "enigma" directly. Instead, you may want the usual names for your services, such as "www" or "blog". CNAME records are basically aliases, exactly to that end. """ - , Style.hr - , Style.h3 "If you have other servers, just add more A or AAAA records." - , Style.p """ + , Web.hr + , Web.h3 "If you have other servers, just add more A or AAAA records." + , Web.p """ Tip: choose relevant names for your servers then add CNAME records. For example, you can have an A record named "server1" and a CNAME "www" pointing to it. The service isn't pointing to an actual IP address directly, @@ -92,24 +92,24 @@ basics = HH.div_ You don't need to remember the IP address of each of your servers. """ - , Style.hr - , Style.h3 "I want an email server." + , Web.hr + , Web.h3 "I want an email server." , expl' """ Hosting a mail server is quite complex. This section will focus on the main parts regarding the DNS. """ - , Style.notification_danger' """ + , Web.notification_danger' """ The actual configuration of your mail server is complex and depends on your choice of software. This won't be covered here. """ - , Style.p """ + , Web.p """ You need an MX record pointing to your "www" A (or AAAA) record. """ - , Style.p """ + , Web.p """ Having an MX record isn't enough to handle a mail server. You need to use a few spam mitigation mechanisms. """ - , Style.columns_ + , Web.columns_ [ col [ expl' """ Spam mitigation 1: tell what are the right mail servers for your domain with Sender Policy Framework (SPF). @@ -138,27 +138,27 @@ basics = HH.div_ , expl_txt """ Last but not least, DMARC. """ - , Style.hr - , Style.p """ + , Web.hr + , Web.p """ DMARC enables to check the "From:" field of a mail, based on the SPF and DKIM mechanisms. Thus, domains with a DMARC record enable to only allow verified mails. Valid emails come from an authorized IP address (SPF), are signed by the verified email server (DKIM) and have an email address coming from a verified domain (DMARC) related to the two previous spam mitigation mechanisms. """ - , Style.hr - , Style.p """ + , Web.hr + , Web.p """ With DMARC, you won't accept an email from "hacker@example.com" because it was sent by another domain with a valid SPF and DKIM. """ ] ] - , Style.hr - , Style.h3 "How to automate the update of my IP address?" - , Style.p "Check out the \"Tokens? 🤨\" tab." + , Web.hr + , Web.h3 "How to automate the update of my IP address?" + , Web.p "Check out the \"Tokens? 🤨\" tab." ] a_introduction :: forall w i. Array (HH.HTML w i) a_introduction = - [ Style.p """ + [ Web.p """ The A record enables to bind an IPv4 address to a domain. """ , HH.p [] @@ -175,7 +175,7 @@ a_introduction = aaaa_introduction :: forall w i. Array (HH.HTML w i) aaaa_introduction = - [ Style.p """ + [ Web.p """ The AAAA record enables to bind an IPv6 address to a domain. """ , HH.p [] @@ -192,7 +192,7 @@ aaaa_introduction = cname_introduction :: forall w i. Array (HH.HTML w i) cname_introduction = - [ Style.p """ + [ Web.p """ The CNAME record enables to provide alternative names to records. """ , HH.p [] @@ -209,7 +209,7 @@ cname_introduction = mx_introduction :: forall w i. Array (HH.HTML w i) mx_introduction = - [ Style.p """ + [ Web.p """ The MX record enables to add a mail server to your zone. """ , HH.p [] @@ -221,20 +221,20 @@ mx_introduction = This page talks about the DNS aspect of it, but doesn't cover all you need to know to actually host a mail server, by a long shot. """ ] - , Style.p """ + , Web.p """ Anyway, the MX record itself is simple to understand. Let's say you have a server named "server1" with your mail service. The MX record can be named "mail" and it will target "server1". Of course, "server1" needs a record for its IP address (A or AAAA). """ - , Style.p """ + , Web.p """ The priority field is important only in case you have multiple mail servers; keep the default value. """ ] txt_introduction :: forall w i. Array (HH.HTML w i) txt_introduction = - [ Style.p """ + [ Web.p """ The TXT record enables to declare a small text. """ , HH.p [] @@ -246,7 +246,7 @@ txt_introduction = TXT records are used in several places, for example for mail security through SPF, DKIM and DMARC records. """ ] - , Style.notification_danger' """ + , Web.notification_danger' """ All of these specific records have a dedicated user interface on this website; use them instead of writing these records by yourself. """ @@ -254,19 +254,19 @@ txt_introduction = ns_introduction :: forall w i. Array (HH.HTML w i) ns_introduction = - [ Style.p """ + [ Web.p """ The NS record enables to declare a new Name Server, meaning a new server that would serve this zone. """ - , Style.notification_danger' "🚨 Advice for beginners: do not use this resource record." + , Web.notification_danger' "🚨 Advice for beginners: do not use this resource record." ] caa_introduction :: forall w i. Array (HH.HTML w i) caa_introduction = - [ Style.p """ + [ Web.p """ The CAA record enables to specify a certification authority that is authorized to issue certificates for the domain. The idea is to reduce the risk of unintended certificate mis-issue. """ - , Style.p """ + , Web.p """ Certification authorities (CA) may issue certificates for any domain. Thus, any CA may provide certificates for a domain (let's say google.com) to any hacker that can now impersonate the domain. The CAA record allows to say what is the authorized CA for the domain, preventing this kind of attacks. @@ -282,7 +282,7 @@ caa_introduction = dkim_introduction :: forall w i. Array (HH.HTML w i) dkim_introduction = - [ Style.p """ + [ Web.p """ DKIM is a way to share a public signature key for the domain. This enables emails to be signed by the sender and for the receiver to verify the origin of the mail. """ @@ -293,14 +293,14 @@ dkim_introduction = """ , HH.u_ [HH.text "selector"] , HH.text " is " - , Style.strong "default" + , Web.strong "default" , HH.text "." ] ] dmarc_introduction :: forall w i. Array (HH.HTML w i) dmarc_introduction = - [ Style.p """ + [ Web.p """ DMARC is a spam mitigation mechanism on top of SPF and DKIM. Upon receiving a mail, the server checks whether the "From:" field of the mail is consistent with the SPF and DKIM records of the sender's domain. @@ -311,11 +311,11 @@ dmarc_introduction = dmarc_policy :: forall w i. Array (HH.HTML w i) dmarc_policy = - [ Style.p """ + [ Web.p """ DMARC record enables to tell receivers what to do with a non-conforming message, i.e. a message that wasn't properly secured with SPF and DKIM. """ - , Style.p """ + , Web.p """ This message can either be accepted ("None") or rejected, or even quarantined, meaning to be considered as suspicious. This can take different forms, such as being flagged, marked as spam or have a high "spam score", it's up to the receiver. """ @@ -323,34 +323,34 @@ dmarc_policy = dmarc_sp_policy :: forall w i. Array (HH.HTML w i) dmarc_sp_policy = - [ Style.p """ + [ Web.p """ Same as the previous entry, but for sub-domains. """ ] dmarc_adkim :: forall w i. Array (HH.HTML w i) dmarc_adkim = - [ Style.p """ + [ Web.p """ Consistency policy for DKIM. Tell what should be considered acceptable. """ - , Style.p """ + , Web.p """ This is about the relation between the email "From:" field and the domain field of the DKIM signature ("d:"). """ - , Style.p """ + , Web.p """ The policy can be either strict (both should be identical) or relaxed (both in the same Organizational Domain). """ ] dmarc_aspf :: forall w i. Array (HH.HTML w i) dmarc_aspf = - [ Style.p """ + [ Web.p """ Consistency policy for SPF. Tell what should be considered acceptable. """ - , Style.p """ + , Web.p """ First, SPF should produce a passing result. Then, the "From:" and the "MailFrom:" fields of the received email are checked. """ - , Style.p """ + , Web.p """ In strict mode, both fields should be identical. In relaxed mode, they can be different, but in the same Organizational Domain. """ @@ -367,28 +367,28 @@ dmarc_aspf = ] , HH.p_ [ HH.text "See " - , Style.outside_link [] "https://publicsuffix.org/" "publicsuffix.org" + , Web.outside_link [] "https://publicsuffix.org/" "publicsuffix.org" , HH.text " for a list of Organizational Domains." ] ] dmarc_contact :: forall w i. Array (HH.HTML w i) dmarc_contact = - [ Style.p """ + [ Web.p """ In case you want to receive error reports, enter email addresses that should receive either an aggregated report or a detailed report of the occurring errors. """ ] dmarc_ri :: forall w i. Array (HH.HTML w i) dmarc_ri = - [ Style.p """ + [ Web.p """ Requested report interval. Default is 86400. """ ] dmarc_pct :: forall w i. Array (HH.HTML w i) dmarc_pct = - [ Style.p """ + [ Web.p """ Sampling rate. Percentage of messages subjected to the requested policy. """ @@ -397,7 +397,7 @@ dmarc_pct = dkim_default_algorithms :: forall w i. Array (HH.HTML w i) dkim_default_algorithms = - [ Style.p """ + [ Web.p """ Default values should be fine (RSA + SHA256), change them only if you know what you are doing. Just enter your public key. """ @@ -435,7 +435,7 @@ spf_introduction = spf_default_behavior :: forall w i. Array (HH.HTML w i) spf_default_behavior = [ - Style.p """ + Web.p """ What should someone do when receiving a mail from your email address but not from a listed domain or IP address? """ , HH.p_ [ HH.text """ @@ -464,7 +464,7 @@ spf_default_behavior = [ srv_introduction :: forall w i. Array (HH.HTML w i) srv_introduction = - [ Style.p "The SRV record is a DNS resource record for specifying the location of services." + [ Web.p "The SRV record is a DNS resource record for specifying the location of services." , HH.p_ [ HH.text "Given a specific " , HH.u_ [HH.text "service name"] , HH.text " (which may be arbitrary) and a " @@ -487,38 +487,38 @@ website_abuse_address = "abuse AT netlib.re" :: String legal_notice :: forall w i. HH.HTML w i legal_notice = HH.div_ - [ Style.h3 "Legal Notice" + [ Web.h3 "Legal Notice" - , Style.strong "Website Publisher" + , Web.strong "Website Publisher" , expl [ HH.p_ [ HH.text "You can contact this website's owner and publisher at: " - , Style.strong website_owner_address + , Web.strong website_owner_address ] , HH.p_ [ HH.text "For legal matter: " - , Style.strong website_abuse_address + , Web.strong website_abuse_address ] ] - , Style.strong "Website Hosting" + , Web.strong "Website Hosting" , expl [ HH.p_ [ HH.text "This website is hosted by " - , Style.strong "Alsace Réseau Neutre" + , Web.strong "Alsace Réseau Neutre" , HH.text "." , HH.br_ , HH.text "Website: " - , Style.outside_link [] "https://arn-fai.net" "arn-fai.net" + , Web.outside_link [] "https://arn-fai.net" "arn-fai.net" , HH.br_ , HH.text "Address & contact: " - , Style.outside_link [] "https://arn-fai.net/fr/mentions" "Legal Notice of ARN" + , Web.outside_link [] "https://arn-fai.net/fr/mentions" "Legal Notice of ARN" ] ] - , Style.strong "Intellectual Property" + , Web.strong "Intellectual Property" , expl' """ The code of this website is released under the ISC License. You are free to copy, modify, and distribute the code, provided that you comply with the terms of the ISC License. """ - , Style.strong "Personal Data Collection" + , Web.strong "Personal Data Collection" , expl' """ This website collects only the personal data necessary for its proper functioning. This includes data such as: a login (arbitrary set of @@ -526,12 +526,12 @@ legal_notice = HH.div_ to contact the owner of the domain, domain names and zone data. """ - , Style.strong "Data Sharing" + , Web.strong "Data Sharing" , expl' """ None of the collected data will be shared to third parties. """ - , Style.strong "Data Retention" + , Web.strong "Data Retention" , expl' """ The personal data collected on this website will be retained for as long as necessary to fulfill the purposes for which it @@ -549,16 +549,16 @@ legal_notice = HH.div_ After this period, all data will be securely deleted. """ - , Style.strong "Liability" + , Web.strong "Liability" , expl - [ Style.p + [ Web.p """ The publisher of this website makes every effort to ensure that the website functions properly and that all data is protected to the best of their ability. """ - , Style.p + , Web.p """ However, despite all reasonable precautions, the publisher cannot guarantee that the website will always be free of errors, @@ -576,7 +576,7 @@ legal_notice = HH.div_ ] ] - , Style.p + , Web.p """ By using this website, users acknowledge that they accept the inherent risks associated with the use of online services. The @@ -585,13 +585,13 @@ legal_notice = HH.div_ """ ] - , Style.strong "GDPR compliance" + , Web.strong "GDPR compliance" , expl [ HH.p_ [ HH.text """ You have the right to access, correct and delete your personal data at any time via this website or by contacting us at the following email address: """ - , Style.strong website_owner_address + , Web.strong website_owner_address ] ] ] diff --git a/src/App/Type/Field.purs b/src/App/Type/Field.purs new file mode 100644 index 0000000..2ec7fc4 --- /dev/null +++ b/src/App/Type/Field.purs @@ -0,0 +1,18 @@ +module App.Type.Field where + +import App.Type.ResourceRecord as RR + +data Field + = Domain String + | TTL String + | Target String + | Priority String + | Weight String + | Port String + | SPF_v String + | SPF_mechanisms (Array RR.Mechanism) + | SPF_modifiers (Array RR.Modifier) + | SPF_q RR.Qualifier + + | CAA_flag String + | CAA_value String diff --git a/src/App/Type/RRForm.purs b/src/App/Type/RRForm.purs new file mode 100644 index 0000000..b1ba039 --- /dev/null +++ b/src/App/Type/RRForm.purs @@ -0,0 +1,149 @@ +-- | `App.Type.RRForm` provides types used to manage the modification +-- | of resource records. +-- | FIXME: this state is messy AF and should be replaced. +module App.Type.RRForm where + +import Prelude +import App.Type.AcceptedRRTypes (AcceptedRRTypes(..)) +import App.Type.ResourceRecord +import App.Type.DKIM as DKIM +import App.Type.DMARC as DMARC +import Data.Maybe +import App.Type.ResourceRecord as RR +import App.Type.CAA as CAA +import App.Validation.Email as Email +import App.Validation.DNS as Validation +import App.Type.RRId + +-- | TMP: temporary stored values regarding specific records such as SPF, +-- | DKIM and DMARC. +type TMP = + { + -- SPF details. + spf :: { mechanism_q :: String + , mechanism_t :: String + , mechanism_v :: String + , modifier_t :: String + , modifier_v :: String + } + + -- DMARC details. + , dmarc_mail :: String + , dmarc_mail_limit :: Maybe Int + , dmarc :: DMARC.DMARC + + -- DKIM details. + , dkim :: DKIM.DKIM + } + +-- | `RRForm` is the necessary state to modify a resource record. +-- | It contains the currently manipulated record, detected errors, along with some temporary values. +type RRForm = + { _rr :: ResourceRecord + , _errors :: Array Validation.Error + , _dmarc_mail_errors :: Array Email.Error + , _zonefile :: Maybe String + , tmp :: TMP + } + +default_empty_rr :: ResourceRecord +default_empty_rr = default_rr A "" + +default_qualifier_str = "hard_fail" :: String +default_caa = { flag: 0, tag: CAA.Issue, value: "letsencrypt.org" } :: CAA.CAA + +default_rr :: AcceptedRRTypes -> String -> ResourceRecord +default_rr t domain = + case t of + A -> emptyRR { rrtype = "A", name = "server1", target = "192.0.2.1" } + AAAA -> emptyRR { rrtype = "AAAA", name = "server1", target = "2001:db8::1" } + TXT -> emptyRR { rrtype = "TXT", name = "txt", target = "some text" } + CNAME -> emptyRR { rrtype = "CNAME", name = "www", target = "server1" } + NS -> emptyRR { rrtype = "NS", name = (domain <> "."), target = "ns0.example.com." } + MX -> emptyRR { rrtype = "MX", name = "mail", target = "server1", priority = Just 10 } + CAA -> emptyRR { rrtype = "CAA", name = "", target = "", caa = Just default_caa } + SRV -> emptyRR { rrtype = "SRV", name = "voip", target = "server1" + , port = Just 5061, weight = Just 100, priority = Just 10, protocol = Just RR.TCP } + SPF -> emptyRR { rrtype = "SPF", name = "", target = "" + , mechanisms = Just default_mechanisms, q = Just RR.HardFail } + DKIM -> emptyRR { rrtype = "DKIM", name = "default._domainkey", target = "" } + DMARC -> emptyRR { rrtype = "DMARC", name = "_dmarc", target = "" } + where + default_mechanisms = maybe [] (\x -> [x]) $ to_mechanism "pass" "mx" "" + +mkEmptyRRForm :: RRForm +mkEmptyRRForm = + { + -- This is the state for the new RR modal. + _rr: default_empty_rr + -- List of errors within the form in new RR modal. + , _errors: [] + , _dmarc_mail_errors: [] + , _zonefile: Nothing + , tmp: { spf: { mechanism_q: "pass" + , mechanism_t: "a" + , mechanism_v: "" + , modifier_t: "redirect" + , modifier_v: "" + } + , dkim: DKIM.emptyDKIMRR + , dmarc: DMARC.emptyDMARCRR + , dmarc_mail: "" + , dmarc_mail_limit: Nothing + } + } + +data RRUpdateValue + -- | Ask a (new) token for a RR. + = NewToken RRId + | CAA_tag Int + | SRV_Protocol Int + | SPF_Mechanism_q Int + | SPF_Mechanism_t Int + | SPF_Mechanism_v String + | SPF_Modifier_t Int + | SPF_Modifier_v String + | SPF_Qualifier Int + + -- | Remove a SPF mechanism of the currently modified (SPF) entry (see `_currentRR`). + | SPF_remove_mechanism Int + -- | Remove a SPF modifier of the currently modified (SPF) entry (see `_currentRR`). + | SPF_remove_modifier Int + + -- | Add a SPF mechanism to the currently modified (SPF) entry (see `_currentRR`). + | SPF_Mechanism_Add + -- | Add a SPF modifier to the currently modified (SPF) entry (see `_currentRR`). + | SPF_Modifier_Add + + -- | Change the temporary mail address for DMARC. + | DMARC_mail String + + -- | Change the temporary report size limit for DMARC. + | DMARC_mail_limit String + + -- | Change the requested report interval. + | DMARC_ri String + + -- | Add a new mail address to the DMARC rua list. + | DMARC_rua_Add + + -- | Add a new mail address to the DMARC ruf list. + | DMARC_ruf_Add + + -- | Remove a mail address of the DMARC rua list. + | DMARC_remove_rua Int + + -- | Remove a mail address of the DMARC ruf list. + | DMARC_remove_ruf Int + + | DMARC_policy Int + | DMARC_sp_policy Int + | DMARC_adkim Int + | DMARC_aspf Int + | DMARC_pct String + | DMARC_fo Int + + | DKIM_hash_algo Int + | DKIM_sign_algo Int + | DKIM_pubkey String + | DKIM_note String diff --git a/src/App/Type/RRId.purs b/src/App/Type/RRId.purs new file mode 100644 index 0000000..340729c --- /dev/null +++ b/src/App/Type/RRId.purs @@ -0,0 +1,3 @@ +module App.Type.RRId where + +type RRId = Int diff --git a/src/App/Type/RRModal.purs b/src/App/Type/RRModal.purs new file mode 100644 index 0000000..c4a1475 --- /dev/null +++ b/src/App/Type/RRModal.purs @@ -0,0 +1,16 @@ +-- | `App.Type.RRModal.RRModal` provides the states of a modal related +-- | to resource management: no modal because no RR is currently +-- | selected, new RR because a new resource will be requested, update +-- | and finally removal, to ask for confirmation. +-- | +-- | FIXME: TODO: WIP: should this be replaced by something like `CRUD`? +module App.Type.RRModal where + +import App.Type.RRId +import App.Type.AcceptedRRTypes (AcceptedRRTypes) + +data RRModal + = NoModal + | NewRRModal AcceptedRRTypes + | UpdateRRModal + | RemoveRRModal RRId diff --git a/src/Bulma.purs b/src/Bulma.purs index 5b2faf6..3e656de 100644 --- a/src/Bulma.purs +++ b/src/Bulma.purs @@ -9,7 +9,6 @@ import DOM.HTML.Indexed as DHI import Halogen.HTML.Properties as HP import Halogen.HTML.Events as HE -import DOM.HTML.Indexed.AutocompleteType (AutocompleteType(..)) import CSSClasses as C import Halogen.HTML.Core (AttrName(..)) diff --git a/src/Web.purs b/src/Web.purs new file mode 100644 index 0000000..6ca5a48 --- /dev/null +++ b/src/Web.purs @@ -0,0 +1,14 @@ +-- | `Web` module is an abstraction over most HTML-related code in order +-- | to have a consistent style through all the website. +module Web + ( module Bulma + , module Web.Button + , module Web.Input + , module Web.Tag + ) where + +import Web.Button +import Web.Input +import Web.Tag + +import Bulma diff --git a/src/Style/Button.purs b/src/Web/Button.purs similarity index 99% rename from src/Style/Button.purs rename to src/Web/Button.purs index 2de89a2..2cdb1fe 100644 --- a/src/Style/Button.purs +++ b/src/Web/Button.purs @@ -1,4 +1,4 @@ -module Style.Button +module Web.Button ( alert_btn , alert_btn_abbr , btn diff --git a/src/Style/Input.purs b/src/Web/Input.purs similarity index 99% rename from src/Style/Input.purs rename to src/Web/Input.purs index 74d8cc7..9bdcbb4 100644 --- a/src/Style/Input.purs +++ b/src/Web/Input.purs @@ -1,4 +1,4 @@ -module Style.Input +module Web.Input ( email_input , password_input , password_input_confirmation diff --git a/src/Style.purs b/src/Web/Tag.purs similarity index 72% rename from src/Style.purs rename to src/Web/Tag.purs index abf3bd6..3887dcc 100644 --- a/src/Style.purs +++ b/src/Web/Tag.purs @@ -1,16 +1,4 @@ -module Style - ( module Bulma - , module Style.Button - , module Style.Input - , tags - , tag - , tag_ro - ) where - -import Style.Button -import Style.Input - -import Bulma +module Web.Tag where import Halogen.HTML as HH import Halogen.HTML.Properties as HP