packages feed

hstratus-0.1.0.0: src/Hstratus/Cli/Auth.hs

{-# LANGUAGE LambdaCase #-}

{- |
Module      : Hstratus.Cli.Auth
Copyright   : (c) 2026 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD-3-Clause

CLI subcommands for iCloud authentication (login, logout, status).
-}
module Hstratus.Cli.Auth
  ( AuthCommand (..)
  , authParser
  , runAuth
  )
where

import Control.Exception (bracket_, catch, displayException)
import Data.String (fromString)
import Network.HStratus.Http
  ( AuthError
  , login
  , mkApiWith
  , withLogger
  )
import Network.HStratus.Http.Cli
  ( CommonOpts (..)
  , commonOptsParser
  , mkLoggerFor
  , resolveLogTarget
  )
import Network.HStratus.Http.Endpoints (Realm (..), realmEndpoints)
import Network.HStratus.Session (Credentials (..), loadSession, saveCredentials)
import Network.HTTP.Client.TLS (newTlsManager)
import Options.Applicative
import System.Exit (exitFailure)
import System.IO (IOMode (..), hFlush, hSetEcho, stdin, stdout, withFile)


-- | Top-level auth subcommand.
data AuthCommand
  = -- | save Apple ID credentials to the config directory
    AuthInit
  | -- | authenticate with iCloud
    AuthLogin !CommonOpts
  deriving (Eq, Show)


-- | Optparse-applicative parser for the @auth@ subcommand.
authParser :: Parser AuthCommand
authParser =
  subparser
    ( command "init" (info (pure AuthInit) (progDesc "Save Apple ID credentials to the config directory"))
        <> command "login" (info (AuthLogin <$> commonOptsParser <**> helper) (progDesc "Authenticate with iCloud"))
    )


-- | Dispatch an 'AuthCommand' to its handler.
runAuth :: AuthCommand -> IO ()
runAuth = \case
  AuthInit -> runInit
  AuthLogin opts -> runLogin opts


runInit :: IO ()
runInit = do
  appleId <- prompt "Apple ID: "
  password <- promptSecret "Password: "
  saveCredentials (Credentials (fromString appleId) (fromString password))
  putStrLn "Credentials saved."


runLogin :: CommonOpts -> IO ()
runLogin opts = do
  session <- loadSession
  mgr <- newTlsManager
  let realm = if optChina opts then China else Usual
  api0 <- mkApiWith session (realmEndpoints realm) mgr
  mbLogPath <- resolveLogTarget opts
  let mkLogger' = mkLoggerFor opts
      go = case mbLogPath of
        Nothing -> login api0 >> putStrLn "Authenticated."
        Just fp -> withFile fp AppendMode $ \h ->
          login (withLogger (mkLogger' h) api0) >> putStrLn "Authenticated."
  go `catch` \e -> do
    putStrLn $ "Login failed: " <> displayException (e :: AuthError)
    exitFailure


prompt :: String -> IO String
prompt label = putStr label >> hFlush stdout >> getLine


promptSecret :: String -> IO String
promptSecret label = do
  putStr label
  hFlush stdout
  bracket_ (hSetEcho stdin False) (hSetEcho stdin True) $ do
    secret <- getLine
    putStrLn ""
    pure secret