Skip to content

Commit 80aa32f

Browse files
lengyel-arpad85DarianMsidvishnoi
authored
feat: request grant confirmation for config updates (#39)
* add open payments library * required grant in api response * OP env vars * wallet ownership modal and grant confirmation * add finalizeValidationPayment * remove query params from form Co-authored-by: darian <darian410@gmail.com> Co-authored-by: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com>
1 parent 7e93c9a commit 80aa32f

25 files changed

+1269
-192
lines changed

backend/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@
2121
},
2222
"dependencies": {
2323
"@aws-sdk/client-s3": "^3.645.0",
24+
"@remix-run/node": "^2.14.0",
2425
"@smithy/node-http-handler": "^3.2.3",
26+
"@types/express-session": "^1.18.1",
2527
"@types/underscore": "^1.11.15",
28+
"cookie-parser": "^1.4.7",
2629
"dotenv": "^16.4.5",
2730
"express": "^4.19.2",
31+
"express-session": "^1.18.1",
2832
"he": "^1.2.0",
2933
"module-alias": "^2.2.3",
3034
"sanitize-html": "^2.14.0",

backend/src/controllers/tools.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
SanitizedFields,
1717
SaveUserConfigRequest
1818
} from './types.js'
19+
import { getSession } from '@/services/session.js'
1920

