Skip to content

Commit

Permalink
Merge pull request #81 from blockydevs/HAF-53-add-verification-signat…
Browse files Browse the repository at this point in the history
…ure-at-beginning-of-operator-sign-up-flow

Haf 53 add verification signature at beginning of operator sign up flow
  • Loading branch information
MicDebBlocky authored May 17, 2024
2 parents e3c5b33 + 62b47e6 commit 851fc63
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 49 deletions.
13 changes: 13 additions & 0 deletions packages/apps/human-app/frontend/src/api/api-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,17 @@ export const apiPaths = {
path: '/email-confirmation/resend-email-verification',
},
},
operator: {
web3Auth: {
prepareSignature: {
path: '/prepare-signature',
},
signUp: {
path: '/auth/web3/signup',
},
signIn: {
path: '/auth/web3/signin',
},
},
},
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { z } from 'zod';
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@/api/api-client';
import { apiPaths } from '@/api/api-paths';

export enum PrepareSignatureType {
SignUp = 'SIGNUP',
DisableOperator = 'DISABLE_OPERATOR',
}

export const prepareSignatureSuccessSchema = z.object({
from: z.string(),
to: z.string(),
contents: z.string(),
});

export type SignatureData = z.infer<typeof prepareSignatureSuccessSchema>;

