Skip to content

Commit 1486d17

Browse files
authored
Merge branch 'master' into feat/token-icon-impros
2 parents c35f31a + 6cae895 commit 1486d17

31 files changed

+1518
-139
lines changed

web/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ parcel-bundle-reports
2727
src/hooks/contracts
2828
src/graphql
2929
generatedGitInfo.json
30+
generatedNetlifyInfo.json
3031

3132
# logs
3233
npm-debug.log*

web/netlify.toml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## Yarn 3 cache does not work out of the box as of Jan 2022. Context:
2+
## https://github.com/netlify/build/issues/1535#issuecomment-1021947989
3+
[build.environment]
4+
NETLIFY_USE_YARN = "true"
5+
NETLIFY_YARN_WORKSPACES = "true"
6+
YARN_ENABLE_GLOBAL_CACHE = "true"
7+
# YARN_CACHE_FOLDER = "$HOME/.yarn_cache"
8+
# YARN_VERSION = "3.2.0"
9+
10+
[functions]
11+
directory = "web/netlify/functions/"
12+
13+
[dev]
14+
framework = "parcel"

web/netlify/config/index.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const config = {
2+
/***** jwt variables *****/
3+
jwtIssuer: process.env.JWT_ISSUER ?? "Kleros", // ex :- Kleros
4+
jwtAudience: process.env.JWT_AUDIENCE ?? "Escrow", // ex :- Court, Curate, Escrow
5+
jwtExpTime: process.env.JWT_EXP_TIME ?? "2h",
6+
jwtSecret: process.env.JWT_SECRET,
7+
8+
/***** supabase variables *****/
9+
supabaseUrl: process.env.SUPABASE_URL,
10+
supabaseApiKey: process.env.SUPABASE_CLIENT_API_KEY,
11+
12+
/***** ipfs variables *****/
13+
filebaseToken: process.env.FILEBASE_TOKEN,
14+
rabbitMqUrl: process.env.RABBITMQ_URL,
15+
};
16+
17+
export default config;

