WIP: new page for the migration.
This commit is contained in:
		
							parent
							
								
									d7e7832555
								
							
						
					
					
						commit
						78a652807c
					
				
					 4 changed files with 254 additions and 21 deletions
				
			
		| 
						 | 
				
			
			@ -31,16 +31,14 @@
 | 
			
		|||
-- |
 | 
			
		||||
-- | Validation:
 | 
			
		||||
-- | - registration page: login, password, mail
 | 
			
		||||
-- | - login and password recovery page: TODO
 | 
			
		||||
-- | - mail verification: TODO
 | 
			
		||||
-- | - login and password recovery page
 | 
			
		||||
-- | - mail verification
 | 
			
		||||
-- | - domain list: domain (`label`) is insufficient.
 | 
			
		||||
-- |
 | 
			
		||||
-- | TODO: when reading a RR `name`, always make it an FQDN by adding `<user-domain>.netlib.re.`.
 | 
			
		||||
-- |
 | 
			
		||||
-- | TODO: remove the FQDN when showing RR names.
 | 
			
		||||
-- |
 | 
			
		||||
-- | TODO: application-level heartbeat to avoid disconnections.
 | 
			
		||||
-- |
 | 
			
		||||
-- | Untested features:
 | 
			
		||||
-- | - mail recovery, password change
 | 
			
		||||
module App.Container where
 | 
			
		||||
| 
						 | 
				
			
			@ -79,6 +77,7 @@ import App.Page.Setup          as SetupInterface
 | 
			
		|||
import App.Page.DomainList     as DomainListInterface
 | 
			
		||||
import App.Page.Zone           as ZoneInterface
 | 
			
		||||
import App.Page.Home           as HomeInterface
 | 
			
		||||
import App.Page.Migration      as MigrationInterface
 | 
			
		||||
import App.Page.Navigation     as NavigationInterface
 | 
			
		||||
 | 
			
		||||
import App.Text.Explanations   as Explanations
 | 
			
		||||
| 
						 | 
				
			
			@ -144,6 +143,9 @@ data Action
 | 
			
		|||
  -- | Handle events from `ZoneInterface`.
 | 
			
		||||
  | ZoneInterfaceEvent            ZoneInterface.Output
 | 
			
		||||
 | 
			
		||||
  -- | Handle events from `MigrationInterface`.
 | 
			
		||||
  | MigrationInterfaceEvent       MigrationInterface.Output
 | 
			
		||||
 | 
			
		||||
  -- | Disconnect from both `authd` and `dnsmanagerd` (remove sockets),
 | 
			
		||||
  -- | then return to the home page.
 | 
			
		||||
  | Disconnection
 | 
			
		||||
| 
						 | 
				
			
			@ -232,6 +234,7 @@ type ChildSlots =
 | 
			
		|||
  , setupi  :: SetupInterface.Slot Unit
 | 
			
		||||
  , dli     :: DomainListInterface.Slot Unit
 | 
			
		||||
  , zi      :: ZoneInterface.Slot Unit
 | 
			
		||||
  , mi      :: MigrationInterface.Slot Unit
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
_ho      = Proxy :: Proxy "ho"       -- Home Interface
 | 
			
		||||
| 
						 | 
				
			
			@ -246,6 +249,7 @@ _admini  = Proxy :: Proxy "admini"   -- Administration Interface
 | 
			
		|||
_setupi  = Proxy :: Proxy "setupi"   -- Setup Interface
 | 
			
		||||
_dli     = Proxy :: Proxy "dli"      -- Domain List
 | 
			
		||||
_zi      = Proxy :: Proxy "zi"       -- Zone Interface
 | 
			
		||||
_mi      = Proxy :: Proxy "mi"       -- Migration Interface
 | 
			
		||||
 | 
			
		||||
component :: forall q i o m. MonadAff m => H.Component q i o m
 | 
			
		||||
component =
 | 
			
		||||
