Skip to content

Commit

Permalink
Authentication builder (#310)
Browse files Browse the repository at this point in the history
* initial authentication and remark-plugin

* CodeTabs component created and vocs.config modified

* CodeTabs component created and vocs.config modified

* initial version and auth functioning

* added tooltip

* finalized UI and flow

* Updated tooltip with shad

* added link to tooltip dialog

* updated codetabs index

* minor improvements to tooltip UX

* minor revisions

* updated README with cors-anywhere

---------

Co-authored-by: Richard Miguel Irala <[email protected]>
  • Loading branch information
JamesLawton and RichardIrala authored Oct 2, 2024
1 parent 6f76fb0 commit a5b14db
Show file tree
Hide file tree
Showing 24 changed files with 1,056 additions and 727 deletions.
1 change: 1 addition & 0 deletions .example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_BUILDER_URL="http://localhost:8080/https://dev-api.sequence.build"
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ docs/.DS_Store
**/.DS_Store
docs/public/.DS_Store
pnpm-lock.yaml
vocs.config.tsx.timestamp-*
vocs.config.ts.timestamp-*
docs/pages/solutions/.DS_Store
.cursorrules
.cursorrules
.env*
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ This is a [Vocs](https://vocs.dev) project bootstrapped with the Vocs CLI.
1. Clone the repo and run `pnpm i` to install packages.
2. Run a local environment with `pnpm run dev`.

In order to run the authentication, if needed, you will need to setup a cors-anywhere docker instance in order to proxy the request to the Builder API:

```
docker run -d -p 8080:8080 --dns 1.1.1.1 --name cors-anywhere redocly/cors-anywhere
```

##

In production, we leverage github actions to deploy a workflow and deploy automatically to github pages via the build branch.

v2 docs of Sequence Stack
v2 docs of Sequence Stack
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"a11y": {
"noSvgWithoutTitle": "off",
"useButtonType": "off",
"useAltText": "off"
"useAltText": "off",
"useValidAnchor": "off"
},
"style": {
"noNonNullAssertion": "off"
Expand Down
265 changes: 265 additions & 0 deletions docs/components/BuilderAuthenticationButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import { useState, type ReactElement } from 'react'
import { sequence } from '0xsequence'
import clsx from 'clsx'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from './Dialog/dialog'

const builderURL = import.meta.env.DEV
? 'http://localhost:8080/https://api.sequence.build' // Routing with a cors-anywhere proxy
: 'https://api.sequence.build' // Production URL

const BuilderAuthenticationButton = (): ReactElement => {
const [isConnected, setIsConnected] = useState(false)
const [showModal, setShowModal] = useState(false)
const [projects, setProjects] = useState<any[]>([])

const handleConnect = async () => {
try {
const connected = await connect('Sequence Docs', setProjects)
if (connected) {
setIsConnected(true)
setShowModal(true) // Open the dialog here
}
} catch (error) {
console.error('Connection failed:', error)
}
}

const handleProjectSelection = async (projectId: string) => {
try {
const projectAccessKey = await getDefaultAccessKey(projectId)
localStorage.setItem('sequenceProjectAccessKey', projectAccessKey.accessKey.accessKey)
console.log(projectAccessKey.accessKey.accessKey)
setShowModal(false)
window.location.reload()
} catch (error) {
console.error('Error getting project access key:', error)
}
}

return (
<>
<button
className="hover-fade font-bold text-white max-w-max h-min text-center rounded-full bg-gradient-to-r from-[#4411E1] to-[#7537F9] px-[20px] py-[4px] text-[12px] top-auth-button_position"
onClick={handleConnect}
>
Login
</button>
<Dialog open={showModal} onOpenChange={setShowModal}>
<DialogContent className="sm:max-w-[600px] bg-black text-white border-gray-800">
<DialogHeader>
<DialogTitle className="text-2xl font-bold text-white">Select a Project</DialogTitle>
<DialogDescription className="text-gray-400">
Choose a project from the list below.
</DialogDescription>
</DialogHeader>

<div className="flex-1 h-fit shrink-0 rounded-xl dark:bg-white-10 bg-white overflow-hidden">
<div className="flex flex-col gap-3 p-4 relative">
<div
className={clsx('absolute inset-0 opacity-20 z-0 pointer', {
'dark:bg-gradient-to-b from-[#000000] to-transparent': true,
})}
/>
{projects.map((project, index) => (
<a
key={index}
onClick={() => handleProjectSelection(project.id)}
className="hover-fade p-4 rounded-md z-10 dark:bg-white-10 bg-black-7 cursor-pointer"
>
<div className="flex gap-2">
<p className="flex items-start gap-2 text-xl leading-7 font-bold text-themed-primary">
{project.name}
</p>
</div>
<p className="text-themed-secondary text-sm font-medium">
Project ID: {project.id}
</p>
</a>
))}
</div>
</div>
</DialogContent>
</Dialog>
</>
)
}

export const connect = async (appName: string, setProjects: (projects: any[]) => void) => {
let isConnected = false
// dev
const projectAccessKey = 'AQAAAAAAABL7m8Y6sdKRhOU2etUqkwJ3uHE'
// prod
// const projectAccessKey = "AQAAAAAAABL7m8Y6sdKRhOU2etUqkwJ3uHE"

const wallet = sequence.initWallet(projectAccessKey, { defaultNetwork: 137 })

try {
const connectDetails = await wallet.connect({
app: appName,
authorize: true,
askForEmail: true,
})

if (!connectDetails.proof) {
wallet.disconnect()
window.location.href = '/'
throw new Error('No proof')
}

const userEmail = connectDetails.email || undefined

const jwtToken = await authenticate(connectDetails.proof.proofString, userEmail)

const projectsResponse = await listProjects()
setProjects(projectsResponse.projects)

isConnected = true
return isConnected
} catch (err) {
console.error(err)
return false
}
}

export async function authenticate(proof: any, email: any) {
const url = `${builderURL}/rpc/Builder/GetAuthToken`

const headers = {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9',
'content-type': 'application/json',
'sec-ch-ua': '"Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'user-agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
}

const body = {
ethauthProof: proof,
email: email,
}

try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body),
})

if (!response.ok) {
throw new Error('Network response was not ok')
}
const { auth } = await response.json()
const { jwtToken } = auth

// Store the JWT in local storage
localStorage.setItem('sequenceJWT', jwtToken)

return jwtToken
} catch (error) {
console.error('There was a problem with the fetch operation:', error)
throw error
}
}

