2026-07-18 13:17:40 +09:00
|
|
|
module Nuthatch.Api
|
|
|
|
|
( Api
|
|
|
|
|
, HealthEndpoint
|
|
|
|
|
, ReadyEndpoint
|
2026-07-19 01:12:29 +09:00
|
|
|
, AppEndpoint
|
|
|
|
|
, StaticAssetsEndpoint
|
|
|
|
|
, ProductApi
|
2026-07-18 13:17:40 +09:00
|
|
|
, LoginEndpoint
|
2026-07-18 18:08:41 +09:00
|
|
|
, LoginStart
|
|
|
|
|
, OidcCallbackEndpoint
|
2026-07-18 13:17:40 +09:00
|
|
|
, LoginSuccess
|
|
|
|
|
, LogoutEndpoint
|
|
|
|
|
, LogoutSuccess
|
|
|
|
|
, DownloadSuccess
|
|
|
|
|
, api
|
|
|
|
|
, HTML
|
|
|
|
|
, HtmlPage (..)
|
|
|
|
|
, SessionContext (..)
|
|
|
|
|
, SessionAuth
|
|
|
|
|
, BrowsePrefix (..)
|
|
|
|
|
, PaginationToken (..)
|
|
|
|
|
, SearchQuery (..)
|
|
|
|
|
, DeclaredSize (..)
|
|
|
|
|
, HealthResponse (..)
|
|
|
|
|
, ReadyResponse (..)
|
|
|
|
|
, BrowseResponse (..)
|
|
|
|
|
, DeleteRequest (..)
|
2026-07-19 01:12:29 +09:00
|
|
|
, SessionResponse (..)
|
|
|
|
|
, DeletePlanResponse (..)
|
|
|
|
|
, DeleteBatchItem (..)
|
|
|
|
|
, DeleteBatchRequest (..)
|
|
|
|
|
, DeleteItemResponse (..)
|
|
|
|
|
, DeleteBatchResponse (..)
|
2026-07-18 13:17:40 +09:00
|
|
|
, UploadResponse (..)
|
|
|
|
|
, DeleteResponse (..)
|
|
|
|
|
, Throws
|
|
|
|
|
, BoundaryError (..)
|
|
|
|
|
, AuthenticationError (..)
|
|
|
|
|
, ReadyError (..)
|
|
|
|
|
, LoginError (..)
|
|
|
|
|
, LogoutError (..)
|
|
|
|
|
, BrowseError (..)
|
|
|
|
|
, DownloadError (..)
|
|
|
|
|
, UploadError (..)
|
|
|
|
|
, DeleteError (..)
|
|
|
|
|
, DeletePrefixError (..)
|
|
|
|
|
, validatePaginationToken
|
|
|
|
|
) where
|
|
|
|
|
|
|
|
|
|
import Data.Aeson
|
|
|
|
|
( FromJSON (..)
|
|
|
|
|
, ToJSON (..)
|
|
|
|
|
, Value
|
|
|
|
|
, object
|
|
|
|
|
, withObject
|
|
|
|
|
, (.:)
|
|
|
|
|
, (.=)
|
|
|
|
|
)
|
|
|
|
|
import qualified Data.ByteString as BS
|
|
|
|
|
import qualified Data.ByteString.Lazy as BL
|
|
|
|
|
import Data.Char (isControl)
|
|
|
|
|
import Data.Proxy (Proxy (..))
|
|
|
|
|
import Data.Text (Text)
|
|
|
|
|
import qualified Data.Text as T
|
|
|
|
|
import qualified Data.Text.Encoding as TE
|
|
|
|
|
import Network.HTTP.Media ((//), (/:))
|
|
|
|
|
import Nuthatch.Types
|
|
|
|
|
import Servant.API
|
|
|
|
|
import Servant.Server (HasServer (..))
|
|
|
|
|
import Servant.Server.Experimental.Auth (AuthServerData)
|
|
|
|
|
|
|
|
|
|
data HTML
|
|
|
|
|
instance Accept HTML where
|
|
|
|
|
contentType _ = "text" // "html" /: ("charset", "utf-8")
|
|
|
|
|
|
|
|
|
|
newtype HtmlPage = HtmlPage { unHtmlPage :: Text }
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance MimeRender HTML HtmlPage where
|
|
|
|
|
mimeRender _ = BL.fromStrict . TE.encodeUtf8 . unHtmlPage
|
|
|
|
|
|
|
|
|
|
data SessionContext = SessionContext
|
|
|
|
|
{ sessionUser :: SessionUser
|
|
|
|
|
, sessionCsrf :: Maybe CsrfToken
|
|
|
|
|
}
|
2026-07-19 03:41:01 +09:00
|
|
|
deriving stock (Eq)
|
2026-07-18 13:17:40 +09:00
|
|
|
|
|
|
|
|
data SessionAuth
|
|
|
|
|
|
|
|
|
|
type instance AuthServerData (AuthProtect SessionAuth) = SessionContext
|
|
|
|
|
|
|
|
|
|
newtype BrowsePrefix = BrowsePrefix { unBrowsePrefix :: Maybe RelativePath }
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
newtype PaginationToken = PaginationToken { unPaginationToken :: Text }
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
deriving newtype (ToJSON, FromJSON)
|
|
|
|
|
|
|
|
|
|
newtype SearchQuery = SearchQuery { unSearchQuery :: Text }
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
deriving newtype (ToJSON, FromJSON)
|
|
|
|
|
|
|
|
|
|
newtype DeclaredSize = DeclaredSize { unDeclaredSize :: Integer }
|
|
|
|
|
deriving stock (Eq, Ord, Show)
|
|
|
|
|
deriving newtype (ToJSON, FromJSON)
|
|
|
|
|
|
|
|
|
|
newtype HealthResponse = HealthResponse { healthOk :: Bool }
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON HealthResponse where
|
|
|
|
|
toJSON response = object ["ok" .= healthOk response]
|
|
|
|
|
|
|
|
|
|
newtype ReadyResponse = ReadyResponse { ready :: Bool }
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON ReadyResponse where
|
|
|
|
|
toJSON response = object ["ready" .= ready response]
|
|
|
|
|
|
|
|
|
|
data BrowseResponse = BrowseResponse
|
|
|
|
|
{ browseEntries :: [ListEntry]
|
|
|
|
|
, browseContinuation :: Maybe PaginationToken
|
|
|
|
|
, browsePartial :: Bool
|
|
|
|
|
}
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON BrowseResponse where
|
|
|
|
|
toJSON response = object
|
|
|
|
|
[ "entries" .= browseEntries response
|
|
|
|
|
, "continuation" .= browseContinuation response
|
|
|
|
|
, "partial" .= browsePartial response
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
data DeleteRequest = DeleteRequest
|
|
|
|
|
{ deleteRoot :: Root
|
|
|
|
|
, deletePath :: RelativePath
|
|
|
|
|
, deleteOperationId :: OperationId
|
|
|
|
|
, deleteCsrf :: CsrfToken
|
|
|
|
|
}
|
2026-07-19 03:41:01 +09:00
|
|
|
deriving stock (Eq)
|
2026-07-18 13:17:40 +09:00
|
|
|
|
|
|
|
|
instance FromJSON DeleteRequest where
|
|
|
|
|
parseJSON = withObject "DeleteRequest" $ \o ->
|
|
|
|
|
DeleteRequest
|
|
|
|
|
<$> o .: "root"
|
|
|
|
|
<*> o .: "path"
|
|
|
|
|
<*> o .: "operation_id"
|
|
|
|
|
<*> o .: "csrf"
|
|
|
|
|
|
|
|
|
|
data UploadResponse = UploadResponse
|
|
|
|
|
{ uploadResponseOperationId :: OperationId
|
|
|
|
|
, uploadResponseResult :: MutationResult
|
2026-07-18 21:25:50 +09:00
|
|
|
, uploadResponseIdempotencyWindowSeconds :: Integer
|
2026-07-18 13:17:40 +09:00
|
|
|
}
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON UploadResponse where
|
|
|
|
|
toJSON response = object
|
|
|
|
|
[ "operation_id" .= uploadResponseOperationId response
|
|
|
|
|
, "result" .= mutationResultText (uploadResponseResult response)
|
2026-07-18 21:25:50 +09:00
|
|
|
, "idempotency_window_seconds"
|
|
|
|
|
.= uploadResponseIdempotencyWindowSeconds response
|
2026-07-18 13:17:40 +09:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
data DeleteResponse = DeleteResponse
|
|
|
|
|
{ deleteResponseOperationId :: OperationId
|
|
|
|
|
, deleteResponseResult :: MutationResult
|
2026-07-18 21:25:50 +09:00
|
|
|
, deleteResponseIdempotencyWindowSeconds :: Integer
|
2026-07-18 13:17:40 +09:00
|
|
|
}
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON DeleteResponse where
|
|
|
|
|
toJSON response = object
|
|
|
|
|
[ "operation_id" .= deleteResponseOperationId response
|
|
|
|
|
, "result" .= mutationResultText (deleteResponseResult response)
|
2026-07-18 21:25:50 +09:00
|
|
|
, "idempotency_window_seconds"
|
|
|
|
|
.= deleteResponseIdempotencyWindowSeconds response
|
2026-07-18 13:17:40 +09:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
data Throws error
|
|
|
|
|
|
|
|
|
|
instance HasServer endpoint context =>
|
|
|
|
|
HasServer (Throws error :> endpoint) context where
|
|
|
|
|
type ServerT (Throws error :> endpoint) m = ServerT endpoint m
|
|
|
|
|
route _ = route (Proxy :: Proxy endpoint)
|
|
|
|
|
hoistServerWithContext _ context transform server =
|
|
|
|
|
hoistServerWithContext
|
|
|
|
|
(Proxy :: Proxy endpoint)
|
|
|
|
|
context
|
|
|
|
|
transform
|
|
|
|
|
server
|
|
|
|
|
|
|
|
|
|
instance HasLink endpoint => HasLink (Throws error :> endpoint) where
|
|
|
|
|
type MkLink (Throws error :> endpoint) result = MkLink endpoint result
|
|
|
|
|
toLink convert _ = toLink convert (Proxy :: Proxy endpoint)
|
|
|
|
|
|
|
|
|
|
data BoundaryError = InvalidUtf8
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON BoundaryError where
|
|
|
|
|
toJSON InvalidUtf8 =
|
|
|
|
|
errorValue "invalid_utf8" "request paths and query values must be valid UTF-8" Nothing
|
|
|
|
|
|
|
|
|
|
data AuthenticationError
|
|
|
|
|
= AuthenticationRequired
|
|
|
|
|
| MalformedAuthentication
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON AuthenticationError where
|
|
|
|
|
toJSON AuthenticationRequired =
|
|
|
|
|
errorValue "authentication_required" "login required" Nothing
|
|
|
|
|
toJSON MalformedAuthentication =
|
|
|
|
|
errorValue "malformed_authentication" "authentication data is malformed" Nothing
|
|
|
|
|
|
|
|
|
|
data ReadyError = ReadyDependencyUnavailable
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON ReadyError where
|
|
|
|
|
toJSON ReadyDependencyUnavailable =
|
|
|
|
|
errorValue "dependency_unavailable" "PostgreSQL is unavailable" Nothing
|
|
|
|
|
|
|
|
|
|
data LoginError
|
2026-07-18 18:08:41 +09:00
|
|
|
= OidcAuthorizationFailed
|
|
|
|
|
| OidcCallbackRejected
|
2026-07-18 13:17:40 +09:00
|
|
|
| SessionCreationFailed Text
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON LoginError where
|
2026-07-18 18:08:41 +09:00
|
|
|
toJSON OidcAuthorizationFailed =
|
|
|
|
|
errorValue "oidc_authorization_failed" "OIDC authorization could not start" Nothing
|
|
|
|
|
toJSON OidcCallbackRejected =
|
|
|
|
|
errorValue "oidc_callback_rejected" "OIDC callback validation failed" Nothing
|
2026-07-18 13:17:40 +09:00
|
|
|
toJSON (SessionCreationFailed message) =
|
|
|
|
|
errorValue "session_error" message Nothing
|
|
|
|
|
|
|
|
|
|
data LogoutError = LogoutCsrfRejected
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON LogoutError where
|
|
|
|
|
toJSON LogoutCsrfRejected =
|
|
|
|
|
errorValue "csrf_validation_failed" "CSRF validation failed" Nothing
|
|
|
|
|
|
|
|
|
|
newtype BrowseError = BrowseObjectStoreFailed Text
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON BrowseError where
|
|
|
|
|
toJSON (BrowseObjectStoreFailed message) =
|
|
|
|
|
errorValue "object_store_error" message Nothing
|
|
|
|
|
|
|
|
|
|
data DownloadError
|
|
|
|
|
= DownloadObjectStoreFailed Text
|
|
|
|
|
| DownloadNotFound
|
2026-07-19 01:12:29 +09:00
|
|
|
| DownloadRangeUnsupported
|
2026-07-18 13:17:40 +09:00
|
|
|
| DownloadThrottleUnavailable
|
|
|
|
|
| DownloadBandwidthExceeded
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON DownloadError where
|
|
|
|
|
toJSON (DownloadObjectStoreFailed message) =
|
|
|
|
|
errorValue "object_store_error" message Nothing
|
|
|
|
|
toJSON DownloadNotFound =
|
|
|
|
|
errorValue "not_found" "object not found" Nothing
|
2026-07-19 01:12:29 +09:00
|
|
|
toJSON DownloadRangeUnsupported =
|
|
|
|
|
errorValue "range_not_supported" "Range downloads are not supported" Nothing
|
2026-07-18 13:17:40 +09:00
|
|
|
toJSON DownloadThrottleUnavailable =
|
|
|
|
|
errorValue "throttle_unavailable" "bandwidth throttle unavailable" Nothing
|
|
|
|
|
toJSON DownloadBandwidthExceeded =
|
|
|
|
|
errorValue "bandwidth_exceeded" "bandwidth limit exceeded" Nothing
|
|
|
|
|
|
|
|
|
|
data UploadError
|
|
|
|
|
= UploadCsrfRejected
|
|
|
|
|
| UploadInvalidBodyStream Text
|
|
|
|
|
| UploadSizeMismatch
|
|
|
|
|
| UploadTooLarge
|
|
|
|
|
| UploadContentIdentityMismatch
|
|
|
|
|
| UploadThrottleUnavailable
|
|
|
|
|
| UploadBandwidthExceeded
|
|
|
|
|
| UploadMutationRejected Text
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON UploadError where
|
|
|
|
|
toJSON UploadCsrfRejected =
|
|
|
|
|
errorValue "csrf_validation_failed" "CSRF validation failed" Nothing
|
|
|
|
|
toJSON (UploadInvalidBodyStream message) =
|
|
|
|
|
errorValue "invalid_body_stream" message Nothing
|
|
|
|
|
toJSON UploadSizeMismatch =
|
|
|
|
|
errorValue "size_mismatch" "request body does not match declared size" Nothing
|
|
|
|
|
toJSON UploadTooLarge =
|
|
|
|
|
errorValue "object_too_large" "object too large" Nothing
|
|
|
|
|
toJSON UploadContentIdentityMismatch =
|
|
|
|
|
errorValue "content_identity_mismatch" "request body digest does not match" Nothing
|
|
|
|
|
toJSON UploadThrottleUnavailable =
|
|
|
|
|
errorValue "throttle_unavailable" "bandwidth throttle unavailable" Nothing
|
|
|
|
|
toJSON UploadBandwidthExceeded =
|
|
|
|
|
errorValue "bandwidth_exceeded" "bandwidth limit exceeded" Nothing
|
|
|
|
|
toJSON (UploadMutationRejected message) =
|
|
|
|
|
errorValue "mutation_rejected" message Nothing
|
|
|
|
|
|
|
|
|
|
data DeleteError
|
|
|
|
|
= DeleteCsrfRejected
|
|
|
|
|
| DeleteMutationRejected Text
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON DeleteError where
|
|
|
|
|
toJSON DeleteCsrfRejected =
|
|
|
|
|
errorValue "csrf_validation_failed" "CSRF validation failed" Nothing
|
|
|
|
|
toJSON (DeleteMutationRejected message) =
|
|
|
|
|
errorValue "mutation_rejected" message Nothing
|
|
|
|
|
|
|
|
|
|
data DeletePrefixError
|
|
|
|
|
= DeletePrefixCsrfRejected
|
|
|
|
|
| DeletePrefixMutationRejected Text
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON DeletePrefixError where
|
|
|
|
|
toJSON DeletePrefixCsrfRejected =
|
|
|
|
|
errorValue "csrf_validation_failed" "CSRF validation failed" Nothing
|
|
|
|
|
toJSON (DeletePrefixMutationRejected message) =
|
|
|
|
|
errorValue "prefix_mutation_rejected" message Nothing
|
|
|
|
|
|
2026-07-19 01:12:29 +09:00
|
|
|
data SessionResponse = SessionResponse
|
|
|
|
|
{ sessionResponseUserId :: UserId
|
|
|
|
|
, sessionResponseDisplay :: Maybe Text
|
|
|
|
|
, sessionResponseCsrf :: CsrfToken
|
|
|
|
|
}
|
2026-07-19 03:41:01 +09:00
|
|
|
deriving stock (Eq)
|
2026-07-19 01:12:29 +09:00
|
|
|
|
|
|
|
|
instance ToJSON SessionResponse where
|
|
|
|
|
toJSON response = object
|
|
|
|
|
[ "user_id" .= sessionResponseUserId response
|
|
|
|
|
, "display" .= sessionResponseDisplay response
|
|
|
|
|
, "csrf" .= sessionResponseCsrf response
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
data DeletePlanResponse = DeletePlanResponse
|
|
|
|
|
{ deletePlanPaths :: [RelativePath]
|
|
|
|
|
, deletePlanContinuation :: Maybe PaginationToken
|
|
|
|
|
}
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON DeletePlanResponse where
|
|
|
|
|
toJSON response = object
|
|
|
|
|
[ "paths" .= map unRelativePath (deletePlanPaths response)
|
|
|
|
|
, "continuation" .= deletePlanContinuation response
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
data DeleteBatchItem = DeleteBatchItem
|
|
|
|
|
{ deleteBatchPath :: RelativePath
|
|
|
|
|
, deleteBatchOperationId :: OperationId
|
|
|
|
|
}
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance FromJSON DeleteBatchItem where
|
|
|
|
|
parseJSON = withObject "DeleteBatchItem" $ \o ->
|
|
|
|
|
DeleteBatchItem
|
|
|
|
|
<$> o .: "path"
|
|
|
|
|
<*> o .: "operation_id"
|
|
|
|
|
|
|
|
|
|
data DeleteBatchRequest = DeleteBatchRequest
|
|
|
|
|
{ deleteBatchRoot :: Root
|
|
|
|
|
, deleteBatchCsrf :: CsrfToken
|
|
|
|
|
, deleteBatchItems :: [DeleteBatchItem]
|
|
|
|
|
}
|
2026-07-19 03:41:01 +09:00
|
|
|
deriving stock (Eq)
|
2026-07-19 01:12:29 +09:00
|
|
|
|
|
|
|
|
instance FromJSON DeleteBatchRequest where
|
|
|
|
|
parseJSON = withObject "DeleteBatchRequest" $ \o ->
|
|
|
|
|
DeleteBatchRequest
|
|
|
|
|
<$> o .: "root"
|
|
|
|
|
<*> o .: "csrf"
|
|
|
|
|
<*> o .: "items"
|
|
|
|
|
|
|
|
|
|
data DeleteItemResponse = DeleteItemResponse
|
|
|
|
|
{ deleteItemPath :: RelativePath
|
|
|
|
|
, deleteItemOperationId :: OperationId
|
|
|
|
|
, deleteItemResult :: Maybe MutationResult
|
|
|
|
|
, deleteItemError :: Maybe Text
|
|
|
|
|
}
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON DeleteItemResponse where
|
|
|
|
|
toJSON response = object
|
|
|
|
|
[ "path" .= unRelativePath (deleteItemPath response)
|
|
|
|
|
, "operation_id" .= deleteItemOperationId response
|
|
|
|
|
, "result" .= fmap mutationResultText (deleteItemResult response)
|
|
|
|
|
, "error" .= deleteItemError response
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
newtype DeleteBatchResponse = DeleteBatchResponse
|
|
|
|
|
{ deleteBatchResults :: [DeleteItemResponse]
|
|
|
|
|
}
|
|
|
|
|
deriving stock (Eq, Show)
|
|
|
|
|
|
|
|
|
|
instance ToJSON DeleteBatchResponse where
|
|
|
|
|
toJSON response = object ["results" .= deleteBatchResults response]
|
|
|
|
|
|
2026-07-18 13:17:40 +09:00
|
|
|
errorValue :: Text -> Text -> Maybe Value -> Value
|
|
|
|
|
errorValue code message details = object
|
|
|
|
|
[ "error" .= code
|
|
|
|
|
, "message" .= message
|
|
|
|
|
, "details" .= details
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
type HealthEndpoint =
|
|
|
|
|
"healthz" :> Get '[JSON] HealthResponse
|
|
|
|
|
|
|
|
|
|
type ReadyEndpoint =
|
|
|
|
|
"readyz" :> Throws ReadyError :> Get '[JSON] ReadyResponse
|
|
|
|
|
|
2026-07-19 01:12:29 +09:00
|
|
|
type AppEndpoint =
|
|
|
|
|
"app"
|
|
|
|
|
:> AuthProtect SessionAuth
|
|
|
|
|
:> Throws AuthenticationError
|
|
|
|
|
:> Get '[HTML]
|
|
|
|
|
(Headers '[Header "Content-Security-Policy" Text] HtmlPage)
|
|
|
|
|
|
|
|
|
|
type StaticAssetsEndpoint =
|
|
|
|
|
"assets"
|
|
|
|
|
:> AuthProtect SessionAuth
|
|
|
|
|
:> Throws AuthenticationError
|
|
|
|
|
:> Raw
|
|
|
|
|
|
2026-07-18 18:08:41 +09:00
|
|
|
type LoginStart =
|
|
|
|
|
Headers
|
|
|
|
|
'[ Header "Set-Cookie" Text
|
|
|
|
|
, Header "Location" Text
|
|
|
|
|
]
|
|
|
|
|
NoContent
|
|
|
|
|
|
|
|
|
|
type LoginEndpoint =
|
|
|
|
|
"login"
|
|
|
|
|
:> Throws LoginError
|
|
|
|
|
:> Verb 'GET 303 '[JSON] LoginStart
|
|
|
|
|
|
2026-07-18 13:17:40 +09:00
|
|
|
type LoginSuccess =
|
|
|
|
|
Headers
|
|
|
|
|
'[ Header "Set-Cookie" Text
|
|
|
|
|
, Header "Set-Cookie" Text
|
2026-07-18 18:08:41 +09:00
|
|
|
, Header "Set-Cookie" Text
|
|
|
|
|
, Header "Location" Text
|
2026-07-18 13:17:40 +09:00
|
|
|
]
|
2026-07-18 18:08:41 +09:00
|
|
|
NoContent
|
2026-07-18 13:17:40 +09:00
|
|
|
|
2026-07-18 18:08:41 +09:00
|
|
|
type OidcCallbackEndpoint =
|
|
|
|
|
"oidc"
|
|
|
|
|
:> "callback"
|
2026-07-18 13:17:40 +09:00
|
|
|
:> Throws LoginError
|
2026-07-18 18:08:41 +09:00
|
|
|
:> QueryParam' '[Required, Strict] "code" Text
|
|
|
|
|
:> QueryParam' '[Required, Strict] "state" Text
|
|
|
|
|
:> Header' '[Required, Strict] "Cookie" Text
|
|
|
|
|
:> Verb 'GET 303 '[JSON] LoginSuccess
|
2026-07-18 13:17:40 +09:00
|
|
|
|
|
|
|
|
type LogoutSuccess =
|
|
|
|
|
Headers
|
|
|
|
|
'[ Header "Set-Cookie" Text
|
2026-07-18 18:08:41 +09:00
|
|
|
, Header "Set-Cookie" Text
|
2026-07-18 13:17:40 +09:00
|
|
|
, Header "Set-Cookie" Text
|
|
|
|
|
]
|
|
|
|
|
NoContent
|
|
|
|
|
|
|
|
|
|
type LogoutEndpoint =
|
|
|
|
|
"logout"
|
|
|
|
|
:> AuthProtect SessionAuth
|
|
|
|
|
:> Throws AuthenticationError
|
|
|
|
|
:> Throws LogoutError
|
|
|
|
|
:> Header' '[Required, Strict] "X-CSRF-Token" CsrfToken
|
|
|
|
|
:> Post '[JSON] LogoutSuccess
|
|
|
|
|
|
|
|
|
|
type DownloadSuccess =
|
|
|
|
|
Headers
|
|
|
|
|
'[ Header "Content-Disposition" Text
|
|
|
|
|
, Header "X-Content-Type-Options" Text
|
2026-07-19 01:12:29 +09:00
|
|
|
, Header "Content-Security-Policy" Text
|
2026-07-18 20:48:26 +09:00
|
|
|
, Header "Content-Length" Integer
|
2026-07-18 13:17:40 +09:00
|
|
|
]
|
|
|
|
|
(SourceIO BS.ByteString)
|
|
|
|
|
|
2026-07-19 01:12:29 +09:00
|
|
|
type ProductApi =
|
|
|
|
|
"api" :>
|
|
|
|
|
( "session"
|
|
|
|
|
:> AuthProtect SessionAuth
|
|
|
|
|
:> Throws AuthenticationError
|
|
|
|
|
:> Get '[JSON] SessionResponse
|
|
|
|
|
:<|> "browse"
|
|
|
|
|
:> AuthProtect SessionAuth
|
|
|
|
|
:> Throws AuthenticationError
|
|
|
|
|
:> Throws BrowseError
|
|
|
|
|
:> QueryParam "root" Root
|
|
|
|
|
:> QueryParam "prefix" BrowsePrefix
|
|
|
|
|
:> QueryParam "q" SearchQuery
|
|
|
|
|
:> QueryParam "continuation" PaginationToken
|
2026-07-19 17:49:03 +09:00
|
|
|
:> Get '[JSON] BrowseResponse
|
2026-07-19 01:12:29 +09:00
|
|
|
:<|> "download"
|
|
|
|
|
:> AuthProtect SessionAuth
|
|
|
|
|
:> Throws AuthenticationError
|
|
|
|
|
:> Throws DownloadError
|
|
|
|
|
:> QueryParam' '[Required, Strict] "root" Root
|
|
|
|
|
:> QueryParam' '[Required, Strict] "path" RelativePath
|
|
|
|
|
:> Header "Range" Text
|
|
|
|
|
:> StreamGet NoFraming OctetStream DownloadSuccess
|
|
|
|
|
:<|> "upload"
|
|
|
|
|
:> AuthProtect SessionAuth
|
|
|
|
|
:> Throws AuthenticationError
|
|
|
|
|
:> Throws UploadError
|
|
|
|
|
:> QueryParam' '[Required, Strict] "root" Root
|
|
|
|
|
:> QueryParam' '[Required, Strict] "path" RelativePath
|
|
|
|
|
:> QueryParam' '[Required, Strict] "operation_id" OperationId
|
|
|
|
|
:> Header' '[Required, Strict] "X-Declared-Size" DeclaredSize
|
|
|
|
|
:> Header' '[Required, Strict] "X-Content-SHA256" ContentSha256
|
|
|
|
|
:> Header' '[Required, Strict] "X-CSRF-Token" CsrfToken
|
|
|
|
|
:> StreamBody NoFraming OctetStream (SourceIO BS.ByteString)
|
|
|
|
|
:> Post '[JSON] UploadResponse
|
|
|
|
|
:<|> "delete"
|
|
|
|
|
:> AuthProtect SessionAuth
|
|
|
|
|
:> Throws AuthenticationError
|
|
|
|
|
:> Throws DeleteError
|
|
|
|
|
:> ReqBody '[JSON] DeleteRequest
|
|
|
|
|
:> Post '[JSON] DeleteResponse
|
|
|
|
|
:<|> "delete-plan"
|
|
|
|
|
:> AuthProtect SessionAuth
|
|
|
|
|
:> Throws AuthenticationError
|
|
|
|
|
:> Throws DeletePrefixError
|
|
|
|
|
:> QueryParam' '[Required, Strict] "root" Root
|
|
|
|
|
:> QueryParam' '[Required, Strict] "path" RelativePath
|
|
|
|
|
:> QueryParam "continuation" PaginationToken
|
|
|
|
|
:> Get '[JSON] DeletePlanResponse
|
|
|
|
|
:<|> "delete-batch"
|
|
|
|
|
:> AuthProtect SessionAuth
|
|
|
|
|
:> Throws AuthenticationError
|
|
|
|
|
:> Throws DeletePrefixError
|
|
|
|
|
:> ReqBody '[JSON] DeleteBatchRequest
|
|
|
|
|
:> Post '[JSON] DeleteBatchResponse
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-18 13:17:40 +09:00
|
|
|
type Api =
|
|
|
|
|
HealthEndpoint
|
|
|
|
|
:<|> ReadyEndpoint
|
2026-07-19 01:12:29 +09:00
|
|
|
:<|> AppEndpoint
|
|
|
|
|
:<|> StaticAssetsEndpoint
|
2026-07-18 13:17:40 +09:00
|
|
|
:<|> LoginEndpoint
|
2026-07-18 18:08:41 +09:00
|
|
|
:<|> OidcCallbackEndpoint
|
2026-07-18 13:17:40 +09:00
|
|
|
:<|> LogoutEndpoint
|
2026-07-19 01:12:29 +09:00
|
|
|
:<|> ProductApi
|
2026-07-18 13:17:40 +09:00
|
|
|
|
|
|
|
|
api :: Proxy Api
|
|
|
|
|
api = Proxy
|
|
|
|
|
|
|
|
|
|
instance ToHttpApiData BrowsePrefix where
|
|
|
|
|
toUrlPiece (BrowsePrefix Nothing) = ""
|
|
|
|
|
toUrlPiece (BrowsePrefix (Just path)) = unRelativePath path
|
|
|
|
|
|
|
|
|
|
instance FromHttpApiData BrowsePrefix where
|
|
|
|
|
parseUrlPiece "" = Right (BrowsePrefix Nothing)
|
|
|
|
|
parseUrlPiece raw = BrowsePrefix . Just <$> validateRelativePath raw
|
|
|
|
|
|
|
|
|
|
instance ToHttpApiData PaginationToken where
|
|
|
|
|
toUrlPiece = unPaginationToken
|
|
|
|
|
|
|
|
|
|
instance FromHttpApiData PaginationToken where
|
|
|
|
|
parseUrlPiece = validatePaginationToken
|
|
|
|
|
|
|
|
|
|
instance ToHttpApiData SearchQuery where
|
|
|
|
|
toUrlPiece = unSearchQuery
|
|
|
|
|
|
|
|
|
|
instance FromHttpApiData SearchQuery where
|
|
|
|
|
parseUrlPiece raw
|
|
|
|
|
| T.null raw = Left "search query must not be empty"
|
|
|
|
|
| T.length raw > 512 = Left "search query is too long"
|
|
|
|
|
| T.any isControl raw = Left "search query contains a control character"
|
|
|
|
|
| otherwise = Right (SearchQuery raw)
|
|
|
|
|
|
|
|
|
|
instance ToHttpApiData DeclaredSize where
|
|
|
|
|
toUrlPiece = T.pack . show . unDeclaredSize
|
|
|
|
|
|
|
|
|
|
instance FromHttpApiData DeclaredSize where
|
|
|
|
|
parseUrlPiece raw =
|
|
|
|
|
case reads (T.unpack raw) of
|
|
|
|
|
[(size, "")] | size > 0 -> Right (DeclaredSize size)
|
|
|
|
|
_ -> Left "declared size must be a positive integer"
|
|
|
|
|
|
|
|
|
|
validatePaginationToken :: Text -> Either Text PaginationToken
|
|
|
|
|
validatePaginationToken raw
|
|
|
|
|
| T.null raw = Left "pagination token must not be empty"
|
|
|
|
|
| T.length raw > 2048 = Left "pagination token is too long"
|
|
|
|
|
| T.any isControl raw = Left "pagination token contains a control character"
|
|
|
|
|
| otherwise = Right (PaginationToken raw)
|
|
|
|
|
|
|
|
|
|
mutationResultText :: MutationResult -> Text
|
|
|
|
|
mutationResultText MutationPending = "pending"
|
|
|
|
|
mutationResultText MutationSucceeded = "succeeded"
|
|
|
|
|
mutationResultText MutationFailed = "failed"
|
|
|
|
|
mutationResultText MutationRejected = "rejected"
|
|
|
|
|
mutationResultText MutationConflict = "conflict"
|