Skip to content

feat(web): notification-system #1210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6f9a5db
feat(web): notification-system
nhestrompia Aug 31, 2023
0927267
refactor: netlify function
nhestrompia Aug 31, 2023
653793e
fix(web): make sure you are connected before settings notifications c…
kemuru Sep 2, 2023
3650b3d
feat(web): add nonce to message
kemuru Sep 7, 2023
ee63a36
chore(web): yarn lock problem
kemuru Sep 7, 2023
7cc3dd4
fix(web): change update settings to viem
kemuru Sep 7, 2023
bcbf73b
fix(web): payload json stringified
kemuru Sep 7, 2023
2b6cfe4
fix(web): wrong netlify folder path
kemuru Sep 7, 2023
ef94986
chore(web): remove new lines on message
kemuru Sep 8, 2023
bdc9403
chore(web): remove logs and add settings type
kemuru Sep 8, 2023
b799786
refactor(web): account abstraction for verifymessage
kemuru Sep 8, 2023
b0ee92d
refactor(web): function name change
kemuru Sep 8, 2023
d1e49c6
fix: added error message
jaybuidl Oct 3, 2023
d4eb2da
feat: added telegram contact field, fixed netlify function sig verifi…
jaybuidl Oct 3, 2023
889cef7
feat: use eip712 typed structured data signing
jaybuidl Oct 3, 2023
2126efc
refactor: notification form filenames
jaybuidl Oct 3, 2023
699e699
feat: close the settings popup if saved successfully
jaybuidl Oct 3, 2023
f72f819
chore: added types generation for the supabase db client
jaybuidl Oct 4, 2023
1365a27
feat: hardened input validation
jaybuidl Oct 6, 2023
657bb22
fix: user message
jaybuidl Oct 9, 2023
d1bd9d1
Merge branch 'dev' into feat(web)/notification-system
jaybuidl Oct 9, 2023
967e78f
fix: interface changes after merge
jaybuidl Oct 9, 2023
788a48b
chore: upgraded to the latest ui-components which decreases the opaci…
jaybuidl Oct 9, 2023
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
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.packageManager": "yarn",
"prettier.useEditorConfig": true,
"prettier.configPath": "prettier-config/.prettierrc.js"
"prettier.configPath": "prettier-config/.prettierrc.js",
"sonarlint.connectedMode.project": {
"connectionId": "kleros",
"projectKey": "kleros_kleros-v2"
}
}
112 changes: 112 additions & 0 deletions web/netlify/functions/update-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Handler } from "@netlify/functions";
import { verifyTypedData } from "viem";
import { createClient } from "@supabase/supabase-js";
import { Database } from "../../src/types/supabase-notification";
import messages from "../../src/consts/eip712-messages";
import { EMAIL_REGEX, TELEGRAM_REGEX, ETH_ADDRESS_REGEX, ETH_SIGNATURE_REGEX } from "../../src/consts/index";

type NotificationSettings = {
email?: string;
telegram?: string;
nonce: `${number}`;
address: `0x${string}`;
signature: string;
};

const parse = (inputString: string): NotificationSettings => {
let input;
try {
input = JSON.parse(inputString);
} catch (err) {
throw new Error("Invalid JSON format");
}

const requiredKeys: (keyof NotificationSettings)[] = ["nonce", "address", "signature"];
const optionalKeys: (keyof NotificationSettings)[] = ["email", "telegram"];
const receivedKeys = Object.keys(input);

for (const key of requiredKeys) {
if (!receivedKeys.includes(key)) {
throw new Error(`Missing key: ${key}`);
}
}

const allExpectedKeys = [...requiredKeys, ...optionalKeys];
for (const key of receivedKeys) {
if (!allExpectedKeys.includes(key as keyof NotificationSettings)) {
throw new Error(`Unexpected key: ${key}`);
}
}

const email = input.email ? input.email.trim() : "";
if (email && !EMAIL_REGEX.test(email)) {
throw new Error("Invalid email format");
}

const telegram = input.telegram ? input.telegram.trim() : "";
if (telegram && !TELEGRAM_REGEX.test(telegram)) {
throw new Error("Invalid Telegram username format");
}

if (!/^\d+$/.test(input.nonce)) {
throw new Error("Invalid nonce format. Expected an integer as a string.");
}

if (!ETH_ADDRESS_REGEX.test(input.address)) {
throw new Error("Invalid Ethereum address format");
}

if (!ETH_SIGNATURE_REGEX.test(input.signature)) {
throw new Error("Invalid signature format");
}

return {
email: input.email.trim(),
telegram: input.telegram.trim(),
nonce: input.nonce,
address: input.address.trim().toLowerCase(),
signature: input.signature.trim(),
};
};

