Skip to content

Commit

Permalink
🦝🧧 ↝ Adding auth fetcher, more graphql hooks to manage signing in #16
Browse files Browse the repository at this point in the history
Currently there's a mutate issue in `lib/auth/useLogin.ts` (in `Server/frontend`). (not assignable to param type ~~...). This is causing the SignInButton (which is derived from @thirdweb-dev/react (sdk) to also have an error with the default return statement (which should provide an access token from Lens IF the user has succesfully connected their wallet, completed a challenge (this challenge will later be replaced by the Flask-based challenge in `Server/app.py`), and signed in with Lens. So I'll be fixing this in the next commit.

More notes available on the Notion page linked in #23 / #22 / #21
  • Loading branch information
Gizmotronn committed Jan 2, 2023
1 parent 95feace commit b84a27c
Show file tree
Hide file tree
Showing 15 changed files with 388 additions and 19 deletions.
59 changes: 59 additions & 0 deletions Server/frontend/components/SignInButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useAddress, useNetworkMismatch, useNetwork, ConnectWallet, ChainId } from '@thirdweb-dev/react';
import React from 'react';
import useLensUser from '../lib/auth/useLensUser';
import useLogin from '../lib/auth/useLogin';

type Props = {};

export default function SignInButton({}: Props) {
const address = useAddress(); // Detect connected wallet
const isOnWrongNetwork = useNetworkMismatch(); // Is different to `activeChainId` in `_app.tsx`
const [, switchNetwork] = useNetwork(); // Switch network to `activeChainId`
const { isSignedInQuery, profileQuery } = useLensUser();
const { mutate: requestLogin } = useLogin();

// Connect wallet
if (!address) {
return (
<ConnectWallet />
);
}

// Switch network to polygon
if (!isOnWrongNetwork) {
return (
<button
onClick={() => switchNetwork?.(ChainId.Polygon)}
>Switch Network</button>
)
}

if (isSignedInQuery.isLoading) { // Loading signed in state
return <div>Loading</div>
}

// Sign in with Lens
if (!isSignedInQuery.data) { // Request a login to Lens
return (
<button
onClick={() => requestLogin()}
>Sign in with Lens</button>
)
};

if (profileQuery.isLoading) { // Show user their Lens Profile
return <div>Loading...</div>;
};

if (!profileQuery.data?.defaultProfile) { // If there's no Lens profile for the connected wallet
return <div>No Lens Profile</div>;
};

if (profileQuery.data?.defaultProfile) { // If profile exists
return <div>Hello {profileQuery.data?.defaultProfile?.handle} </div>
};

return (
<div>Something went wrong</div>
);
}
25 changes: 23 additions & 2 deletions Server/frontend/graphql/auth-fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TableDataCellComponent } from "react-markdown/lib/ast-to-react";
import { isTokenExpired, readAccessToken } from "../lib/auth/helpers";
import refreshAccessToken from "../lib/auth/refreshAccessToken";

const endpoint = 'https://api.lens.dev';