export const listProjects = async () => {
const url = `${builderURL}/rpc/Builder/ListProjects`

// Retrieve the Bearer token from local storage
const token = localStorage.getItem('sequenceJWT')

const headers = {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9',
authorization: `BEARER ${token}`, // Use the token from local storage
'content-type': 'application/json',
priority: 'u=1, i',
'sec-ch-ua': '"Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'empty',
'sec-fetch-site': 'same-site',
'user-agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
}

const body = JSON.stringify({
page: {
column: '+projects.id',
},
includeExternal: true,
})

try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: body,
})

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}

const data = await response.json()
return data
} catch (error) {
console.error('There was an error!', error)
}
}

export const getDefaultAccessKey = async (projectId: any) => {
const url = `${builderURL}/rpc/QuotaControl/GetDefaultAccessKey`

// Retrieve the Bearer token from local storage
const token = localStorage.getItem('sequenceJWT')

const headers = {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9',
authorization: `BEARER ${token}`, // Use the token from local storage
'content-type': 'application/json',
priority: 'u=1, i',
'sec-ch-ua': '"Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'user-agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
}

const body = JSON.stringify({
projectID: projectId,
})

try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: body,
})

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}

const data = await response.json()
return data
} catch (error) {
console.error('There was an error!', error)
}
}

export default BuilderAuthenticationButton
Loading

0 comments on commit a5b14db

Please sign in to comment.