Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanced fetching of Definitions via radar-self-enrolment-definitions #15

24 changes: 24 additions & 0 deletions config/github-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const GITHUB_CONFIG = {
ORGANIZATION_NAME: 'RADAR-base',
REPOSITORY_NAME: process.env.GITHUB_REPO_NAME || 'radar-self-enrolment-definitions',
DEFINITIONS_BRANCH: process.env.GITHUB_REPO_BRANCH_NAME || 'test',
MAX_CONTENT_LENGTH: parseInt(process.env.GITHUB_RESPONSE_CONTENT_LENGTH || '1000000', 10),
CACHE_DURATION: parseInt(process.env.GITHUB_RESPONSE_CACHE_DURATION || '180000', 10),
CACHE_SIZE: parseInt(process.env.GITHUB_RESPONSE_CACHE_SIZE || '50', 10),
API_URL: 'https://api.github.com',
ACCEPT_HEADER: 'application/vnd.github.v3+json',
}

export const GITHUB_AUTH_CONFIG = {
GITHUB_AUTH_TOKEN: process.env.GITHUB_AUTH_TOKEN || '',
}

export const REMOTE_DEFINITIONS_CONFIG = {
CONSENT_VERSION: 'v1',
ELIGIBILITY_VERSION: 'v1',
STUDY_INFO_VERSION: 'v1',

CONSENT_DEFINITION_FILE_NAME_CONTENT: 'consent',
ELIGIBILITY_DEFINITION_FILE_NAME_CONTENT: 'eligibility',
STUDY_INFO_DEFINITION_FILE_NAME_CONTENT: 'study_info'
}
40 changes: 33 additions & 7 deletions pages/eligibility.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { RegistrationFlow, UpdateRegistrationFlowBody } from "@ory/client"
import { AxiosError } from "axios"
import type { NextPage } from "next"
import type {GetServerSideProps, NextPage} from "next"
import Head from "next/head"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import {MutableRefObject, useEffect, useState} from "react"
import { toast } from "react-toastify"

import { eligibilityQuestions } from "../data/eligibility-questionnaire"
// Import render helpers
import { MarginCard, CardTitle, TextCenterButton } from "../pkg"
import githubService from "../services/github-service";
import {REMOTE_DEFINITIONS_CONFIG} from "../config/github-config";
import {Definition} from "../utils/structures";

interface EligibilityFormProps {
questions: any[]
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
}

interface EligibilityPageProps {
definitions: string
}

// Renders the eligibility page
const Eligibility: NextPage = () => {
const Eligibility: NextPage<EligibilityPageProps> = ({definitions}) => {
const IS_ELIGIBLE = "yes"
const router = useRouter()
const [eligibility, setEligibility] = useState<boolean>()
const questions: any[] = eligibilityQuestions
const [eligibilityQuestions, setEligibilityQuestions] = useState<Definition[]>([])

const checkEligibility = async (values: any) => {
// Eligibility check
Expand All @@ -33,7 +39,10 @@ const Eligibility: NextPage = () => {
if (!router.isReady) {
return
}
})
if (definitions != null) {
setEligibilityQuestions(JSON.parse(definitions) as Definition[])
}
}, [router.isReady, definitions])

