Skip to content

Commit

Permalink
Add support for automatically logging in to armt
Browse files Browse the repository at this point in the history
  • Loading branch information
mpgxvii committed Dec 2, 2024
1 parent 0b57af0 commit c45a68c
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 10 deletions.
8 changes: 2 additions & 6 deletions pages/apps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ const Apps: NextPage = () => {

const handleNavigation = (type: string) => {
if (type === 'app') {
const appUrl = `org.phidatalab.radar-armt:/?session=${encodeURIComponent("test")}`;
window.location.href = "org.phidatalab.radar-armt:/"
router.replace("/armt")
}
else {
router.replace("/fitbit")
Expand Down Expand Up @@ -93,10 +92,7 @@ const QrForm: React.FC<QrFormProps> = ({ projects, baseUrl, navigate }) => {
<div key={project.id} className="project-form">
<h3>{project.name}</h3>
<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 />
<p>Click the button below to login with the active app.</p>
<button className="col-xs-4" onClick={() => navigate('app')}>Login with Active App</button>
<br />
<br />
Expand Down
153 changes: 153 additions & 0 deletions pages/armt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
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 armtClient from "../services/armt-client"

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

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

const Armt: NextPage = () => {
const router = useRouter()
const { flow: flowId, return_to: returnTo } = router.query
const [traits, setTraits] = useState<any>()
const [projects, setProjects] = useState<any>([])
const [tokenHandled, setTokenHandled] = useState(false)
const [isFetchingToken, setIsFetchingToken] = useState(false) // Prevent multiple calls
const [isMobile, setIsMobile] = useState(false)
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null) // New state to store the URL for QR code

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

const isMobileDevice = () => {
return typeof window !== "undefined" && /Mobi|Android/i.test(window.navigator.userAgent)
}

useEffect(() => {
setIsMobile(isMobileDevice())

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

useEffect(() => {
const handleToken = async () => {
if (!router.isReady || !projects.length || tokenHandled || isFetchingToken) return

// Token is either missing or expired; fetch a new one
setIsFetchingToken(true)
try {
const tokenResponse = await armtClient.getAccessTokenFromRedirect()
if (tokenResponse?.access_token && tokenResponse?.expires_in) {
tokenResponse['iat'] = Math.floor(Date.now() / 1000)
const shortToken = {
iat: tokenResponse.iat,
expires_in: tokenResponse.expires_in,
refresh_token: tokenResponse.refresh_token,
scope: tokenResponse.scope,
token_type: tokenResponse.token_type }

const url = await armtClient.getAuthLink(
shortToken,
projects[0]
)
setQrCodeUrl(url)
if (isMobile) {
window.location.href = url
}
setTokenHandled(true)
}
} catch (error) {
console.error("Failed to fetch token:", error)
} finally {
setIsFetchingToken(false)
}
}

handleToken()
}, [router.isReady, projects, tokenHandled, isFetchingToken])

return (
<>
<Head>
<title>App Login</title>
<meta name="description" content="RADAR-base SEP" />
</Head>
<AppLoginCard>
<CardTitle>App Login</CardTitle>
<QrForm
projects={projects}
navigate={handleNavigation}
qrCodeUrl={qrCodeUrl} // Pass the QR code URL to the form
/>
</AppLoginCard>
<ActionCard wide>
<Link href="/" passHref>
<CenterLink>Go back</CenterLink>
</Link>
</ActionCard>
</>
)
}

interface QrFormProps {
projects: any[]
navigate: any
qrCodeUrl: string | null
}

const QrForm: React.FC<QrFormProps> = ({ projects, navigate, qrCodeUrl }) => {
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 App</label>
<p>Click the button below to redirect to login.</p>
<button className="col-xs-4" onClick={navigate}>
Login with the aRMT app
</button>
<br/>
<br/>
<p>Or scan to login.</p>
{qrCodeUrl && <QRCode value={qrCodeUrl} size={300} />}
<br />
<br />
</div>
</div>
))}
</div>
)
} else {
return (
<div className="center">
<label className="inputLabel">No projects.</label>
</div>
)
}
}

export default Armt
12 changes: 8 additions & 4 deletions pages/oauth2-login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const OAuth2Login = () => {
const [challenge, setChallenge] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [traits, setTraits] = useState<any>(null)
const [projects, setProjects] = useState<any>([])
const [projects, setProjects] = useState<any>(null)
const [id, setId] = useState<any>(null)

const basePath = process.env.BASE_PATH || "/kratos-ui"
Expand All @@ -26,13 +26,17 @@ const OAuth2Login = () => {
const traits = data?.identity?.traits
const projects = traits?.projects
const id = data?.identity?.id
const schema_id = data?.identity?.schema_id
setId(data?.identity?.id)
setTraits(traits)
setProjects(traits?.projects)
setChallenge(String(login_challenge))

if (traits && login_challenge) {
const subject = projects && projects[0] ? projects[0].userId : id
let subject = id
if (schema_id == 'subject') {
subject = projects[0].userId
}
handleLogin(subject, login_challenge)
}
} catch (error) {
Expand All @@ -44,7 +48,7 @@ const OAuth2Login = () => {
}
}

if (!challenge) {
if (!traits) {
checkSession()
}
}, [router])
Expand Down Expand Up @@ -90,4 +94,4 @@ const OAuth2Login = () => {
)
}

export default OAuth2Login
export default OAuth2Login
85 changes: 85 additions & 0 deletions services/armt-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import getConfig from "next/config"

const { publicRuntimeConfig } = getConfig()

export class ArmtClient {
private readonly AUTH_BASE_URL = `${publicRuntimeConfig.hydraPublicUrl}/oauth2`
private readonly GRANT_TYPE = "authorization_code"
private readonly CLIENT_ID = `aRMT`
private readonly CLIENT_SECRET = ``

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

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

if (!response.ok) {
throw new Error(
`Failed to retrieve access token: ${response.statusText}`,
)
}

const data = await response.json()
return data || null
} catch (error) {
console.error(error)
return null
}
}

async getAccessTokenFromRedirect(): Promise<any> {
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",
"MEASUREMENT.CREATE",
"SOURCEDATA.CREATE",
"SOURCETYPE.UPDATE",
"offline_access"
].join("%20")

const audience = ["res_ManagementPortal", "res_gateway", "res_AppServer"].join("%20")

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

window.location.href = authUrl
}

async getAuthLink(
accessToken: any,
project: any,
): Promise<string> {
const token = JSON.stringify(accessToken)
const referrer = window.location.href.split("?")[0]
const appUrl = `org.phidatalab.radar-armt://enrol?data=${encodeURIComponent(token)}&referrer=${referrer}`
return appUrl
}
}

export default new ArmtClient()

0 comments on commit c45a68c

Please sign in to comment.