| 
						 | 
				
			
			@ -287,6 +291,7 @@ render state
 | 
			
		|||
        Zone domain    -> render_zone domain
 | 
			
		||||
        Setup          -> render_setup
 | 
			
		||||
        Administration -> render_authd_admin_interface
 | 
			
		||||
        Migration      -> render_migration
 | 
			
		||||
        LegalNotice    -> render_legal_notice
 | 
			
		||||
    -- The footer includes logs and both the WS child components.
 | 
			
		||||
    , Bulma.hr
 | 
			
		||||
| 
						 | 
				
			
			@ -341,6 +346,9 @@ render state
 | 
			
		|||
    render_authd_admin_interface :: forall monad. MonadAff monad => H.ComponentHTML Action ChildSlots monad
 | 
			
		||||
    render_authd_admin_interface = HH.slot _admini unit AdminInterface.component unit AdministrationEvent
 | 
			
		||||
 | 
			
		||||
    render_migration :: forall monad. MonadAff monad => H.ComponentHTML Action ChildSlots monad
 | 
			
		||||
    render_migration = HH.slot_ _mi unit MigrationInterface.component unit
 | 
			
		||||
 | 
			
		||||
    render_legal_notice :: forall monad. MonadAff monad => H.ComponentHTML Action ChildSlots monad
 | 
			
		||||
    render_legal_notice
 | 
			
		||||
      = Bulma.section_small [ Explanations.legal_notice
 | 
			
		||||
| 
						 | 
				
			
			@ -421,6 +429,9 @@ handleAction = case _ of
 | 
			
		|||
 | 
			
		||||
    H.modify_ _ { current_page = page }
 | 
			
		||||
 | 
			
		||||
    -- Finally, when changing page, the notification should be discarded.
 | 
			
		||||
    handleAction CloseNotif
 | 
			
		||||
 | 
			
		||||
  Log message -> do
 | 
			
		||||
    _ <- case message of
 | 
			
		||||
           UnableToSend err -> handleAction $ AddNotif $ BadNotification err
 | 
			
		||||
| 
						 | 
				
			
			@ -747,16 +758,29 @@ handleAction = case _ of
 | 
			
		|||
          (AuthD.GotToken msg) -> do
 | 
			
		||||
            handleAction $ Log $ SuccessLog $ "Authenticated to authd."
 | 
			
		||||
            H.modify_ _ { token = Just msg.token
 | 
			
		||||
                        , user_data = Just (Tuple msg.current_email msg.pending_email) }
 | 
			
		||||
                        , user_data = Just (Tuple msg.current_email msg.pending_email)
 | 
			
		||||
                        }
 | 
			
		||||
            handleAction $ ToggleAuthenticated (Just msg.token)
 | 
			
		||||
 | 
			
		||||
            sessionstorage <- H.liftEffect $ Window.sessionStorage =<< HTML.window
 | 
			
		||||
            _ <- H.liftEffect $ Storage.setItem "user-authd-token" msg.token sessionstorage
 | 
			
		||||
 | 
			
		||||
            handleAction AuthenticateToDNSManager
 | 
			
		||||
 | 
			
		||||
            -- In case the account doesn't have a valid email address, the user
 | 
			
		||||
            -- shouldn't be able to do anything else than to add their address.
 | 
			
		||||
            case msg.current_email of
 | 
			
		||||
              Nothing -> handleAction $ Routing Migration
 | 
			
		||||
              _       -> pure unit
 | 
			
		||||
 | 
			
		||||
          (AuthD.GotKeepAlive _) -> pure unit
 | 
			
		||||
    pure unit
 | 
			
		||||
 | 
			
		||||
  -- TODO
 | 
			
		||||
  MigrationInterfaceEvent ev -> case ev of
 | 
			
		||||
    MigrationInterface.MessageToSend message  -> H.tell _ws_dns unit (WS.ToSend message)
 | 
			
		||||
    MigrationInterface.Log message            -> handleAction $ Log message
 | 
			
		||||
 | 
			
		||||
  -- | Send a received authentication daemon message `AuthD.AnswerMessage` to a component.
 | 
			
		||||
  DispatchAuthDaemonMessage message -> do
 | 
			
		||||
    { current_page } <- H.get
 | 
			
		||||
