hercules-ci-cli-0.1.0: src/Hercules/CLI/Credentials.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveAnyClass #-}
-- | Manages the ~/.config/hercules-ci/credentials.json
module Hercules.CLI.Credentials where
import Data.Aeson (FromJSON, ToJSON, eitherDecode)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.Map as M
import Hercules.CLI.Client (determineDefaultApiBaseUrl)
import Hercules.CLI.JSON (writeJsonFile)
import Hercules.Error
import qualified Network.URI as URI
import Protolude
import System.Directory (XdgDirectory (XdgConfig), createDirectoryIfMissing, doesFileExist, getXdgDirectory)
import System.FilePath (takeDirectory, (</>))
data Credentials = Credentials
{ domains :: Map Text DomainCredentials
}
deriving (Eq, Generic, FromJSON, ToJSON)
data DomainCredentials = DomainCredentials
{ personalToken :: Text
}
deriving (Eq, Generic, FromJSON, ToJSON)
data CredentialsParsingException = CredentialsParsingException
{ filePath :: FilePath,
message :: Text
}
deriving (Show, Eq)
instance Exception CredentialsParsingException where
displayException e = "Could not parse credentials file " <> filePath e <> ": " <> toS (message e)
data NoCredentialException = NoCredentialException
{ noCredentialDomain :: Text
}
deriving (Show, Eq)
instance Exception NoCredentialException where
displayException e = "Could not find credentials for domain " <> toS (noCredentialDomain e) <> ". Please run hci login."
data ApiBaseUrlParsingException = ApiBaseUrlParsingException
{ apiBaseUrlParsingMessage :: Text
}
deriving (Show, Eq)
instance Exception ApiBaseUrlParsingException where
displayException e = "Could not parse the api domain: " <> toS (apiBaseUrlParsingMessage e) <> ". Please correct the HERCULES_CI_API_BASE_URL environment variable."
getCredentialsFilePath :: IO FilePath
getCredentialsFilePath = do
dir <- getXdgDirectory XdgConfig "hercules-ci"
pure $ dir </> "credentials.json"
readCredentials :: IO Credentials
readCredentials = do
filePath_ <- getCredentialsFilePath
doesFileExist filePath_ >>= \case
False -> pure (Credentials mempty)
True -> do
bs <- BS.readFile filePath_
escalate $ parseCredentials filePath_ bs
parseCredentials :: FilePath -> ByteString -> Either CredentialsParsingException Credentials
parseCredentials filePath_ bs =
case eitherDecode (BL.fromStrict bs) of
Right a -> Right a
Left e -> Left (CredentialsParsingException {filePath = filePath_, message = toS e})
writeCredentials :: Credentials -> IO ()
writeCredentials credentials = do
filePath_ <- getCredentialsFilePath
createDirectoryIfMissing True (takeDirectory filePath_)
writeJsonFile filePath_ credentials
urlDomain :: Text -> Either Text Text
urlDomain urlText = do
uri <- maybeToEither "could not parse HERCULES_CI_API_BASE_URL" $ URI.parseAbsoluteURI (toS urlText)
authority <- maybeToEither "HERCULES_CI_API_BASE_URL has no domain/authority part" $ URI.uriAuthority uri
let name = URI.uriRegName authority
maybeToEither "HERCULES_CI_API_BASE_URL domain name must not be empty" $ guard (name /= "")
pure (toS name)
determineDomain :: IO Text
determineDomain = do
baseUrl <- determineDefaultApiBaseUrl
escalateAs ApiBaseUrlParsingException (urlDomain baseUrl)
writePersonalToken :: Text -> Text -> IO ()
writePersonalToken domain token = do
creds <- readCredentials
let creds' = creds {domains = domains creds & M.insert domain (DomainCredentials token)}
writeCredentials creds'
readPersonalToken :: Text -> IO Text
readPersonalToken domain = do
creds <- readCredentials
case M.lookup domain (domains creds) of
Nothing -> throwIO NoCredentialException {noCredentialDomain = domain}
Just cred -> pure (personalToken cred)