From d2daed4cac0187c599d526ceb80a0cf5c33eb7be Mon Sep 17 00:00:00 2001 From: Gilbert Date: Tue, 23 Apr 2024 21:10:20 -0500 Subject: [PATCH] Sign in with google --- packages/backend/src/routes/auth.ts | 42 ++++++--- packages/backend/src/server.ts | 2 +- packages/backend/src/service.ts | 10 ++- packages/frontend/package.json | 14 +-- packages/frontend/src/App.tsx | 8 +- packages/frontend/src/pages/auth/Login.tsx | 94 ++++++++++++++++++--- packages/frontend/src/pages/auth/SignUp.tsx | 80 ++++++++++++++---- packages/frontend/src/utils/siwe.ts | 19 +++-- packages/frontend/src/utils/use-snowball.ts | 9 +- yarn.lock | 14 +-- 10 files changed, 222 insertions(+), 70 deletions(-) diff --git a/packages/backend/src/routes/auth.ts b/packages/backend/src/routes/auth.ts index 4d2da019..e1214bbc 100644 --- a/packages/backend/src/routes/auth.ts +++ b/packages/backend/src/routes/auth.ts @@ -1,37 +1,55 @@ import { Router } from 'express'; import { SiweMessage } from 'siwe'; +import { Service } from '../service'; const router = Router(); router.post('/validate', async (req, res) => { - const { message, signature } = req.body; + const { message, signature, action } = req.body; const { success, data } = await new SiweMessage(message).verify({ signature, }); - if (success) { - req.session.address = data.address; - req.session.chainId = data.chainId; + if (!success) { + return res.send({ success }); } + const service: Service = req.app.get('service'); + const user = await service.getUserByEthAddress(data.address); + + if (action === 'signup') { + if (user) { + return res.send({ success: false, error: 'user_already_exists' }); + } + const newUser = await service.loadOrCreateUser(data.address); + req.session.userId = newUser.id; + } else if (action === 'login') { + if (!user) { + return res.send({ success: false, error: 'user_not_found' }); + } + req.session.userId = user.id; + } + + req.session.address = data.address; + res.send({ success }); }); router.get('/session', (req, res) => { - if (req.session.address && req.session.chainId) { - res.send({ address: req.session.address, chainId: req.session.chainId }); + if (req.session.address) { + res.send({ + userId: req.session.userId, + address: req.session.address, + }); } else { res.status(401).send({ error: 'Unauthorized: No active session' }); } }); router.post('/logout', (req, res) => { - req.session.destroy((err) => { - if (err) { - return res.send({ success: false }); - } - res.send({ success: true }); - }); + // This is how you clear cookie-session + (req as any).session = null; + res.send({ success: true }); }); export default router; diff --git a/packages/backend/src/server.ts b/packages/backend/src/server.ts index f89aa979..fa8b7d46 100644 --- a/packages/backend/src/server.ts +++ b/packages/backend/src/server.ts @@ -23,8 +23,8 @@ const log = debug('snowball:server'); declare module 'express-session' { interface SessionData { + userId: string; address: string; - chainId: number; } } diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 5211585c..ff304fde 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -161,13 +161,17 @@ export class Service { }); } - async loadOrCreateUser (ethAddress: string): Promise { - // Get user by ETH address - let user = await this.db.getUser({ + async getUserByEthAddress (ethAddress: string): Promise { + return await this.db.getUser({ where: { ethAddress } }); + } + + async loadOrCreateUser (ethAddress: string): Promise { + // Get user by ETH address + let user = await this.getUserByEthAddress(ethAddress); if (!user) { const [org] = await this.db.getOrganizations({}); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 84bca501..91b671f8 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -21,14 +21,14 @@ "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", - "@snowballtools/auth": "0.1.0", - "@snowballtools/auth-lit": "0.1.0", - "@snowballtools/js-sdk": "0.1.0", - "@snowballtools/link-lit-alchemy-light": "0.1.0", + "@snowballtools/auth": "^0.1.0", + "@snowballtools/auth-lit": "^0.1.0", + "@snowballtools/js-sdk": "^0.1.0", + "@snowballtools/link-lit-alchemy-light": "^0.1.0", "@snowballtools/material-tailwind-react-fork": "^2.1.10", - "@snowballtools/smartwallet-alchemy-light": "0.1.0", - "@snowballtools/types": "0.1.0", - "@snowballtools/utils": "0.1.0", + "@snowballtools/smartwallet-alchemy-light": "^0.1.0", + "@snowballtools/types": "^0.1.0", + "@snowballtools/utils": "^0.1.0", "@tanstack/react-query": "^5.22.2", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 50533798..eae3b12c 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -64,10 +64,8 @@ function App() { }).then((res) => { if (res.status !== 200) { localStorage.clear(); - if ( - window.location.pathname !== '/login' && - window.location.pathname !== '/signup' - ) { + const path = window.location.pathname; + if (path !== '/login' && path !== '/signup') { window.location.pathname = '/login'; } } @@ -76,7 +74,7 @@ function App() { return ( - ; + ); } diff --git a/packages/frontend/src/pages/auth/Login.tsx b/packages/frontend/src/pages/auth/Login.tsx index 8b95143d..0757cf26 100644 --- a/packages/frontend/src/pages/auth/Login.tsx +++ b/packages/frontend/src/pages/auth/Login.tsx @@ -9,12 +9,15 @@ import { import { GoogleIcon } from 'components/shared/CustomIcon/GoogleIcon'; import { DotBorder } from 'components/shared/DotBorder'; import { WavyBorder } from 'components/shared/WavyBorder'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { CreatePasskey } from './CreatePasskey'; import { AppleIcon } from 'components/shared/CustomIcon/AppleIcon'; import { KeyIcon } from 'components/shared/CustomIcon/KeyIcon'; import { useToast } from 'components/shared/Toast'; import { Link } from 'react-router-dom'; +import { PKPEthersWallet } from '@lit-protocol/pkp-ethers'; +import { signInWithEthereum } from 'utils/siwe'; +import { useSnowball } from 'utils/use-snowball'; type Provider = 'google' | 'github' | 'apple' | 'email' | 'passkey'; @@ -23,6 +26,8 @@ type Props = { }; export const Login = ({ onDone }: Props) => { + const snowball = useSnowball(); + const [error, setError] = useState(''); const [provider, setProvider] = useState(false); // const loading = snowball.auth.state.loading && provider; @@ -33,6 +38,59 @@ export const Login = ({ onDone }: Props) => { return ; } + async function handleSigninRedirect() { + let wallet: PKPEthersWallet | undefined; + const { google } = snowball.auth; + if (google.canHandleOAuthRedirectBack()) { + setProvider('google'); + console.log('Handling google redirect back'); + try { + await google.handleOAuthRedirectBack(); + wallet = await google.getEthersWallet(); + const result = await signInWithEthereum(1, 'login', wallet); + if (result.error) { + setError(result.error); + setProvider(false); + wallet = undefined; + return; + } + } catch (err: any) { + setError(err.message); + console.log(err.message, err.name, err.details); + setProvider(false); + return; + } + } + // if (apple.canHandleOAuthRedirectBack()) { + // setProvider('apple'); + // console.log('Handling apple redirect back'); + // try { + // await apple.handleOAuthRedirectBack(); + // wallet = await apple.getEthersWallet(); + // const result = await signInWithEthereum(1, 'login', wallet); + // if (result.error) { + // setError(result.error); + // setProvider(false); + // wallet = undefined; + // return; + // } + // } catch (err: any) { + // setError(err.message); + // console.log(err.message, err.name, err.details); + // setProvider(false); + // return; + // } + // } + + if (wallet) { + window.location.pathname = '/'; + } + } + + useEffect(() => { + handleSigninRedirect(); + }, []); + return (
@@ -114,7 +172,7 @@ export const Login = ({ onDone }: Props) => { } onClick={() => { setProvider('google'); - // snowball.auth.createPasskey(); + snowball.auth.google.startOAuthRedirect(); }} className="flex-1 self-stretch" variant={'tertiary'} @@ -157,6 +215,7 @@ export const Login = ({ onDone }: Props) => { } onClick={async () => { setProvider('apple'); + // snowball.auth.apple.startOAuthRedirect(); await new Promise((resolve) => setTimeout(resolve, 800)); setProvider(false); toast({ @@ -175,17 +234,26 @@ export const Login = ({ onDone }: Props) => { Continue with Apple
-
-
- Don't have an account? -
-
- - Sign up now - + +
+ {error && ( +
+
Error: {error}
+
+ )} + +
+
+ Don't have an account? +
+
+ + Sign up now + +
diff --git a/packages/frontend/src/pages/auth/SignUp.tsx b/packages/frontend/src/pages/auth/SignUp.tsx index a9ac9588..bcfa48db 100644 --- a/packages/frontend/src/pages/auth/SignUp.tsx +++ b/packages/frontend/src/pages/auth/SignUp.tsx @@ -19,12 +19,15 @@ import { signInWithEthereum } from 'utils/siwe'; type Provider = 'google' | 'github' | 'apple' | 'email'; +type Err = { type: 'email' | 'provider'; message: string }; + type Props = { onDone: () => void; }; export const SignUp = ({ onDone }: Props) => { const [email, setEmail] = useState(''); + const [error, setError] = useState(); const [provider, setProvider] = useState(false); const { toast } = useToast(); @@ -32,13 +35,43 @@ export const SignUp = ({ onDone }: Props) => { async function handleSignupRedirect() { let wallet: PKPEthersWallet | undefined; - const google = snowball.auth.google; + const { google } = snowball.auth; if (google.canHandleOAuthRedirectBack()) { setProvider('google'); - await google.handleOAuthRedirectBack(); - wallet = await google.getEthersWallet(); - await signInWithEthereum(wallet); + try { + await google.handleOAuthRedirectBack(); + wallet = await google.getEthersWallet(); + const result = await signInWithEthereum(1, 'signup', wallet); + if (result.error) { + setError({ type: 'provider', message: result.error }); + setProvider(false); + wallet = undefined; + return; + } + } catch (err: any) { + setError({ type: 'provider', message: err.message }); + setProvider(false); + return; + } } + // if (apple.canHandleOAuthRedirectBack()) { + // setProvider('apple'); + // try { + // await apple.handleOAuthRedirectBack(); + // wallet = await apple.getEthersWallet(); + // const result = await signInWithEthereum(1, 'signup', wallet); + // if (result.error) { + // setError({ type: 'provider', message: result.error }); + // setProvider(false); + // wallet = undefined; + // return; + // } + // } catch (err: any) { + // setError({ type: 'provider', message: err.message }); + // setProvider(false); + // return; + // } + // } if (wallet) { onDone(); @@ -118,6 +151,7 @@ export const SignUp = ({ onDone }: Props) => { } onClick={async () => { setProvider('apple'); + // snowball.auth.apple.startOAuthRedirect(); await new Promise((resolve) => setTimeout(resolve, 800)); setProvider(false); toast({ @@ -137,6 +171,12 @@ export const SignUp = ({ onDone }: Props) => {
+ {error && error.type === 'provider' && ( +
+
Error: {error.message}
+
+ )} +
@@ -166,18 +206,26 @@ export const SignUp = ({ onDone }: Props) => { > Continue with Email - -
-
- Already an user? -
-
- - Sign in now - +
+ {error && error.type === 'email' && ( +
+
+ Error: {error.message} +
+
+ )} +
+
+ Already an user? +
+
+ + Sign in now + +
diff --git a/packages/frontend/src/utils/siwe.ts b/packages/frontend/src/utils/siwe.ts index b66e436c..4eccc740 100644 --- a/packages/frontend/src/utils/siwe.ts +++ b/packages/frontend/src/utils/siwe.ts @@ -5,8 +5,13 @@ import { v4 as uuid } from 'uuid'; const domain = window.location.host; const origin = window.location.origin; -export async function signInWithEthereum(wallet: PKPEthersWallet) { +export async function signInWithEthereum( + chainId: number, + action: 'signup' | 'login', + wallet: PKPEthersWallet, +) { const message = await createSiweMessage( + chainId, await wallet.getAddress(), 'Sign in with Ethereum to the app.', ); @@ -17,20 +22,24 @@ export async function signInWithEthereum(wallet: PKPEthersWallet) { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ message, signature }), + body: JSON.stringify({ action, message, signature }), credentials: 'include', }); - console.log(await res.text()); + return (await res.json()) as { success: boolean; error?: string }; } -async function createSiweMessage(address: string, statement: string) { +async function createSiweMessage( + chainId: number, + address: string, + statement: string, +) { const message = new SiweMessage({ domain, address, statement, uri: origin, version: '1', - chainId: 1, + chainId, nonce: uuid().replace(/[^a-z0-9]/g, ''), }); return message.prepareMessage(); diff --git a/packages/frontend/src/utils/use-snowball.ts b/packages/frontend/src/utils/use-snowball.ts index 45e174b7..1b04f90b 100644 --- a/packages/frontend/src/utils/use-snowball.ts +++ b/packages/frontend/src/utils/use-snowball.ts @@ -1,11 +1,18 @@ import { useEffect, useState } from 'react'; import { Snowball, SnowballChain } from '@snowballtools/js-sdk'; -import { LitGoogleAuth, LitPasskeyAuth } from '@snowballtools/auth-lit'; +import { + // LitAppleAuth, + LitGoogleAuth, + LitPasskeyAuth, +} from '@snowballtools/auth-lit'; export const snowball = Snowball.withAuth({ google: LitGoogleAuth.configure({ litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!, }), + // apple: LitAppleAuth.configure({ + // litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!, + // }), passkey: LitPasskeyAuth.configure({ litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!, }), diff --git a/yarn.lock b/yarn.lock index baa23f4b..cbd9ec1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3960,7 +3960,7 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== -"@snowballtools/auth-lit@0.1.0": +"@snowballtools/auth-lit@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@snowballtools/auth-lit/-/auth-lit-0.1.0.tgz#1ed97cf55dd20c29b46ee3e5ad053662e17fdc41" integrity sha512-WfGbdqd34I5wDcviSn9f8I1aTpY0ExJYGvkrwy/l0aeEotRBXoMDFNAM23RQN/aYzaewCOYGTPl1DJ1/hBYDyw== @@ -3975,7 +3975,7 @@ "@snowballtools/types" "*" "@snowballtools/utils" "*" -"@snowballtools/auth@*", "@snowballtools/auth@0.1.0": +"@snowballtools/auth@*", "@snowballtools/auth@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@snowballtools/auth/-/auth-0.1.0.tgz#f6bca8e631d754b524525153769bf28fa956cfa8" integrity sha512-jsviORyBcDporAFDCKGNHK4WCNBD68DdMJJ4wcnIa5DNXHjYLU4YYLqcbpccgnL1l+02o2nC/FyIwwDNcxWtjw== @@ -3986,7 +3986,7 @@ "@snowballtools/utils" "*" debug "*" -"@snowballtools/js-sdk@0.1.0": +"@snowballtools/js-sdk@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@snowballtools/js-sdk/-/js-sdk-0.1.0.tgz#69835d4c0fdb1023a2ff3e75d916eb23e98084e1" integrity sha512-ejyzeRjUiffaWZiBwLhCi9vVyJp+eNBlTYQIwfTipAQlr1q0yCfCHJic2z2CIt2w6Vzayfgi2KRmNyQpRd3img== @@ -3995,7 +3995,7 @@ "@snowballtools/types" "*" "@snowballtools/utils" "*" -"@snowballtools/link-lit-alchemy-light@0.1.0": +"@snowballtools/link-lit-alchemy-light@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@snowballtools/link-lit-alchemy-light/-/link-lit-alchemy-light-0.1.0.tgz#3198bd75ad8002f76481680b1c792a7a13b84111" integrity sha512-f6CEaol7qunra+1Tnk0Yb/M7l/EmYg40dlA7C+lYr0TQcGmIBQhT3rWtuluAlIsmKDPm1Ri7CCGfAYD7ioR/JQ== @@ -4024,7 +4024,7 @@ react-dom "18.2.0" tailwind-merge "1.8.1" -"@snowballtools/smartwallet-alchemy-light@*", "@snowballtools/smartwallet-alchemy-light@0.1.0": +"@snowballtools/smartwallet-alchemy-light@*", "@snowballtools/smartwallet-alchemy-light@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@snowballtools/smartwallet-alchemy-light/-/smartwallet-alchemy-light-0.1.0.tgz#659be4924c15c015b56453c508ee78cd3d64f837" integrity sha512-gR69Kq3Bl8qxmMqBjac5lINRlABH25U+oUmrzUsul9TtUdfJMtA/96jR48v6upliKyncGoSIf+KJQ8opA5DqHw== @@ -4037,12 +4037,12 @@ "@snowballtools/types" "*" "@snowballtools/utils" "*" -"@snowballtools/types@*", "@snowballtools/types@0.1.0": +"@snowballtools/types@*", "@snowballtools/types@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@snowballtools/types/-/types-0.1.0.tgz#b76b20f76cc4192b250712d148991f04d68bade6" integrity sha512-lYLtUGjTO2BDqpM/KA83ojRB9sKw7IPQ9IVrd0FWJlyHtmQ5MvDRIcXJXO85lIUUe4SIkxXdrJMvda0GMDMV0A== -"@snowballtools/utils@*", "@snowballtools/utils@0.1.0": +"@snowballtools/utils@*", "@snowballtools/utils@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@snowballtools/utils/-/utils-0.1.0.tgz#1f0c69f357a899301d0716e0b30121242617c464" integrity sha512-0dx3ct6pSbMdhSi/Yg3unM3sPuDIk+lv57YNvqRhv8e+wz+5IfRj0Bm12BB10Dav1PMJAXkLMYKJ5OYJJn6ALA==