packages feed

shomei-servant-0.2.0.0: src/Shomei/Servant/Api.hs

-- | Thin composition roots for Shōmei's concept-owned route records.
module Shomei.Servant.Api
  ( ShomeiRoutes (..),
    shomeiRoutesApi,
    shomeiThrottledRoutes,
    ApplicationApi (..),
    applicationApi,
    AppApi,
    ApplicationRoutes,
    OAuthRoutes,
    WellKnownRoutes,
    HealthRoutes,
    OpenApiRoute,
    Project (..),
  )
where

import Data.Aeson (Value)
import Servant.API
import Servant.Health (HealthApi)
import Shomei.Account.Admin.Api (AdminAccountApi)
import Shomei.Account.Api (AccountApi)
import Shomei.Account.User.Domain (User)
import Shomei.Audit.Api (AuditApi)
import Shomei.Authorization.Api (AuthorizationApi)
import Shomei.Mfa.Api (MfaApi)
import Shomei.OAuth.Api (OAuthApi)
import Shomei.Passkey.Api (PasskeyApi)
import Shomei.Prelude
import Shomei.Servant.Auth (Authenticated)
import Shomei.Servant.Authz (RequireRole)
import Shomei.Servant.Throttle (ThrottledRoute, throttledRoutesOf)
import Shomei.Session.Admin.Api (AdminSessionApi)
import Shomei.Session.Api (SessionApi)
import Shomei.SigningKey.Api (WellKnownApi)

-- | Versioned application routes, grouped by the concept that owns their wire contract and
-- handler adapter. Several fields intentionally share a path prefix; NamedRoutes dispatch uses
-- the complete route beneath each field rather than record declaration order.
data ApplicationApi mode = ApplicationApi
  { account :: mode :- "auth" :> NamedRoutes AccountApi,
    session :: mode :- "auth" :> NamedRoutes SessionApi,
    passkey :: mode :- "auth" :> NamedRoutes PasskeyApi,
    mfa :: mode :- "auth" :> NamedRoutes MfaApi,
    adminAccount :: mode :- "admin" :> NamedRoutes AdminAccountApi,
    adminSession :: mode :- "admin" :> NamedRoutes AdminSessionApi,
    authorization :: mode :- "admin" :> NamedRoutes AuthorizationApi,
    audit :: mode :- "admin" :> NamedRoutes AuditApi
  }
  deriving stock (Generic)

applicationApi :: Proxy (NamedRoutes ApplicationApi)
applicationApi = Proxy

-- | The exact served API. Standalone, embedded, OpenAPI, and client entry points all consume
-- this proxy.
type ApplicationRoutes = "v1" :> NamedRoutes ApplicationApi

type OAuthRoutes = "oauth" :> NamedRoutes OAuthApi

type WellKnownRoutes = ".well-known" :> NamedRoutes WellKnownApi

type HealthRoutes = "health" :> NamedRoutes HealthApi

type OpenApiRoute = "openapi.json" :> Get '[JSON] Value

data ShomeiRoutes mode = ShomeiRoutes
  { application :: mode :- ApplicationRoutes,
    oauth :: mode :- OAuthRoutes,
    wellKnown :: mode :- WellKnownRoutes,
    health :: mode :- HealthRoutes,
    openapi :: mode :- OpenApiRoute
  }
  deriving stock (Generic)

shomeiRoutesApi :: Proxy (NamedRoutes ShomeiRoutes)
shomeiRoutesApi = Proxy

shomeiThrottledRoutes :: [ThrottledRoute]
shomeiThrottledRoutes = throttledRoutesOf shomeiRoutesApi

newtype Project = Project {projectId :: Text}
  deriving stock (Generic)
  deriving anyclass (FromJSON, ToJSON)

-- | Embeddability proof: a host may mount the complete API beside its own authenticated routes.
type AppApi =
  NamedRoutes ShomeiRoutes
    :<|> Authenticated :> "projects" :> Get '[JSON] [Project]
    :<|> RequireRole "admin" :> "admin" :> "users" :> Get '[JSON] [User]