web/netlify/functions/authUser.ts

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import middy from "@middy/core";
2+
import jsonBodyParser from "@middy/http-json-body-parser";
3+
import { createClient } from "@supabase/supabase-js";
4+
import * as jwt from "jose";
5+
import { SiweMessage } from "siwe";
6+
7+
import { DEFAULT_CHAIN } from "consts/chains";
8+
import { ETH_SIGNATURE_REGEX } from "consts/index";
9+
10+
import { netlifyUri, netlifyDeployUri, netlifyDeployPrimeUri } from "src/generatedNetlifyInfo.json";
11+
import { Database } from "src/types/supabase-notification";
12+
13+
import config from "../config";
14+
15+
const authUser = async (event) => {
16+
try {
17+
if (!event.body) {
18+
throw new Error("No body provided");
19+
}
20+
21+
const signature = event?.body?.signature;
22+
if (!signature) {
23+
throw new Error("Missing key : signature");
24+
}
25+
26+
if (!ETH_SIGNATURE_REGEX.test(signature)) {
27+
throw new Error("Invalid signature");
28+
}
29+
30+
const message = event?.body?.message;
31+
if (!message) {
32+
throw new Error("Missing key : message");
33+
}
34+
35+
const address = event?.body?.address;
36+
if (!address) {
37+
throw new Error("Missing key : address");
38+
}
39+
40+
const siweMessage = new SiweMessage(message);
41+
42+
if (
43+
!(
44+
(netlifyUri && netlifyUri === siweMessage.uri) ||
45+
(netlifyDeployUri && netlifyDeployUri === siweMessage.uri) ||
46+
(netlifyDeployPrimeUri && netlifyDeployPrimeUri === siweMessage.uri)
47+
)
48+
) {
49+
console.debug(
50+
`Invalid URI: expected one of [${netlifyUri} ${netlifyDeployUri} ${netlifyDeployPrimeUri}] but got ${siweMessage.uri}`
51+
);
52+
throw new Error(`Invalid URI`);
53+
}
54+
55+
if (siweMessage.chainId !== DEFAULT_CHAIN) {
56+
console.debug(`Invalid chain ID: expected ${DEFAULT_CHAIN} but got ${siweMessage.chainId}`);
57+
throw new Error(`Invalid chain ID`);
58+
}
59+
60+
const lowerCaseAddress = siweMessage.address.toLowerCase();
61+
if (lowerCaseAddress !== address.toLowerCase()) {
62+
throw new Error("Address mismatch in provided address and message");
63+
}
64+
65+
if (!config.supabaseUrl || !config.supabaseApiKey) {
66+
throw new Error("Supabase URL or API key is undefined");
67+
}
68+
const supabase = createClient<Database>(config.supabaseUrl, config.supabaseApiKey);
69+
70+
// get nonce from db, if its null that means it was already used
71+
const { error: nonceError, data: nonceData } = await supabase
72+
.from("user-nonce")
73+
.select("nonce")
74+
.eq("address", lowerCaseAddress)
75+
.single();
76+
77+
if (nonceError || !nonceData?.nonce) {
78+
throw new Error("Unable to fetch nonce from DB");
79+
}
80+
81+
try {
82+
await siweMessage.verify({ signature, nonce: nonceData.nonce, time: new Date().toISOString() });
83+
} catch (err) {
84+
throw new Error("Invalid signer");
85+
}
86+
87+
const { error } = await supabase.from("user-nonce").delete().match({ address: lowerCaseAddress });
88+
89+
if (error) {
90+
throw new Error("Error updating nonce in DB");
91+
}
92+
93+
if (!config.jwtSecret) {
94+
throw new Error("Secret not set in environment");
95+
}
96+
// user verified, generate auth token
97+
const encodedSecret = new TextEncoder().encode(config.jwtSecret);
98+
99+
const token = await new jwt.SignJWT({ id: address.toLowerCase() })
100+
.setProtectedHeader({ alg: "HS256" })
101+
.setIssuer(config.jwtIssuer)
102+
.setAudience(config.jwtAudience)
103+
.setExpirationTime(config.jwtExpTime)
104+
.sign(encodedSecret);
105+
106+
return { statusCode: 200, body: JSON.stringify({ message: "User authorised", token }) };
107+
} catch (err) {
108+
return { statusCode: 500, body: JSON.stringify({ message: `${err}` }) };
109+
}
110+
};
111+
112+
export const handler = middy(authUser).use(jsonBodyParser());
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import middy from "@middy/core";
2+
import { createClient } from "@supabase/supabase-js";
3+
4+
import { Database } from "../../src/types/supabase-notification";
5+
import { authMiddleware } from "../middleware/authMiddleware";
6+
7+
import config from "../config";
8+
9+
const fetchSettings = async (event) => {
10+
try {
11+
const address = event.auth.id;
12+
const lowerCaseAddress = address.toLowerCase() as `0x${string}`;
13+
14+
if (!config.supabaseUrl || !config.supabaseApiKey) {
15+
throw new Error("Supabase URL or API key is undefined");
16+
}
17+
const supabase = createClient<Database>(config.supabaseUrl, config.supabaseApiKey);
18+
19+
const { error, data } = await supabase
20+
.from("user-settings")
21+
.select("address, email, telegram")
22+
.eq("address", lowerCaseAddress)
23+
.single();
24+
25+
if (!data) {
26+
return { statusCode: 404, message: "Error : User not found" };
27+
}
28+
29+
if (error) {
30+
throw error;
31+
}
32+
return { statusCode: 200, body: JSON.stringify({ data }) };
33+
} catch (err) {
34+
return { statusCode: 500, message: `Error ${err?.message ?? err}` };
35+
}
36+
};
37+
38+
export const handler = middy(fetchSettings).use(authMiddleware());

