Implement authentication with SIWE (#4)
All checks were successful
Lint / lint (20.x) (push) Successful in 5m23s
All checks were successful
Lint / lint (20.x) (push) Successful in 5m23s
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75) - Remove LIT authentication Co-authored-by: Neeraj <neeraj.rtly@gmail.com> Reviewed-on: #4
This commit is contained in:
parent
5aefda1248
commit
bc52b34462
@ -5,19 +5,6 @@ import { authenticateUser, createUser } from '../turnkey-backend';
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
//
|
|
||||||
// Access Code
|
|
||||||
//
|
|
||||||
router.post('/accesscode', async (req, res) => {
|
|
||||||
console.log('Access Code', req.body);
|
|
||||||
const { accesscode } = req.body;
|
|
||||||
if (accesscode === '44444') {
|
|
||||||
return res.send({ isValid: true });
|
|
||||||
} else {
|
|
||||||
return res.sendStatus(204);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Turnkey
|
// Turnkey
|
||||||
//
|
//
|
||||||
@ -40,7 +27,7 @@ router.post('/register', async (req, res) => {
|
|||||||
userEmail: email,
|
userEmail: email,
|
||||||
userName: email.split('@')[0],
|
userName: email.split('@')[0],
|
||||||
});
|
});
|
||||||
req.session.userId = user.id;
|
req.session.address = user.id;
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -52,19 +39,15 @@ router.post('/authenticate', async (req, res) => {
|
|||||||
signedWhoamiRequest,
|
signedWhoamiRequest,
|
||||||
);
|
);
|
||||||
if (user) {
|
if (user) {
|
||||||
req.session.userId = user.id;
|
req.session.address = user.id;
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(401);
|
res.sendStatus(401);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
|
||||||
// Lit
|
|
||||||
//
|
|
||||||
|
|
||||||
router.post('/validate', async (req, res) => {
|
router.post('/validate', async (req, res) => {
|
||||||
const { message, signature, action } = req.body;
|
const { message, signature } = req.body;
|
||||||
const { success, data } = await new SiweMessage(message).verify({
|
const { success, data } = await new SiweMessage(message).verify({
|
||||||
signature,
|
signature,
|
||||||
});
|
});
|
||||||
@ -75,10 +58,7 @@ router.post('/validate', async (req, res) => {
|
|||||||
const service: Service = req.app.get('service');
|
const service: Service = req.app.get('service');
|
||||||
const user = await service.getUserByEthAddress(data.address);
|
const user = await service.getUserByEthAddress(data.address);
|
||||||
|
|
||||||
if (action === 'signup') {
|
if (!user) {
|
||||||
if (user) {
|
|
||||||
return res.send({ success: false, error: 'user_already_exists' });
|
|
||||||
}
|
|
||||||
const newUser = await service.createUser({
|
const newUser = await service.createUser({
|
||||||
ethAddress: data.address,
|
ethAddress: data.address,
|
||||||
email: '',
|
email: '',
|
||||||
@ -86,12 +66,12 @@ router.post('/validate', async (req, res) => {
|
|||||||
subOrgId: '',
|
subOrgId: '',
|
||||||
turnkeyWalletId: '',
|
turnkeyWalletId: '',
|
||||||
});
|
});
|
||||||
req.session.userId = newUser.id;
|
// SIWESession from the web3modal library requires both address and chain ID
|
||||||
} else if (action === 'login') {
|
req.session.address = newUser.id;
|
||||||
if (!user) {
|
req.session.chainId = data.chainId;
|
||||||
return res.send({ success: false, error: 'user_not_found' });
|
} else {
|
||||||
}
|
req.session.address = user.id;
|
||||||
req.session.userId = user.id;
|
req.session.chainId = data.chainId;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send({ success });
|
res.send({ success });
|
||||||
@ -101,9 +81,10 @@ router.post('/validate', async (req, res) => {
|
|||||||
// General
|
// General
|
||||||
//
|
//
|
||||||
router.get('/session', (req, res) => {
|
router.get('/session', (req, res) => {
|
||||||
if (req.session.userId) {
|
if (req.session.address && req.session.chainId) {
|
||||||
res.send({
|
res.send({
|
||||||
userId: req.session.userId,
|
address: req.session.address,
|
||||||
|
chainId: req.session.chainId
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(401).send({ error: 'Unauthorized: No active session' });
|
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||||
@ -111,9 +92,12 @@ router.get('/session', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post('/logout', (req, res) => {
|
router.post('/logout', (req, res) => {
|
||||||
// This is how you clear cookie-session
|
req.session.destroy((err) => {
|
||||||
(req as any).session = null;
|
if (err) {
|
||||||
|
return res.send({ success: false });
|
||||||
|
}
|
||||||
res.send({ success: true });
|
res.send({ success: true });
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
ApolloServerPluginLandingPageLocalDefault,
|
ApolloServerPluginLandingPageLocalDefault,
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
} from 'apollo-server-core';
|
} from 'apollo-server-core';
|
||||||
import cookieSession from 'cookie-session';
|
import session from 'express-session';
|
||||||
|
|
||||||
import { TypeSource } from '@graphql-tools/utils';
|
import { TypeSource } from '@graphql-tools/utils';
|
||||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||||
@ -22,9 +22,13 @@ import { Service } from './service';
|
|||||||
|
|
||||||
const log = debug('snowball:server');
|
const log = debug('snowball:server');
|
||||||
|
|
||||||
|
// Set cookie expiration to 1 month in milliseconds
|
||||||
|
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
declare module 'express-session' {
|
declare module 'express-session' {
|
||||||
interface SessionData {
|
interface SessionData {
|
||||||
userId: string;
|
address: string;
|
||||||
|
chainId: number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,14 +58,13 @@ export const createAndStartServer = async (
|
|||||||
context: async ({ req }) => {
|
context: async ({ req }) => {
|
||||||
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
||||||
|
|
||||||
const { userId } = req.session;
|
const { address } = req.session;
|
||||||
|
|
||||||
if (!userId) {
|
if (!address) {
|
||||||
throw new AuthenticationError('Unauthorized: No active session');
|
throw new AuthenticationError('Unauthorized: No active session');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await service.getUser(userId);
|
const user = await service.getUser(address);
|
||||||
|
|
||||||
return { user };
|
return { user };
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
@ -80,20 +83,25 @@ export const createAndStartServer = async (
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sessionOptions: session.SessionOptions = {
|
||||||
|
secret: secret,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true,
|
||||||
|
cookie: {
|
||||||
|
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||||
|
maxAge: COOKIE_MAX_AGE,
|
||||||
|
domain: domain || undefined,
|
||||||
|
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (trustProxy) {
|
if (trustProxy) {
|
||||||
// trust first proxy
|
// trust first proxy
|
||||||
app.set('trust proxy', 1);
|
app.set('trust proxy', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
cookieSession({
|
session(sessionOptions)
|
||||||
secret: secret,
|
|
||||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
|
||||||
// 23 hours (less than 24 hours to avoid sessionSigs expiration issues)
|
|
||||||
maxAge: 23 * 60 * 60 * 1000,
|
|
||||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
|
||||||
domain: domain || undefined,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
server.applyMiddleware({
|
server.applyMiddleware({
|
||||||
|
@ -46,8 +46,8 @@
|
|||||||
"@turnkey/sdk-react": "^0.1.0",
|
"@turnkey/sdk-react": "^0.1.0",
|
||||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||||
"@walletconnect/ethereum-provider": "^2.12.2",
|
"@walletconnect/ethereum-provider": "^2.12.2",
|
||||||
"@web3modal/siwe": "^4.0.5",
|
"@web3modal/siwe": "4.0.5",
|
||||||
"@web3modal/wagmi": "^4.0.5",
|
"@web3modal/wagmi": "4.0.5",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
@ -68,11 +68,12 @@
|
|||||||
"react-oauth-popup": "^1.0.5",
|
"react-oauth-popup": "^1.0.5",
|
||||||
"react-router-dom": "^6.20.1",
|
"react-router-dom": "^6.20.1",
|
||||||
"react-timer-hook": "^3.0.7",
|
"react-timer-hook": "^3.0.7",
|
||||||
"siwe": "^2.1.4",
|
"siwe": "2.1.4",
|
||||||
"tailwind-variants": "^0.2.0",
|
"tailwind-variants": "^0.2.0",
|
||||||
"usehooks-ts": "^2.15.1",
|
"usehooks-ts": "^2.15.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"viem": "^2.7.11",
|
"viem": "^2.7.11",
|
||||||
|
"wagmi": "2.5.7",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -12,7 +12,7 @@ import Index from './pages';
|
|||||||
import AuthPage from './pages/AuthPage';
|
import AuthPage from './pages/AuthPage';
|
||||||
import { DashboardLayout } from './pages/org-slug/layout';
|
import { DashboardLayout } from './pages/org-slug/layout';
|
||||||
import Web3Provider from 'context/Web3Provider';
|
import Web3Provider from 'context/Web3Provider';
|
||||||
import { baseUrl } from 'utils/constants';
|
import { BASE_URL } from 'utils/constants';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -50,25 +50,26 @@ const router = createBrowserRouter([
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
element: <AuthPage />,
|
element: <AuthPage />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/signup',
|
|
||||||
element: <AuthPage />,
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// Hacky way of checking session
|
// Hacky way of checking session
|
||||||
// TODO: Handle redirect backs
|
// TODO: Handle redirect backs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${baseUrl}/auth/session`, {
|
fetch(`${BASE_URL}/auth/session`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
|
const path = window.location.pathname;
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
const path = window.location.pathname;
|
|
||||||
if (path !== '/login' && path !== '/signup') {
|
if (path !== '/login') {
|
||||||
window.location.pathname = '/login';
|
window.location.pathname = '/login';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (path === '/login') {
|
||||||
|
window.location.pathname = '/';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { NavLink, useNavigate, useParams } from 'react-router-dom';
|
import { NavLink, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Organization, User } from 'gql-client';
|
import { Organization, User } from 'gql-client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { useDisconnect } from 'wagmi';
|
||||||
|
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import {
|
import {
|
||||||
@ -20,7 +21,7 @@ import { cn } from 'utils/classnames';
|
|||||||
import { useMediaQuery } from 'usehooks-ts';
|
import { useMediaQuery } from 'usehooks-ts';
|
||||||
import { SIDEBAR_MENU } from './constants';
|
import { SIDEBAR_MENU } from './constants';
|
||||||
import { UserSelect } from 'components/shared/UserSelect';
|
import { UserSelect } from 'components/shared/UserSelect';
|
||||||
import { baseUrl } from 'utils/constants';
|
import { BASE_URL } from 'utils/constants';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
mobileOpen?: boolean;
|
mobileOpen?: boolean;
|
||||||
@ -33,6 +34,7 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
const isDesktop = useMediaQuery('(min-width: 960px)');
|
const isDesktop = useMediaQuery('(min-width: 960px)');
|
||||||
|
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
|
const { disconnect } = useDisconnect();
|
||||||
|
|
||||||
const fetchUser = useCallback(async () => {
|
const fetchUser = useCallback(async () => {
|
||||||
const { user } = await client.getUser();
|
const { user } = await client.getUser();
|
||||||
@ -84,13 +86,14 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
}, [orgSlug]);
|
}, [orgSlug]);
|
||||||
|
|
||||||
const handleLogOut = useCallback(async () => {
|
const handleLogOut = useCallback(async () => {
|
||||||
await fetch(`${baseUrl}/auth/logout`, {
|
await fetch(`${BASE_URL}/auth/logout`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
disconnect();
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
}, [navigate]);
|
}, [disconnect, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.nav
|
<motion.nav
|
||||||
|
@ -1,16 +1,116 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import assert from 'assert';
|
||||||
|
import { SiweMessage, generateNonce } from 'siwe';
|
||||||
|
import { WagmiProvider } from 'wagmi';
|
||||||
|
import { arbitrum, mainnet } from 'wagmi/chains';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { createWeb3Modal } from '@web3modal/wagmi/react';
|
||||||
|
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config';
|
||||||
|
import { createSIWEConfig } from '@web3modal/siwe';
|
||||||
|
import type {
|
||||||
|
SIWECreateMessageArgs,
|
||||||
|
SIWEVerifyMessageArgs,
|
||||||
|
} from '@web3modal/core';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
|
import { VITE_WALLET_CONNECT_ID, BASE_URL } from 'utils/constants';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
if (!VITE_WALLET_CONNECT_ID) {
|
if (!VITE_WALLET_CONNECT_ID) {
|
||||||
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
|
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
|
||||||
}
|
}
|
||||||
|
assert(BASE_URL, 'VITE_SERVER_URL is not set in env');
|
||||||
|
|
||||||
export default function Web3Provider({ children }: { children: ReactNode }) {
|
const queryClient = new QueryClient();
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
const metadata = {
|
||||||
|
name: 'Web3Modal',
|
||||||
|
description: 'Snowball Web3Modal',
|
||||||
|
url: window.location.origin,
|
||||||
|
icons: ['https://avatars.githubusercontent.com/u/37784886'],
|
||||||
|
};
|
||||||
|
const chains = [mainnet, arbitrum] as const;
|
||||||
|
const config = defaultWagmiConfig({
|
||||||
|
chains,
|
||||||
|
projectId: VITE_WALLET_CONNECT_ID,
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
const siweConfig = createSIWEConfig({
|
||||||
|
createMessage: ({ nonce, address, chainId }: SIWECreateMessageArgs) =>
|
||||||
|
new SiweMessage({
|
||||||
|
version: '1',
|
||||||
|
domain: window.location.host,
|
||||||
|
uri: window.location.origin,
|
||||||
|
address,
|
||||||
|
chainId,
|
||||||
|
nonce,
|
||||||
|
// Human-readable ASCII assertion that the user will sign, and it must not contain `\n`.
|
||||||
|
statement: 'Sign in With Ethereum.',
|
||||||
|
}).prepareMessage(),
|
||||||
|
getNonce: async () => {
|
||||||
|
return generateNonce()
|
||||||
|
},
|
||||||
|
getSession: async () => {
|
||||||
|
try {
|
||||||
|
const session = (await axiosInstance.get('/auth/session')).data;
|
||||||
|
const { address, chainId } = session;
|
||||||
|
return { address, chainId };
|
||||||
|
} catch (err) {
|
||||||
|
if (window.location.pathname !== '/login') {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
throw new Error('Failed to get session!');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verifyMessage: async ({ message, signature }: SIWEVerifyMessageArgs) => {
|
||||||
|
try {
|
||||||
|
const { success } = (
|
||||||
|
await axiosInstance.post('/auth/validate', {
|
||||||
|
message,
|
||||||
|
signature,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
return success;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
signOut: async () => {
|
||||||
|
try {
|
||||||
|
const { success } = (await axiosInstance.post('/auth/logout')).data;
|
||||||
|
return success;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSignOut: () => {
|
||||||
|
window.location.href = '/login';
|
||||||
|
},
|
||||||
|
onSignIn: () => {
|
||||||
|
window.location.href = '/';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
createWeb3Modal({
|
||||||
|
siweConfig,
|
||||||
|
wagmiConfig: config,
|
||||||
|
projectId: VITE_WALLET_CONNECT_ID,
|
||||||
|
});
|
||||||
|
export default function Web3ModalProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
|
<WagmiProvider config={config}>
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
</WagmiProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,9 @@ import { GQLClientProvider } from './context/GQLClientContext';
|
|||||||
import { SERVER_GQL_PATH } from './constants';
|
import { SERVER_GQL_PATH } from './constants';
|
||||||
import { Toaster } from 'components/shared/Toast';
|
import { Toaster } from 'components/shared/Toast';
|
||||||
import { LogErrorBoundary } from 'utils/log-error';
|
import { LogErrorBoundary } from 'utils/log-error';
|
||||||
import { baseUrl } from 'utils/constants';
|
import { BASE_URL } from 'utils/constants';
|
||||||
|
import Web3ModalProvider from './context/Web3Provider';
|
||||||
|
|
||||||
|
|
||||||
console.log(`v-0.0.9`);
|
console.log(`v-0.0.9`);
|
||||||
|
|
||||||
@ -22,8 +24,8 @@ const root = ReactDOM.createRoot(
|
|||||||
document.getElementById('root') as HTMLElement,
|
document.getElementById('root') as HTMLElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(baseUrl, 'VITE_SERVER_URL is not set in env');
|
assert(BASE_URL, 'VITE_SERVER_URL is not set in env');
|
||||||
const gqlEndpoint = `${baseUrl}/${SERVER_GQL_PATH}`;
|
const gqlEndpoint = `${BASE_URL}/${SERVER_GQL_PATH}`;
|
||||||
|
|
||||||
const gqlClient = new GQLClient({ gqlEndpoint });
|
const gqlClient = new GQLClient({ gqlEndpoint });
|
||||||
|
|
||||||
@ -31,10 +33,12 @@ root.render(
|
|||||||
<LogErrorBoundary>
|
<LogErrorBoundary>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
|
<Web3ModalProvider>
|
||||||
<GQLClientProvider client={gqlClient}>
|
<GQLClientProvider client={gqlClient}>
|
||||||
<App />
|
<App />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</GQLClientProvider>
|
</GQLClientProvider>
|
||||||
|
</Web3ModalProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
</LogErrorBoundary>,
|
</LogErrorBoundary>,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CloudyFlow } from 'components/CloudyFlow';
|
import { CloudyFlow } from 'components/CloudyFlow';
|
||||||
import { SnowballAuth } from './auth/SnowballAuth';
|
import { Login } from './auth/Login';
|
||||||
|
|
||||||
const AuthPage = () => {
|
const AuthPage = () => {
|
||||||
return (
|
return (
|
||||||
@ -18,7 +18,7 @@ const AuthPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="pb-12 relative z-10 flex-1 flex-center">
|
<div className="pb-12 relative z-10 flex-1 flex-center">
|
||||||
<div className="max-w-[520px] w-full bg-white rounded-xl shadow">
|
<div className="max-w-[520px] w-full bg-white rounded-xl shadow">
|
||||||
<SnowballAuth />
|
<Login />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CloudyFlow>
|
</CloudyFlow>
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import {
|
|
||||||
ArrowRightCircleFilledIcon,
|
|
||||||
LoaderIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
|
||||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
|
||||||
import { VerifyCodeInput } from 'components/shared/VerifyCodeInput';
|
|
||||||
import { verifyAccessCode } from 'utils/accessCode';
|
|
||||||
|
|
||||||
type AccessMethod = 'accesscode' | 'passkey';
|
|
||||||
|
|
||||||
type Err = { type: AccessMethod; message: string };
|
|
||||||
|
|
||||||
type AccessCodeProps = {
|
|
||||||
onCorrectAccessCode: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AccessCode: React.FC<AccessCodeProps> = ({
|
|
||||||
onCorrectAccessCode,
|
|
||||||
}) => {
|
|
||||||
const [accessCode, setAccessCode] = useState(' ');
|
|
||||||
const [error, setError] = useState<Err | null>();
|
|
||||||
const [accessMethod, setAccessMethod] = useState<AccessMethod | false>(false);
|
|
||||||
|
|
||||||
async function validateAccessCode() {
|
|
||||||
setAccessMethod('accesscode');
|
|
||||||
try {
|
|
||||||
const isValidAccessCode = await verifyAccessCode(accessCode);
|
|
||||||
|
|
||||||
// add a pause for ux
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
||||||
if (isValidAccessCode) {
|
|
||||||
localStorage.setItem('accessCode', accessCode);
|
|
||||||
onCorrectAccessCode();
|
|
||||||
} else {
|
|
||||||
setError({
|
|
||||||
type: 'accesscode',
|
|
||||||
message: 'Invalid access code',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setError({ type: 'accesscode', message: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loading = accessMethod;
|
|
||||||
const isValidAccessCodeLength = accessCode.trim().length === 5;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
|
||||||
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-tight">
|
|
||||||
Access Code
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<WavyBorder className="self-stretch" variant="stroke" />
|
|
||||||
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
|
||||||
<div className="self-stretch flex-col gap-8 flex">
|
|
||||||
<div className="flex-col justify-start items-start gap-2 inline-flex">
|
|
||||||
<VerifyCodeInput
|
|
||||||
loading={!!loading}
|
|
||||||
code={accessCode}
|
|
||||||
setCode={setAccessCode}
|
|
||||||
submitCode={validateAccessCode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'accesscode' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRightCircleFilledIcon height="16" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={validateAccessCode}
|
|
||||||
variant={'secondary'}
|
|
||||||
disabled={!accessCode || !isValidAccessCodeLength || !!loading}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
{error && error.type === 'accesscode' && (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-red-500 text-sm">
|
|
||||||
Error: {error.message}.{' '}
|
|
||||||
<a href="/signup" className="underline">
|
|
||||||
Try again?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,20 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import { AccessCode } from './AccessCode';
|
|
||||||
import { SignUp } from './SignUp';
|
|
||||||
|
|
||||||
type AccessSignUpProps = {
|
|
||||||
onDone: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AccessSignUp: React.FC<AccessSignUpProps> = ({ onDone }) => {
|
|
||||||
const [isValidAccessCode, setIsValidAccessCode] = useState<boolean>(
|
|
||||||
!!localStorage.getItem('accessCode'),
|
|
||||||
);
|
|
||||||
|
|
||||||
return isValidAccessCode ? (
|
|
||||||
<SignUp onDone={onDone} />
|
|
||||||
) : (
|
|
||||||
<AccessCode onCorrectAccessCode={() => setIsValidAccessCode(true)} />
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,100 +1,6 @@
|
|||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import {
|
|
||||||
ArrowRightCircleFilledIcon,
|
|
||||||
GithubIcon,
|
|
||||||
LinkIcon,
|
|
||||||
LoaderIcon,
|
|
||||||
QuestionMarkRoundFilledIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
|
||||||
import { GoogleIcon } from 'components/shared/CustomIcon/GoogleIcon';
|
|
||||||
import { DotBorder } from 'components/shared/DotBorder';
|
|
||||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||||
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';
|
|
||||||
import { logError } from 'utils/log-error';
|
|
||||||
|
|
||||||
type Provider = 'google' | 'github' | 'apple' | 'email' | 'passkey';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDone: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Login = ({ onDone }: Props) => {
|
|
||||||
const snowball = useSnowball();
|
|
||||||
const [error, setError] = useState<string>('');
|
|
||||||
const [provider, setProvider] = useState<Provider | false>(false);
|
|
||||||
|
|
||||||
// const loading = snowball.auth.state.loading && provider;
|
|
||||||
const loading = provider;
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
if (provider === 'email') {
|
|
||||||
return <CreatePasskey onDone={onDone} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
// @ts-ignore
|
|
||||||
wallet = await google.getEthersWallet();
|
|
||||||
// @ts-ignore
|
|
||||||
const result = await signInWithEthereum(1, 'login', wallet);
|
|
||||||
if (result.error) {
|
|
||||||
setError(result.error);
|
|
||||||
setProvider(false);
|
|
||||||
wallet = undefined;
|
|
||||||
logError(new Error(result.error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message);
|
|
||||||
logError(err);
|
|
||||||
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();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
export const Login = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
||||||
@ -105,160 +11,8 @@ export const Login = ({ onDone }: Props) => {
|
|||||||
<WavyBorder className="self-stretch" variant="stroke" />
|
<WavyBorder className="self-stretch" variant="stroke" />
|
||||||
|
|
||||||
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
||||||
<div className="self-stretch p-5 bg-slate-50 rounded-xl shadow flex-col justify-center items-center gap-6 flex">
|
|
||||||
<div className="self-stretch flex-col justify-center items-center gap-4 flex">
|
|
||||||
<KeyIcon />
|
|
||||||
<div className="self-stretch flex-col justify-center items-center gap-2 flex">
|
|
||||||
<div className="self-stretch text-center text-sky-950 text-lg font-medium font-display leading-normal">
|
|
||||||
Got a Passkey?
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Use it to sign in securely without using a password.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch justify-center items-stretch xxs:items-center gap-3 flex flex-col xxs:flex-row">
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
leftIcon={<QuestionMarkRoundFilledIcon />}
|
|
||||||
variant={'tertiary'}
|
|
||||||
target="_blank"
|
|
||||||
href="https://safety.google/authentication/passkey/"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'passkey' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRightCircleFilledIcon height="16" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={!!loading}
|
|
||||||
onClick={async () => {
|
|
||||||
setProvider('passkey');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Sign In with Passkey
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-5 justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Lost your passkey?
|
|
||||||
</div>
|
|
||||||
<div className="justify-center items-center gap-1.5 flex">
|
|
||||||
<button className="text-sky-950 text-sm font-normal font-['Inter'] underline leading-tight">
|
|
||||||
Recover account
|
|
||||||
</button>
|
|
||||||
<LinkIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="self-stretch justify-start items-center gap-8 inline-flex">
|
|
||||||
<DotBorder className="flex-1" />
|
|
||||||
<div className="text-center text-slate-400 text-xs font-normal font-['JetBrains Mono'] leading-none">
|
|
||||||
OR
|
|
||||||
</div>
|
|
||||||
<DotBorder className="flex-1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="self-stretch flex-col justify-center items-center gap-3 flex">
|
<div className="self-stretch flex-col justify-center items-center gap-3 flex">
|
||||||
<Button
|
<w3m-button />
|
||||||
leftIcon={<GoogleIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'google' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setProvider('google');
|
|
||||||
snowball.auth.google.startOAuthRedirect();
|
|
||||||
}}
|
|
||||||
className="flex-1 self-stretch"
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with Google
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
leftIcon={<GithubIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'github' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
setProvider('github');
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
setProvider(false);
|
|
||||||
toast({
|
|
||||||
id: 'coming-soon',
|
|
||||||
title: 'Sign-in with GitHub is coming soon!',
|
|
||||||
variant: 'info',
|
|
||||||
onDismiss() {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="flex-1 self-stretch"
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with GitHub
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
leftIcon={<AppleIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'apple' ? (
|
|
||||||
<LoaderIcon className="animate-spin text-white" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
setProvider('apple');
|
|
||||||
// snowball.auth.apple.startOAuthRedirect();
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
setProvider(false);
|
|
||||||
toast({
|
|
||||||
id: 'coming-soon',
|
|
||||||
title: 'Sign-in with Apple is coming soon!',
|
|
||||||
variant: 'info',
|
|
||||||
onDismiss() {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className={`flex-1 self-stretch border-black enabled:bg-black text-white ${
|
|
||||||
loading && loading === 'apple' ? 'disabled:bg-black' : ''
|
|
||||||
}`}
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with Apple
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{error && (
|
|
||||||
<div className="justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-red-500 text-sm">Error: {error}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="h-5 justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Don't have an account?
|
|
||||||
</div>
|
|
||||||
<div className="justify-center items-center gap-1.5 flex">
|
|
||||||
<Link
|
|
||||||
to="/signup"
|
|
||||||
className="text-sky-950 text-sm font-normal font-['Inter'] underline leading-tight"
|
|
||||||
>
|
|
||||||
Sign up now
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,287 +0,0 @@
|
|||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import {
|
|
||||||
ArrowRightCircleFilledIcon,
|
|
||||||
GithubIcon,
|
|
||||||
LoaderIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
|
||||||
import { GoogleIcon } from 'components/shared/CustomIcon/GoogleIcon';
|
|
||||||
import { DotBorder } from 'components/shared/DotBorder';
|
|
||||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useSnowball } from 'utils/use-snowball';
|
|
||||||
import { Input } from 'components/shared/Input';
|
|
||||||
import { AppleIcon } from 'components/shared/CustomIcon/AppleIcon';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { useToast } from 'components/shared/Toast';
|
|
||||||
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
|
|
||||||
import { signInWithEthereum } from 'utils/siwe';
|
|
||||||
import { logError } from 'utils/log-error';
|
|
||||||
import {
|
|
||||||
subOrganizationIdForEmail,
|
|
||||||
turnkeySignin,
|
|
||||||
turnkeySignup,
|
|
||||||
} from 'utils/turnkey-frontend';
|
|
||||||
import { verifyAccessCode } from 'utils/accessCode';
|
|
||||||
|
|
||||||
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<Err | null>();
|
|
||||||
const [provider, setProvider] = useState<Provider | false>(false);
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
|
||||||
const snowball = useSnowball();
|
|
||||||
|
|
||||||
async function handleSignupRedirect() {
|
|
||||||
let wallet: PKPEthersWallet | undefined;
|
|
||||||
const { google } = snowball.auth;
|
|
||||||
if (google.canHandleOAuthRedirectBack()) {
|
|
||||||
setProvider('google');
|
|
||||||
try {
|
|
||||||
await google.handleOAuthRedirectBack();
|
|
||||||
// @ts-ignore
|
|
||||||
wallet = await google.getEthersWallet();
|
|
||||||
// @ts-ignore
|
|
||||||
const result = await signInWithEthereum(1, 'signup', wallet);
|
|
||||||
if (result.error) {
|
|
||||||
setError({ type: 'provider', message: result.error });
|
|
||||||
setProvider(false);
|
|
||||||
wallet = undefined;
|
|
||||||
logError(new Error(result.error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setError({ type: 'provider', message: err.message });
|
|
||||||
setProvider(false);
|
|
||||||
logError(err);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authEmail() {
|
|
||||||
setProvider('email');
|
|
||||||
try {
|
|
||||||
const orgId = await subOrganizationIdForEmail(email);
|
|
||||||
console.log('orgId', orgId);
|
|
||||||
if (orgId) {
|
|
||||||
await turnkeySignin(orgId);
|
|
||||||
window.location.href = '/dashboard';
|
|
||||||
} else {
|
|
||||||
await turnkeySignup(email);
|
|
||||||
onDone();
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setError({ type: 'email', message: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleSignupRedirect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loading = provider;
|
|
||||||
const emailValid = /.@./.test(email);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const validateAccessCode = async () => {
|
|
||||||
const accessCode = localStorage.getItem('accessCode');
|
|
||||||
if (!accessCode) {
|
|
||||||
redirectToSignup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await verifyAccessCode(accessCode);
|
|
||||||
} catch (err: any) {
|
|
||||||
redirectToSignup();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const redirectToSignup = () => {
|
|
||||||
localStorage.removeItem('accessCode');
|
|
||||||
window.location.href = '/signup';
|
|
||||||
};
|
|
||||||
|
|
||||||
validateAccessCode();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
|
||||||
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-tight">
|
|
||||||
Sign up to Snowball
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<WavyBorder className="self-stretch" variant="stroke" />
|
|
||||||
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
|
||||||
<div className="self-stretch flex-col justify-center items-center gap-3 flex">
|
|
||||||
<Button
|
|
||||||
leftIcon={loading && loading === 'google' ? null : <GoogleIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'google' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setProvider('google');
|
|
||||||
snowball.auth.google.startOAuthRedirect();
|
|
||||||
}}
|
|
||||||
className="flex-1 self-stretch"
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with Google
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
leftIcon={<GithubIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'github' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
setProvider('github');
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
setProvider(false);
|
|
||||||
toast({
|
|
||||||
id: 'coming-soon',
|
|
||||||
title: 'Sign-in with GitHub is coming soon!',
|
|
||||||
variant: 'info',
|
|
||||||
onDismiss() {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="flex-1 self-stretch"
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with GitHub
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
leftIcon={<AppleIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'apple' ? (
|
|
||||||
<LoaderIcon className="animate-spin text-white" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
setProvider('apple');
|
|
||||||
// snowball.auth.apple.startOAuthRedirect();
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
setProvider(false);
|
|
||||||
toast({
|
|
||||||
id: 'coming-soon',
|
|
||||||
title: 'Sign-in with Apple is coming soon!',
|
|
||||||
variant: 'info',
|
|
||||||
onDismiss() {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className={`flex-1 self-stretch border-black enabled:bg-black text-white ${
|
|
||||||
loading && loading === 'apple' ? 'disabled:bg-black' : ''
|
|
||||||
}`}
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with Apple
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && error.type === 'provider' && (
|
|
||||||
<div className="-mt-3 justify-center items-center inline-flex">
|
|
||||||
<div className="text-red-500 text-sm">Error: {error.message}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="self-stretch justify-start items-center gap-8 inline-flex">
|
|
||||||
<DotBorder className="flex-1" />
|
|
||||||
<div className="text-center text-slate-400 text-xs font-normal font-['JetBrains Mono'] leading-none">
|
|
||||||
OR
|
|
||||||
</div>
|
|
||||||
<DotBorder className="flex-1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="self-stretch flex-col gap-8 flex">
|
|
||||||
<div className="flex-col justify-start items-start gap-2 inline-flex">
|
|
||||||
<div className="text-sky-950 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Email
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
disabled={!!loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'email' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRightCircleFilledIcon height="16" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
authEmail();
|
|
||||||
}}
|
|
||||||
variant={'secondary'}
|
|
||||||
disabled={!email || !emailValid || !!loading}
|
|
||||||
>
|
|
||||||
Continue with Email
|
|
||||||
</Button>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{error && error.type === 'email' && (
|
|
||||||
<div className="justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-red-500 text-sm">
|
|
||||||
Error: {error.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Already an user?
|
|
||||||
</div>
|
|
||||||
<div className="justify-center items-center gap-1.5 flex">
|
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="text-sky-950 text-sm font-normal font-['Inter'] underline leading-tight"
|
|
||||||
>
|
|
||||||
Sign in now
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,53 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { snowball } from 'utils/use-snowball';
|
|
||||||
import { Login } from './Login';
|
|
||||||
import { Done } from './Done';
|
|
||||||
import { AccessSignUp } from './AccessSignUp';
|
|
||||||
|
|
||||||
type Screen = 'login' | 'signup' | 'success';
|
|
||||||
|
|
||||||
const DASHBOARD_URL = '/';
|
|
||||||
|
|
||||||
export const SnowballAuth: React.FC = () => {
|
|
||||||
const path = window.location.pathname;
|
|
||||||
const [screen, setScreen] = useState<Screen>(
|
|
||||||
path === '/login' ? 'login' : 'signup',
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (snowball.session) {
|
|
||||||
window.location.href = DASHBOARD_URL;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (path === '/login') {
|
|
||||||
setScreen('login');
|
|
||||||
} else if (path === '/signup') {
|
|
||||||
setScreen('signup');
|
|
||||||
}
|
|
||||||
}, [path]);
|
|
||||||
|
|
||||||
if (screen === 'signup') {
|
|
||||||
return (
|
|
||||||
<AccessSignUp
|
|
||||||
onDone={() => {
|
|
||||||
setScreen('success');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (screen === 'login') {
|
|
||||||
return (
|
|
||||||
<Login
|
|
||||||
onDone={() => {
|
|
||||||
setScreen('success');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (screen === 'success') {
|
|
||||||
return <Done continueTo={DASHBOARD_URL} />;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,9 +1,9 @@
|
|||||||
import { baseUrl } from './constants';
|
import { BASE_URL } from './constants';
|
||||||
|
|
||||||
export async function verifyAccessCode(
|
export async function verifyAccessCode(
|
||||||
accesscode: string,
|
accesscode: string,
|
||||||
): Promise<boolean | null> {
|
): Promise<boolean | null> {
|
||||||
const res = await fetch(`${baseUrl}/auth/accesscode`, {
|
const res = await fetch(`${BASE_URL}/auth/accesscode`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
accesscode,
|
accesscode,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const baseUrl = import.meta.env.VITE_SERVER_URL;
|
export const BASE_URL = import.meta.env.VITE_SERVER_URL;
|
||||||
export const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
|
export const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
|
||||||
export const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
|
export const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
|
||||||
export const VITE_GITHUB_PWA_TEMPLATE_REPO = import.meta.env
|
export const VITE_GITHUB_PWA_TEMPLATE_REPO = import.meta.env
|
||||||
|
@ -2,7 +2,7 @@ import { SiweMessage } from 'siwe';
|
|||||||
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
|
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { baseUrl } from './constants';
|
import { BASE_URL } from './constants';
|
||||||
|
|
||||||
const domain = window.location.host;
|
const domain = window.location.host;
|
||||||
const origin = window.location.origin;
|
const origin = window.location.origin;
|
||||||
@ -19,7 +19,7 @@ export async function signInWithEthereum(
|
|||||||
);
|
);
|
||||||
const signature = await wallet.signMessage(message);
|
const signature = await wallet.signMessage(message);
|
||||||
|
|
||||||
const res = await fetch(`${baseUrl}/auth/validate`, {
|
const res = await fetch(`${BASE_URL}/auth/validate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http';
|
import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http';
|
||||||
import { WebauthnStamper } from '@turnkey/webauthn-stamper';
|
import { WebauthnStamper } from '@turnkey/webauthn-stamper';
|
||||||
|
|
||||||
import { baseUrl, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants';
|
import { BASE_URL, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants';
|
||||||
|
|
||||||
// All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
|
// All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
|
||||||
// We only support ES256, which is listed here
|
// We only support ES256, which is listed here
|
||||||
@ -10,7 +10,7 @@ const es256 = -7;
|
|||||||
export async function subOrganizationIdForEmail(
|
export async function subOrganizationIdForEmail(
|
||||||
email: string,
|
email: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const res = await fetch(`${baseUrl}/auth/registration/${email}`);
|
const res = await fetch(`${BASE_URL}/auth/registration/${email}`);
|
||||||
|
|
||||||
// If API returns a non-empty 200, this email maps to an existing user.
|
// If API returns a non-empty 200, this email maps to an existing user.
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
@ -64,7 +64,7 @@ export async function turnkeySignup(email: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await fetch(`${baseUrl}/auth/register`, {
|
const res = await fetch(`${BASE_URL}/auth/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email,
|
email,
|
||||||
@ -108,7 +108,7 @@ export async function turnkeySignin(subOrganizationId: string) {
|
|||||||
throw new Error(`Error during webauthn prompt: ${e}`);
|
throw new Error(`Error during webauthn prompt: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${baseUrl}/auth/authenticate`, {
|
const res = await fetch(`${BASE_URL}/auth/authenticate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
signedWhoamiRequest: signedRequest,
|
signedWhoamiRequest: signedRequest,
|
||||||
|
Loading…
Reference in New Issue
Block a user