From 6e31c8d8c8fc534c6821a18a9ced9987886532b7 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 18 Sep 2024 15:01:59 +0100 Subject: [PATCH 01/37] Add initial implementation of app login with qr code --- package-lock.json | 56 +++++++++++++++++++++++++--------- package.json | 1 + pages/apps.tsx | 78 +++++++++++++++++++++++++++++++++++++++++++++++ pages/index.tsx | 6 ++++ 4 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 pages/apps.tsx diff --git a/package-lock.json b/package-lock.json index 475b492..d02211b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "next": "12.1.6", "react": "17.0.2", "react-dom": "17.0.2", + "react-qr-code": "^2.0.15", "react-toastify": "^8.0.3", "styled-components": "^5.3.1", "typescript": "^4.4.2" @@ -7226,21 +7227,19 @@ } }, "node_modules/prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", - "react-is": "^16.8.1" + "react-is": "^16.13.1" } }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/proxy-from-env": { "version": "1.0.0", @@ -7271,6 +7270,11 @@ "node": ">=6" } }, + "node_modules/qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==" + }, "node_modules/qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", @@ -7346,6 +7350,18 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "peer": true }, + "node_modules/react-qr-code": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz", + "integrity": "sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==", + "dependencies": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-toastify": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-8.1.0.tgz", @@ -13649,21 +13665,19 @@ "dev": true }, "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", - "react-is": "^16.8.1" + "react-is": "^16.13.1" }, "dependencies": { "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" } } }, @@ -13693,6 +13707,11 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==" + }, "qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", @@ -13741,6 +13760,15 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "peer": true }, + "react-qr-code": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz", + "integrity": "sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==", + "requires": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + } + }, "react-toastify": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-8.1.0.tgz", diff --git a/package.json b/package.json index 9ff242c..004e36d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "next": "12.1.6", "react": "17.0.2", "react-dom": "17.0.2", + "react-qr-code": "^2.0.15", "react-toastify": "^8.0.3", "styled-components": "^5.3.1", "typescript": "^4.4.2" diff --git a/pages/apps.tsx b/pages/apps.tsx new file mode 100644 index 0000000..6f7f606 --- /dev/null +++ b/pages/apps.tsx @@ -0,0 +1,78 @@ +import { + SettingsFlow, + UiNode, + UiNodeInputAttributes, + UpdateSettingsFlowBody, +} from "@ory/client" +import { AxiosError } from "axios" +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 { profileQuestions } from "../data/profile-questionnaire" +import { + ActionCard, + CenterLink, + Flow, + Messages, + Methods, + CardTitle, + InnerCard, +} from "../pkg" +import { handleFlowError } from "../pkg/errors" +import ory from "../pkg/sdk" +import { H3 } from "@ory/themes" + +interface Props { + flow?: SettingsFlow + only?: Methods +} + +function ProfileCard({ children }: Props & { children: ReactNode }) { + return ( + + {children} + + ) +} + +const Apps: NextPage = () => { + const [flow, setFlow] = useState() + + // Get ?flow=... from the URL + const router = useRouter() + const DefaultHydraUrl = "http://localhost:4444" + + useEffect(() => { + // If the router is not ready yet, or we already have a flow, do nothing. + if (!router.isReady) { + return + } + }, []) + + return ( + <> + + App Login + + + + App Login +
+

Scan the QR code below with your app.

