Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/app/api/google-health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getGoogleHealthData, saveHealthData } from "@/lib/google-health/data";
import { authenticateGoogleHealth } from "@/lib/google-health/auth";

export async function GET(req: NextRequest) {
try {
const authToken = await authenticateGoogleHealth(req);
const healthData = await getGoogleHealthData(authToken);
return NextResponse.json({ healthData });
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

export async function POST(req: NextRequest) {
try {
const data = await req.json();
const savedData = await saveHealthData(data);
return NextResponse.json({ savedData });
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
66 changes: 62 additions & 4 deletions src/components/source/source-add-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// TODO typesafe the form data
/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unused-vars */
'use client';

import {Document, Page, pdfjs} from 'react-pdf';
Expand All @@ -21,6 +19,8 @@ import dynamic from "next/dynamic";
import {HealthDataParserVisionListResponse} from "@/app/api/health-data-parser/visions/route";
import {HealthDataGetResponse} from "@/app/api/health-data/[id]/route";
import {HealthDataParserDocumentListResponse} from "@/app/api/health-data-parser/documents/route";
import { getAuthUrl, getToken } from '@/lib/google-health/auth';
import { getGoogleHealthData } from '@/lib/google-health/data';

const Select = dynamic(() => import('react-select'), {ssr: false});

Expand Down Expand Up @@ -55,6 +55,7 @@ interface Field {
interface AddSourceDialogProps {
onFileUpload: (e: ChangeEvent<HTMLInputElement>) => void;
onAddSymptoms: (date: string) => void;
onImportGoogleHealthData: () => void;
isSetUpVisionParser: boolean;
isSetUpDocumentParser: boolean;
}
Expand Down Expand Up @@ -105,6 +106,10 @@ const HealthDataType = {
SYMPTOMS: {
id: 'SYMPTOMS',
name: 'Symptoms'
},
GOOGLE_HEALTH: {
id: 'GOOGLE_HEALTH',
name: 'Google Health'
}
};

Expand Down Expand Up @@ -173,7 +178,8 @@ const AddSourceDialog: React.FC<AddSourceDialogProps> = ({
isSetUpVisionParser,
isSetUpDocumentParser,
onFileUpload,
onAddSymptoms
onAddSymptoms,
onImportGoogleHealthData
}) => {
const [open, setOpen] = useState(false);
const [showSettingsAlert, setShowSettingsAlert] = useState(false);
Expand Down Expand Up @@ -205,6 +211,11 @@ const AddSourceDialog: React.FC<AddSourceDialogProps> = ({
setOpen(false);
};

const handleImportGoogleHealthData = async () => {
onImportGoogleHealthData();
setOpen(false);
};

return (
<>
<Dialog open={open} onOpenChange={setOpen}>
Expand Down Expand Up @@ -256,6 +267,17 @@ const AddSourceDialog: React.FC<AddSourceDialogProps> = ({
<p className="text-sm text-gray-500">Record today&#39;s symptoms</p>
</div>
</button>

<button
className="flex items-center gap-4 p-4 border rounded-lg cursor-pointer hover:bg-gray-50 w-full"
onClick={handleImportGoogleHealthData}
>
<Activity className="w-6 h-6 text-gray-500"/>
<div className="flex-1 text-left">
<h3 className="font-medium">Import Google Health Data</h3>
<p className="text-sm text-gray-500">Import data from Google Health</p>
</div>
</button>
</div>
</DialogContent>
</Dialog>
Expand Down Expand Up @@ -297,6 +319,8 @@ const HealthDataItem: React.FC<HealthDataItemProps> = ({healthData, isSelected,
return <User className="h-5 w-5"/>;
case HealthDataType.SYMPTOMS.id:
return <Activity className="h-5 w-5"/>;
case HealthDataType.GOOGLE_HEALTH.id:
return <Activity className="h-5 w-5"/>;
default:
return <FileText className="h-5 w-5"/>;
}
Expand Down Expand Up @@ -1112,6 +1136,39 @@ export default function SourceAddScreen() {
}
};

const handleImportGoogleHealthData = async () => {
try {
const authUrl = getAuthUrl();
const authCode = prompt(`Please visit the following URL to authorize the app:\n\n${authUrl}\n\nThen enter the authorization code here:`);
if (!authCode) {
alert('Authorization code is required to import Google Health data.');
return;
}

const tokens = await getToken(authCode);
const healthData = await getGoogleHealthData(tokens.access_token);

const now = new Date();
const body = {
id: cuid(),
type: HealthDataType.GOOGLE_HEALTH.id,
data: healthData,
status: 'COMPLETED',
filePath: null,
fileType: null,
createdAt: now,
updatedAt: now
} as HealthData;

setSelectedId(body.id);
setFormData(body.data as Record<string, any>);
await mutate({healthDataList: [...healthDataList?.healthDataList || [], body]});
} catch (error) {
console.error('Failed to import Google Health data:', error);
alert('Failed to import Google Health data. Please try again.');
}
};

useEffect(() => {
if (visionDataList?.visions && visionParser === undefined) {
const {name, models} = visionDataList.visions[0];
Expand Down Expand Up @@ -1140,7 +1197,8 @@ export default function SourceAddScreen() {
isSetUpVisionParser={visionParser !== undefined && visionParserModel !== undefined && visionParserApiKey.length > 0}
isSetUpDocumentParser={documentParser !== undefined && documentParserModel !== undefined && documentParserApiKey.length > 0}
onFileUpload={handleFileUpload}
onAddSymptoms={handleAddSymptoms}/>
onAddSymptoms={handleAddSymptoms}
onImportGoogleHealthData={handleImportGoogleHealthData}/>
<div className="flex-1 overflow-y-auto">
{healthDataList?.healthDataList?.map((item) => (
<HealthDataItem
Expand Down
43 changes: 43 additions & 0 deletions src/lib/google-health/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';

const oauth2Client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT_URI
);

export function getAuthUrl() {
const scopes = [
'https://www.googleapis.com/auth/fitness.activity.read',
'https://www.googleapis.com/auth/fitness.body.read',
'https://www.googleapis.com/auth/fitness.location.read',
];

return oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
});
}

export async function getToken(code: string) {
const { tokens } = await oauth2Client.getToken(code);
oauth2Client.setCredentials(tokens);
return tokens;
}

export async function refreshToken(refreshToken: string) {
oauth2Client.setCredentials({ refresh_token: refreshToken });
const { credentials } = await oauth2Client.refreshAccessToken();
return credentials;
}

export async function authenticateGoogleHealth(req: any) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
throw new Error('No token provided');
}

oauth2Client.setCredentials({ access_token: token });
return token;
}
59 changes: 59 additions & 0 deletions src/lib/google-health/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';

const oauth2Client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT_URI
);

async function fetchGoogleHealthData(authToken: string) {
oauth2Client.setCredentials({ access_token: authToken });

const fitness = google.fitness({ version: 'v1', auth: oauth2Client });

const dataSources = await fitness.users.dataSources.list({
userId: 'me',
});

const healthData = [];

for (const dataSource of dataSources.data.dataSource || []) {
const dataSet = await fitness.users.dataSources.datasets.get({
userId: 'me',
dataSourceId: dataSource.dataStreamId,
datasetId: '0-9999999999999',
});

healthData.push({
dataSource: dataSource.dataStreamName,
data: dataSet.data.point,
});
}

return healthData;
}

function parseGoogleHealthData(rawData: any) {
return rawData.map((data: any) => {
return {
source: data.dataSource,
values: data.data.map((point: any) => ({
startTime: point.startTimeNanos,
endTime: point.endTimeNanos,
value: point.value,
})),
};
});
}

export async function getGoogleHealthData(authToken: string) {
const rawData = await fetchGoogleHealthData(authToken);
return parseGoogleHealthData(rawData);
}

export async function saveHealthData(data: any) {
// Implement the logic to save health data to the database
// This is a placeholder function and should be replaced with actual implementation
return data;
}