Expand All @@ -7,13 +8,33 @@ export const fetcher = <TData, TVariables>(
variables?: TVariables,
options?: RequestInit['headers']
): (() => Promise<TData>) => {
async function getAccessToken() { // Authentication headers
// Check local storage for access token
const token = readAccessToken();
if (!token) return null;
let accessToken = token?.accessToken;

// Check expiration of token
if (isTokenExpired( token.exp )) {
// Update token (using refresh) IF expired
const newToken = await refreshAccessToken();
if (!newToken) return null;
accessToken = newToken;
}

return accessToken; // Return the access token
};

return async () => {
const token = typeof window !=='undefined' ? await getAccessToken() : null; // Either a string or null (depending on auth/localStorage state)

const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options,
// Add Lens auth token here
'x-access-token': token ? token : '', // Lens auth token here (auth header
'Access-Control-Allow-Origin': "*",
},
body: JSON.stringify({
query,
Expand Down
6 changes: 6 additions & 0 deletions Server/frontend/graphql/authenticate.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mutation authenticate($request: SignedAuthChallenge!) {
authenticate(request: $request) {
accessToken
refreshToken
}
}
5 changes: 5 additions & 0 deletions Server/frontend/graphql/challenge.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query Challenge($request: ChallengeRequest!) {
challenge(request: $request) {
text
}
}
104 changes: 103 additions & 1 deletion Server/frontend/graphql/generated.ts

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Server/frontend/graphql/get-default-profile.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query defaultProfile($request: DefaultProfileRequest!) {
defaultProfile(request: $request) {
...ProfileFields
}
}
6 changes: 6 additions & 0 deletions Server/frontend/graphql/refresh.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mutation Refresh($request: RefreshRequest!) {
refresh(request: $request) {
accessToken
refreshToken
}
}
10 changes: 10 additions & 0 deletions Server/frontend/lib/auth/generateChallenge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { fetcher } from "../../graphql/auth-fetcher";
import { ChallengeDocument, ChallengeQuery, ChallengeQueryVariables } from "../../graphql/generated";

export default async function generateChallenge( address:string ) {
return await fetcher<ChallengeQuery, ChallengeQueryVariables>(ChallengeDocument, {
request: {
address,
},
})();
}
68 changes: 68 additions & 0 deletions Server/frontend/lib/auth/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const STORAGE_KEY = 'LH_STORAGE_KEY'; // lens hub storage key

// Determine if exp date is expired
export function isTokenExpired(exp: number) {
if (!exp) return true;
if (Date.now() >= exp * 1000) {
return false;
}
return true;
}

// Read access token from Lens (local storage)
export function readAccessToken () {
// Ensure user is on client environment
if (typeof window === 'undefined') return null;
const ls = localStorage || window.localStorage;
if (!ls) {
throw new Error("LocalStorage is not available");
}

const data = ls.getItem(STORAGE_KEY);
if (!data) return null;

return JSON.parse(data) as {
accessToken: string;
refreshToken: string;
exp: number;
};
}

// Set access token in storage
export function setAccessToken (
accessToken: string,
refreshToken: string,
) {
// Parse JWT token to get expiration date
const { exp } = parseJwt(accessToken);

// Set all three variables in local storage
const ls = localStorage || window.localStorage;

if (!ls) {
throw new Error("LocalStorage is not available");
}

ls.setItem(STORAGE_KEY, JSON.stringify({
accessToken,
refreshToken,
exp
}));
}

// Parse JWT token and extract params
export function parseJwt (token: string) {
var base64Url = token.split(".")[1];
var base64 = base64Url.replace(/-/g, "+").replace(/_/g, '/');
var jsonPayload = decodeURIComponent(
window
.atob(base64)
.split("")
.map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join("")
);

return JSON.parse(jsonPayload);
}
24 changes: 24 additions & 0 deletions Server/frontend/lib/auth/refreshAccessToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { fetcher } from "../../graphql/auth-fetcher";
import { RefreshMutation, RefreshMutationVariables, RefreshDocument } from "../../graphql/generated";
import { readAccessToken, setAccessToken } from "./helpers";

export default async function refreshAccessToken () { // Take current refresh, access token to Lens to generate a new Access token
// Read refresh token from local storage
const currentRefreshToken = readAccessToken()?.refreshToken;
if (!currentRefreshToken) return null;

// Send refresh token to Lens
const result = await fetcher<RefreshMutation, RefreshMutationVariables>(RefreshDocument, {
request: {
refreshToken: currentRefreshToken
},
})();

// Set new refresh token
const {
accessToken, refreshToken: newRefreshToken
} = result.refresh;
setAccessToken(accessToken, newRefreshToken);

return accessToken as string;
}
28 changes: 28 additions & 0 deletions Server/frontend/lib/auth/useLensUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useQuery } from "@tanstack/react-query";
import { useAddress } from "@thirdweb-dev/react";
import { useDefaultProfileQuery } from "../../graphql/generated";
import { readAccessToken } from "./helpers";

export default function useLensUser() {
// Make a react query for the local storage key
const address = useAddress();
const localStorageQuery = useQuery(
['lens-user', address],
() => readAccessToken(),
);

// If wallet is connected, check for the default profile (on Lens) connected to that wallet
const profileQuery = useDefaultProfileQuery({
request: {
ethereumAddress: address,
}
},
{
enabled: !!address,
});

return {
isSignedInQuery: localStorageQuery,
profileQuery: profileQuery,
}
}
40 changes: 40 additions & 0 deletions Server/frontend/lib/auth/useLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useMutation } from "@apollo/client";
import { useAddress, useSDK } from "@thirdweb-dev/react";
import { useAuthenticateMutation } from "../../graphql/generated";
import generateChallenge from "./generateChallenge";
import { setAccessToken } from "./helpers";

// Store access token inside local storage

export default function useLogin() {
const address = useAddress(); // Ensure user has connected wallet
const sdk = useSDK();
const {
mutateAsync: sendSignedMessage
} = useAuthenticateMutation();

async function login () {
if (!address) {
console.error('No address found. Please try connecting your wallet to continue signing into Lens');
return null;
}

const { challenge } = await generateChallenge(address); // Generate challenge from the Lens API
const signature = await sdk?.wallet.sign(challenge.text); // Sign the returned challenge with the user's wallet
const { // Send the signed challenge to the Lens API
authenticate
} = await sendSignedMessage({
request: {
address,
signature,
},
});

const { accessToken, refreshToken} = authenticate;

setAccessToken(accessToken, refreshToken);
}

// Receive an access token from Lens API
return useMutation(login);
}
1 change: 1 addition & 0 deletions Server/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@thirdweb-dev/auth": "^2.0.38",
"@thirdweb-dev/react": "^3.6.8",
"@thirdweb-dev/sdk": "^3.6.8",
"@thirdweb-dev/storage": "^1.0.6",
"ethers": "^5.7.2",
"graphql": "^16.6.0",
"moralis": "^2.10.3",
Expand Down
12 changes: 6 additions & 6 deletions Server/frontend/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { AppProps } from "next/app";
import { ChainId, ThirdwebProvider } from "@thirdweb-dev/react";
import Navbar from './lens/components/Navbar';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MoralisProvider } from "react-moralis";