+ +
+
+ + + Go back + + + + ) +} + +export default Apps diff --git a/pages/index.tsx b/pages/index.tsx index 43dc663..4269395 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -99,6 +99,12 @@ const Home: NextPage = () => { title="Study Consent" disabled={!hasSession} /> + Date: Wed, 18 Sep 2024 15:08:17 +0100 Subject: [PATCH 02/37] Fix formatting --- pages/apps.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/apps.tsx b/pages/apps.tsx index 6f7f606..0345b8c 100644 --- a/pages/apps.tsx +++ b/pages/apps.tsx @@ -4,6 +4,7 @@ import { UiNodeInputAttributes, UpdateSettingsFlowBody, } from "@ory/client" +import { H3 } from "@ory/themes" import { AxiosError } from "axios" import type { NextPage } from "next" import Head from "next/head" @@ -24,7 +25,6 @@ import { } from "../pkg" import { handleFlowError } from "../pkg/errors" import ory from "../pkg/sdk" -import { H3 } from "@ory/themes" interface Props { flow?: SettingsFlow From cdf3e2eaaa97af1588d37cd6f1e9224ece8f31b9 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 23 Sep 2024 13:42:22 +0100 Subject: [PATCH 03/37] Fix login for multiple projects --- pages/apps.tsx | 78 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/pages/apps.tsx b/pages/apps.tsx index 0345b8c..31632d5 100644 --- a/pages/apps.tsx +++ b/pages/apps.tsx @@ -1,11 +1,4 @@ -import { - SettingsFlow, - UiNode, - UiNodeInputAttributes, - UpdateSettingsFlowBody, -} from "@ory/client" -import { H3 } from "@ory/themes" -import { AxiosError } from "axios" +import { SettingsFlow } from "@ory/client" import type { NextPage } from "next" import Head from "next/head" import Link from "next/link" @@ -13,17 +6,7 @@ import { useRouter } from "next/router" import { ReactNode, useEffect, useState } from "react" import QRCode from "react-qr-code" -import { profileQuestions } from "../data/profile-questionnaire" -import { - ActionCard, - CenterLink, - Flow, - Messages, - Methods, - CardTitle, - InnerCard, -} from "../pkg" -import { handleFlowError } from "../pkg/errors" +import { ActionCard, CenterLink, Methods, CardTitle } from "../pkg" import ory from "../pkg/sdk" interface Props { @@ -31,7 +14,7 @@ interface Props { only?: Methods } -function ProfileCard({ children }: Props & { children: ReactNode }) { +function AppLoginCard({ children }: Props & { children: ReactNode }) { return ( {children} @@ -40,18 +23,26 @@ function ProfileCard({ children }: Props & { children: ReactNode }) { } const Apps: NextPage = () => { - const [flow, setFlow] = useState() - - // Get ?flow=... from the URL const router = useRouter() - const DefaultHydraUrl = "http://localhost:4444" + const DefaultHydraUrl = + process.env.HYDRA_PUBLIC_URL || "http://localhost:4444" + const { flow: flowId, return_to: returnTo } = router.query + const [traits, setTraits] = useState() + const [projects, setProjects] = useState([]) useEffect(() => { // If the router is not ready yet, or we already have a flow, do nothing. if (!router.isReady) { return } - }, []) + + // Otherwise we initialize it + ory.toSession().then(({ data }) => { + const traits = data?.identity?.traits + setTraits(traits) + setProjects(traits.projects) + }) + }, [flowId, router, router.isReady, returnTo]) return ( <> @@ -59,13 +50,10 @@ const Apps: NextPage = () => { App Login - + App Login -
-

Scan the QR code below with your app.

- -
-
+ + Go back @@ -75,4 +63,32 @@ const Apps: NextPage = () => { ) } +interface QrFormProps { + projects: any[] + baseUrl: string +} + +const QrForm: React.FC = ({ projects, baseUrl }) => { + if (projects) { + return ( +
+ {projects.map((project) => ( +
+

{project.name}

+ +

Scan the QR code below with your app.

+ + +
+ ) + } +} + export default Apps From c3b9f8985b1f473e4126165b1a20f44a1eb69e55 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 25 Sep 2024 21:09:56 +0100 Subject: [PATCH 04/37] Fix setting of scope --- pages/api/consent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/consent.ts b/pages/api/consent.ts index 5632cf5..7a43e60 100644 --- a/pages/api/consent.ts +++ b/pages/api/consent.ts @@ -49,7 +49,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { const acceptResponse = await hydra.acceptOAuth2ConsentRequest({ consentChallenge, acceptOAuth2ConsentRequest: { - grant_scope: grantScope, + grant_scope: session.access_token.scope, grant_access_token_audience: body.requested_access_token_audience, session, remember: Boolean(remember), From d40091b101a9541e8577db579632157770c0be56 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 26 Sep 2024 17:42:51 +0100 Subject: [PATCH 05/37] Add support for rest source auth redirect --- pages/apps.tsx | 22 ++++++- pages/fitbit.tsx | 117 +++++++++++++++++++++++++++++++++ services/rest-source-client.ts | 89 +++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 pages/fitbit.tsx create mode 100644 services/rest-source-client.ts diff --git a/pages/apps.tsx b/pages/apps.tsx index 31632d5..7d6b118 100644 --- a/pages/apps.tsx +++ b/pages/apps.tsx @@ -30,6 +30,10 @@ const Apps: NextPage = () => { const [traits, setTraits] = useState() const [projects, setProjects] = useState([]) + const handleNavigation = () => { + router.replace("/fitbit") + } + useEffect(() => { // If the router is not ready yet, or we already have a flow, do nothing. if (!router.isReady) { @@ -52,7 +56,7 @@ const Apps: NextPage = () => { App Login - + @@ -65,10 +69,11 @@ const Apps: NextPage = () => { interface QrFormProps { projects: any[] - baseUrl: string + baseUrl: string, + navigate: any } -const QrForm: React.FC = ({ projects, baseUrl }) => { +const QrForm: React.FC = ({ projects, baseUrl, navigate }) => { if (projects) { return (
@@ -78,6 +83,17 @@ const QrForm: React.FC = ({ projects, baseUrl }) => {

Scan the QR code below with your app.

Login with Active App +
+
+
+
+ +

Click the button below to redirect to Fitbit.

+ +
))}
diff --git a/pages/fitbit.tsx b/pages/fitbit.tsx new file mode 100644 index 0000000..065bdf1 --- /dev/null +++ b/pages/fitbit.tsx @@ -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 ( + + {children} + + ) +} + +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() + const [projects, setProjects] = useState([]) + + 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 ( + <> + + App Login + + + + App Login + + + + + Go back + + + + ) +} + +interface QrFormProps { + projects: any[] + baseUrl: string + navigate: any +} + +const QrForm: React.FC = ({ projects, baseUrl, navigate }) => { + if (projects) { + return ( +
+ {projects.map((project) => ( +
+

{project.name}

+
+ +

Click the button below to redirect to Fitbit.

+ +
+
+ ))} +
+ ) + } else { + return ( +
+ +
+ ) + } +} + +export default Fitbit diff --git a/services/rest-source-client.ts b/services/rest-source-client.ts new file mode 100644 index 0000000..ba1b5fe --- /dev/null +++ b/services/rest-source-client.ts @@ -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 { + 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 { + 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 { + 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 { + const url = await this.getRestSourceAuthLink(accessToken) + if (url) { + console.log("Redirecting to: ", url) + window.location.href = url + } + } +} + +export default new RestSourceClient() From 4763a56a51e25aaa08c952106ae22af1ccb75410 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 26 Sep 2024 17:45:12 +0100 Subject: [PATCH 06/37] Fix formatting --- pages/apps.tsx | 12 +++++++++--- pages/fitbit.tsx | 19 +++++++++---------- services/rest-source-client.ts | 6 +++++- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/pages/apps.tsx b/pages/apps.tsx index 7d6b118..21708a9 100644 --- a/pages/apps.tsx +++ b/pages/apps.tsx @@ -56,7 +56,11 @@ const Apps: NextPage = () => { App Login - + @@ -69,7 +73,7 @@ const Apps: NextPage = () => { interface QrFormProps { projects: any[] - baseUrl: string, + baseUrl: string navigate: any } @@ -92,7 +96,9 @@ const QrForm: React.FC = ({ projects, baseUrl, navigate }) => {

Click the button below to redirect to Fitbit.

- +
))} diff --git a/pages/fitbit.tsx b/pages/fitbit.tsx index 065bdf1..50d692f 100644 --- a/pages/fitbit.tsx +++ b/pages/fitbit.tsx @@ -35,20 +35,19 @@ const Fitbit: NextPage = () => { return restSourceClient.redirectToAuthRequestLink() } - useEffect(() => { const handleToken = async () => { - if (!router.isReady) return; - - const token = await restSourceClient.getAccessTokenFromRedirect(); + if (!router.isReady) return + + const token = await restSourceClient.getAccessTokenFromRedirect() if (token) { - localStorage.setItem("access_token", token); - await restSourceClient.redirectToRestSourceAuthLink(token); + localStorage.setItem("access_token", token) + await restSourceClient.redirectToRestSourceAuthLink(token) } - }; - - handleToken(); - }, [router.isReady]); + } + + handleToken() + }, [router.isReady]) useEffect(() => { ory.toSession().then(({ data }) => { diff --git a/services/rest-source-client.ts b/services/rest-source-client.ts index ba1b5fe..fd86e2a 100644 --- a/services/rest-source-client.ts +++ b/services/rest-source-client.ts @@ -48,7 +48,11 @@ export class RestSourceClient { "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]}` + 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 } From 49aed0507c27d68249ace3f9ebf226340389ae4f Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 26 Sep 2024 20:07:10 +0100 Subject: [PATCH 07/37] Fix scope loading --- pages/consent.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pages/consent.tsx b/pages/consent.tsx index fa202ec..f13d225 100644 --- a/pages/consent.tsx +++ b/pages/consent.tsx @@ -9,11 +9,13 @@ const Consent = () => { const [consent, setConsent] = useState(null) const [identity, setIdentity] = useState(null) const [csrfToken, setCsrfToken] = useState("") + const [isLoading, setIsLoading] = useState(false) useEffect(() => { const { consent_challenge } = router.query const fetchSessionAndConsent = async () => { + setIsLoading(true) try { const sessionResponse = await ory.toSession() const sessionData = sessionResponse.data @@ -56,12 +58,13 @@ const Consent = () => { if (skipData.error) { throw new Error(skipData.error) } - router.push(skipData.redirect_to) + return } } catch (error) { console.error("Error fetching session or consent:", error) } + setIsLoading(false) } if (router.query.consent_challenge) { @@ -113,7 +116,7 @@ const Consent = () => { } } - if (!consent) { + if (!consent || isLoading) { return
Loading...
} From 34c130e6ebeb8dadb8b7d1a0cc539485d0de03a9 Mon Sep 17 00:00:00 2001 From: Pauline Date: Sat, 28 Sep 2024 20:31:54 +0100 Subject: [PATCH 08/37] Fix npm cache permission issues by setting new dir --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 721ae7c..acb5693 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,12 @@ RUN mkdir -p /app WORKDIR /app +ENV NPM_CONFIG_CACHE=/home/node/.npm + COPY package*.json ./ +RUN mkdir -p $NPM_CONFIG_CACHE && chown -R node:node $NPM_CONFIG_CACHE + RUN npm install COPY . . @@ -15,5 +19,4 @@ RUN npm run build EXPOSE 3000 -# Start the app on port 4455 as recommended by ory CMD ["npm", "start", "--", "-p", "3000"] From 6ca83f7ee05578c05560d501f995eb658a89dfd9 Mon Sep 17 00:00:00 2001 From: Pauline Date: Sat, 28 Sep 2024 21:42:02 +0100 Subject: [PATCH 09/37] Add user id when creating subject in MP --- package-lock.json | 36 +++++++++++++++++++++++++++--------- package.json | 3 ++- pages/registration.tsx | 2 ++ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index d02211b..1981c49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "react-qr-code": "^2.0.15", "react-toastify": "^8.0.3", "styled-components": "^5.3.1", - "typescript": "^4.4.2" + "typescript": "^4.4.2", + "uuid": "^10.0.0" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^3.1.0", @@ -751,6 +752,15 @@ "node": ">= 0.12" } }, + "node_modules/@cypress/request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@cypress/xvfb": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", @@ -8333,10 +8343,13 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -9045,6 +9058,12 @@ "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true } } }, @@ -14491,10 +14510,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" }, "v8-compile-cache": { "version": "2.3.0", diff --git a/package.json b/package.json index 004e36d..5ce678c 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "react-qr-code": "^2.0.15", "react-toastify": "^8.0.3", "styled-components": "^5.3.1", - "typescript": "^4.4.2" + "typescript": "^4.4.2", + "uuid": "^10.0.0" }, "peerDependencies": { "next": "12.1.6", diff --git a/pages/registration.tsx b/pages/registration.tsx index a578cb4..cd2a7c8 100644 --- a/pages/registration.tsx +++ b/pages/registration.tsx @@ -4,6 +4,7 @@ import type { NextPage } from "next" import Head from "next/head" import { useRouter } from "next/router" import { useEffect, useRef, useState } from "react" +import { v4 as uuid } from "uuid" // Import render helpers import { ActionCard, CenterLink, Flow, MarginCard, CardTitle } from "../pkg" @@ -74,6 +75,7 @@ const Registration: NextPage = () => { const onSubmit = async (values: UpdateRegistrationFlowBody) => { const project = { id: projectId, + userId: uuid(), name: projectId, eligibility: JSON.parse(eligibility), } From 549a64f4f4e1be083b5a2d822cad61a8d5535a06 Mon Sep 17 00:00:00 2001 From: Pauline Date: Sat, 28 Sep 2024 21:42:17 +0100 Subject: [PATCH 10/37] Add sample health check endpoints --- pages/health/alive.ts | 5 +++++ pages/health/ready.ts | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 pages/health/alive.ts create mode 100644 pages/health/ready.ts diff --git a/pages/health/alive.ts b/pages/health/alive.ts new file mode 100644 index 0000000..a3a10f8 --- /dev/null +++ b/pages/health/alive.ts @@ -0,0 +1,5 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + return res.status(200).send("ok"); +} diff --git a/pages/health/ready.ts b/pages/health/ready.ts new file mode 100644 index 0000000..ad31511 --- /dev/null +++ b/pages/health/ready.ts @@ -0,0 +1,5 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + res.status(200).send("ok"); +} From f0a1423bec1d4200fa4d49c0fffb3aa9ab3041eb Mon Sep 17 00:00:00 2001 From: Pauline Date: Sat, 28 Sep 2024 21:42:47 +0100 Subject: [PATCH 11/37] Fix formatting --- pages/health/alive.ts | 4 ++-- pages/health/ready.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pages/health/alive.ts b/pages/health/alive.ts index a3a10f8..abf8045 100644 --- a/pages/health/alive.ts +++ b/pages/health/alive.ts @@ -1,5 +1,5 @@ -import { NextApiRequest, NextApiResponse } from "next"; +import { NextApiRequest, NextApiResponse } from "next" export default function handler(req: NextApiRequest, res: NextApiResponse) { - return res.status(200).send("ok"); + return res.status(200).send("ok") } diff --git a/pages/health/ready.ts b/pages/health/ready.ts index ad31511..138ccac 100644 --- a/pages/health/ready.ts +++ b/pages/health/ready.ts @@ -1,5 +1,5 @@ -import { NextApiRequest, NextApiResponse } from "next"; +import { NextApiRequest, NextApiResponse } from "next" export default function handler(req: NextApiRequest, res: NextApiResponse) { - res.status(200).send("ok"); + res.status(200).send("ok") } From 20e62f61d1cc86ee427e61f7d4c247d4f0613131 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 2 Oct 2024 18:29:34 +0100 Subject: [PATCH 12/37] Add support for custom base path and add health check --- .env.template | 5 +++++ next.config.js | 2 ++ package-lock.json | 13 +++++++++++++ package.json | 1 + pages/_app.tsx | 4 ++++ pages/health/alive.ts | 5 ----- pages/health/alive.tsx | 23 +++++++++++++++++++++++ pages/health/ready.ts | 5 ----- pages/health/ready.tsx | 21 +++++++++++++++++++++ pkg/sdk/index.ts | 2 +- pkg/styled/index.tsx | 31 ++++++++++++++++++------------- 11 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 .env.template delete mode 100644 pages/health/alive.ts create mode 100644 pages/health/alive.tsx delete mode 100644 pages/health/ready.ts create mode 100644 pages/health/ready.tsx diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..34c3e05 --- /dev/null +++ b/.env.template @@ -0,0 +1,5 @@ +ORY_SDK_URL=http://localhost:4433 +HYDRA_ADMIN_URL=http://localhost:4445 +HYDRA_PUBLIC_URL=http://localhost:4444 +BASE_PATH=/kratos-ui +NEXT_PUBLIC_KRATOS_PUBLIC_URL=http://localhost:3000/kratos-ui \ No newline at end of file diff --git a/next.config.js b/next.config.js index 8b61df4..3db7216 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,6 @@ /** @type {import('next').NextConfig} */ module.exports = { reactStrictMode: true, + basePath: process.env.BASE_PATH || "", + assetPrefix: process.env.BASE_PATH ?? "" + "/", } diff --git a/package-lock.json b/package-lock.json index 1981c49..89a10ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@types/react": "~17.0.19", "@types/request": "^2.48.7", "@types/styled-components": "^5.1.13", + "@types/uuid": "^10.0.0", "cypress": "^8.7.0", "eslint": "7.32.0", "eslint-config-next": "12.0.3", @@ -1613,6 +1614,12 @@ "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==", "dev": true }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, "node_modules/@types/yauzl": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", @@ -9619,6 +9626,12 @@ "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==", "dev": true }, + "@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, "@types/yauzl": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", diff --git a/package.json b/package.json index 5ce678c..2f9ad4d 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@types/react": "~17.0.19", "@types/request": "^2.48.7", "@types/styled-components": "^5.1.13", + "@types/uuid": "^10.0.0", "cypress": "^8.7.0", "eslint": "7.32.0", "eslint-config-next": "12.0.3", diff --git a/pages/_app.tsx b/pages/_app.tsx index d0dc0ac..aee98ce 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,6 +1,7 @@ import "../styles/globals.css" import { theme, globalStyles, ThemeProps } from "@ory/themes" import type { AppProps } from "next/app" +import Head from "next/head" import { ToastContainer } from "react-toastify" import "react-toastify/dist/ReactToastify.css" import { ThemeProvider } from "styled-components" @@ -13,6 +14,9 @@ const GlobalStyle = createGlobalStyle((props: ThemeProps) => function MyApp({ Component, pageProps }: AppProps) { return (
+ + + diff --git a/pages/health/alive.ts b/pages/health/alive.ts deleted file mode 100644 index abf8045..0000000 --- a/pages/health/alive.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next" - -export default function handler(req: NextApiRequest, res: NextApiResponse) { - return res.status(200).send("ok") -} diff --git a/pages/health/alive.tsx b/pages/health/alive.tsx new file mode 100644 index 0000000..e705016 --- /dev/null +++ b/pages/health/alive.tsx @@ -0,0 +1,23 @@ +import { ServerResponse } from "http" +import { GetServerSideProps } from "next" +import React from "react" + +// Use Node.js's ServerResponse + +const Alive = () => { + return
Healthy!
+} + +export const getServerSideProps: GetServerSideProps = async ({ + res, +}: { + res: ServerResponse +}) => { + res.statusCode = 200 + + return { + props: {}, + } +} + +export default Alive diff --git a/pages/health/ready.ts b/pages/health/ready.ts deleted file mode 100644 index 138ccac..0000000 --- a/pages/health/ready.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next" - -export default function handler(req: NextApiRequest, res: NextApiResponse) { - res.status(200).send("ok") -} diff --git a/pages/health/ready.tsx b/pages/health/ready.tsx new file mode 100644 index 0000000..4a95235 --- /dev/null +++ b/pages/health/ready.tsx @@ -0,0 +1,21 @@ +import { ServerResponse } from "http" +import { GetServerSideProps } from "next" +import React from "react" + +const Ready = () => { + return
Ready!
+} + +export const getServerSideProps: GetServerSideProps = async ({ + res, +}: { + res: ServerResponse +}) => { + res.statusCode = 200 + + return { + props: {}, + } +} + +export default Ready diff --git a/pkg/sdk/index.ts b/pkg/sdk/index.ts index 5d741aa..8ac0471 100644 --- a/pkg/sdk/index.ts +++ b/pkg/sdk/index.ts @@ -2,7 +2,7 @@ import { Configuration, FrontendApi } from "@ory/client" import { edgeConfig } from "@ory/integrations/next" const localConfig = { - basePath: process.env.NEXT_PUBLIC_KRATOS_PUBLIC_URL, + basePath: process.env.NEXT_PUBLIC_KRATOS_PUBLIC_URL ?? "" + "/api/.ory", baseOptions: { withCredentials: true, }, diff --git a/pkg/styled/index.tsx b/pkg/styled/index.tsx index b3afb70..2e2573e 100644 --- a/pkg/styled/index.tsx +++ b/pkg/styled/index.tsx @@ -8,6 +8,8 @@ import { import cn from "classnames" import styled from "styled-components" +const BASE_URL = process.env.BASE_PATH || "" + export const MarginCard = styled(Card)` margin-top: 70px; margin-bottom: 18px; @@ -106,17 +108,20 @@ export const DocsButton = ({ testid, disabled, unresponsive, -}: DocsButtonProps) => ( -
-
- - {title} - +}: DocsButtonProps) => { + const url = BASE_URL + href + return ( +
+
+ + {title} + +
-
-) + ) +} From 3548a828dcc9d3aab2858754dca8bfac2a6dc500 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 3 Oct 2024 16:34:19 +0100 Subject: [PATCH 13/37] Fix navigation to support base href --- .gitignore | 3 ++- pages/_app.tsx | 3 --- pages/index.tsx | 21 +++++++++++---------- pkg/sdk/index.ts | 4 ++-- pkg/styled/index.tsx | 5 +---- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 509ec38..de427c6 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ dist/ cypress/videos cypress/screenshots -.idea \ No newline at end of file +.idea +.env \ No newline at end of file diff --git a/pages/_app.tsx b/pages/_app.tsx index aee98ce..d4bb9e3 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -14,9 +14,6 @@ const GlobalStyle = createGlobalStyle((props: ThemeProps) => function MyApp({ Component, pageProps }: AppProps) { return (
- - - diff --git a/pages/index.tsx b/pages/index.tsx index 4269395..f9ba0d9 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -15,6 +15,7 @@ const Home: NextPage = () => { const [hasSession, setHasSession] = useState(false) const router = useRouter() const onLogout = LogoutLink() + const handleNavigation = (href: string) => () => router.push(href) useEffect(() => { ory @@ -60,62 +61,62 @@ const Home: NextPage = () => {
{ - const url = BASE_URL + href return (
@@ -117,7 +114,7 @@ export const DocsButton = ({ onClick={onClick} disabled={disabled} data-testid={testid} - href={url} + href={href} > {title} From 6546a7359f4bb2c4e9198735bbd395aef787061c Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 8 Oct 2024 21:37:58 +0800 Subject: [PATCH 14/37] Update consent component to use http requests directly and support custom base path --- pages/api/consent.ts | 49 ++++++++++++++++++++++---------------------- pages/consent.tsx | 13 ++++++------ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/pages/api/consent.ts b/pages/api/consent.ts index 7a43e60..925afb5 100644 --- a/pages/api/consent.ts +++ b/pages/api/consent.ts @@ -1,15 +1,7 @@ -import { Configuration, OAuth2Api } from "@ory/client" import { NextApiRequest, NextApiResponse } from "next" +import axios from "axios" // Using axios for HTTP requests -const hydra = new OAuth2Api( - new Configuration({ - basePath: process.env.HYDRA_ADMIN_URL, - baseOptions: { - "X-Forwarded-Proto": "https", - withCredentials: true, - }, - }), -) +const baseURL = process.env.HYDRA_ADMIN_URL // Helper function to extract session data const extractSession = (identity: any, grantScope: string[]) => { @@ -33,40 +25,49 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { try { if (req.method === "GET") { const { consent_challenge } = req.query - const response = await hydra.getOAuth2ConsentRequest({ - consentChallenge: String(consent_challenge), - }) + const response = await axios.get( + `${baseURL}/oauth2/auth/requests/consent`, + { + params: { + consent_challenge: String(consent_challenge), + }, + }, + ) return res.status(200).json(response.data) } else { if (!consentChallenge || !consentAction) { return res.status(400).json({ error: "Missing required parameters" }) } if (consentAction === "accept") { - const { data: body } = await hydra.getOAuth2ConsentRequest({ - consentChallenge, - }) + const { data: body } = await axios.get( + `${baseURL}/oauth2/auth/requests/consent`, + { + params: { consent_challenge: consentChallenge }, + }, + ) + const session = extractSession(identity, grantScope) - const acceptResponse = await hydra.acceptOAuth2ConsentRequest({ - consentChallenge, - acceptOAuth2ConsentRequest: { + const acceptResponse = await axios.put( + `${baseURL}/oauth2/auth/requests/consent/accept?consent_challenge=${consentChallenge}`, + { grant_scope: session.access_token.scope, grant_access_token_audience: body.requested_access_token_audience, session, remember: Boolean(remember), remember_for: 3600, }, - }) + ) return res .status(200) .json({ redirect_to: acceptResponse.data.redirect_to }) } else { - const rejectResponse = await hydra.rejectOAuth2ConsentRequest({ - consentChallenge, - rejectOAuth2Request: { + const rejectResponse = await axios.put( + `${baseURL}/oauth2/auth/requests/consent/${consentChallenge}/reject`, + { error: "access_denied", error_description: "The resource owner denied the request", }, - }) + ) return res .status(200) diff --git a/pages/consent.tsx b/pages/consent.tsx index f13d225..f4d25d0 100644 --- a/pages/consent.tsx +++ b/pages/consent.tsx @@ -11,6 +11,8 @@ const Consent = () => { const [csrfToken, setCsrfToken] = useState("") const [isLoading, setIsLoading] = useState(false) + const basePath = process.env.BASE_PATH || "" + useEffect(() => { const { consent_challenge } = router.query @@ -27,7 +29,7 @@ const Consent = () => { } const consentResponse = await fetch( - `/api/consent?consent_challenge=${consent_challenge}`, + `${basePath}/api/consent?consent_challenge=${consent_challenge}`, ) const consentData = await consentResponse.json() @@ -40,7 +42,7 @@ const Consent = () => { // Automatically handle skipping consent if enabled if (consentData.client?.skip_consent) { console.log("Skipping consent, automatically submitting.") - const skipResponse = await fetch("/api/consent", { + const skipResponse = await fetch(`${basePath}/api/consent`, { method: "POST", headers: { "Content-Type": "application/json", @@ -58,7 +60,7 @@ const Consent = () => { if (skipData.error) { throw new Error(skipData.error) } - router.push(skipData.redirect_to) + window.location.href = skipData.redirect_to return } } catch (error) { @@ -90,7 +92,7 @@ const Consent = () => { } try { - const response = await fetch("/api/consent", { + const response = await fetch(`${basePath}/api/consent`, { method: "POST", headers: { "Content-Type": "application/json", @@ -109,8 +111,7 @@ const Consent = () => { console.error("Error submitting consent:", data.error) return } - - router.push(data.redirect_to) + window.location.href = data.redirect_to } catch (error) { console.error("Error during consent submission:", error) } From fb905dcae67bacb154b2636d863c194e15ac86e7 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 9 Oct 2024 00:29:25 +0800 Subject: [PATCH 15/37] Fix base path in consent --- pages/consent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/consent.tsx b/pages/consent.tsx index f4d25d0..25f57dd 100644 --- a/pages/consent.tsx +++ b/pages/consent.tsx @@ -11,7 +11,7 @@ const Consent = () => { const [csrfToken, setCsrfToken] = useState("") const [isLoading, setIsLoading] = useState(false) - const basePath = process.env.BASE_PATH || "" + const basePath = process.env.BASE_PATH || "/kratos-ui" useEffect(() => { const { consent_challenge } = router.query From 26f9dc2f22a595996169b6f7145308f992fb1719 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 9 Oct 2024 00:35:56 +0800 Subject: [PATCH 16/37] Update cypress baseurl --- cypress.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress.json b/cypress.json index 17ef242..00989e0 100644 --- a/cypress.json +++ b/cypress.json @@ -1,3 +1,3 @@ { - "baseUrl": "http://localhost:3000" + "baseUrl": "http://localhost:3000/kratos-ui" } From 5268450353fe5b9b399b4c43860d4f406fed1316 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 9 Oct 2024 00:38:56 +0800 Subject: [PATCH 17/37] Fix lint errors --- cypress.json | 2 +- pages/api/consent.ts | 4 ++-- pkg/sdk/index.ts | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cypress.json b/cypress.json index 00989e0..91aab53 100644 --- a/cypress.json +++ b/cypress.json @@ -1,3 +1,3 @@ { - "baseUrl": "http://localhost:3000/kratos-ui" + "baseUrl": "http://localhost:3000/kratos-ui" } diff --git a/pages/api/consent.ts b/pages/api/consent.ts index 925afb5..13c6ad7 100644 --- a/pages/api/consent.ts +++ b/pages/api/consent.ts @@ -1,7 +1,7 @@ +import axios from "axios" import { NextApiRequest, NextApiResponse } from "next" -import axios from "axios" // Using axios for HTTP requests -const baseURL = process.env.HYDRA_ADMIN_URL +const baseURL = process.env.HYDRA_ADMIN_URL // Helper function to extract session data const extractSession = (identity: any, grantScope: string[]) => { diff --git a/pkg/sdk/index.ts b/pkg/sdk/index.ts index 365dd10..1436b0b 100644 --- a/pkg/sdk/index.ts +++ b/pkg/sdk/index.ts @@ -8,8 +8,4 @@ const localConfig = { }, } -export default new FrontendApi( - new Configuration( - localConfig, - ), -) +export default new FrontendApi(new Configuration(localConfig)) From b8e4e7a18b021087613bf386c3c5df50f66377bb Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 9 Oct 2024 00:46:35 +0800 Subject: [PATCH 18/37] Update cypress baseurl --- cypress.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress.json b/cypress.json index 91aab53..17ef242 100644 --- a/cypress.json +++ b/cypress.json @@ -1,3 +1,3 @@ { - "baseUrl": "http://localhost:3000/kratos-ui" + "baseUrl": "http://localhost:3000" } From c9040f87745c681894d67eda7f230ed9daedf26c Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 11 Oct 2024 22:32:57 +0800 Subject: [PATCH 19/37] Fix base path vars and Dockerfile --- .dockerignore | 37 +++++++++++++++++++++++++++++++++---- Dockerfile | 10 +++++++--- docker/30-env-subst.sh | 19 +++++++++++++++++++ env.js | 3 +++ next.config.js | 13 ++++++++++--- pkg/sdk/index.ts | 6 +++++- 6 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 docker/30-env-subst.sh create mode 100644 env.js diff --git a/.dockerignore b/.dockerignore index 7a38b3f..125256f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,33 @@ -node_modules/ -.github/ -.idea/ -.vscode/ \ No newline at end of file +# Node modules +node_modules +npm-debug.log + +# Build artifacts +.next +out + +# Local environment files +.env + +# Logs +logs +*.log + +# IDE and editor files +.vscode +.idea +*.swp +*.swo + +# Operating system files +.DS_Store +Thumbs.db + +# Git files +.git +.gitignore + +# Other files you may want to ignore +coverage +test-results +*.tgz diff --git a/Dockerfile b/Dockerfile index acb5693..861cb50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,13 +9,17 @@ ENV NPM_CONFIG_CACHE=/home/node/.npm COPY package*.json ./ -RUN mkdir -p $NPM_CONFIG_CACHE && chown -R node:node $NPM_CONFIG_CACHE - RUN npm install COPY . . -RUN npm run build +RUN npm run build + +# Copy the env substitution script and ensure it is executable +COPY docker/30-env-subst.sh /docker-entrypoint.d/30-env-subst.sh +RUN chmod +x /docker-entrypoint.d/30-env-subst.sh + +ENTRYPOINT ["/bin/sh", "-c", "/docker-entrypoint.d/30-env-subst.sh && exec \"$@\"", "--"] EXPOSE 3000 diff --git a/docker/30-env-subst.sh b/docker/30-env-subst.sh new file mode 100644 index 0000000..0133a1f --- /dev/null +++ b/docker/30-env-subst.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# Log that the script is running +echo "Running docker-entrypoint.sh." + +# Replace placeholder with actual environment variable values +if [ -n "$BASE_PATH" ]; then + echo "Replacing BASE_PATH in env.template.js with ${BASE_PATH}" + sed -i "s|/__BASE_PATH__|${BASE_PATH}|g" /app/env.js +else + echo "BASE_PATH is not set." +fi + +# Log the contents of env.template.js after modification +echo "Contents of env.template.js after replacement:" +cat /app/env.js + +# Start the application +echo "Starting the application..." \ No newline at end of file diff --git a/env.js b/env.js new file mode 100644 index 0000000..a3418d1 --- /dev/null +++ b/env.js @@ -0,0 +1,3 @@ +module.exports = { + basePath: "/kratos-ui" +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 3db7216..a26fe7e 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,13 @@ -/** @type {import('next').NextConfig} */ +const envConfig = require("./env.js") // Load the generated config + module.exports = { reactStrictMode: true, - basePath: process.env.BASE_PATH || "", - assetPrefix: process.env.BASE_PATH ?? "" + "/", + + // Set basePath and assetPrefix dynamically + basePath: envConfig.basePath || "", + assetPrefix: `${envConfig.basePath}/` || "", + + publicRuntimeConfig: { + basePath: envConfig.basePath || "", + }, } diff --git a/pkg/sdk/index.ts b/pkg/sdk/index.ts index 1436b0b..ac5dab2 100644 --- a/pkg/sdk/index.ts +++ b/pkg/sdk/index.ts @@ -1,8 +1,12 @@ import { Configuration, FrontendApi } from "@ory/client" import { edgeConfig } from "@ory/integrations/next" +import getConfig from "next/config" + +const { publicRuntimeConfig } = getConfig() +const { basePath } = publicRuntimeConfig const localConfig = { - basePath: `${process.env.NEXT_PUBLIC_KRATOS_PUBLIC_URL}/api/.ory`, + basePath: `${basePath}/api/.ory`, baseOptions: { withCredentials: true, }, From 7d359ce525e107f0c5e6b9632b95bb46d9a4df16 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 11 Oct 2024 22:35:41 +0800 Subject: [PATCH 20/37] Fix formatting --- env.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/env.js b/env.js index a3418d1..39caac0 100644 --- a/env.js +++ b/env.js @@ -1,3 +1,3 @@ module.exports = { - basePath: "/kratos-ui" -} \ No newline at end of file + basePath: "/kratos-ui", +} From e51d271d01c59cc51df6a6dc6e8ccfcff9f84f5a Mon Sep 17 00:00:00 2001 From: Pauline Date: Sat, 12 Oct 2024 01:06:08 +0800 Subject: [PATCH 21/37] Update cypress config --- cypress.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress.json b/cypress.json index 17ef242..91aab53 100644 --- a/cypress.json +++ b/cypress.json @@ -1,3 +1,3 @@ { - "baseUrl": "http://localhost:3000" + "baseUrl": "http://localhost:3000/kratos-ui" } From c896c88c130d8e391c8217f5af8b480a8af74922 Mon Sep 17 00:00:00 2001 From: Pauline Date: Sun, 13 Oct 2024 03:53:28 +0800 Subject: [PATCH 22/37] Fix rest source client configs --- services/rest-source-client.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/services/rest-source-client.ts b/services/rest-source-client.ts index fd86e2a..999ff23 100644 --- a/services/rest-source-client.ts +++ b/services/rest-source-client.ts @@ -1,12 +1,15 @@ +import getConfig from "next/config" + +const { publicRuntimeConfig } = getConfig() + export class RestSourceClient { - private readonly AUTH_BASE_URL = "http://localhost:4444/oauth2" + private readonly AUTH_BASE_URL = `${publicRuntimeConfig.hydraPublicUrl}/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" + private readonly CLIENT_ID = `${publicRuntimeConfig.frontEndClientId}` + private readonly CLIENT_SECRET = `${publicRuntimeConfig.frontEndClientSecret}` + private readonly REGISTRATION_ENDPOINT = `${publicRuntimeConfig.restSourceRegistrationEndpoint}/registrations` + private readonly USER_ENDPOINT = `${publicRuntimeConfig.restSourceRegistrationEndpoint}/users` + private readonly FRONTEND_ENDPOINT = `${publicRuntimeConfig.restSourceFrontendEndpoint}` async getAccessToken( code: string, From b4d2ef2fd3dcc6854140baa1dd7ec00911412102 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 14 Oct 2024 16:01:47 +0800 Subject: [PATCH 23/37] Fix rest source client: create user before registering --- pages/fitbit.tsx | 23 +++---- services/rest-source-client.ts | 117 ++++++++++++++++++++++++--------- 2 files changed, 99 insertions(+), 41 deletions(-) diff --git a/pages/fitbit.tsx b/pages/fitbit.tsx index 50d692f..a8d1ffc 100644 --- a/pages/fitbit.tsx +++ b/pages/fitbit.tsx @@ -35,27 +35,28 @@ const Fitbit: NextPage = () => { return restSourceClient.redirectToAuthRequestLink() } + useEffect(() => { + 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) return + if (!router.isReady || !projects.length) return const token = await restSourceClient.getAccessTokenFromRedirect() if (token) { localStorage.setItem("access_token", token) - await restSourceClient.redirectToRestSourceAuthLink(token) + await restSourceClient.redirectToRestSourceAuthLink(token, projects[0]) } } handleToken() - }, [router.isReady]) - - useEffect(() => { - ory.toSession().then(({ data }) => { - const traits = data?.identity?.traits - setTraits(traits) - setProjects(traits.projects) - }) - }, [flowId, router, router.isReady, returnTo]) + }, [router.isReady, projects]) return ( <> diff --git a/services/rest-source-client.ts b/services/rest-source-client.ts index 999ff23..55a8c8a 100644 --- a/services/rest-source-client.ts +++ b/services/rest-source-client.ts @@ -7,9 +7,45 @@ export class RestSourceClient { private readonly GRANT_TYPE = "authorization_code" private readonly CLIENT_ID = `${publicRuntimeConfig.frontEndClientId}` private readonly CLIENT_SECRET = `${publicRuntimeConfig.frontEndClientSecret}` - private readonly REGISTRATION_ENDPOINT = `${publicRuntimeConfig.restSourceRegistrationEndpoint}/registrations` - private readonly USER_ENDPOINT = `${publicRuntimeConfig.restSourceRegistrationEndpoint}/users` + private readonly REGISTRATION_ENDPOINT = `${publicRuntimeConfig.restSourceBackendEndpoint}/registrations` + private readonly USER_ENDPOINT = `${publicRuntimeConfig.restSourceBackendEndpoint}/users` private readonly FRONTEND_ENDPOINT = `${publicRuntimeConfig.restSourceFrontendEndpoint}` + private readonly SOURCE_TYPE = "Oura" + + async getRestSourceUser(accessToken: string, project: any): Promise { + try { + const response = await fetch(this.USER_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + userId: project.userId, + projectId: project.id, + sourceType: this.SOURCE_TYPE, + startDate: new Date().toISOString(), + }), + }) + + if (!response.ok) { + const data = await response.json() + if (response.status === 409 && data.user) { + console.warn("User already exists:", data.message) + return data.user.id + } else { + throw new Error(`Failed to create user: ${data.message || response.statusText}`) + } + } + + const userDto = await response.json() + return userDto.id + + } catch (error) { + console.error(error) + return null + } + } async getAccessToken( code: string, @@ -23,14 +59,23 @@ export class RestSourceClient { 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, - }) + 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.access_token || null + const data = await response.json() + return data.access_token || null + } catch (error) { + console.error(error) + return null + } } async getAccessTokenFromRedirect(): Promise { @@ -60,32 +105,44 @@ export class RestSourceClient { window.location.href = authUrl } - // Make a POST request to the registration endpoint to retrieve the authorization link - async getRestSourceAuthLink(accessToken: string): Promise { - const response = await fetch(this.REGISTRATION_ENDPOINT, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - userId: "4", - persistent: true, - }), - }) + async getRestSourceAuthLink(accessToken: string, project: any): Promise { + try { + const userId = await this.getRestSourceUser(accessToken, project) + if (!userId) { + throw new Error("Failed to retrieve or create user") + } + + const response = await fetch(this.REGISTRATION_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + userId, + persistent: false, + }), + }) - const data = await response.json() - if (!data.token || !data.secret) { - console.error("Failed to retrieve auth link") + if (!response.ok) { + throw new Error(`Failed to retrieve registration token: ${response.statusText}`) + } + + const data = await response.json() + if (!data.token || !data.secret) { + throw new Error("Failed to retrieve auth link") + } + + return `${this.FRONTEND_ENDPOINT}/users:auth?token=${data.token}&secret=${data.secret}` + + } catch (error) { + console.error(error) 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 { - const url = await this.getRestSourceAuthLink(accessToken) + async redirectToRestSourceAuthLink(accessToken: string, project: any): Promise { + const url = await this.getRestSourceAuthLink(accessToken, project) if (url) { console.log("Redirecting to: ", url) window.location.href = url From ae18d88624ba0b679490f2a40bb16c6942a96375 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 14 Oct 2024 16:04:31 +0800 Subject: [PATCH 24/37] Add separate oauth2 login --- pages/api/login.ts | 35 ++++++++++++++++ pages/oauth2-login.tsx | 94 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 pages/api/login.ts create mode 100644 pages/oauth2-login.tsx diff --git a/pages/api/login.ts b/pages/api/login.ts new file mode 100644 index 0000000..bf97243 --- /dev/null +++ b/pages/api/login.ts @@ -0,0 +1,35 @@ +import axios from "axios" +import type { NextApiRequest, NextApiResponse } from "next" + +const baseURL = process.env.HYDRA_ADMIN_URL + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method === "POST") { + const { loginChallenge, subject, remember } = req.body + + try { + const response = await axios.put( + `${baseURL}/oauth2/auth/requests/login/accept?login_challenge=${loginChallenge}`, + { + subject, + remember, + }, + ) + + res.status(200).json({ redirect_to: response.data.redirect_to }) + } catch (error) { + console.error("Error in API handler:", error) + if (axios.isAxiosError(error) && error.response) { + res.status(error.response.status).json({ message: error.response.data }) + } else { + res.status(500).json({ message: "Internal Server Error" }) + } + } + } else { + res.setHeader("Allow", ["POST"]) + res.status(405).end(`Method ${req.method} Not Allowed`) + } +} diff --git a/pages/oauth2-login.tsx b/pages/oauth2-login.tsx new file mode 100644 index 0000000..7303bbe --- /dev/null +++ b/pages/oauth2-login.tsx @@ -0,0 +1,94 @@ +import axios from "axios" +import Head from "next/head" +import Link from "next/link" +import { useRouter } from "next/router" +import { useEffect, useState } from "react" + +import ory from "../pkg/sdk" +import { MarginCard } from "../pkg" + +const OAuth2Login = () => { + const router = useRouter() + const [challenge, setChallenge] = useState(null) + const [error, setError] = useState(null) + const [traits, setTraits] = useState() + const [projects, setProjects] = useState([]) + + const basePath = process.env.BASE_PATH || "/kratos-ui" + + useEffect(() => { + ory.toSession().then(({ data }) => { + const traits = data?.identity?.traits + setTraits(traits) + setProjects(traits.projects) // + }) + }, [router, router.isReady]) + + useEffect(() => { + // Get the login challenge from the query parameters + const { login_challenge } = router.query + if (login_challenge) { + setChallenge(String(login_challenge)) + } + }, [router.query]) + + const handleLogin = async () => { + if (!challenge) { + setError("No login challenge found.") + return + } + + try { + const id = projects[0].userId + const response = await fetch(`${basePath}/api/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + loginChallenge: challenge, + subject: id, + remember: true, + }), + }) + + const data = await response.json() + window.location.href = data.redirect_to + } catch (err) { + console.error("Error during login:", err) + setError("Login failed. Please try again.") + } + } + + if (!challenge) { + return ( +
+ + OAuth2 Login + + +

OAuth2 Login

+

Waiting for login challenge...

+
+
+ ) + } + + return ( +
+ + OAuth2 Login + + +

OAuth2 Login

+ {error &&

{error}

} +

To continue, please log in.

+ +
+ Cancel +
+
+ ) +} + +export default OAuth2Login From c364a4a9cfcc4d03ab0fd6f0cb5fb00f293c0bbe Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 14 Oct 2024 16:09:45 +0800 Subject: [PATCH 25/37] Fix formatting --- pages/fitbit.tsx | 1 - services/rest-source-client.ts | 29 +++++++++++++++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pages/fitbit.tsx b/pages/fitbit.tsx index a8d1ffc..a3c27dc 100644 --- a/pages/fitbit.tsx +++ b/pages/fitbit.tsx @@ -40,7 +40,6 @@ const Fitbit: NextPage = () => { const traits = data?.identity?.traits setTraits(traits) setProjects(traits.projects) // - }) }, [flowId, router, router.isReady, returnTo]) diff --git a/services/rest-source-client.ts b/services/rest-source-client.ts index 55a8c8a..4950c5a 100644 --- a/services/rest-source-client.ts +++ b/services/rest-source-client.ts @@ -12,7 +12,10 @@ export class RestSourceClient { private readonly FRONTEND_ENDPOINT = `${publicRuntimeConfig.restSourceFrontendEndpoint}` private readonly SOURCE_TYPE = "Oura" - async getRestSourceUser(accessToken: string, project: any): Promise { + async getRestSourceUser( + accessToken: string, + project: any, + ): Promise { try { const response = await fetch(this.USER_ENDPOINT, { method: "POST", @@ -34,13 +37,14 @@ export class RestSourceClient { console.warn("User already exists:", data.message) return data.user.id } else { - throw new Error(`Failed to create user: ${data.message || response.statusText}`) + throw new Error( + `Failed to create user: ${data.message || response.statusText}`, + ) } } const userDto = await response.json() return userDto.id - } catch (error) { console.error(error) return null @@ -67,7 +71,9 @@ export class RestSourceClient { }) if (!response.ok) { - throw new Error(`Failed to retrieve access token: ${response.statusText}`) + throw new Error( + `Failed to retrieve access token: ${response.statusText}`, + ) } const data = await response.json() @@ -105,7 +111,10 @@ export class RestSourceClient { window.location.href = authUrl } - async getRestSourceAuthLink(accessToken: string, project: any): Promise { + async getRestSourceAuthLink( + accessToken: string, + project: any, + ): Promise { try { const userId = await this.getRestSourceUser(accessToken, project) if (!userId) { @@ -125,7 +134,9 @@ export class RestSourceClient { }) if (!response.ok) { - throw new Error(`Failed to retrieve registration token: ${response.statusText}`) + throw new Error( + `Failed to retrieve registration token: ${response.statusText}`, + ) } const data = await response.json() @@ -134,14 +145,16 @@ export class RestSourceClient { } return `${this.FRONTEND_ENDPOINT}/users:auth?token=${data.token}&secret=${data.secret}` - } catch (error) { console.error(error) return null } } - async redirectToRestSourceAuthLink(accessToken: string, project: any): Promise { + async redirectToRestSourceAuthLink( + accessToken: string, + project: any, + ): Promise { const url = await this.getRestSourceAuthLink(accessToken, project) if (url) { console.log("Redirecting to: ", url) From 2a69e873b85f02cb40d3ec9cc6ba1b45dad8f0d8 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 14 Oct 2024 23:27:44 +0800 Subject: [PATCH 26/37] Fix Oauth login redirect and Fitbit token --- pages/fitbit.tsx | 19 ++++++++++++--- pages/oauth2-login.tsx | 53 +++++++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/pages/fitbit.tsx b/pages/fitbit.tsx index a3c27dc..9d5b180 100644 --- a/pages/fitbit.tsx +++ b/pages/fitbit.tsx @@ -30,6 +30,7 @@ const Fitbit: NextPage = () => { const { flow: flowId, return_to: returnTo } = router.query const [traits, setTraits] = useState() const [projects, setProjects] = useState([]) + const [tokenHandled, setTokenHandled] = useState(false) // Flag to ensure token is handled once const handleNavigation = () => { return restSourceClient.redirectToAuthRequestLink() @@ -39,23 +40,35 @@ const Fitbit: NextPage = () => { ory.toSession().then(({ data }) => { const traits = data?.identity?.traits setTraits(traits) - setProjects(traits.projects) // + setProjects(traits.projects) }) }, [flowId, router, router.isReady, returnTo]) useEffect(() => { const handleToken = async () => { - if (!router.isReady || !projects.length) return + if (!router.isReady || !projects.length || tokenHandled) return + + const existingToken = localStorage.getItem("access_token") + + if (existingToken) { + await restSourceClient.redirectToRestSourceAuthLink( + existingToken, + projects[0], + ) + setTokenHandled(true) + return + } const token = await restSourceClient.getAccessTokenFromRedirect() if (token) { localStorage.setItem("access_token", token) await restSourceClient.redirectToRestSourceAuthLink(token, projects[0]) + setTokenHandled(true) } } handleToken() - }, [router.isReady, projects]) + }, [router.isReady, projects, tokenHandled]) return ( <> diff --git a/pages/oauth2-login.tsx b/pages/oauth2-login.tsx index 7303bbe..de11d7e 100644 --- a/pages/oauth2-login.tsx +++ b/pages/oauth2-login.tsx @@ -4,8 +4,8 @@ import Link from "next/link" import { useRouter } from "next/router" import { useEffect, useState } from "react" -import ory from "../pkg/sdk" import { MarginCard } from "../pkg" +import ory from "../pkg/sdk" const OAuth2Login = () => { const router = useRouter() @@ -13,23 +13,44 @@ const OAuth2Login = () => { const [error, setError] = useState(null) const [traits, setTraits] = useState() const [projects, setProjects] = useState([]) + const [redirect, setRedirect] = useState(null) const basePath = process.env.BASE_PATH || "/kratos-ui" useEffect(() => { - ory.toSession().then(({ data }) => { - const traits = data?.identity?.traits - setTraits(traits) - setProjects(traits.projects) // - }) - }, [router, router.isReady]) + const checkSession = async () => { + try { + const { data } = await ory.toSession() + const traits = data?.identity?.traits + setTraits(traits) + setProjects(traits.projects) + + if (!traits || !traits.projects || traits.projects.length === 0) { + console.log(redirect) + const currentUrl = window.location.href // Get the current page URL for return_to + router.push(`/login?return_to=${redirect}`) + return + } + } catch (error) { + console.error("Error fetching session:", error) + // Handle session fetch error, possibly redirect to login + router.push("/login") + } + } + + checkSession() + }, [router]) useEffect(() => { // Get the login challenge from the query parameters - const { login_challenge } = router.query + const { login_challenge, redirect_to } = router.query if (login_challenge) { setChallenge(String(login_challenge)) } + if (redirect_to) { + console.log(redirect_to) + setRedirect(redirect_to) + } }, [router.query]) const handleLogin = async () => { @@ -67,8 +88,8 @@ const OAuth2Login = () => { OAuth2 Login -

OAuth2 Login

-

Waiting for login challenge...

+

OAuth2 Login

+

Waiting for login challenge...

) @@ -80,12 +101,12 @@ const OAuth2Login = () => { OAuth2 Login -

OAuth2 Login

- {error &&

{error}

} -

To continue, please log in.

- -
- Cancel +

OAuth2 Login

+ {error &&

{error}

} +

To continue, please log in.

+ +
+ Cancel
) From 4f4de501d2ed1b2a6e86bb1fc66a49b44c07417f Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 15 Oct 2024 00:24:53 +0800 Subject: [PATCH 27/37] Fix env vars --- env.js | 3 +++ next.config.js | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/env.js b/env.js index 39caac0..44a6c76 100644 --- a/env.js +++ b/env.js @@ -1,3 +1,6 @@ module.exports = { basePath: "/kratos-ui", + restSourceBackendEndpoint: "", + restSourceFrontendEndpoint:"", + hydraPublicUrl: "", } diff --git a/next.config.js b/next.config.js index a26fe7e..b9f1dd7 100644 --- a/next.config.js +++ b/next.config.js @@ -6,8 +6,16 @@ module.exports = { // Set basePath and assetPrefix dynamically basePath: envConfig.basePath || "", assetPrefix: `${envConfig.basePath}/` || "", + restSourceBackendEndpoint: `${envConfig.restSourceBackendEndpoint}/` || "", + restSourceFrontendEndpoint: `${envConfig.restSourceFrontendEndpoint}/` || "", + hydraPublicUrl: `${envConfig.hydraPublicUrl}/` || "", publicRuntimeConfig: { basePath: envConfig.basePath || "", + frontEndClientId: "SEP", + frontEndClientSecret: "secret", + restSourceBackendEndpoint: envConfig.restSourceBackendEndpoint, + restSourceFrontendEndpoint: envConfig.restSourceFrontendEndpoint, + hydraPublicUrl: envConfig.hydraPublicUrl, }, } From bdbd90f6bd06e7a680ac4957a6f31296de4dfbcc Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 16 Oct 2024 00:03:15 +0800 Subject: [PATCH 28/37] Fix Oauth login component --- pages/login.tsx | 13 +++----- pages/oauth2-login.tsx | 70 +++++++++++++++--------------------------- 2 files changed, 30 insertions(+), 53 deletions(-) diff --git a/pages/login.tsx b/pages/login.tsx index 512db40..963a00d 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -80,8 +80,6 @@ const Login: NextPage = () => { const onSubmit = (values: UpdateLoginFlowBody) => router - // On submission, add the flow ID to the URL but do not navigate. This prevents the user loosing - // his data when she/he reloads the page. .push(`/login?flow=${flow?.id}`, undefined, { shallow: true }) .then(() => ory @@ -89,20 +87,19 @@ const Login: NextPage = () => { flow: String(flow?.id), updateLoginFlowBody: values, }) - // We logged in successfully! Let's bring the user home. .then(() => { if (flow?.return_to) { - window.location.href = flow?.return_to - return + window.location.href = flow.return_to + } else if (loginChallenge) { + window.location.href = `/oauth2/auth/requests/login?login_challenge=${loginChallenge}` + } else { + router.push("/") } - router.push("/") }) .then(() => {}) .catch(handleFlowError(router, "login", setFlow)) .catch((err: AxiosError) => { - // If the previous handler did not catch the error it's most likely a form validation error if (err.response?.status === 400) { - // Yup, it is! setFlow(err.response?.data as LoginFlow) return } diff --git a/pages/oauth2-login.tsx b/pages/oauth2-login.tsx index de11d7e..7fdd9c5 100644 --- a/pages/oauth2-login.tsx +++ b/pages/oauth2-login.tsx @@ -11,56 +11,47 @@ const OAuth2Login = () => { const router = useRouter() const [challenge, setChallenge] = useState(null) const [error, setError] = useState(null) - const [traits, setTraits] = useState() + const [traits, setTraits] = useState(null) const [projects, setProjects] = useState([]) - const [redirect, setRedirect] = useState(null) + const [id, setId] = useState(null) const basePath = process.env.BASE_PATH || "/kratos-ui" useEffect(() => { const checkSession = async () => { try { + // Check if a valid Ory Kratos session exists + const { login_challenge } = router.query const { data } = await ory.toSession() const traits = data?.identity?.traits + const projects = traits?.projects + const id = data?.identity?.id + setId(data?.identity?.id) setTraits(traits) - setProjects(traits.projects) + setProjects(traits?.projects) + setChallenge(String(login_challenge)) - if (!traits || !traits.projects || traits.projects.length === 0) { - console.log(redirect) - const currentUrl = window.location.href // Get the current page URL for return_to - router.push(`/login?return_to=${redirect}`) - return + if (traits && login_challenge) { + const subject = projects && projects[0] ? projects[0].userId : id + handleLogin(subject, login_challenge) } } catch (error) { console.error("Error fetching session:", error) - // Handle session fetch error, possibly redirect to login - router.push("/login") + const { login_challenge } = router.query + if (login_challenge) { + router.push(`/login?login_challenge=${login_challenge}`) + } } } - checkSession() - }, [router]) - - useEffect(() => { - // Get the login challenge from the query parameters - const { login_challenge, redirect_to } = router.query - if (login_challenge) { - setChallenge(String(login_challenge)) - } - if (redirect_to) { - console.log(redirect_to) - setRedirect(redirect_to) - } - }, [router.query]) - - const handleLogin = async () => { if (!challenge) { - setError("No login challenge found.") - return + checkSession() } + }, [router]) + const handleLogin = async (subject: any, challenge: any) => { try { - const id = projects[0].userId + if (!subject || !challenge) throw Error("Subject cannot be null") const response = await fetch(`${basePath}/api/login`, { method: "POST", headers: { @@ -68,7 +59,7 @@ const OAuth2Login = () => { }, body: JSON.stringify({ loginChallenge: challenge, - subject: id, + subject: subject, remember: true, }), }) @@ -81,19 +72,7 @@ const OAuth2Login = () => { } } - if (!challenge) { - return ( -
- - OAuth2 Login - - -

OAuth2 Login

-

Waiting for login challenge...

-
-
- ) - } + const isLoginReady = traits return (
@@ -104,9 +83,10 @@ const OAuth2Login = () => {

OAuth2 Login

{error &&

{error}

}

To continue, please log in.

- +
- Cancel
) From b67ccd969bbc06c2d08350469aa429c9c5755d49 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 16 Oct 2024 00:11:34 +0800 Subject: [PATCH 29/37] Update titles --- pages/apps.tsx | 2 +- pages/fitbit.tsx | 2 +- pages/index.tsx | 6 +++--- pages/login.tsx | 4 ++-- pages/profile.tsx | 2 +- pages/recovery.tsx | 4 ++-- pages/settings.tsx | 2 +- pages/study-consent.tsx | 2 +- pages/verification.tsx | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pages/apps.tsx b/pages/apps.tsx index 21708a9..d40c3de 100644 --- a/pages/apps.tsx +++ b/pages/apps.tsx @@ -52,7 +52,7 @@ const Apps: NextPage = () => { <> App Login - + App Login diff --git a/pages/fitbit.tsx b/pages/fitbit.tsx index 9d5b180..bc5cf96 100644 --- a/pages/fitbit.tsx +++ b/pages/fitbit.tsx @@ -74,7 +74,7 @@ const Fitbit: NextPage = () => { <> App Login - + App Login diff --git a/pages/index.tsx b/pages/index.tsx index f9ba0d9..2ba2956 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -47,12 +47,12 @@ const Home: NextPage = () => { return (
- Ory NextJS Integration Example - + RADAR Base + - RADAR Base Ory! + RADAR Base Self-Enrolment Portal

Welcome to the RADAR Base self-enrolment portal.

diff --git a/pages/login.tsx b/pages/login.tsx index 963a00d..f85b75e 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -111,8 +111,8 @@ const Login: NextPage = () => { return ( <> - Sign in - Ory NextJS Integration Example - + Sign in - RADAR Base + diff --git a/pages/profile.tsx b/pages/profile.tsx index 58e0e8a..cce06d1 100644 --- a/pages/profile.tsx +++ b/pages/profile.tsx @@ -148,7 +148,7 @@ const Profile: NextPage = () => { <> Profile Page - + User Information diff --git a/pages/recovery.tsx b/pages/recovery.tsx index cfdad09..0712bb9 100644 --- a/pages/recovery.tsx +++ b/pages/recovery.tsx @@ -86,8 +86,8 @@ const Recovery: NextPage = () => { return ( <> - Recover your account - Ory NextJS Integration Example - + Recover your account - RADAR Base + Recover your account diff --git a/pages/settings.tsx b/pages/settings.tsx index 3aa327d..65c588d 100644 --- a/pages/settings.tsx +++ b/pages/settings.tsx @@ -147,7 +147,7 @@ const Settings: NextPage = () => { Profile Management and Security Settings - Ory NextJS Integration Example - + Profile Management and Security Settings diff --git a/pages/study-consent.tsx b/pages/study-consent.tsx index 9534140..1163bc8 100644 --- a/pages/study-consent.tsx +++ b/pages/study-consent.tsx @@ -159,7 +159,7 @@ const StudyConsent: NextPage = () => { <> Study Consent - + Study Consent diff --git a/pages/verification.tsx b/pages/verification.tsx index 2af6318..896585f 100644 --- a/pages/verification.tsx +++ b/pages/verification.tsx @@ -104,8 +104,8 @@ const Verification: NextPage = () => { return ( <> - Verify your account - Ory NextJS Integration Example - + Verify your account - RADAR Base + Verify your account From 54706562bf74c68a9488525509412fe1cea4b297 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 16 Oct 2024 00:12:15 +0800 Subject: [PATCH 30/37] Fix formatting --- pages/oauth2-login.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pages/oauth2-login.tsx b/pages/oauth2-login.tsx index 7fdd9c5..30f44b5 100644 --- a/pages/oauth2-login.tsx +++ b/pages/oauth2-login.tsx @@ -83,9 +83,7 @@ const OAuth2Login = () => {

OAuth2 Login

{error &&

{error}

}

To continue, please log in.

- +
From dd9adbfbf2a8d1689dda78b87cda7447e1fc13b3 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 16 Oct 2024 00:31:25 +0800 Subject: [PATCH 31/37] Fix formatting --- env.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env.js b/env.js index 44a6c76..1977647 100644 --- a/env.js +++ b/env.js @@ -1,6 +1,6 @@ module.exports = { basePath: "/kratos-ui", restSourceBackendEndpoint: "", - restSourceFrontendEndpoint:"", + restSourceFrontendEndpoint: "", hydraPublicUrl: "", } From 74bae265ad19fbca6379e849c5bf4a1e1b6968f7 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 4 Nov 2024 12:21:26 +0000 Subject: [PATCH 32/37] Update configs --- pages/apps.tsx | 6 ++++-- pages/fitbit.tsx | 6 +----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pages/apps.tsx b/pages/apps.tsx index d40c3de..9904096 100644 --- a/pages/apps.tsx +++ b/pages/apps.tsx @@ -5,10 +5,13 @@ import Link from "next/link" import { useRouter } from "next/router" import { ReactNode, useEffect, useState } from "react" import QRCode from "react-qr-code" +import getConfig from "next/config" import { ActionCard, CenterLink, Methods, CardTitle } from "../pkg" import ory from "../pkg/sdk" +const { publicRuntimeConfig } = getConfig() + interface Props { flow?: SettingsFlow only?: Methods @@ -24,8 +27,7 @@ function AppLoginCard({ children }: Props & { children: ReactNode }) { const Apps: NextPage = () => { const router = useRouter() - const DefaultHydraUrl = - process.env.HYDRA_PUBLIC_URL || "http://localhost:4444" + const DefaultHydraUrl = publicRuntimeConfig.hydraPublicUrl const { flow: flowId, return_to: returnTo } = router.query const [traits, setTraits] = useState() const [projects, setProjects] = useState([]) diff --git a/pages/fitbit.tsx b/pages/fitbit.tsx index bc5cf96..b0d91e3 100644 --- a/pages/fitbit.tsx +++ b/pages/fitbit.tsx @@ -25,8 +25,6 @@ function AppLoginCard({ children }: Props & { children: ReactNode }) { 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() const [projects, setProjects] = useState([]) @@ -80,7 +78,6 @@ const Fitbit: NextPage = () => { App Login @@ -95,11 +92,10 @@ const Fitbit: NextPage = () => { interface QrFormProps { projects: any[] - baseUrl: string navigate: any } -const QrForm: React.FC = ({ projects, baseUrl, navigate }) => { +const QrForm: React.FC = ({ projects, navigate }) => { if (projects) { return (
From 426f638aefc5ebd8415567b590fa156db0c7d4a2 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 27 Nov 2024 18:37:06 +0000 Subject: [PATCH 33/37] Store access token expiry in apps login page --- pages/fitbit.tsx | 48 +++++++++++++++++++++++++--------- pkg/hooks.tsx | 3 ++- services/rest-source-client.ts | 14 +++++----- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/pages/fitbit.tsx b/pages/fitbit.tsx index b0d91e3..5c9fd5d 100644 --- a/pages/fitbit.tsx +++ b/pages/fitbit.tsx @@ -4,7 +4,6 @@ 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" @@ -28,12 +27,18 @@ const Fitbit: NextPage = () => { const { flow: flowId, return_to: returnTo } = router.query const [traits, setTraits] = useState() const [projects, setProjects] = useState([]) - const [tokenHandled, setTokenHandled] = useState(false) // Flag to ensure token is handled once + const [tokenHandled, setTokenHandled] = useState(false) + const [isFetchingToken, setIsFetchingToken] = useState(false) // Prevent multiple calls const handleNavigation = () => { return restSourceClient.redirectToAuthRequestLink() } + const isTokenExpired = (expiryTime: string | null) => { + if (!expiryTime) return true + return new Date().getTime() > new Date(expiryTime).getTime() + } + useEffect(() => { ory.toSession().then(({ data }) => { const traits = data?.identity?.traits @@ -44,29 +49,48 @@ const Fitbit: NextPage = () => { useEffect(() => { const handleToken = async () => { - if (!router.isReady || !projects.length || tokenHandled) return + if (!router.isReady || !projects.length || tokenHandled || isFetchingToken) return const existingToken = localStorage.getItem("access_token") + const tokenExpiry = localStorage.getItem("access_token_expiry") - if (existingToken) { + if (existingToken && !isTokenExpired(tokenExpiry)) { await restSourceClient.redirectToRestSourceAuthLink( existingToken, - projects[0], + projects[0] ) setTokenHandled(true) return } - const token = await restSourceClient.getAccessTokenFromRedirect() - if (token) { - localStorage.setItem("access_token", token) - await restSourceClient.redirectToRestSourceAuthLink(token, projects[0]) - setTokenHandled(true) + // Token is either missing or expired; fetch a new one + setIsFetchingToken(true) + try { + const tokenResponse = await restSourceClient.getAccessTokenFromRedirect() + if (tokenResponse?.access_token && tokenResponse?.expires_in) { + const accessToken = tokenResponse.access_token + const expiryTime = new Date( + new Date().getTime() + tokenResponse.expires_in * 1000 + ).toISOString() + + localStorage.setItem("access_token", accessToken) + localStorage.setItem("access_token_expiry", expiryTime) + + await restSourceClient.redirectToRestSourceAuthLink( + accessToken, + projects[0] + ) + setTokenHandled(true) + } + } catch (error) { + console.error("Failed to fetch token:", error) + } finally { + setIsFetchingToken(false) } } handleToken() - }, [router.isReady, projects, tokenHandled]) + }, [router.isReady, projects, tokenHandled, isFetchingToken]) return ( <> @@ -122,4 +146,4 @@ const QrForm: React.FC = ({ projects, navigate }) => { } } -export default Fitbit +export default Fitbit \ No newline at end of file diff --git a/pkg/hooks.tsx b/pkg/hooks.tsx index d48944e..03ee77b 100644 --- a/pkg/hooks.tsx +++ b/pkg/hooks.tsx @@ -31,8 +31,9 @@ export function LogoutLink(deps?: DependencyList) { if (logoutToken) { ory .updateLogoutFlow({ token: logoutToken }) + .then(() => Promise.resolve(localStorage.clear())) .then(() => router.push("/login")) .then(() => router.reload()) } } -} +} \ No newline at end of file diff --git a/services/rest-source-client.ts b/services/rest-source-client.ts index 4950c5a..d05e5ad 100644 --- a/services/rest-source-client.ts +++ b/services/rest-source-client.ts @@ -54,7 +54,7 @@ export class RestSourceClient { async getAccessToken( code: string, redirectUri: string, - ): Promise { + ): Promise { const bodyParams = new URLSearchParams({ grant_type: this.GRANT_TYPE, code, @@ -77,14 +77,14 @@ export class RestSourceClient { } const data = await response.json() - return data.access_token || null + return data || null } catch (error) { console.error(error) return null } } - async getAccessTokenFromRedirect(): Promise { + async getAccessTokenFromRedirect(): Promise { const url = new URL(window.location.href) const code = url.searchParams.get("code") if (!code) return null @@ -102,11 +102,9 @@ export class RestSourceClient { "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] - }` + 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 } From ddff18aaefe460a4c1b93493cfb10d7fa3792072 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 27 Nov 2024 22:02:03 +0000 Subject: [PATCH 34/37] Fix app login page --- pages/apps.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pages/apps.tsx b/pages/apps.tsx index 9904096..c2983df 100644 --- a/pages/apps.tsx +++ b/pages/apps.tsx @@ -32,8 +32,14 @@ const Apps: NextPage = () => { const [traits, setTraits] = useState() const [projects, setProjects] = useState([]) - const handleNavigation = () => { - router.replace("/fitbit") + const handleNavigation = (type: string) => { + if (type === 'app') { + const appUrl = `org.phidatalab.radar-armt:/?session=${encodeURIComponent("test")}`; + window.location.href = "org.phidatalab.radar-armt:/" + } + else { + router.replace("/fitbit") + } } useEffect(() => { @@ -91,14 +97,14 @@ const QrForm: React.FC = ({ projects, baseUrl, navigate }) => { Login with Active App +


Click the button below to redirect to Fitbit.

-
@@ -115,4 +121,4 @@ const QrForm: React.FC = ({ projects, baseUrl, navigate }) => { } } -export default Apps +export default Apps \ No newline at end of file From 0b57af017da3ef78beda558f9cd2fc5691c7bce0 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 28 Nov 2024 16:46:38 +0000 Subject: [PATCH 35/37] Update consent component --- pages/api/consent.ts | 2 +- pages/consent.tsx | 8 +++++--- pages/index.tsx | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pages/api/consent.ts b/pages/api/consent.ts index 13c6ad7..9f2bd65 100644 --- a/pages/api/consent.ts +++ b/pages/api/consent.ts @@ -8,7 +8,7 @@ const extractSession = (identity: any, grantScope: string[]) => { const session: any = { access_token: { roles: identity.metadata_public.roles, - scope: identity.metadata_public.scope, + scope: grantScope, authorities: identity.metadata_public.authorities, sources: identity.metadata_public.sources, user_name: identity.metadata_public.mp_login, diff --git a/pages/consent.tsx b/pages/consent.tsx index 25f57dd..91c0dec 100644 --- a/pages/consent.tsx +++ b/pages/consent.tsx @@ -1,7 +1,7 @@ import { useRouter } from "next/router" import React, { useEffect, useState } from "react" -import { MarginCard, CardTitle, TextCenterButton } from "../pkg" +import { MarginCard, CardTitle, TextCenterButton, LogoutLink } from "../pkg" import ory from "../pkg/sdk" const Consent = () => { @@ -13,6 +13,8 @@ const Consent = () => { const basePath = process.env.BASE_PATH || "/kratos-ui" + const onLogout = LogoutLink() + useEffect(() => { const { consent_challenge } = router.query @@ -167,7 +169,7 @@ const Consent = () => {

diff --git a/pages/armt.tsx b/pages/armt.tsx new file mode 100644 index 0000000..9ca3a32 --- /dev/null +++ b/pages/armt.tsx @@ -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 ( + + {children} + + ) +} + +const Armt: NextPage = () => { + const router = useRouter() + const { flow: flowId, return_to: returnTo } = router.query + const [traits, setTraits] = useState() + const [projects, setProjects] = useState([]) + const [tokenHandled, setTokenHandled] = useState(false) + const [isFetchingToken, setIsFetchingToken] = useState(false) // Prevent multiple calls + const [isMobile, setIsMobile] = useState(false) + const [qrCodeUrl, setQrCodeUrl] = useState(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 ( + <> + + App Login + + + + App Login + + + + + Go back + + + + ) +} + +interface QrFormProps { + projects: any[] + navigate: any + qrCodeUrl: string | null +} + +const QrForm: React.FC = ({ projects, navigate, qrCodeUrl }) => { + if (projects) { + return ( +
+ {projects.map((project) => ( +
+

{project.name}

+
+ +

Click the button below to redirect to login.

+ +
+
+

Or scan to login.

+ {qrCodeUrl && } +
+
+
+
+ ))} +
+ ) + } else { + return ( +
+ +
+ ) + } +} + +export default Armt \ No newline at end of file diff --git a/pages/oauth2-login.tsx b/pages/oauth2-login.tsx index 30f44b5..efc5c78 100644 --- a/pages/oauth2-login.tsx +++ b/pages/oauth2-login.tsx @@ -12,7 +12,7 @@ const OAuth2Login = () => { const [challenge, setChallenge] = useState(null) const [error, setError] = useState(null) const [traits, setTraits] = useState(null) - const [projects, setProjects] = useState([]) + const [projects, setProjects] = useState(null) const [id, setId] = useState(null) const basePath = process.env.BASE_PATH || "/kratos-ui" @@ -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) { @@ -44,7 +48,7 @@ const OAuth2Login = () => { } } - if (!challenge) { + if (!traits) { checkSession() } }, [router]) @@ -90,4 +94,4 @@ const OAuth2Login = () => { ) } -export default OAuth2Login +export default OAuth2Login \ No newline at end of file diff --git a/services/armt-client.ts b/services/armt-client.ts new file mode 100644 index 0000000..cd61f74 --- /dev/null +++ b/services/armt-client.ts @@ -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 { + 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 { + 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 { + 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() From 67fcf70af5f7a2d8b3fcbfd37ac5e513fd10c689 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 2 Dec 2024 22:03:03 +0000 Subject: [PATCH 37/37] Fix scopes for skipped consent --- pages/consent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/consent.tsx b/pages/consent.tsx index 91c0dec..e29bee9 100644 --- a/pages/consent.tsx +++ b/pages/consent.tsx @@ -52,7 +52,7 @@ const Consent = () => { body: JSON.stringify({ consentChallenge: consent_challenge, consentAction: "accept", - grantScope: [], + grantScope: consentData.requested_scope, remember: false, identity: sessionData.identity, }),