Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions govtool/backend/app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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")] }
Expand Down
1 change: 1 addition & 0 deletions govtool/backend/example-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"port" : 5432
},
"pinataapijwt": "",
"ipfsuploadapikey": "",
"port" : 9999,
"host" : "localhost",
"cachedurationseconds": 20,
Expand Down
20 changes: 16 additions & 4 deletions govtool/backend/src/VVA/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions govtool/backend/src/VVA/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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.
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions govtool/backend/src/VVA/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ data AppError
| CriticalError Text
| InternalError Text
| AppIpfsError IpfsError
| UnauthorizedError Text
deriving (Show)

instance Exception AppError
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions govtool/frontend/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
13 changes: 10 additions & 3 deletions govtool/frontend/src/services/requests/postIpfs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { API } from "../API";
import { env } from "@/config/env";

export const postIpfs = async ({ content }: { content: string }) => {
const headers: Record<string, string> = {
"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;
};