diff --git a/govtool/backend/app/Main.hs b/govtool/backend/app/Main.hs index 522b40bb6..cf0b58d10 100644 --- a/govtool/backend/app/Main.hs +++ b/govtool/backend/app/Main.hs @@ -298,6 +298,7 @@ liftServer appEnv = NotFoundError _ -> err404 CriticalError _ -> err500 InternalError _ -> err500 + UnauthorizedError _ -> err401 AppIpfsError (OtherIpfsError _) -> err400 AppIpfsError _ -> err503 throwError $ status { errBody = encode appError, errHeaders = [("Content-Type", "application/json")] } diff --git a/govtool/backend/example-config.json b/govtool/backend/example-config.json index 7cfb4d5f2..7b3fea2f2 100644 --- a/govtool/backend/example-config.json +++ b/govtool/backend/example-config.json @@ -7,6 +7,7 @@ "port" : 5432 }, "pinataapijwt": "", + "ipfsuploadapikey": "", "port" : 9999, "host" : "localhost", "cachedurationseconds": 20, diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index c0a594f7a..0051cd776 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -53,12 +53,12 @@ import qualified VVA.Proposal as Proposal import qualified VVA.Transaction as Transaction import qualified VVA.Types as Types import VVA.Types (App, AppEnv (..), - AppError (AppIpfsError, CriticalError, InternalError, ValidationError), + AppError (AppIpfsError, CriticalError, InternalError, UnauthorizedError, ValidationError), CacheEnv (..)) type VVAApi = "ipfs" - :> "upload" :> QueryParam "fileName" Text :> ReqBody '[PlainText] Text :> Post '[JSON] UploadResponse + :> "upload" :> Header "Authorization" Text :> QueryParam "fileName" Text :> ReqBody '[PlainText] Text :> Post '[JSON] UploadResponse :<|> "drep" :> "list" :> QueryParam "search" Text :> QueryParams "status" DRepStatus @@ -118,9 +118,21 @@ server = upload :<|> getNetworkTotalStake :<|> getAccountInfo -upload :: App m => Maybe Text -> Text -> m UploadResponse -upload mFileName fileContentText = do +upload :: App m => Maybe Text -> Maybe Text -> Text -> m UploadResponse +upload mAuthHeader mFileName fileContentText = do AppEnv {vvaConfig} <- ask + let configuredApiKey = ipfsUploadApiKey vvaConfig + case configuredApiKey of + Nothing -> throwError $ UnauthorizedError "IPFS upload is not configured on this server" + Just expectedKey -> do + case mAuthHeader of + Nothing -> throwError $ UnauthorizedError "Missing Authorization header. Please provide a valid API key." + Just authHeader -> do + let strippedKey = if "Bearer " `isPrefixOf` authHeader + then Text.drop (Text.length "Bearer ") authHeader + else authHeader + when (strippedKey /= expectedKey) $ + throwError $ UnauthorizedError "Invalid API key" let fileContent = TL.encodeUtf8 $ TL.fromStrict fileContentText vvaPinataJwt = pinataApiJwt vvaConfig fileName = fromMaybe "data.txt" mFileName -- Default to data.txt if no filename is provided diff --git a/govtool/backend/src/VVA/Config.hs b/govtool/backend/src/VVA/Config.hs index b11358f3d..ff1b749ce 100644 --- a/govtool/backend/src/VVA/Config.hs +++ b/govtool/backend/src/VVA/Config.hs @@ -83,6 +83,8 @@ data VVAConfigInternal , vVAConfigInternalSentryEnv :: String -- | Pinata API JWT , vVAConfigInternalPinataApiJwt :: Maybe Text + -- | API key required to authorize IPFS uploads + , vVAConfigInternalIpfsUploadApiKey :: Maybe Text } deriving (FromConfig, Generic, Show) @@ -97,7 +99,8 @@ instance DefaultConfig VVAConfigInternal where vVaConfigInternalDRepListCacheDurationSeconds = 600, vVAConfigInternalSentrydsn = "https://username:password@senty.host/id", vVAConfigInternalSentryEnv = "development", - vVAConfigInternalPinataApiJwt = Nothing + vVAConfigInternalPinataApiJwt = Nothing, + vVAConfigInternalIpfsUploadApiKey = Nothing } -- | DEX configuration. @@ -119,6 +122,8 @@ data VVAConfig , sentryEnv :: String -- | Pinata API JWT , pinataApiJwt :: Maybe Text + -- | API key required to authorize IPFS uploads + , ipfsUploadApiKey :: Maybe Text } deriving (Generic, Show, ToJSON) @@ -161,7 +166,8 @@ convertConfig VVAConfigInternal {..} = dRepListCacheDurationSeconds = vVaConfigInternalDRepListCacheDurationSeconds, sentryDSN = vVAConfigInternalSentrydsn, sentryEnv = vVAConfigInternalSentryEnv, - pinataApiJwt = vVAConfigInternalPinataApiJwt + pinataApiJwt = vVAConfigInternalPinataApiJwt, + ipfsUploadApiKey = vVAConfigInternalIpfsUploadApiKey } -- | Load configuration from a file specified on the command line. Load from diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index 9ace0e5bc..ffde7911b 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -61,6 +61,7 @@ data AppError | CriticalError Text | InternalError Text | AppIpfsError IpfsError + | UnauthorizedError Text deriving (Show) instance Exception AppError @@ -71,6 +72,7 @@ instance ToJSON AppError where toJSON (CriticalError msg) = object ["errorType" .= A.String "CriticalError", "message" .= msg] toJSON (InternalError msg) = object ["errorType" .= A.String "InternalError", "message" .= msg] toJSON (AppIpfsError err) = toJSON err + toJSON (UnauthorizedError msg) = object ["errorType" .= A.String "UnauthorizedError", "message" .= msg] data Vote = Vote diff --git a/govtool/frontend/src/config/env.ts b/govtool/frontend/src/config/env.ts index 9252934f8..9d0f8c0bf 100644 --- a/govtool/frontend/src/config/env.ts +++ b/govtool/frontend/src/config/env.ts @@ -25,6 +25,7 @@ export const env = { VITE_OUTCOMES_API_URL: getEnv("VITE_OUTCOMES_API_URL"), VITE_IPFS_GATEWAY: getEnv("VITE_IPFS_GATEWAY"), VITE_IPFS_PROJECT_ID: getEnv("VITE_IPFS_PROJECT_ID"), + VITE_IPFS_UPLOAD_API_KEY: getEnv("VITE_IPFS_UPLOAD_API_KEY"), VITE_GTM_ID: getEnv("VITE_GTM_ID"), VITE_SENTRY_DSN: getEnv("VITE_SENTRY_DSN"), diff --git a/govtool/frontend/src/services/requests/postIpfs.ts b/govtool/frontend/src/services/requests/postIpfs.ts index 346dff3c4..fd514c659 100644 --- a/govtool/frontend/src/services/requests/postIpfs.ts +++ b/govtool/frontend/src/services/requests/postIpfs.ts @@ -1,10 +1,17 @@ import { API } from "../API"; +import { env } from "@/config/env"; export const postIpfs = async ({ content }: { content: string }) => { + const headers: Record = { + "Content-Type": "text/plain;charset=utf-8", + }; + + if (env.VITE_IPFS_UPLOAD_API_KEY) { + headers["Authorization"] = `Bearer ${env.VITE_IPFS_UPLOAD_API_KEY}`; + } + const response = await API.post("/ipfs/upload", content, { - headers: { - "Content-Type": "text/plain;charset=utf-8" - } + headers, }); return response.data; };