export function usePrepareSignature({
address,
type,
}: {
address: string;
type: PrepareSignatureType;
}) {
return useQuery({
queryFn: () =>
apiClient(apiPaths.operator.web3Auth.prepareSignature.path, {
successSchema: prepareSignatureSuccessSchema,
options: { method: 'POST', body: JSON.stringify({ address, type }) },
}),
queryKey: [address, type],
refetchInterval: 0,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useMutation } from '@tanstack/react-query';
import { z } from 'zod';
import { useConnectedWallet } from '@/auth-web3/use-connected-wallet';
import { apiClient } from '@/api/api-client';
import { apiPaths } from '@/api/api-paths';

export function useWeb3SignUp() {
const { address, chainId } = useConnectedWallet();

return useMutation({
mutationFn: async ({ signature }: { signature: string }) =>
apiClient(apiPaths.operator.web3Auth.signUp.path, {
successSchema: z.unknown(),
options: {
method: 'POST',
body: JSON.stringify({ address, signature }),
},
}),
mutationKey: ['web3SignUp', address, chainId],
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { routerPaths } from '@/router/router-paths';
export const AuthWeb3Context =
createContext<WalletConnectContextConnectedAccount | null>(null);

export function RequireWeb3Auth({ children }: { children: JSX.Element }) {
export function RequireWalletConnect({ children }: { children: JSX.Element }) {
const walletConnect = useWalletConnect();
const location = useLocation();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContext } from 'react';
import { AuthWeb3Context } from '@/auth-web3/require-web3-auth';
import { AuthWeb3Context } from '@/auth-web3/require-wallet-connect';

export const useConnectedWallet = () => {
const context = useContext(AuthWeb3Context);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useState, createContext, useEffect } from 'react';
import { jwtDecode } from 'jwt-decode';
import { z } from 'zod';
import type { SignInSuccessResponse } from '@/api/servieces/worker/sign-in';
import { web3browserAuthProvider } from '@/auth-web3/web3-browser-auth-provider';

const web3userDataSchema = z.object({
// TODO add valid schema that defines JWT payload
address: z.string().nullable().optional(),
});

type Web3UserData = z.infer<typeof web3userDataSchema>;

type AuthStatus = 'loading' | 'error' | 'success' | 'idle';
export interface Web3AuthenticatedUserContextType {
user: Web3UserData;
status: AuthStatus;
signOut: () => void;
signIn: (singIsSuccess: SignInSuccessResponse) => void;
}

interface Web3UnauthenticatedUserContextType {
user: null;
status: AuthStatus;
signOut: () => void;
signIn: (singIsSuccess: SignInSuccessResponse) => void;
}

export const Web3AuthContext = createContext<
Web3AuthenticatedUserContextType | Web3UnauthenticatedUserContextType | null
>(null);

export function Web3AuthProvider({ children }: { children: React.ReactNode }) {
const [web3AuthState, setWeb3AuthState] = useState<{
user: Web3UserData | null;
status: AuthStatus;
}>({ user: null, status: 'loading' });

// TODO update SignInSuccessResponse according to new endpoint web3/auth/signin
const handleSignIn = () => {
try {
const accessToken = web3browserAuthProvider.getAccessToken();
if (!accessToken) {
setWeb3AuthState({ user: null, status: 'idle' });
return;
}
const userData = jwtDecode(accessToken);
const validUserData = web3userDataSchema.parse(userData);
setWeb3AuthState({ user: validUserData, status: 'success' });
web3browserAuthProvider.subscribeSignOut(() => {
setWeb3AuthState({ user: null, status: 'idle' });
});
} catch {
web3browserAuthProvider.signOut();
setWeb3AuthState({ user: null, status: 'error' });
}
};
// TODO correct interface of singIsSuccess from auth/web3/signin
const signIn = (singIsSuccess: SignInSuccessResponse) => {
web3browserAuthProvider.signIn(singIsSuccess);
handleSignIn();
};

const signOut = () => {
web3browserAuthProvider.signOut();
setWeb3AuthState({ user: null, status: 'idle' });
};

useEffect(() => {
handleSignIn();
}, []);

return (
<Web3AuthContext.Provider
value={
web3AuthState.user && web3AuthState.status === 'success'
? {
...web3AuthState,
signOut,
signIn,
}
: {
...web3AuthState,
signOut,
signIn,
}
}
>
{children}
</Web3AuthContext.Provider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { SignInSuccessResponse } from '@/api/servieces/worker/sign-in';

const web3accessTokenKey = btoa('web3_access_token');

const web3browserAuthProvider = {
isAuthenticated: false,
signOutCallback: (() => undefined) as () => void,
signIn(singIsSuccess: SignInSuccessResponse) {
web3browserAuthProvider.isAuthenticated = true;
localStorage.setItem(web3accessTokenKey, btoa(singIsSuccess.access_token));
},
signOut() {
web3browserAuthProvider.isAuthenticated = false;
localStorage.removeItem(web3accessTokenKey);
web3browserAuthProvider.triggerSignOutSubscriptions();
},
getAccessToken() {
const result = localStorage.getItem(web3accessTokenKey);

if (!result) {
return null;
}

return atob(result);
},
getRefreshToken() {
const result = localStorage.getItem(web3accessTokenKey);

if (!result) {
return null;
}

return atob(result);
},
subscribeSignOut(callback: () => void) {
web3browserAuthProvider.signOutCallback = callback;
},
unsubscribeSignOut() {
web3browserAuthProvider.signOutCallback = () => undefined;
},
triggerSignOutSubscriptions() {
web3browserAuthProvider.signOutCallback();
},
};

export { web3browserAuthProvider };
62 changes: 29 additions & 33 deletions packages/apps/human-app/frontend/src/auth/auth-context.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState, createContext, useCallback } from 'react';
import { useState, createContext, useEffect } from 'react';
import { jwtDecode } from 'jwt-decode';
import { z } from 'zod';
import { useQuery } from '@tanstack/react-query';
import { browserAuthProvider } from '@/auth/browser-auth-provider';
import type { SignInSuccessResponse } from '@/api/servieces/worker/sign-in';

Expand All @@ -21,80 +20,77 @@ const userDataSchema = z.object({

export type UserData = z.infer<typeof userDataSchema>;

type AuthStatus = 'loading' | 'error' | 'success' | 'idle';
export interface AuthenticatedUserContextType {
user: UserData;
isUserDataError: false;
status: AuthStatus;
signOut: () => void;
signIn: (singIsSuccess: SignInSuccessResponse) => void;
isPending: false;
}

interface UnauthenticatedUserContextType {
user: null;
isUserDataError: boolean;
userDataError: unknown;
status: AuthStatus;
signOut: () => void;
signIn: (singIsSuccess: SignInSuccessResponse) => void;
isPending: boolean;
}

export const AuthContext = createContext<
AuthenticatedUserContextType | UnauthenticatedUserContextType | null
>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<UserData | null>(null);
const signOutSubscription = useCallback(() => {
setUser(null);
}, []);
const [authState, setAuthState] = useState<{
user: UserData | null;
status: AuthStatus;
}>({ user: null, status: 'loading' });

const { data, isPending, isError, refetch, error } = useQuery({
queryFn: () => {
const handleSignIn = () => {
try {
const accessToken = browserAuthProvider.getAccessToken();
if (!accessToken) {
setAuthState({ user: null, status: 'idle' });
return;
}
const userData = jwtDecode(accessToken);
const validUserData = userDataSchema.parse(userData);
browserAuthProvider.subscribeSignOut(signOutSubscription);
return { accessToken, userData: validUserData };
},
queryKey: [
'singIn',
browserAuthProvider.getAccessToken(),
user,
signOutSubscription,
],
});
setAuthState({ user: validUserData, status: 'success' });
browserAuthProvider.subscribeSignOut(() => {
setAuthState({ user: null, status: 'idle' });
});
} catch {
browserAuthProvider.signOut();
setAuthState({ user: null, status: 'error' });
}
};

const signIn = (singIsSuccess: SignInSuccessResponse) => {
browserAuthProvider.signIn(singIsSuccess);
void refetch();
handleSignIn();
};

const signOut = () => {
browserAuthProvider.signOut();
void refetch();
setAuthState({ user: null, status: 'idle' });
};

useEffect(() => {
handleSignIn();
}, []);

return (
<AuthContext.Provider
value={
data?.userData && !isError
authState.user && authState.status === 'success'
? {
user: data.userData,
...authState,
signOut,
signIn,
isUserDataError: false,
isPending: false,
}
: {
user: null,
...authState,
signOut,
signIn,
isUserDataError: isError,
userDataError: error,
isPending,
}
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const refreshTokenKey = btoa('refresh_token');

const browserAuthProvider = {
isAuthenticated: false,
signOutCallbacks: [] as (() => void)[],
signOutCallback: (() => undefined) as () => void,
signIn(singIsSuccess: SignInSuccessResponse) {
browserAuthProvider.isAuthenticated = true;
localStorage.setItem(accessTokenKey, btoa(singIsSuccess.access_token));
Expand Down Expand Up @@ -36,15 +36,13 @@ const browserAuthProvider = {
return atob(result);
},
subscribeSignOut(callback: () => void) {
browserAuthProvider.signOutCallbacks.push(callback);
browserAuthProvider.signOutCallback = callback;
},
unsubscribeSignOut() {
browserAuthProvider.signOutCallbacks = [];
browserAuthProvider.signOutCallback = () => undefined;
},
triggerSignOutSubscriptions() {
browserAuthProvider.signOutCallbacks.forEach((callback) => {
callback();
});
browserAuthProvider.signOutCallback();
},
};

Expand Down
2 changes: 1 addition & 1 deletion packages/apps/human-app/frontend/src/auth/require-auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function RequireAuth({ children }: { children: JSX.Element }) {
const auth = useAuth();
const location = useLocation();

if (auth.isPending) {
if (auth.status === 'loading') {
return <PageCardLoader />;
}

Expand Down
Loading

0 comments on commit 851fc63

Please sign in to comment.