| 
						 | 
				
			
			@ -766,11 +790,8 @@ handleAction = case _ of
 | 
			
		|||
      _              -> handleAction $ Log $ SystemLog "unexpected message from authd"
 | 
			
		||||
    pure unit
 | 
			
		||||
 | 
			
		||||
  AddNotif n -> do
 | 
			
		||||
    H.modify_ _ { notif = n }
 | 
			
		||||
 | 
			
		||||
  CloseNotif -> do
 | 
			
		||||
    H.modify_ _ { notif = NoNotification }
 | 
			
		||||
  AddNotif n -> H.modify_ _ { notif = n }
 | 
			
		||||
  CloseNotif -> H.modify_ _ { notif = NoNotification }
 | 
			
		||||
 | 
			
		||||
  Reconnection -> do
 | 
			
		||||
    H.tell _ws_auth unit WS.Connect
 | 
			
		||||
| 
						 | 
				
			
			@ -975,6 +996,7 @@ handleAction = case _ of
 | 
			
		|||
      Just "Setup"          -> handleAction $ Routing Setup
 | 
			
		||||
      Just "Administration" -> handleAction $ Routing Administration
 | 
			
		||||
      Just "LegalNotice"    -> handleAction $ Routing LegalNotice
 | 
			
		||||
      Just "Migration"      -> handleAction $ Routing Migration
 | 
			
		||||
      Just "Zone" -> do
 | 
			
		||||
        domain <- H.liftEffect $ Storage.getItem "current-zone" sessionstorage
 | 
			
		||||
        case domain of
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,6 +60,9 @@ codecRegister
 | 
			
		|||
      , email: CAR.optional Email.codec })
 | 
			
		||||
 | 
			
		||||
{- 2  -}
 | 
			
		||||
{- This message is about validating the user account, before they can be authenticated.
 | 
			
		||||
   This message isn't about migrated accounts.
 | 
			
		||||
-}
 | 
			
		||||
type ValidateUser = { user :: String, activation_key :: String }
 | 
			
		||||
codecValidateUser ∷ CA.JsonCodec ValidateUser
 | 
			
		||||
codecValidateUser
 | 
			
		||||
| 
						 | 
				
			
			@ -170,6 +173,17 @@ type AuthByToken = { token :: String }
 | 
			
		|||
codecAuthByToken ∷ CA.JsonCodec AuthByToken
 | 
			
		||||
codecAuthByToken = CA.object "AuthByToken" (CAR.record { token: CA.string })
 | 
			
		||||
 | 
			
		||||
{- Add (or change) your email address. -}
 | 
			
		||||
{- 16 -}
 | 
			
		||||
type NewEmailAddress = { email :: String }
 | 
			
		||||
codecNewEmailAddress ∷ CA.JsonCodec NewEmailAddress
 | 
			
		||||
codecNewEmailAddress = CA.object "NewEmailAddress" (CAR.record { email: CA.string })
 | 
			
		||||
 | 
			
		||||
{- 17 -}
 | 
			
		||||
type NewEmailAddressToken = { token :: String }
 | 
			
		||||
codecNewEmailAddressToken ∷ CA.JsonCodec NewEmailAddressToken
 | 
			
		||||
codecNewEmailAddressToken = CA.object "NewEmailAddressToken" (CAR.record { token: CA.string })
 | 
			
		||||
 | 
			
		||||
{- 250 -}
 | 
			
		||||
type KeepAlive = { }
 | 
			
		||||
codecKeepAlive ∷ CA.JsonCodec KeepAlive
 | 
			
		||||
| 
						 | 
				
			
			@ -380,6 +394,8 @@ data RequestMessage
 | 
			
		|||
  | MkSetPermission        SetPermission        -- 11
 | 
			
		||||
  | MkSearchUser           SearchUser           -- 12
 | 
			
		||||
  | MkAuthByToken          AuthByToken          -- 15
 | 
			
		||||
  | MkNewEmailAddress      NewEmailAddress      -- 16
 | 
			
		||||
  | MkNewEmailAddressToken NewEmailAddressToken -- 16
 | 
			
		||||
  | MkKeepAlive            KeepAlive            -- 250
 | 
			
		||||
 | 
			
		||||
-- All possible answers from the authentication daemon (authd).
 | 
			
		||||