/*import Navbar from './lens/components/Navbar';
import { LensProvider } from '../context/lensContext';
import { ApolloProvider } from "@apollo/client";
import { lensClient } from './lens/constants/lensConstants';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const activeChainId = ChainId.Mumbai;
import { lensClient } from './lens/constants/lensConstants';*/

function MyApp({ Component, pageProps }: AppProps) {
const activeChainId = ChainId.Polygon; // Set to `.Mumbai` for testnet interaction
const queryClient = new QueryClient();

return (
Expand All @@ -23,7 +23,7 @@ function MyApp({ Component, pageProps }: AppProps) {
}}
>
<MoralisProvider initializeOnMount={false}>
<Component {...pageProps} />
<Component {...pageProps} />
</MoralisProvider>
</ThirdwebProvider>
</QueryClientProvider>
Expand Down
14 changes: 4 additions & 10 deletions Server/frontend/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import type { NextPage } from "next";
import useAuthenticate from '../hooks/useAuthenticate';
import { useAddress, useDisconnect, useUser, useLogin, useLogout, useMetamask } from "@thirdweb-dev/react";
import { useAddress, useDisconnect, useUser, useLogout, useMetamask, ConnectWallet } from "@thirdweb-dev/react";
import { useEffect, useState } from "react";
import { PublicationSortCriteria, useExplorePublicationsQuery } from "../graphql/generated";
import useLogin from "../lib/auth/useLogin";
import SignInButton from "../components/SignInButton";

export default function Home () {
const { data, isLoading, error } = useExplorePublicationsQuery({
request: {
sortCriteria: PublicationSortCriteria.TopCollected,
},
});

return (
<div>Hello World</div>
);
return <SignInButton />;
};

0 comments on commit b84a27c

Please sign in to comment.