const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
Expand All @@ -60,7 +69,7 @@ const Eligibility: NextPage = () => {
{eligibility === false ? (
<NotEligibleMessage />
) : (
<EligibilityForm questions={questions} onSubmit={onSubmit} />
<EligibilityForm questions={eligibilityQuestions} onSubmit={onSubmit} />
)}
</>
)
Expand Down Expand Up @@ -103,4 +112,21 @@ const EligibilityForm: React.FC<EligibilityFormProps> = ({
</MarginCard>
)

export const getServerSideProps: GetServerSideProps = async (context) => {
const {projectId} = context.query
Copy link
Member Author

@this-Aditya this-Aditya Aug 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find a way to use sessionStorage in getServerSideProps, so I am also utilizing projectId in both the eligibility and study-consent pages.


if (typeof projectId === "string") {
const consentDefinitions: string | undefined = await githubService.initiateFetch(projectId,
REMOTE_DEFINITIONS_CONFIG.ELIGIBILITY_DEFINITION_FILE_NAME_CONTENT ,REMOTE_DEFINITIONS_CONFIG.ELIGIBILITY_VERSION)

if (consentDefinitions == undefined) return {props: {}}

return {
props: {
definitions: consentDefinitions,
}
}
} else return {props: {}}
}

export default Eligibility
38 changes: 33 additions & 5 deletions pages/study-consent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import {
} from "@ory/client"
import { H3, P } from "@ory/themes"
import { AxiosError } from "axios"
import type { NextPage } from "next"
import type {GetServerSideProps, 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 {MutableRefObject, ReactNode, useEffect, useState} from "react"

import { consentQuestions } from "../data/consent-questionnaire"
import {
ActionCard,
CenterLink,
Expand All @@ -24,12 +23,19 @@ import {
} from "../pkg"
import { handleFlowError } from "../pkg/errors"
import ory from "../pkg/sdk"
import githubService from "../services/github-service";
import {Definition} from "../utils/structures";
import {REMOTE_DEFINITIONS_CONFIG} from "../config/github-config";

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

interface StudyConsentPageProps {
definitions: string
}

function StudyConsentCard({ children }: Props & { children: ReactNode }) {
return (
<ActionCard wide className="cardMargin">
Expand All @@ -38,7 +44,7 @@ function StudyConsentCard({ children }: Props & { children: ReactNode }) {
)
}

const StudyConsent: NextPage = () => {
const StudyConsent: NextPage<StudyConsentPageProps> = ({definitions}) => {
const [flow, setFlow] = useState<SettingsFlow>()

// Get ?flow=... from the URL
Expand All @@ -48,11 +54,16 @@ const StudyConsent: NextPage = () => {
const [traits, setTraits] = useState<any>()
const [consent, setConsent] = useState<any>({})

const [consentQuestions, setConsentQuestions] = useState<Definition[]>([])

useEffect(() => {
// If the router is not ready yet, or we already have a flow, do nothing.
if (!router.isReady || flow) {
return
}
if (definitions != null) {
setConsentQuestions(JSON.parse(definitions) as Definition[])
}

// If ?flow=.. was in the URL, we fetch it
if (flowId) {
Expand Down Expand Up @@ -83,7 +94,7 @@ const StudyConsent: NextPage = () => {
setConsent(traits.consent)
})
.catch(handleFlowError(router, "settings", setFlow))
}, [flowId, router, router.isReady, returnTo, flow])
}, [flowId, router, router.isReady, returnTo, flow, definitions])

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setConsent({
Expand Down Expand Up @@ -210,4 +221,21 @@ const ConsentForm: React.FC<any> = ({
)
}

export const getServerSideProps: GetServerSideProps = async (context) => {
const {projectId} = context.query

if (typeof projectId === "string") {
const consentDefinitions: string | undefined = await githubService.initiateFetch(projectId,
REMOTE_DEFINITIONS_CONFIG.CONSENT_DEFINITION_FILE_NAME_CONTENT ,REMOTE_DEFINITIONS_CONFIG.CONSENT_VERSION)

if (consentDefinitions == undefined) return {props: {}}

return {
props: {
definitions: consentDefinitions,
}
}
} else return {props: {}}
}

export default StudyConsent
40 changes: 33 additions & 7 deletions pages/study.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import type { NextPage } from "next"
import type {GetServerSideProps, NextPage} from "next"
import Head from "next/head"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import {MutableRefObject, useEffect, useRef, useState} from "react"

import { studyInfo } from "../data/study-questionnaire"
// Import render helpers
import { MarginCard, CardTitle, TextCenterButton, InnerCard } from "../pkg"
import githubService from "../services/github-service";
import {REMOTE_DEFINITIONS_CONFIG} from "../config/github-config";
import {Definition} from "../utils/structures";

interface StudyPageProps {
definitions: string
}

// Renders the eligibility page
const Study: NextPage = () => {
const Study: NextPage<StudyPageProps> = ({definitions}) => {
const router = useRouter()
const [projectId, setProjectId] = useState<string | null>(null)
const studyInfo: MutableRefObject<Definition[]> = useRef([])

useEffect(() => {
// If the router is not ready yet, or we already have a flow, do nothing.
if (router.isReady) {
const { projectId } = router.query
const {projectId} = router.query
if (definitions != null) {
studyInfo.current = JSON.parse(definitions) as Definition[]
}
if (typeof projectId === "string") {
sessionStorage.setItem("project_id", projectId)
setProjectId(projectId)
}
}
})
}, [router.query, router.isReady, definitions])

return (
<>
Expand All @@ -31,7 +41,7 @@ const Study: NextPage = () => {
<MarginCard>
<CardTitle>{projectId} Research Study</CardTitle>
<img src="image.png" />
<StudyInfo questions={studyInfo} />
<StudyInfo questions={studyInfo.current} />
<TextCenterButton className="" data-testid="" href="/eligibility">
Join Now
</TextCenterButton>
Expand Down Expand Up @@ -64,4 +74,20 @@ const StudyInfo: React.FC<any> = ({ questions }) => {
)
}

export const getServerSideProps: GetServerSideProps = async (context) => {
const {projectId} = context.query
if (typeof projectId === "string") {
const studyDefinitions: string | undefined = await githubService.initiateFetch(projectId,
REMOTE_DEFINITIONS_CONFIG.STUDY_INFO_DEFINITION_FILE_NAME_CONTENT, REMOTE_DEFINITIONS_CONFIG.STUDY_INFO_VERSION)

if (studyDefinitions == undefined) return {props: {}}

return {
props: {
definitions: studyDefinitions,
}
}
} else return {props: {}}
}

export default Study
64 changes: 64 additions & 0 deletions services/github-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { GITHUB_AUTH_CONFIG, GITHUB_CONFIG } from "../config/github-config";

/**
* A client for interacting with the GitHub API.
* Manages authentication, request headers, content length validation, and error handling.
*/
class GithubClient {
private readonly authorizationHeader: string;
private readonly maxContentLength: number;

constructor() {
this.authorizationHeader = GITHUB_AUTH_CONFIG.GITHUB_AUTH_TOKEN ? `token ${GITHUB_AUTH_CONFIG.GITHUB_AUTH_TOKEN}` : "";
this.maxContentLength = GITHUB_CONFIG.MAX_CONTENT_LENGTH;
}

/**
* Fetches data from a specified GitHub API URL.
*
* @param url The GitHub API endpoint URL.
* @returns A promise that resolves to the fetched data in JSON format.
* @throws Error if the request is unauthorized, forbidden, or if the response content is too large.
*/
getData: (url: string) => Promise<any> = async (url: string) => {
const headers = {
Accept: GITHUB_CONFIG.ACCEPT_HEADER,
Authorization: this.authorizationHeader
};
const response = await fetch(url, {
headers,
method: 'GET',
});

if (response.status === 401) {
throw new Error("Unauthorized: Please check your GitHub token.");
}

if (response.status === 403) {
throw new Error(`Forbidden: ${await response.text()}`);
}

if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch content from GitHub: ${errorText}`);
}

this.checkContentLength(parseInt(response.headers.get('Content-Length') || '0', 10));

return await response.json();
}

/**
* Validates the content length of the API response.
*
* @param contentLength The length of the content received from the API.
* @throws Error if the content length exceeds the maximum allowed limit.
*/
private checkContentLength(contentLength: number) {
if (contentLength >= this.maxContentLength) {
throw new Error('Data received is too large to process');
}
}
}

export default new GithubClient()
Loading
Loading