| 
						 | 
				
			
			@ -436,6 +452,8 @@ encode m = case m of
 | 
			
		|||
    (MkSetPermission        request) -> get_tuple 11  codecSetPermission        request
 | 
			
		||||
    (MkSearchUser           request) -> get_tuple 12  codecSearchUser           request
 | 
			
		||||
    (MkAuthByToken          request) -> get_tuple 15  codecAuthByToken          request
 | 
			
		||||
    (MkNewEmailAddress      request) -> get_tuple 16  codecNewEmailAddress      request
 | 
			
		||||
    (MkNewEmailAddressToken request) -> get_tuple 17  codecNewEmailAddressToken request
 | 
			
		||||
    (MkKeepAlive            request) -> get_tuple 250 codecKeepAlive            request
 | 
			
		||||
    where
 | 
			
		||||
      get_tuple :: forall a. Int -> CA.JsonCodec a -> a -> Tuple UInt String
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										194
									
								
								src/App/Page/Migration.purs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								src/App/Page/Migration.purs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,194 @@
 | 
			
		|||
-- | `App.Page.Migration` is the interface for migrated users before they validate their email address.
 | 
			
		||||
-- | To complete their migration, they need to provide an email address that will be validated with a token.
 | 
			
		||||
-- | This token will be required before the user can do anything else on the website.
 | 
			
		||||
-- |
 | 
			
		||||
-- | Exchanges between the webapp and authd:
 | 
			
		||||
-- |
 | 
			
		||||
-- | ```
 | 
			
		||||
-- | User gives a valid email address -> authd
 | 
			
		||||
-- | (authd accepts the email address and sends an email)
 | 
			
		||||
-- |
 | 
			
		||||
-- | authd -> email pending
 | 
			
		||||
-- | (we switch tab to "mail validation tab")
 | 
			
		||||
-- |
 | 
			
		||||
-- | User gives the received token -> authd
 | 
			
		||||
-- | (authd accepts the token and validates the email address)
 | 
			
		||||
-- |
 | 
			
		||||
-- | authd -> EmailChanged
 | 
			
		||||
-- | (webapp switches to domain list)
 | 
			
		||||
-- | ```
 | 
			
		||||
module App.Page.Migration where
 | 
			
		||||
 | 
			
		||||
import Prelude (Unit, between, bind, discard, map, pure, ($), (<>))
 | 
			
		||||
 | 
			
		||||
import Data.Array as A
 | 
			
		||||
import Data.ArrayBuffer.Types (ArrayBuffer)
 | 
			
		||||
import Data.Either (Either(..))
 | 
			
		||||
import Data.Maybe (Maybe(..))
 | 
			
		||||
import Data.String as S
 | 
			
		||||
import Effect.Aff.Class (class MonadAff)
 | 
			
		||||
import Halogen as H
 | 
			
		||||
import Halogen.HTML as HH
 | 
			
		||||
import Halogen.HTML.Events as HE
 | 
			
		||||
import Web.Event.Event as Event
 | 
			
		||||
import Web.Event.Event (Event)
 | 
			
		||||
 | 
			
		||||
-- import Data.Generic.Rep (class Generic)
 | 
			
		||||
-- import Data.Show.Generic (genericShow)
 | 
			
		||||
 | 
			
		||||
import Bulma as Bulma
 | 
			
		||||
import Scroll (scrollToTop)
 | 
			
		||||
 | 
			
		||||
import App.Type.LogMessage
 | 
			
		||||
import App.Message.AuthenticationDaemon as AuthD
 | 
			
		||||
import App.DisplayErrors (show_error_email)
 | 
			
		||||
import App.Validation.Email as E
 | 
			
		||||
 | 
			
		||||
data Output = MessageToSend ArrayBuffer | Log LogMessage
 | 
			
		||||
 | 
			
		||||
-- | Once the new email address has been accepted by `authd` as "pending",
 | 
			
		||||
-- | this page automatically switches to a second tab.
 | 
			
		||||
data Query a = WaitingForToken a
 | 
			
		||||
 | 
			
		||||
type Slot = H.Slot Query Output
 | 
			
		||||
 | 
			
		||||
