Skip to content

Commit

Permalink
Add support for rest source auth redirect
Browse files Browse the repository at this point in the history
  • Loading branch information
mpgxvii committed Sep 26, 2024
1 parent f5f94a1 commit d40091b
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 3 deletions.
22 changes: 19 additions & 3 deletions pages/apps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const Apps: NextPage = () => {
const [traits, setTraits] = useState<any>()
const [projects, setProjects] = useState<any>([])

const handleNavigation = () => {
router.replace("/fitbit")
}

useEffect(() => {
// If the router is not ready yet, or we already have a flow, do nothing.
if (!router.isReady) {
Expand All @@ -52,7 +56,7 @@ const Apps: NextPage = () => {
</Head>
<AppLoginCard>
<CardTitle>App Login</CardTitle>
<QrForm projects={projects} baseUrl={DefaultHydraUrl} />
<QrForm projects={projects} baseUrl={DefaultHydraUrl} navigate={handleNavigation} />
</AppLoginCard>
<ActionCard wide>
<Link href="/" passHref>
Expand All @@ -65,10 +69,11 @@ const Apps: NextPage = () => {

interface QrFormProps {
projects: any[]
baseUrl: string
baseUrl: string,
navigate: any
}

const QrForm: React.FC<QrFormProps> = ({ projects, baseUrl }) => {
const QrForm: React.FC<QrFormProps> = ({ projects, baseUrl, navigate }) => {
if (projects) {
return (
<div className="center">
Expand All @@ -78,6 +83,17 @@ const QrForm: React.FC<QrFormProps> = ({ projects, baseUrl }) => {
<label className="inputLabel">Active App</label>
<p>Scan the QR code below with your app.</p>
<QRCode value={baseUrl + "?projectId=" + project.id} size={140} />
<br />
<br />
<button className="col-xs-4">Login with Active App</button>
<br />
<br />
<br />
<div>
<label className="inputLabel">Connect Your Fitbit</label>
<p>Click the button below to redirect to Fitbit.</p>
<button className="col-xs-4" onClick={navigate}>Login with Fitbit</button>
</div>
</div>
))}
</div>
Expand Down
117 changes: 117 additions & 0 deletions pages/fitbit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { SettingsFlow } from "@ory/client"
import type { NextPage } from "next"
import Head from "next/head"
import Link from "next/link"
import { useRouter } from "next/router"
import { ReactNode, useEffect, useState } from "react"
import QRCode from "react-qr-code"

import { ActionCard, CenterLink, Methods, CardTitle } from "../pkg"
import ory from "../pkg/sdk"
import restSourceClient from "../services/rest-source-client"

interface Props {
flow?: SettingsFlow
only?: Methods
}

function AppLoginCard({ children }: Props & { children: ReactNode }) {
return (
<ActionCard wide className="cardMargin">
{children}
</ActionCard>
)
}

const Fitbit: NextPage = () => {
const router = useRouter()
const DefaultHydraUrl =
process.env.HYDRA_PUBLIC_URL || "http://localhost:4444"
const { flow: flowId, return_to: returnTo } = router.query
const [traits, setTraits] = useState<any>()
const [projects, setProjects] = useState<any>([])

const handleNavigation = () => {
return restSourceClient.redirectToAuthRequestLink()
}


useEffect(() => {
const handleToken = async () => {
if (!router.isReady) return;

const token = await restSourceClient.getAccessTokenFromRedirect();
if (token) {
localStorage.setItem("access_token", token);
await restSourceClient.redirectToRestSourceAuthLink(token);
}
};

handleToken();
}, [router.isReady]);

useEffect(() => {
ory.toSession().then(({ data }) => {
const traits = data?.identity?.traits
setTraits(traits)
setProjects(traits.projects)
})
}, [flowId, router, router.isReady, returnTo])

return (
<>
<Head>
<title>App Login</title>
<meta name="description" content="NextJS + React + Vercel + Ory" />
</Head>
<AppLoginCard>
<CardTitle>App Login</CardTitle>
<QrForm
projects={projects}
baseUrl={DefaultHydraUrl}
navigate={handleNavigation}
/>
</AppLoginCard>
<ActionCard wide>
<Link href="/" passHref>
<CenterLink>Go back</CenterLink>
</Link>
</ActionCard>
</>
)
}

interface QrFormProps {
projects: any[]
baseUrl: string
navigate: any
}

const QrForm: React.FC<QrFormProps> = ({ projects, baseUrl, navigate }) => {
if (projects) {
return (
<div className="center">
{projects.map((project) => (
<div key={project.id} className="project-form">
<h3>{project.name}</h3>
<div>
<label className="inputLabel">Connect Your Fitbit</label>
<p>Click the button below to redirect to Fitbit.</p>
<button className="col-xs-4" onClick={navigate}>
Login with Fitbit
</button>
</div>
</div>
))}
</div>
)
} else {
return (
<div className="center">
<label className="inputLabel">No projects.</label>
</div>
)
}
}

export default Fitbit
89 changes: 89 additions & 0 deletions services/rest-source-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
export class RestSourceClient {
private readonly AUTH_BASE_URL = "http://localhost:4444/oauth2"
private readonly GRANT_TYPE = "authorization_code"
private readonly CLIENT_ID = "SEP"
private readonly CLIENT_SECRET = "secret"
private readonly REGISTRATION_ENDPOINT =
"http://localhost:8085/rest-sources/backend/registrations"
private readonly FRONTEND_ENDPOINT =
"http://localhost:8081/rest-sources/authorizer"

async getAccessToken(
code: string,
redirectUri: string,
): Promise<string | null> {
const bodyParams = new URLSearchParams({
grant_type: this.GRANT_TYPE,
code,
redirect_uri: redirectUri,
client_id: this.CLIENT_ID,
client_secret: this.CLIENT_SECRET,
})

const response = await fetch(`${this.AUTH_BASE_URL}/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: bodyParams,
})

const data = await response.json()
return data.access_token || null
}

async getAccessTokenFromRedirect(): Promise<string | null> {
const url = new URL(window.location.href)
const code = url.searchParams.get("code")
if (!code) return null

const redirectUri = window.location.href.split("?")[0]
return this.getAccessToken(code, redirectUri)
}

redirectToAuthRequestLink(): void {
const scopes = [
"SOURCETYPE.READ",
"PROJECT.READ",
"SUBJECT.READ",
"SUBJECT.UPDATE",
"SUBJECT.CREATE",
].join("%20")

const authUrl = `${this.AUTH_BASE_URL}/auth?client_id=${this.CLIENT_ID}&response_type=code&state=${Date.now()}&audience=res_restAuthorizer&scope=${scopes}&redirect_uri=${window.location.href.split("?")[0]}`

window.location.href = authUrl
}

// Make a POST request to the registration endpoint to retrieve the authorization link
async getRestSourceAuthLink(accessToken: string): Promise<string | null> {
const response = await fetch(this.REGISTRATION_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
userId: "4",
persistent: true,
}),
})

const data = await response.json()
if (!data.token || !data.secret) {
console.error("Failed to retrieve auth link")
return null
}

return `${this.FRONTEND_ENDPOINT}/users:auth?token=${data.token}&secret=${data.secret}`
}

// Redirect user to the authorization link for the rest source
async redirectToRestSourceAuthLink(accessToken: string): Promise<void> {
const url = await this.getRestSourceAuthLink(accessToken)
if (url) {
console.log("Redirecting to: ", url)
window.location.href = url
}
}
}

export default new RestSourceClient()

0 comments on commit d40091b

Please sign in to comment.