export const handler: Handler = async (event) => {
try {
if (!event.body) {
throw new Error("No body provided");
}
const { email, telegram, nonce, address, signature } = parse(event.body);
const lowerCaseAddress = address.toLowerCase() as `0x${string}`;
// Note: this does NOT work for smart contract wallets, but viem's publicClient.verifyMessage() fails to verify atm.
// https://viem.sh/docs/utilities/verifyTypedData.html
const data = messages.contactDetails(address, nonce, telegram, email);
const isValid = await verifyTypedData({
...data,
signature,
});
if (!isValid) {
// If the recovered address does not match the provided address, return an error
throw new Error("Signature verification failed");
}

const supabase = createClient<Database>(process.env.SUPABASE_URL!, process.env.SUPABASE_CLIENT_API_KEY!);

// If the message is empty, delete the user record
if (email === "" && telegram === "") {
const { error } = await supabase.from("users").delete().match({ address: lowerCaseAddress });
if (error) throw error;
return { statusCode: 200, body: JSON.stringify({ message: "Record deleted successfully." }) };
}

// For a user matching this address, upsert the user record
const { error } = await supabase
.from("user-settings")
.upsert({ address: lowerCaseAddress, email: email, telegram: telegram })
.match({ address: lowerCaseAddress });
if (error) {
throw error;
}
return { statusCode: 200, body: JSON.stringify({ message: "Record updated successfully." }) };
} catch (err) {
return { statusCode: 500, body: JSON.stringify({ message: `Error: ${err}` }) };
}
};
11 changes: 7 additions & 4 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"check-types": "tsc --noEmit",
"generate": "yarn generate:gql && yarn generate:hooks",
"generate:gql": "graphql-codegen --require tsconfig-paths/register",
"generate:hooks": "NODE_NO_WARNINGS=1 wagmi generate"
"generate:hooks": "NODE_NO_WARNINGS=1 wagmi generate",
"generate:supabase": "scripts/generateSupabaseTypes.sh"
},
"prettier": "@kleros/kleros-v2-prettier-config",
"devDependencies": {
Expand All @@ -54,22 +55,24 @@
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.61.0",
"@typescript-eslint/utils": "^5.58.0",
"@wagmi/cli": "^1.3.0",
"@wagmi/cli": "^1.5.2",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-parcel": "^1.10.6",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"lru-cache": "^7.18.3",
"parcel": "2.8.3",
"supabase": "^1.102.2",
"typescript": "^4.9.5"
},
"dependencies": {
"@filebase/client": "^0.0.5",
"@kleros/kleros-v2-contracts": "workspace:^",
"@kleros/ui-components-library": "^2.6.1",
"@kleros/ui-components-library": "^2.6.2",
"@sentry/react": "^7.55.2",
"@sentry/tracing": "^7.55.2",
"@supabase/supabase-js": "^2.33.1",
"@tanstack/react-query": "^4.28.0",
"@types/react-modal": "^3.16.0",
"@web3modal/ethereum": "^2.7.1",
Expand Down Expand Up @@ -99,7 +102,7 @@
"react-use": "^17.4.0",
"styled-components": "^5.3.9",
"viem": "^1.0.0",
"wagmi": "^1.1.0"
"wagmi": "^1.4.3"
},
"volta": {
"node": "16.20.1",
Expand Down
10 changes: 10 additions & 0 deletions web/scripts/generateSupabaseTypes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

mkdir -p $SCRIPT_DIR/../src/types

# Docs: https://supabase.com/docs/guides/api/rest/generating-types#generating-types-using-supabase-cli
supabase=$SCRIPT_DIR/../../node_modules/supabase/bin/supabase
$supabase gen types typescript --project-id elokscrfhzodpgvaixfd --schema public > $SCRIPT_DIR/../src/types/supabase-notification.ts
$supabase gen types typescript --project-id pyyfdntuqbsfquzzatkz --schema public > $SCRIPT_DIR/../src/types/supabase-datalake.ts
24 changes: 24 additions & 0 deletions web/src/consts/eip712-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default {
contactDetails: (address: `0x${string}`, nonce, telegram = "", email = "") =>
({
address: address.toLowerCase() as `0x${string}`,
domain: {
name: "Kleros v2",
version: "1",
chainId: 421_613,
},
types: {
ContactDetails: [
{ name: "email", type: "string" },
{ name: "telegram", type: "string" },
{ name: "nonce", type: "string" },
],
},
primaryType: "ContactDetails",
message: {
email,
telegram,
nonce,
},
} as const),
};
7 changes: 7 additions & 0 deletions web/src/consts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ export const GIT_HASH = gitCommitShortHash;
export const GIT_DIRTY = clean ? "" : "-dirty";
export const GIT_URL = `https://github.com/kleros/kleros-v2/tree/${gitCommitHash}/web`;
export const RELEASE_VERSION = version;

// https://www.w3.org/TR/2012/WD-html-markup-20120329/input.email.html#input.email.attrs.value.single
// eslint-disable-next-line security/detect-unsafe-regex
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
export const TELEGRAM_REGEX = /^@\w{5,32}$/;
export const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
export const ETH_SIGNATURE_REGEX = /^0x[a-fA-F0-9]{130}$/;
5 changes: 1 addition & 4 deletions web/src/layout/Header/navbar/Menu/Help.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Bug from "svgs/icons/bug.svg";
import ETH from "svgs/icons/eth.svg";
import Faq from "svgs/menu-icons/help.svg";
import Telegram from "svgs/socialmedia/telegram.svg";
import { IHelp } from "..";

const Container = styled.div`
display: flex;
Expand Down Expand Up @@ -96,10 +97,6 @@ const ITEMS = [
},
];

interface IHelp {
toggleIsHelpOpen: () => void;
}

const Help: React.FC<IHelp> = ({ toggleIsHelpOpen }) => {
const containerRef = useRef(null);
useFocusOutside(containerRef, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { Dispatch, SetStateAction, useMemo, useEffect } from "react";
import styled from "styled-components";

import { Field } from "@kleros/ui-components-library";

const StyledLabel = styled.label`
display: flex;
justify-content: space-between;
margin-bottom: 10px;
`;

const StyledField = styled(Field)`
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
`;

interface IForm {
contactLabel: string;
contactPlaceholder: string;
contactInput: string;
contactIsValid: boolean;
setContactInput: Dispatch<SetStateAction<string>>;
setContactIsValid: Dispatch<SetStateAction<boolean>>;
validator: RegExp;
}

const FormContact: React.FC<IForm> = ({
contactLabel,
contactPlaceholder,
contactInput,
contactIsValid,
setContactInput,
setContactIsValid,
validator,
}) => {
useEffect(() => {
setContactIsValid(validator.test(contactInput));
}, [contactInput, setContactIsValid, validator]);

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
setContactInput(event.target.value);
};

const fieldVariant = useMemo(() => {
if (contactInput === "") {
return undefined;
}
return contactIsValid ? "success" : "error";
}, [contactInput, contactIsValid]);

return (
<>
<StyledLabel>{contactLabel}</StyledLabel>
<StyledField
variant={fieldVariant}
value={contactInput}
onChange={handleInputChange}
placeholder={contactPlaceholder}
/>
</>
);
};

export default FormContact;
Loading