web/netlify/functions/getNonce.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import middy from "@middy/core";
2+
import { createClient } from "@supabase/supabase-js";
3+
import { generateNonce } from "siwe";
4+
5+
import { ETH_ADDRESS_REGEX } from "src/consts";
6+
7+
import { Database } from "../../src/types/supabase-notification";
8+
9+
import config from "../config";
10+
11+
const getNonce = async (event) => {
12+
try {
13+
const { queryStringParameters } = event;
14+
15+
if (!queryStringParameters?.address) {
16+
return {
17+
statusCode: 400,
18+
body: JSON.stringify({ message: "Invalid query parameters" }),
19+
};
20+
}
21+
22+
const { address } = queryStringParameters;
23+
24+
if (!ETH_ADDRESS_REGEX.test(address)) {
25+
throw new Error("Invalid Ethereum address format");
26+
}
27+
28+
const lowerCaseAddress = address.toLowerCase() as `0x${string}`;
29+
30+
if (!config.supabaseUrl || !config.supabaseApiKey) {
31+
throw new Error("Supabase URL or API key is undefined");
32+
}
33+
const supabase = createClient<Database>(config.supabaseUrl, config.supabaseApiKey);
34+
35+
// generate nonce and save in db
36+
const nonce = generateNonce();
37+
38+
const { error } = await supabase
39+
.from("user-nonce")
40+
.upsert({ address: lowerCaseAddress, nonce: nonce })
41+
.eq("address", lowerCaseAddress);
42+
43+
if (error) {
44+
throw error;
45+
}
46+
47+
return { statusCode: 200, body: JSON.stringify({ nonce }) };
48+
} catch (err) {
49+
console.log(err);
50+
51+
return { statusCode: 500, message: `Error ${err?.message ?? err}` };
52+
}
53+
};
54+
55+
export const handler = middy(getNonce);
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import middy from "@middy/core";
2+
import jsonBodyParser from "@middy/http-json-body-parser";
3+
import { createClient } from "@supabase/supabase-js";
4+
5+
import { EMAIL_REGEX, TELEGRAM_REGEX, ETH_ADDRESS_REGEX } from "../../src/consts/index";
6+
import { Database } from "../../src/types/supabase-notification";
7+
import { authMiddleware } from "../middleware/authMiddleware";
8+
9+
import config from "../config";
10+
11+
type NotificationSettings = {
12+
email?: string;
13+
telegram?: string;
14+
address: `0x${string}`;
15+
};
16+
17+
const validate = (input: any): NotificationSettings => {
18+
const requiredKeys: (keyof NotificationSettings)[] = ["address"];
19+
const optionalKeys: (keyof NotificationSettings)[] = ["email", "telegram"];
20+
const receivedKeys = Object.keys(input);
21+
22+
for (const key of requiredKeys) {
23+
if (!receivedKeys.includes(key)) {
24+
throw new Error(`Missing key: ${key}`);
25+
}
26+
}
27+
28+
const allExpectedKeys = [...requiredKeys, ...optionalKeys];
29+
for (const key of receivedKeys) {
30+
if (!allExpectedKeys.includes(key as keyof NotificationSettings)) {
31+
throw new Error(`Unexpected key: ${key}`);
32+
}
33+
}
34+
35+
const email = input.email ? input.email.trim() : "";
36+
if (email && !EMAIL_REGEX.test(email)) {
37+
throw new Error("Invalid email format");
38+
}
39+
40+
const telegram = input.telegram ? input.telegram.trim() : "";
41+
if (telegram && !TELEGRAM_REGEX.test(telegram)) {
42+
throw new Error("Invalid Telegram username format");
43+
}
44+
45+
if (!ETH_ADDRESS_REGEX.test(input.address)) {
46+
throw new Error("Invalid Ethereum address format");
47+
}
48+
49+
return {
50+
email: input.email?.trim(),
51+
telegram: input.telegram?.trim(),
52+
address: input.address.trim().toLowerCase(),
53+
};
54+
};
55+
56+
const updateSettings = async (event) => {
57+
try {
58+
if (!event.body) {
59+
throw new Error("No body provided");
60+
}
61+
62+
const { email, telegram, address } = validate(event.body);
63+
const lowerCaseAddress = address.toLowerCase() as `0x${string}`;
64+
65+
// Prevent using someone else's token
66+
if (event?.auth?.id.toLowerCase() !== lowerCaseAddress) {
67+
throw new Error("Unauthorised user");
68+
}
69+
70+
if (!config.supabaseUrl || !config.supabaseApiKey) {
71+
throw new Error("Supabase URL or API key is undefined");
72+
}
73+
const supabase = createClient<Database>(config.supabaseUrl, config.supabaseApiKey);
74+
75+
// If the message is empty, delete the user record
76+
if (email === "" && telegram === "") {
77+
const { error } = await supabase.from("user-settings").delete().match({ address: lowerCaseAddress });
78+
if (error) throw error;
79+
return { statusCode: 200, body: JSON.stringify({ message: "Record deleted successfully." }) };
80+
}
81+
82+
// For a user matching this address, upsert the user record
83+
const { error } = await supabase
84+
.from("user-settings")
85+
.upsert({ address: lowerCaseAddress, email: email, telegram: telegram })
86+
.match({ address: lowerCaseAddress });
87+
if (error) {
88+
throw error;
89+
}
90+
return { statusCode: 200, body: JSON.stringify({ message: "Record updated successfully." }) };
91+
} catch (err) {
92+
return { statusCode: 500, body: JSON.stringify({ message: `${err}` }) };
93+
}
94+
};
95+
96+
export const handler = middy(updateSettings).use(jsonBodyParser()).use(authMiddleware());

0 commit comments

Comments
 (0)