-- | `App.ZoneInterface` provides an interface to display and modify a DNS zone. -- | -- | This interface allows to: -- | - display all resource records of a zone (SOA, NS, A, AAAA, CNAME, TXT, MX, SRV) -- | - TODO: dedicated interfaces for: SPF, DKIM, DMARC -- | - add, modify, remove resource records -- | -- | **WIP**: Display relevant information for each record type in the (add/mod) modal. -- | This includes explaining use cases and displaying an appropriate interface for the -- | task at hand. For example, having a dedicated interface for DKIM. -- | -- | TODO: display errors not only for a record but for the whole zone. -- | A DNS zone is bound by a set of rules, the whole zone must be consistent. -- | For example, a CNAME `target` has to point to the `name` of an existing record. -- | -- | TODO: do not allow for the modification of read-only resource records. -- | -- | TODO: move all serialization code to a single module. module App.Page.Zone where import Prelude (Unit, unit, void , bind, pure , not, comparing, discard, map, show , (+), (&&), ($), (/=), (<<<), (<>), (==), (>), (#)) import Data.Eq (class Eq) import Data.Array as A import Data.Int (fromString) import Data.Tuple (Tuple(..)) import Data.ArrayBuffer.Types (ArrayBuffer) import Data.Array.NonEmpty as NonEmpty import Data.Either (Either(..)) import Data.String.CodePoints as CP -- import Data.Foldable as Foldable import Data.Maybe (Maybe(..), fromMaybe, maybe) import Effect.Aff.Class (class MonadAff) import Halogen as H import Halogen.HTML as HH import Halogen.HTML.Properties as HP import Bulma as Bulma import CSSClasses as C import App.Text.Explanations as Explanations import App.Type.AcceptedRRTypes (AcceptedRRTypes(..)) import App.Type.ResourceRecord (ResourceRecord, emptyRR , show_qualifier, show_qualifier_char , show_mechanism_type, show_mechanism, to_mechanism , show_modifier_type, show_modifier, to_modifier , all_qualifiers , mechanism_types, qualifier_types, modifier_types) import App.Type.ResourceRecord (Mechanism, Modifier, Qualifier(..)) as RR import App.Type.DKIM as DKIM import App.DisplayErrors (error_to_paragraph) import App.Type.LogMessage (LogMessage(..)) import App.Message.DNSManagerDaemon as DNSManager import App.Validation.DNS as Validation type RRId = Int id :: forall a. a -> a id x = x -- | `App.ZoneInterface` can send messages through websocket interface -- | connected to dnsmanagerd. See `App.WS`. -- | -- | Also, this component can log messages and ask its parent (`App.Container`) to -- | reconnect the websocket to `dnsmanagerd`. data Output = MessageToSend ArrayBuffer | Log LogMessage -- | `App.ZoneInterface` can receive messages from `dnsmanagerd`. -- | -- | The component is also informed when the connection is lost or up again. data Query a = MessageReceived DNSManager.AnswerMessage a | ConnectionIsDown a | ConnectionIsUp a type Slot = H.Slot Query Output -- | `App.ZoneInterface` has a single input: the domain name. type Input = String data Field = Field_Domain String | Field_TTL String | Field_Target String | Field_Priority String | Field_Protocol 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 -- | 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. -- | 3. `ValidateRR AcceptedRRTypes`: validate the new RR stored in `_currentRR`. -- | In case it works, automatically call `AddRR` then `CancelModal`. -- | 4. `AddRR AcceptedRRTypes ResourceRecord`: send a message to `dnsmanagerd`. -- | -- | Steps to update an entry: -- | 1. `CreateUpdateRRModal RRId`: create a modal from the values of the RR in `_resources` to update. -- | 2. `UpdateCurrentRR Field`: modify the currently displayed RR. -- | 3. `ValidateLocal RRId AcceptedRRTypes`: validate the RR. -- | 4. `SaveRR ResourceRecord`: save the _validated_ RR by sending a message to `dnsmanagerd`. data Action -- | Initiate the component. This means asking the content of the zone to `dnsmanagerd`. = Initialize -- | Cancel the current displayed modal. | CancelModal -- | Create a new resource record modal (a form) for a certain type of component. | CreateNewRRModal AcceptedRRTypes -- | Create modal (a form) for a resource record to update. | CreateUpdateRRModal RRId -- | Create a modal to ask confirmation before deleting a resource record. | DeleteRRModal RRId -- | Change the current tab. | ChangeTab Tab -- | Update new entry form (in the `rr_modal` modal). | UpdateCurrentRR Field -- | Validate a new resource record before adding it. | ValidateRR AcceptedRRTypes -- | Validate the entries in an already existing resource record. -- | Automatically calls for `SaveRR` once record is verified. | ValidateLocal -- | Add a new resource record to the zone. | AddRR AcceptedRRTypes ResourceRecord -- | Save the changes done in an already existing resource record. | SaveRR ResourceRecord -- | Send a message to remove a resource record. -- | Automatically closes the modal. | RemoveRR RRId -- | Ask a (new) token for a RR. | NewToken RRId -- | Ask `dnsmanagerd` for the generated zone file. | AskZoneFile | 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 | DKIM_hash_algo Int | DKIM_sign_algo Int | DKIM_pubkey String | DKIM_note String data RRModal = NoModal | NewRRModal AcceptedRRTypes | UpdateRRModal | RemoveRRModal RRId show_accepted_type :: AcceptedRRTypes -> String show_accepted_type = case _ of A -> "A" AAAA -> "AAAA" TXT -> "TXT" CNAME -> "CNAME" NS -> "NS" MX -> "MX" SRV -> "SRV" SPF -> "SPF" DKIM -> "DKIM" string_to_acceptedtype :: String -> Maybe AcceptedRRTypes string_to_acceptedtype str = case str of "A" -> Just A "AAAA" -> Just AAAA "TXT" -> Just TXT "CNAME" -> Just CNAME "NS" -> Just NS "MX" -> Just MX "SRV" -> Just SRV "SPF" -> Just SPF "DKIM" -> Just DKIM _ -> Nothing data Tab = Zone | TokenExplanation derive instance eqTab :: Eq Tab type State = { _domain :: String , wsUp :: Boolean -- A modal to present a form for adding a new RR. , rr_modal :: RRModal -- | All resource records. , _resources :: Array ResourceRecord --, _local_errors :: Hash.HashMap RRId (Array Validation.Error) -- Unique RR form. , _currentRR :: ResourceRecord , _currentRR_errors :: Array Validation.Error -- SPF details. , spf_mechanism_q :: String , spf_mechanism_t :: String , spf_mechanism_v :: String , spf_modifier_t :: String , spf_modifier_v :: String , dkim :: DKIM.DKIM , _zonefile :: Maybe String , current_tab :: Tab } component :: forall m. MonadAff m => H.Component Query Input Output m component = H.mkComponent { initialState , render , eval: H.mkEval $ H.defaultEval { initialize = Just Initialize , handleAction = handleAction , handleQuery = handleQuery } } -- | Default available domain: netlib.re. default_domain :: String default_domain = "netlib.re" default_rr_A :: ResourceRecord default_rr_A = emptyRR { rrtype = "A", name = "www", target = "192.0.2.1" } default_empty_rr :: ResourceRecord default_empty_rr = default_rr_A default_qualifier_str = "hard_fail" :: String initialState :: Input -> State initialState domain = { wsUp: true , rr_modal: NoModal , _domain: 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: [] , _zonefile: Nothing , spf_mechanism_q: "pass" , spf_mechanism_t: "a" , spf_mechanism_v: "" , spf_modifier_t: "redirect" , spf_modifier_v: "" , dkim: DKIM.emptyDKIMRR , current_tab: Zone } type SortableRecord l = Record (rrtype :: String, rrid :: Int | l) render :: forall m. State -> H.ComponentHTML Action () m render state = Bulma.section_small [ fancy_tab , case state.current_tab of Zone -> render_zone TokenExplanation -> Explanations.tokens ] where fancy_tab = Bulma.fancy_tabs [ Bulma.tab_entry (is_tab_active Zone) "Zone" (ChangeTab Zone) , Bulma.tab_entry (is_tab_active TokenExplanation) "Tokens? ๐Ÿคจ" (ChangeTab TokenExplanation) ] is_tab_active tab = state.current_tab == tab render_zone = case state.wsUp, state.rr_modal of false, _ -> Bulma.p "You are disconnected." true, RemoveRRModal rr_id -> modal_rr_delete rr_id true, NewRRModal _ -> render_current_rr_modal true, UpdateRRModal -> render_current_rr_modal true, NoModal -> HH.div_ [ Bulma.h1 state._domain , Bulma.hr , render_resources $ sorted state._resources , Bulma.hr , render_new_records state , render_zonefile state._zonefile ] sorted :: forall l. Array (SortableRecord (l)) -> Array (SortableRecord (l)) sorted array = A.groupAllBy (comparing (_.rrtype)) array -- [x2 z2 x1 y z1 z3] -> [ NE[x2x1] NE[y] NE[z2z1z3] ] # map NonEmpty.toArray -- -> [[x2x1][y][z2z1z3]] # 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 = Bulma.modal "Deleting a resource record" [warning_message] [modal_delete_button, Bulma.cancel_button CancelModal] where modal_delete_button = Bulma.alert_btn "Delete the resource record." (RemoveRR rr_id) warning_message = HH.p [] [ HH.text "You are about to delete a resource record, this actions is " , Bulma.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 (foot_content A) "AAAA" -> template modal_content_simple (foot_content AAAA) "TXT" -> template modal_content_simple (foot_content TXT) "CNAME" -> template modal_content_simple (foot_content CNAME) "NS" -> template modal_content_simple (foot_content NS) "MX" -> template modal_content_mx (foot_content MX) "SRV" -> template modal_content_srv (foot_content SRV) "SPF" -> template modal_content_spf (foot_content SPF) "DKIM" -> template modal_content_dkim (foot_content DKIM) _ -> Bulma.p $ "Invalid type: " <> state._currentRR.rrtype where -- DRY updateForm x = UpdateCurrentRR <<< x render_errors = if A.length state._currentRR_errors > 0 then HH.div_ $ [ Bulma.h3 "Errors: " ] <> map error_to_paragraph state._currentRR_errors else HH.div_ [ ] modal_content_simple :: Array (HH.HTML w Action) modal_content_simple = [ render_errors , Bulma.input_with_side_text ("domain" <> state._currentRR.rrtype) "Name" "www" (updateForm Field_Domain) state._currentRR.name display_domain_side , Bulma.box_input ("ttl" <> state._currentRR.rrtype) "TTL" "600" (updateForm Field_TTL) (show state._currentRR.ttl) should_be_disabled , Bulma.box_input ("target" <> state._currentRR.rrtype) "Target" "198.51.100.5" (updateForm Field_Target) state._currentRR.target should_be_disabled ] <> case state.rr_modal of UpdateRRModal -> if A.elem state._currentRR.rrtype ["A", "AAAA"] then [ Bulma.labeled_field ("token" <> state._currentRR.rrtype) "Token" (Bulma.p $ fromMaybe "โŒโ€‹" state._currentRR.token) ] else [] _ -> [] modal_content_mx :: Array (HH.HTML w Action) modal_content_mx = [ render_errors , Bulma.input_with_side_text "domainMX" "Name" "www" (updateForm Field_Domain) state._currentRR.name display_domain_side , Bulma.box_input ("ttlMX") "TTL" "600" (updateForm Field_TTL) (show state._currentRR.ttl) should_be_disabled , Bulma.box_input ("targetMX") "Target" "www" (updateForm Field_Target) state._currentRR.target should_be_disabled , Bulma.box_input ("priorityMX") "Priority" "10" (updateForm Field_Priority) (maybe "" show state._currentRR.priority) should_be_disabled ] modal_content_srv :: Array (HH.HTML w Action) modal_content_srv = [ Bulma.div_content [Bulma.explanation Explanations.srv_introduction] , render_errors , Bulma.box_input "domainSRV" "Service name" "service name" (updateForm Field_Domain) state._currentRR.name should_be_disabled , Bulma.box_input ("protocolSRV") "Protocol" "tcp" (updateForm Field_Protocol) (fromMaybe "tcp" state._currentRR.protocol) should_be_disabled , Bulma.box_input ("targetSRV") "Where the server is" "www" (updateForm Field_Target) state._currentRR.target should_be_disabled , Bulma.box_input ("portSRV") "Port of the service" "5061" (updateForm Field_Port) (maybe "" show state._currentRR.port) should_be_disabled , Bulma.box_input ("prioritySRV") "Priority" "10" (updateForm Field_Priority) (maybe "" show state._currentRR.priority) should_be_disabled , Bulma.box_input ("ttlSRV") "TTL" "600" (updateForm Field_TTL) (show state._currentRR.ttl) should_be_disabled , Bulma.box_input ("weightSRV") "Weight" "100" (updateForm Field_Weight) (maybe "" show state._currentRR.weight) should_be_disabled ] modal_content_spf :: Array (HH.HTML w Action) modal_content_spf = [ Bulma.div_content [Bulma.explanation Explanations.spf_introduction] , render_errors , Bulma.input_with_side_text "domainSPF" "Name" "Let this alone." (updateForm Field_Domain) state._currentRR.name display_domain_side , Bulma.box_input "ttlSPF" "TTL" "600" (updateForm Field_TTL) (show state._currentRR.ttl) should_be_disabled --, case state._currentRR.v of -- Nothing -> Bulma.p "default value for the version (spf1)" -- Just v -> Bulma.box_input "vSPF" "Version" "spf1" (updateForm Field_SPF_v) v should_be_disabled , Bulma.hr , maybe (Bulma.p "no mechanism") display_mechanisms state._currentRR.mechanisms , Bulma.box [ Bulma.h3 "New mechanism" , Bulma.selection_field "idMechanismQ" "Policy" SPF_Mechanism_q qualifier_types state.spf_mechanism_q , Bulma.selection_field "idMechanismT" "Type" SPF_Mechanism_t mechanism_types state.spf_mechanism_t , Bulma.box_input "valueNewMechanismSPF" "Value" "" SPF_Mechanism_v state.spf_mechanism_v should_be_disabled , Bulma.btn "Add" SPF_Mechanism_Add ] , Bulma.hr , maybe (Bulma.p "no modifier") display_modifiers state._currentRR.modifiers , Bulma.box [ Bulma.h3 "New modifier" , Bulma.selection_field "idModifierT" "Modifier" SPF_Modifier_t modifier_types state.spf_modifier_t , Bulma.box_input "valueNewModifierSPF" "Value" "" SPF_Modifier_v state.spf_modifier_v should_be_disabled , Bulma.btn "Add" SPF_Modifier_Add ] , Bulma.hr , Bulma.box [ Bulma.h3 "Default behavior" , Bulma.div_content [Bulma.explanation Explanations.spf_default_behavior] , Bulma.selection SPF_Qualifier qualifier_types (maybe default_qualifier_str show_qualifier state._currentRR.q) ] ] modal_content_dkim :: Array (HH.HTML w Action) modal_content_dkim = [ Bulma.div_content [Bulma.explanation Explanations.dkim_introduction] , render_errors , Bulma.input_with_side_text "domainDKIM" "Name" "default._domainkey" (updateForm Field_Domain) state._currentRR.name display_domain_side , Bulma.box_input "ttlDKIM" "TTL" "600" (updateForm Field_TTL) (show state._currentRR.ttl) should_be_disabled , Bulma.hr , Bulma.div_content [Bulma.explanation Explanations.dkim_default_algorithms] , Bulma.selection_field "idDKIMSignature" "Signature algo" DKIM_sign_algo (map DKIM.show_signature_algorithm DKIM.sign_algos) (DKIM.show_signature_algorithm $ fromMaybe DKIM.RSA state.dkim.k) , Bulma.selection_field "idDKIMHash" "Hash algo" DKIM_hash_algo (map DKIM.show_hashing_algorithm DKIM.hash_algos) (DKIM.show_hashing_algorithm $ fromMaybe DKIM.SHA256 state.dkim.h) , Bulma.box_input "pkDKIM" "Public Key" "Your public key, such as 'MIIBIjANBgqh...'" DKIM_pubkey state.dkim.p should_be_disabled , Bulma.box_input "noteDKIM" "Note" "Note for fellow administrators." DKIM_note (fromMaybe "" state.dkim.n) should_be_disabled ] display_domain_side = (if state._currentRR.name == (state._domain <> ".") then "" else "." <> state._domain) should_be_disabled = (if true then (HP.enabled true) else (HP.disabled true)) newtokenbtn = Bulma.btn "๐Ÿโ€‹ Ask for a token!" (NewToken state._currentRR.rrid) foot_content x = case state.rr_modal of NewRRModal _ -> [Bulma.btn_add (ValidateRR x)] UpdateRRModal -> [Bulma.btn_save ValidateLocal] <> case x of A -> [newtokenbtn] AAAA -> [newtokenbtn] _ -> [] _ -> [Bulma.p "state.rr_modal should either be NewRRModal or UpdateRRModal."] template content foot_ = Bulma.modal title content foot where title = case state.rr_modal of NoModal -> "Error: no modal should be displayed" NewRRModal t_ -> "New " <> show_accepted_type t_ <> " resource record" UpdateRRModal -> "Update RR " <> show state._currentRR.rrid RemoveRRModal rr_id -> "Error: should display removal modal instead (for RR " <> show rr_id <> ")" foot = foot_ <> [Bulma.cancel_button CancelModal] handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit handleAction = case _ of -- | Cancel the current modal being presented. -- | Works for both "new RR", "update RR" and "remove RR" modals. CancelModal -> do H.modify_ _ { rr_modal = NoModal } H.modify_ _ { _currentRR_errors = [] } -- | Create the RR modal. DeleteRRModal rr_id -> do H.modify_ _ { rr_modal = RemoveRRModal rr_id } -- | Change the current tab. ChangeTab new_tab -> do H.modify_ _ { current_tab = new_tab } -- | Create modal (a form) for a resource record to update. CreateUpdateRRModal rr_id -> do state <- H.get case first (\rr -> rr.rrid == rr_id) state._resources of Nothing -> H.raise $ Log $ ErrorLog $ "RR not found (RR " <> show rr_id <> ")" Just rr -> do H.modify_ _ { _currentRR = rr } _ <- case rr.rrtype of "DKIM" -> H.modify_ _ { dkim = fromMaybe DKIM.emptyDKIMRR rr.dkim } _ -> pure unit H.modify_ _ { rr_modal = UpdateRRModal } -- | 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 = "www", target = "2001:db8::1" } default_rr_TXT = emptyRR { rrtype = "TXT", name = "txt", target = "some text" } default_rr_CNAME = emptyRR { rrtype = "CNAME", name = "blog", target = "www" } default_rr_NS = emptyRR { rrtype = "NS", name = (state._domain <> "."), target = "ns0.example.com." } default_rr_MX = emptyRR { rrtype = "MX", name = "mail", target = "www", priority = Just 10 } default_rr_SRV = emptyRR { rrtype = "SRV", name = "voip", target = "server1" , port = Just 5061, weight = Just 100, priority = Just 10, protocol = Just "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 = "" } 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 } SRV -> H.modify_ _ { _currentRR = default_rr_SRV } SPF -> H.modify_ _ { _currentRR = default_rr_SPF } DKIM -> H.modify_ _ { _currentRR = default_rr_DKIM } -- | Initialize the ZoneInterface component: ask for the domain zone to `dnsmanagerd`. Initialize -> do { _domain } <- H.get H.raise $ Log $ SystemLog $ "Asking the domain " <> _domain message <- H.liftEffect $ DNSManager.serialize $ DNSManager.MkGetZone { domain: _domain } H.raise $ MessageToSend message -- | Perform validation. In case the record is valid, it is added to the zone then the modal is closed. -- | 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 } -- 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`. _ <- case t of DKIM -> H.modify_ \state -> state { _currentRR { dkim = Just state.dkim } } _ -> pure unit state <- H.get case Validation.validation state._currentRR of Left actual_errors -> do -- H.raise $ Log $ ErrorLog $ "Cannot add this " <> show t <> " RR, some errors occured in the record:" -- loopE (\v -> H.raise $ Log $ ErrorLog $ "==> " <> show_error v) actual_errors H.modify_ _ { _currentRR_errors = actual_errors } Right newrr -> do H.modify_ _ { _currentRR_errors = [], dkim = DKIM.emptyDKIMRR } handleAction $ AddRR t newrr handleAction CancelModal -- | Try to add a resource record to the zone. -- | Can fail if the content of the form isn't valid. AddRR t newrr -> do state <- H.get H.raise $ Log $ SystemLog $ "Add new " <> show t message <- H.liftEffect $ DNSManager.serialize $ DNSManager.MkAddRR { domain: state._domain, rr: newrr } H.raise $ MessageToSend message -- | 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 } -- | 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 } -- Since _currentRR.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 } } _ -> pure unit state <- H.get case Validation.validation state._currentRR of Left actual_errors -> do H.modify_ _ { _currentRR_errors = actual_errors } Right rr -> do H.modify_ _ { _currentRR_errors = [] } handleAction $ SaveRR rr SaveRR rr -> do state <- H.get H.raise $ Log $ SystemLog $ "Updating RR " <> show rr.rrid message <- H.liftEffect $ DNSManager.serialize $ DNSManager.MkUpdateRR { domain: state._domain, rr: rr } H.raise $ MessageToSend message RemoveRR rr_id -> do { _domain } <- H.get H.raise $ Log $ SystemLog $ "Ask to remove rr (rrid: " <> show rr_id <> ")" -- Send a removal message. message <- H.liftEffect $ DNSManager.serialize $ DNSManager.MkDeleteRR { domain: _domain, rrid: rr_id } H.raise $ MessageToSend message -- Modal doesn't need to be active anymore. handleAction CancelModal NewToken rr_id -> do { _domain } <- H.get H.raise $ Log $ SystemLog $ "Ask a token for rrid " <> show rr_id -- Send a NewToken message. message <- H.liftEffect $ DNSManager.serialize $ DNSManager.MkNewToken { domain: _domain, rrid: rr_id } H.raise $ MessageToSend message AskZoneFile -> do state <- H.get H.raise $ Log $ SystemLog $ "Asking for the '" <> state._domain <> "' zonefile" message <- H.liftEffect $ DNSManager.serialize $ DNSManager.MkAskGeneratedZoneFile { domain: state._domain } H.raise $ MessageToSend message 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 = all_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 } } 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 } } 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 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 }} SPF_Modifier_Add -> do state <- H.get let m = state._currentRR.modifiers m_t = state.spf_modifier_t m_v = state.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 }} DKIM_hash_algo v -> H.modify_ _ { dkim { h = DKIM.hash_algos A.!! v } } DKIM_sign_algo v -> H.modify_ _ { dkim { k = DKIM.sign_algos A.!! v } } DKIM_pubkey v -> H.modify_ _ { dkim { p = v } } DKIM_note v -> H.modify_ _ { dkim { n = Just v } } where -- In case the `name` part of the resource record is empty replace it with the domain name. replace_name domain rr = case rr.name of "" -> rr { name = domain <> "." } _ -> rr handleQuery :: forall a m. MonadAff m => Query a -> H.HalogenM State Action () Output m (Maybe a) handleQuery = case _ of MessageReceived message a -> do case message of (DNSManager.MkRRUpdated response) -> do replace_entry response.rr -- When an update is received for a record, it means -- the update request has been accepted, the current modal can be closed. H.modify_ _ { rr_modal = NoModal } (DNSManager.MkRRAdded response) -> do state <- H.get H.put $ add_RR state response.rr (DNSManager.MkRRDeleted response) -> do -- Remove the resource record. state <- H.get H.modify_ _ { _resources = A.filter (\rr -> rr.rrid /= response.rrid) state._resources } (DNSManager.MkGeneratedZoneFile response) -> do H.modify_ _ { _zonefile = Just response.zonefile } (DNSManager.MkZone response) -> do add_entries response.zone.resources _ -> H.raise $ Log $ ErrorLog $ "Message not handled in ZoneInterface." pure (Just a) ConnectionIsDown a -> do H.modify_ _ { wsUp = false } pure (Just a) ConnectionIsUp a -> do H.modify_ _ { wsUp = true } pure (Just a) where -- replace_entry :: ResourceRecord replace_entry new_rr = do state <- H.get H.modify_ _ { _resources = A.filter (\rr -> rr.rrid /= new_rr.rrid) state._resources } new_state <- H.get H.put $ add_RR new_state new_rr add_entries [] = pure unit add_entries arr = do case A.head arr, A.tail arr of Nothing, _ -> pure unit Just new_rr, tail -> do state <- H.get H.put $ add_RR state new_rr add_entries $ fromMaybe [] tail add_RR :: State -> ResourceRecord -> State add_RR state new_rr = state { _resources = (state._resources <> [ new_rr ]) } -- Rendering bg_color_ro = C.has_background_warning_light :: Array HH.ClassName tag :: forall w i. String -> HH.HTML w i tag str = HH.span [HP.classes (C.tag <> C.is_dark)] [HH.text str] tag_ro :: forall w i. String -> HH.HTML w i tag_ro str = HH.span [HP.classes (C.tag <> C.is_warning)] [HH.text str] tags :: forall w i. Array (HH.HTML w i) -> HH.HTML w i tags xs = HH.span [HP.classes (C.tags <> C.no_margin_bottom <> C.no_padding_bottom)] xs -- | Render all Resource Records. render_resources :: forall w. Array ResourceRecord -> HH.HTML w Action render_resources [] = Bulma.box [Bulma.zone_rr_title "Resource records", Bulma.subtitle "No records for now"] render_resources records = HH.div_ $ (rr_box tag_soa bg_color_ro Bulma.soa_table_header table_content all_soa_rr) <> (rr_box tag_basic [] Bulma.simple_table_header table_content_w_seps all_basic_rr) <> (rr_box tag_mx [] Bulma.mx_table_header table_content all_mx_rr) <> (rr_box tag_srv [] Bulma.srv_table_header table_content all_srv_rr) <> (rr_box tag_spf [] Bulma.spf_table_header table_content all_spf_rr) <> (rr_box tag_dkim [] Bulma.dkim_table_header table_content all_dkim_rr) <> (rr_box tag_basic_ro bg_color_ro Bulma.simple_table_header_ro table_content_w_seps all_basic_ro_rr) where all_basic_rr = A.filter (\rr -> A.elem rr.rrtype baseRecords && not rr.readonly) records all_basic_ro_rr = A.filter (\rr -> A.elem rr.rrtype baseRecords && rr.readonly) records all_XX_rr str = A.filter (\rr -> rr.rrtype == str) records all_soa_rr = all_XX_rr "SOA" all_mx_rr = all_XX_rr "MX" all_srv_rr = all_XX_rr "SRV" all_spf_rr = all_XX_rr "SPF" all_dkim_rr = all_XX_rr "DKIM" tag_soa = tags [tag_ro "SOA", tag_ro "read only"] tag_basic = tags [tag "Basic RRs (A, AAAA, PTR, NS, TXT)"] tag_mx = tags [tag "MX"] tag_srv = tags [tag "SRV"] tag_spf = tags [tag "SPF"] tag_dkim = tags [tag "DKIM"] tag_basic_ro = tags [tag_ro "Basic RRs", tag_ro "read only"] rr_box :: HH.HTML w Action -- box title (type of data) -> Array HH.ClassName -> HH.HTML w Action -- table title -> (Array ResourceRecord -> HH.HTML w Action) -> Array ResourceRecord -> Array (HH.HTML w Action) rr_box title colors header dp rrs = if A.length rrs > 0 then [ Bulma.box_ (C.no_padding_left <> C.no_padding_top <> colors) [title, Bulma.table_ (C.margin_left 3) [] [header, dp rrs]] ] else [] --title_col_props = C.is 1 table_content_w_seps records_ = HH.tbody_ $ A.groupAllBy (comparing (_.rrtype)) records_ -- [x x y y z] -> [NE[xx], NE[yy], NE[z]] # map NonEmpty.toArray -- -> [[xx], [yy], [z]] # map (map rows) -- -> [[hh], [hh], [h]] ('h' means 'html') # A.intersperse [emptyline] -- -> [[hh], [line], [hh], [line], [h]] # A.concat -- -> [h h line h h line h] emptyline = HH.tr_ [ Bulma.txt_name "", HH.td_ [], HH.td_ [], HH.td_ [], HH.td_ [], HH.td_ [] ] table_content records_ = HH.tbody_ $ map rows records_ rows rr = if rr.readonly then HH.tr [ HP.classes C.has_background_warning_light ] $ render_row rr else HH.tr_ $ render_row rr render_row :: ResourceRecord -> Array (HH.HTML w Action) render_row rr = case rr.rrtype of "SOA" -> [ HH.td_ [ HH.text rr.name ] , HH.td_ [ HH.text $ show rr.ttl ] , HH.td_ [ HH.text rr.target ] , HH.td_ [ HH.text $ maybe "" id rr.mname ] , HH.td_ [ HH.text $ maybe "" id rr.rname ] , HH.td_ [ HH.text $ maybe "" show rr.serial ] , HH.td_ [ HH.text $ maybe "" show rr.refresh ] , HH.td_ [ HH.text $ maybe "" show rr.retry ] , HH.td_ [ HH.text $ maybe "" show rr.expire ] , HH.td_ [ HH.text $ maybe "" show rr.minttl ] ] "SRV" -> [ HH.td_ [ Bulma.p rr.name ] , HH.td_ [ Bulma.p $ maybe "" id rr.protocol ] , HH.td_ [ Bulma.p rr.target ] , HH.td_ [ Bulma.p $ maybe "" show rr.port ] , HH.td_ [ Bulma.p $ show rr.ttl ] , HH.td_ [ Bulma.p $ maybe "" show rr.priority ] , HH.td_ [ Bulma.p $ maybe "" show rr.weight ] , if rr.readonly then HH.td_ [ Bulma.btn_readonly ] else HH.td_ [ Bulma.btn_modify (CreateUpdateRRModal rr.rrid), Bulma.btn_delete (DeleteRRModal rr.rrid) ] ] "SPF" -> [ HH.td_ [ Bulma.p rr.name ] , HH.td_ [ Bulma.p $ show rr.ttl ] -- , HH.td_ [ Bulma.p $ maybe "(default)" id rr.v ] -- For now, version isn't displayed. , HH.td_ [ Bulma.p $ maybe "" (A.fold <<< A.intersperse " " <<< map show_mechanism) rr.mechanisms ] , HH.td_ [ Bulma.p $ maybe "" (A.fold <<< A.intersperse " " <<< map show_modifier) rr.modifiers ] , HH.td_ [ Bulma.p $ maybe "" fancy_qualifier_display rr.q ] , if rr.readonly then HH.td_ [ Bulma.btn_readonly ] else HH.td_ [ Bulma.btn_modify (CreateUpdateRRModal rr.rrid), Bulma.btn_delete (DeleteRRModal rr.rrid) ] ] "DKIM" -> [ HH.td_ [ Bulma.p rr.name ] , HH.td_ [ Bulma.p $ show rr.ttl ] ] <> case rr.dkim of Just dkim -> [ -- , HH.td_ [ Bulma.p $ maybe "(default)" id rr.v ] -- For now, version isn't displayed. Assume DKIM1. HH.td_ [ Bulma.p $ maybe "" DKIM.show_hashing_algorithm dkim.h ] , HH.td_ [ Bulma.p $ maybe "" DKIM.show_signature_algorithm dkim.k ] , HH.td_ [ Bulma.p $ CP.take 5 dkim.p ] , HH.td_ [ Bulma.p $ fromMaybe "" dkim.n ] , if rr.readonly then HH.td_ [ Bulma.btn_readonly ] else HH.td_ [ Bulma.btn_modify (CreateUpdateRRModal rr.rrid), Bulma.btn_delete (DeleteRRModal rr.rrid) ] ] Nothing -> [Bulma.p "Problem: there is no DKIM data." ] "MX" -> [ HH.td_ [ Bulma.p rr.name ] , HH.td_ [ Bulma.p $ show rr.ttl ] , HH.td_ [ Bulma.p $ maybe "" show rr.priority ] , HH.td_ [ Bulma.p rr.target ] , if rr.readonly then HH.td_ [ Bulma.btn_readonly ] else HH.td_ [ Bulma.btn_modify (CreateUpdateRRModal rr.rrid), Bulma.btn_delete (DeleteRRModal rr.rrid) ] ] _ -> [ Bulma.txt_name rr.rrtype , HH.td_ [ Bulma.p rr.name ] , HH.td_ [ Bulma.p $ show rr.ttl ] , HH.td_ [ Bulma.p rr.target ] ] <> if rr.readonly then [ HH.td_ [ Bulma.btn_readonly ] ] else [ HH.td_ [ Bulma.btn_modify (CreateUpdateRRModal rr.rrid), Bulma.btn_delete (DeleteRRModal rr.rrid) ] , HH.td_ [ maybe (show_token_or_btn rr) Bulma.p rr.token ] ] show_token_or_btn rr = case rr.rrtype of "A" -> Bulma.btn_ (C.is_small) "๐Ÿโ€‹ Ask for a token!" (NewToken rr.rrid) "AAAA" -> Bulma.btn_ (C.is_small) "๐Ÿโ€‹ Ask for a token!" (NewToken rr.rrid) _ -> HH.text "" fancy_qualifier_display :: RR.Qualifier -> String fancy_qualifier_display qualifier = "(" <> show_qualifier_char qualifier <> ") " <> show_qualifier qualifier display_mechanisms :: forall w. Array RR.Mechanism -> HH.HTML w Action display_mechanisms ms = Bulma.box_ C.has_background_warning_light [ Bulma.table [] [ Bulma.mechanism_table_header, HH.tbody_ $ map render_mechanism_row $ attach_id 0 ms] ] where render_mechanism_row :: (Tuple Int RR.Mechanism) -> HH.HTML w Action render_mechanism_row (Tuple i m) = HH.tr_ [ Bulma.txt_name $ maybe "" show_qualifier m.q , HH.td_ [ Bulma.p $ show_mechanism_type m.t ] , HH.td_ [ Bulma.p m.v ] , HH.td_ [ Bulma.alert_btn "x" (SPF_remove_mechanism i) ] ] display_modifiers :: forall w. Array RR.Modifier -> HH.HTML w Action display_modifiers ms = Bulma.box_ C.has_background_warning_light [ Bulma.table [] [ Bulma.modifier_table_header, HH.tbody_ $ map render_modifier_row $ attach_id 0 ms] ] where render_modifier_row :: (Tuple Int RR.Modifier) -> HH.HTML w Action render_modifier_row (Tuple i m) = HH.tr_ [ HH.td_ [ Bulma.p $ show_modifier_type m.t ] , HH.td_ [ Bulma.p m.v ] , HH.td_ [ Bulma.alert_btn "x" (SPF_remove_modifier i) ] ] baseRecords :: Array String baseRecords = [ "A", "AAAA", "CNAME", "TXT", "NS" ] -- Component definition and initial state render_new_records :: forall (w :: Type). State -> HH.HTML w Action render_new_records _ = Bulma.hdiv [ Bulma.h1 "Adding new records" -- use "level" to get horizontal buttons next to each other (probably vertical on mobile) , Bulma.level [ Bulma.btn "A" (CreateNewRRModal A) , Bulma.btn "AAAA" (CreateNewRRModal AAAA) , Bulma.btn "TXT" (CreateNewRRModal TXT) , Bulma.btn "CNAME" (CreateNewRRModal CNAME) , Bulma.btn "NS" (CreateNewRRModal NS) , Bulma.btn "MX" (CreateNewRRModal MX) , Bulma.btn "SRV" (CreateNewRRModal SRV) ] [] , Bulma.hr , Bulma.h1 "Special records about the mail system (soon)" -- use "level" to get horizontal buttons next to each other (probably vertical on mobile) , Bulma.level [ Bulma.btn "SPF" (CreateNewRRModal SPF) , Bulma.btn "DKIM" (CreateNewRRModal DKIM) , Bulma.btn_ro (C.is_small <> C.is_warning) "DMARC" ] [] , Bulma.hr , Bulma.level [ Bulma.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 = Bulma.box [ maybe (HH.text "") (\x -> HH.pre_ [HH.text x]) zonefile ] -- ACTIONS first :: forall a. (a -> Boolean) -> Array a -> Maybe a first condition = A.head <<< (A.filter condition) loopE :: forall state action input output m a b . (a -> H.HalogenM state action input output m b) -> Array a -> H.HalogenM state action input output m Unit loopE f a = case (A.head a) of Nothing -> pure unit Just x -> do void $ f x case (A.tail a) of Nothing -> pure unit Just xs -> loopE f xs update_field :: ResourceRecord -> Field -> ResourceRecord update_field rr updated_field = case updated_field of Field_Domain val -> rr { name = val } Field_Target val -> rr { target = val } Field_TTL val -> rr { ttl = fromMaybe 0 (fromString val) } Field_Priority val -> rr { priority = fromString val } Field_Protocol val -> rr { protocol = Just 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 } attach_id :: forall a. Int -> Array a -> Array (Tuple Int a) attach_id _ [] = [] attach_id i arr = case A.head arr of Just x -> [Tuple i x] <> attach_id (i + 1) (fromMaybe [] $ A.tail arr) Nothing -> [] remove_id :: forall a. Int -> Array (Tuple Int a) -> Array a remove_id _ [] = [] remove_id i arr = case A.head arr of Just (Tuple n x) -> if i == n then remove_id i (fromMaybe [] $ A.tail arr) else [x] <> remove_id i (fromMaybe [] $ A.tail arr) Nothing -> []