type Input = Unit
 | 
			
		||||
 | 
			
		||||
-- | Both value types to validate before sending the appropriate messages to `authd`.
 | 
			
		||||
data Subject = EmailAddress | Token
 | 
			
		||||
 | 
			
		||||
--derive instance eqSubject :: Eq Subject
 | 
			
		||||
--derive instance genericSubject :: Generic Subject _
 | 
			
		||||
--instance showSubject :: Show Subject where
 | 
			
		||||
--  show = genericShow
 | 
			
		||||
 | 
			
		||||
data Action
 | 
			
		||||
  -- | Copy user input in the different HTML inputs.
 | 
			
		||||
  = UserInput Subject String
 | 
			
		||||
 | 
			
		||||
  -- | Verify either the format of the new email address or the token then send the request.
 | 
			
		||||
  | Verify Subject Event
 | 
			
		||||
 | 
			
		||||
  -- | Send either the new email address or the token to `authd`.
 | 
			
		||||
  | ContactAuthd Subject
 | 
			
		||||
 | 
			
		||||
  -- | Change the current tab.
 | 
			
		||||
  -- | ChangeTab Subject
 | 
			
		||||
 | 
			
		||||
-- | The possible errors from the email format.
 | 
			
		||||
-- | TODO: check the token.
 | 
			
		||||
data Error = Email (Array E.Error)
 | 
			
		||||
 | 
			
		||||
-- | State is composed of the new email address, the token and the possible errors.
 | 
			
		||||