2021
export const getDefault = async (_: Request, res: Response) => {
2122
try {
@@ -33,15 +34,24 @@ export const createUserConfig = async (req: Request, res: Response) => {
3334
const data: CreateConfigRequest = req.body
3435
const tag = data.version || data.tag
3536

36-
if (!data.walletAddress) {
37+
if (!data?.walletAddress) {
3738
throw 'Wallet address is required'
3839
}
40+
const walletAddress = decodeURIComponent(`https://${data.walletAddress}`)
41+
42+
const cookieHeader = req.headers.cookie
43+
const session = await getSession(cookieHeader)
44+
45+
const validForWallet = session?.get('validForWallet')
46+
47+
if (!session || validForWallet !== walletAddress) {
48+
throw 'Grant confirmation is required'
49+
}
50+
3951
const defaultData = await getDefaultData()
4052
const defaultDataContent: ConfigVersions['default'] =
4153
JSON.parse(defaultData).default
42-
defaultDataContent.walletAddress = decodeURIComponent(
43-
`https://${data.walletAddress}`
44-
)
54+
defaultDataContent.walletAddress = walletAddress
4555

4656
sanitizeConfigFields({ ...defaultDataContent, tag })
4757

@@ -51,6 +61,7 @@ export const createUserConfig = async (req: Request, res: Response) => {
5161
try {
5262
// existing config
5363
const s3data = await s3.send(new GetObjectCommand(params))
64+
5465
// Convert the file stream to a string
5566
fileContentString = await streamToString(
5667
s3data.Body as NodeJS.ReadableStream
@@ -97,10 +108,17 @@ export const createUserConfig = async (req: Request, res: Response) => {
97108
export const saveUserConfig = async (req: Request, res: Response) => {
98109
try {
99110
const data: SaveUserConfigRequest = req.body
111+
const cookieHeader = req.headers.cookie
112+
const session = await getSession(cookieHeader)
113+
114+
const validForWallet = session?.get('validForWallet')
100115

101116
if (!data.walletAddress) {
102117
throw 'Wallet address is required'
103118
}
119+
if (!session || validForWallet !== data.walletAddress) {
120+
throw 'Grant confirmation is required'
121+
}
104122

105123
const { s3, params } = getS3AndParams(data.walletAddress)
106124

@@ -132,19 +150,22 @@ export const getUserConfig = async (req: Request, res: Response) => {
132150
if (!id) {
133151
throw new S3FileNotFoundError('Wallet address is required')
134152
}
153+
const walletAddress = decodeURIComponent(`https://${id}`)
135154

136155
// ensure we have all keys w default values, user config will overwrite values that exist in saved json
137156
const defaultData = await getDefaultData()
157+
const parsedDefaultData = JSON.parse(defaultData)
158+
parsedDefaultData.default.walletAddress = walletAddress
138159

139-
const { s3, params } = getS3AndParams(id)
160+
const { s3, params } = getS3AndParams(walletAddress)
140161
const data = await s3.send(new GetObjectCommand(params))
141162
// Convert the file stream to a string
142163
const fileContentString = await streamToString(
143164
data.Body as NodeJS.ReadableStream
144165
)
145166

146167
let fileContent = Object.assign(
147-
JSON.parse(defaultData),
168+
parsedDefaultData,
148169
...[JSON.parse(fileContentString)]
149170
)
150171
fileContent = filterDeepProperties(fileContent)

backend/src/server.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import https from 'https'
22
import http from 'http'
33
import fs from 'fs'
44
import express, { Express } from 'express'
5+
import session from 'express-session'
56
import routes from './routes/index.js'
7+
import { SESSION_COOKIE_SECRET_KEY } from './services/session.js'
68

79
const router: Express = express()
810

@@ -21,10 +23,24 @@ if (isDevelopment) {
2123
router.use(express.urlencoded({ extended: true }))
2224
router.use(express.json())
2325

26+
// Session middleware
27+
router.use(
28+
session({
29+
secret: SESSION_COOKIE_SECRET_KEY,
30+
resave: false,
31+
saveUninitialized: true, // Only save the session if it is modified
32+
cookie: {
33+
httpOnly: true,
34+
secure: true,
35+
sameSite: 'none'
36+
}
37+
})
38+
)
39+
2440
router.use((req, res, next) => {
2541
// set the CORS policy
2642
res.header('Access-Control-Allow-Origin', '*')
27-
// set the CORS headers
43+
res.header('Access-Control-Allow-Credentials', 'true')
2844
res.header(
2945
'Access-Control-Allow-Headers',
3046
'origin,X-Requested-With,Content-Type,Accept,Authorization'

backend/src/services/session.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createCookieSessionStorage } from '@remix-run/node'
2+
3+
export const SESSION_COOKIE_SECRET_KEY =
4+
process.env.SESSION_COOKIE_SECRET_KEY || 'supersecretilpaystring'
5+
6+
const { getSession, commitSession, destroySession } =
7+
createCookieSessionStorage({
8+
cookie: {
9+
name: 'wmtools-session',
10+
httpOnly: true,
11+
secure: true,
12+
sameSite: 'none',
13+
secrets: [SESSION_COOKIE_SECRET_KEY]
14+
}
15+
})
16+
17+
export { getSession, commitSession, destroySession }

backend/src/types/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import 'express-session'
2+
3+
declare module 'express-session' {
4+
interface Session {
5+
validForWallet?: string
6+
}
7+
}

backend/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
}
1111
},
1212
"include": ["src/**/*", "tests/**/*"],
13-
"exclude": ["node_modules", "**/node_modules/*"]
13+
"exclude": ["node_modules", "**/node_modules/*"],
14+
"files": ["src/types/index.ts"]
1415
}

docker/dev/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ SCRIPT_FRONTEND_URL=https://localhost:5100/
55
SCRIPT_ILPAY_URL=https://interledgerpay.com/extension/
66
SCRIPT_EMBED_URL=https://localhost:5100/
77

8+
OP_KEY_ID=
9+
OP_PRIVATE_KEY=
10+
OP_WALLET_ADDRESS=
11+
OP_REDIRECT_URL=
12+
813
# BACKEND
914
AWS_ACCESS_KEY_ID=
1015
AWS_SECRET_ACCESS_KEY=

docker/dev/docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ services:
5050
ILPAY_URL: ${SCRIPT_ILPAY_URL}
5151
FRONTEND_URL: ${SCRIPT_FRONTEND_URL}
5252
INIT_SCRIPT_URL: ${SCRIPT_EMBED_URL}
53+
OP_KEY_ID: ${OP_KEY_ID}
54+
OP_PRIVATE_KEY: ${OP_PRIVATE_KEY}
55+
OP_WALLET_ADDRESS: ${OP_WALLET_ADDRESS}
56+
OP_REDIRECT_URL: ${OP_REDIRECT_URL}
5357
networks:
5458
- wm-tools
5559
command: pnpm run dev

frontend/Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ RUN pnpm --filter frontend build
4444
ARG VITE_SCRIPT_API_URL
4545
ARG VITE_SCRIPT_FRONTEND_URL
4646
ARG VITE_SCRIPT_ILPAY_URL
47+
ARG VITE_INIT_SCRIPT_URL
4748

4849
ENV VITE_SCRIPT_API_URL=$VITE_SCRIPT_API_URL \
4950
VITE_SCRIPT_FRONTEND_URL=$VITE_SCRIPT_FRONTEND_URL \
50-
VITE_SCRIPT_ILPAY_URL=$VITE_SCRIPT_ILPAY_URL
51+
VITE_SCRIPT_ILPAY_URL=$VITE_SCRIPT_ILPAY_URL \
52+
VITE_INIT_SCRIPT_URL=$VITE_INIT_SCRIPT_URL
5153

5254
RUN pnpm --filter frontend build:init
5355

frontend/Dockerfile.dev

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ EXPOSE 5100
3131
ARG VITE_SCRIPT_API_URL
3232
ARG VITE_SCRIPT_FRONTEND_URL
3333
ARG VITE_SCRIPT_ILPAY_URL
34+
ARG VITE_INIT_SCRIPT_URL
3435

3536
ENV VITE_SCRIPT_API_URL=$VITE_SCRIPT_API_URL \
3637
VITE_SCRIPT_FRONTEND_URL=$VITE_SCRIPT_FRONTEND_URL \
37-
VITE_SCRIPT_ILPAY_URL=$VITE_SCRIPT_ILPAY_URL
38+
VITE_SCRIPT_ILPAY_URL=$VITE_SCRIPT_ILPAY_URL \
39+
VITE_INIT_SCRIPT_URL=$VITE_INIT_SCRIPT_URL
3840

3941
WORKDIR /app/frontend
4042

0 commit comments

Comments
 (0)