type State
 | 
			
		||||
  = { email :: String
 | 
			
		||||
    , token :: String
 | 
			
		||||
    , errors :: Array Error
 | 
			
		||||
    -- , current_tab :: Subject
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
initialState :: Input -> State
 | 
			
		||||
initialState _
 | 
			
		||||
  = { email: ""
 | 
			
		||||
    , token: ""
 | 
			
		||||
    , errors: []
 | 
			
		||||
    -- , current_tab: EmailAddress
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
component :: forall m. MonadAff m => H.Component Query Input Output m
 | 
			
		||||
component =
 | 
			
		||||
  H.mkComponent
 | 
			
		||||
    { initialState
 | 
			
		||||
    , render
 | 
			
		||||
    , eval: H.mkEval $ H.defaultEval
 | 
			
		||||
        { handleAction = handleAction
 | 
			
		||||
        , handleQuery  = handleQuery
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
handleQuery :: forall a m. MonadAff m => Query a -> H.HalogenM State Action () Output m (Maybe a)
 | 
			
		||||
handleQuery = case _ of
 | 
			
		||||
  -- TODO
 | 
			
		||||
  WaitingForToken a -> pure (Just a)
 | 
			
		||||
 | 
			
		||||
render :: forall m. State -> H.ComponentHTML Action () m
 | 
			
		||||
render state
 | 
			
		||||
  = Bulma.section_small [Bulma.columns_
 | 
			
		||||
      [ b email_form
 | 
			
		||||
      , b token_form
 | 
			
		||||
      ]]
 | 
			
		||||
 | 
			
		||||
  where
 | 
			
		||||
    b e = Bulma.column_ [ Bulma.box e ]
 | 
			
		||||
 | 
			
		||||
    email_form
 | 
			
		||||
      = [ Bulma.h3 "New Email address"
 | 
			
		||||
        -- TODO: put some text here
 | 
			
		||||
        , HH.form
 | 
			
		||||
            [ HE.onSubmit (Verify EmailAddress) ]
 | 
			
		||||
            [ email_input, email_error, Bulma.btn_validation ]
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    email_input = Bulma.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 -> Bulma.error_box "newAddress" "Email error" (show_error $ Email errors)
 | 
			
		||||
          _, Right _     -> HH.text ""
 | 
			
		||||
 | 
			
		||||
    token_form
 | 
			
		||||
      = [ Bulma.h3 "Email validation token"
 | 
			
		||||
        -- TODO: put some text here
 | 
			
		||||
        , HH.form
 | 
			
		||||
            [ HE.onSubmit (Verify Token) ]
 | 
			
		||||
            [ token_input {-, token_error -}, Bulma.btn_validation ]
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    token_input = Bulma.token_input "Token" state.token (UserInput Token)
 | 
			
		||||
 | 
			
		||||
handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit
 | 
			
		||||
handleAction = case _ of
 | 
			
		||||
  UserInput subject value -> do
 | 
			
		||||
    case subject of
 | 
			
		||||
      EmailAddress -> H.modify_ _ { email = value }
 | 
			
		||||
      Token        -> H.modify_ _ { token = value }
 | 
			
		||||
 | 
			
		||||
  -- Validate either the email address or the token then send the related requests to `authd`.
 | 
			
		||||
  Verify subject ev -> do
 | 
			
		||||
    H.liftEffect $ Event.preventDefault ev
 | 
			
		||||
 | 
			
		||||
    state <- H.get
 | 
			
		||||
    case subject of
 | 
			
		||||
      EmailAddress -> do
 | 
			
		||||
        case state.email of
 | 
			
		||||
          "" -> do
 | 
			
		||||
            H.raise $ Log $ UnableToSend "Please, write your new email address."
 | 
			
		||||
            H.liftEffect scrollToTop
 | 
			
		||||
          _ -> do
 | 
			
		||||
            case E.email state.email of
 | 
			
		||||
              Left errors  -> H.raise $ Log $ UnableToSend $ show_error $ Email errors
 | 
			
		||||
              Right _      -> handleAction $ ContactAuthd EmailAddress
 | 
			
		||||
 | 
			
		||||
      Token -> do
 | 
			
		||||
        case state.token of
 | 
			
		||||
          "" -> do
 | 
			
		||||
            H.raise $ Log $ UnableToSend "Please, write your validation token."
 | 
			
		||||
            H.liftEffect scrollToTop
 | 
			
		||||
          _ -> handleAction $ ContactAuthd Token
 | 
			
		||||
 | 
			
		||||
  ContactAuthd subject -> do
 | 
			
		||||
    state <- H.get
 | 
			
		||||
    case subject of
 | 
			
		||||
      EmailAddress -> do
 | 
			
		||||
        message <- H.liftEffect $ AuthD.serialize $ AuthD.MkNewEmailAddress { email: state.email }
 | 
			
		||||
        H.raise $ MessageToSend message
 | 
			
		||||
        H.raise $ Log $ SystemLog $ "Sending a new email address."
 | 
			
		||||
      Token -> do
 | 
			
		||||
        message <- H.liftEffect $ AuthD.serialize $ AuthD.MkNewEmailAddressToken { token: state.token }
 | 
			
		||||
        H.raise $ MessageToSend message
 | 
			
		||||
        H.raise $ Log $ SystemLog $ "Sending a validation token."
 | 
			
		||||
 | 
			
		||||
show_error :: Error -> String
 | 
			
		||||
show_error = case _ of
 | 
			
		||||
  Email    arr -> "Error with the Email: "    <> (A.fold $ map show_error_email arr)
 | 
			
		||||
| 
						 | 
				
			
			@ -3,9 +3,7 @@ module App.Type.Pages where
 | 
			
		|||
import Prelude
 | 
			
		||||
import Data.Generic.Rep (class Generic)
 | 
			
		||||
import Data.Show.Generic (genericShow)
 | 
			
		||||
-- | This list will grow in a near future.
 | 
			
		||||
-- |
 | 
			
		||||
-- | TODO: 
 | 
			
		||||
 | 
			
		||||
data Page
 | 
			
		||||
  = Home           -- | `Home`: presentation of the project.
 | 
			
		||||
  | Authentication -- | `Authentication`: authentication page.
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +14,7 @@ data Page
 | 
			
		|||
  | Setup          -- | `Setup`: user account administration page
 | 
			
		||||
  | Administration -- | `Administration`: administration page (for both `authd` and `dnsmanagerd`).
 | 
			
		||||
  | LegalNotice    -- | `LegalNotice`: to learn about the website host, user agreements, etc.
 | 
			
		||||
  | Migration      -- | `Migration`: ask for an email address before anything else.
 | 
			
		||||
 | 
			
		||||
derive instance genericPage :: Generic Page _
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue