mirror of
https://github.com/nprasad2077/health-auto-export-server.git
synced 2026-11-04 07:33:41 +00:00
Merge pull request #1 from john-theo/main
feat: add api-key authorization
This commit is contained in:
8 files changed
+155
-18
No files matched your search
@@ -53,6 +53,7 @@ In order to use this project, you will need:
|
||||
4. Configure the automation with the following settings:
|
||||
- Automation Type: `REST API`
|
||||
- URL: `http://your-computer-ip:3001/api/data`
|
||||
- Headers: `api-key` `sk-xxx` (write token in `.env`)
|
||||
- Data Type: `Health Metrics` or `Workouts` (create separate automations for each if you want to sync different data types)
|
||||
- Export Format: `JSON`
|
||||
- Aggregate Data: `Enabled` 🟢
|
||||
|
||||
+11
-1
@@ -8,6 +8,11 @@ DATABASE="health-auto-export"
|
||||
ENVIRONMENT="production"
|
||||
PORT="27017"
|
||||
|
||||
|
||||
# Generate random tokens for authentication
|
||||
READ_TOKEN=sk-$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32)
|
||||
WRITE_TOKEN=sk-$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32)
|
||||
|
||||
echo "Creating .env file..."
|
||||
cat > .env << EOF
|
||||
NODE_ENV=${ENVIRONMENT}
|
||||
@@ -16,6 +21,11 @@ MONGO_USERNAME=${USERNAME}
|
||||
MONGO_PASSWORD=${PASSWORD}
|
||||
MONGO_DB=${DATABASE}
|
||||
MONGO_PORT=${PORT}
|
||||
READ_TOKEN=${READ_TOKEN}
|
||||
WRITE_TOKEN=${WRITE_TOKEN}
|
||||
EOF
|
||||
|
||||
echo "Environment configuration saved to .env file ✅"
|
||||
echo "Environment configuration saved to .env file ✅"
|
||||
echo "Generated READ_TOKEN: ${READ_TOKEN}"
|
||||
echo "Generated WRITE_TOKEN: ${WRITE_TOKEN}"
|
||||
echo "Store these tokens securely!"
|
||||
@@ -42,6 +42,8 @@ services:
|
||||
- MONGO_DB=${MONGO_DB}
|
||||
- MONGO_USERNAME=${MONGO_USERNAME}
|
||||
- MONGO_PASSWORD=${MONGO_PASSWORD}
|
||||
- READ_TOKEN=${READ_TOKEN}
|
||||
- WRITE_TOKEN=${WRITE_TOKEN}
|
||||
depends_on:
|
||||
- hae-mongo
|
||||
networks:
|
||||
|
||||
+7
-3
@@ -5,6 +5,7 @@ import mongodb from './database/mongodb';
|
||||
import ingesterRouter from './routes/ingester';
|
||||
import metricsRouter from './routes/metrics';
|
||||
import workoutsRouter from './routes/workouts';
|
||||
import { requireReadAuth, requireWriteAuth } from './middleware/auth';
|
||||
|
||||
const app = express();
|
||||
const port = 3001;
|
||||
@@ -22,9 +23,12 @@ app.use(cors(corsOptions));
|
||||
app.use(express.json({ limit: '200mb' }));
|
||||
app.use(express.urlencoded({ limit: '200mb', extended: true }));
|
||||
|
||||
app.use('/api/data', ingesterRouter);
|
||||
app.use('/api/metrics', metricsRouter);
|
||||
app.use('/api/workouts', workoutsRouter);
|
||||
// Apply write auth middleware to data ingestion routes
|
||||
app.use('/api/data', requireWriteAuth, ingesterRouter);
|
||||
|
||||
// Apply read auth middleware to data retrieval routes
|
||||
app.use('/api/metrics', requireReadAuth, metricsRouter);
|
||||
app.use('/api/workouts', requireReadAuth, workoutsRouter);
|
||||
|
||||
app.get('/', (req: express.Request, res: express.Response) => {
|
||||
res.json({ message: 'Hello world!' });
|
||||
|
||||
@@ -15,24 +15,23 @@ import {
|
||||
createMetricModel,
|
||||
} from '../models/Metric';
|
||||
import { MetricName } from '../models/MetricName';
|
||||
import { filterFields, parseDate } from '../utils';
|
||||
|
||||
export const getMetrics = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { from, to } = req.query;
|
||||
const { from, to, include, exclude } = req.query;
|
||||
const selectedMetric = req.params.selected_metric as MetricName;
|
||||
|
||||
if (!selectedMetric) {
|
||||
throw new Error('No metric selected');
|
||||
}
|
||||
|
||||
const fromDate = new Date(Number(from));
|
||||
const toDate = new Date(Number(to));
|
||||
|
||||
const isDate = (date: Date) => !isNaN(date.getTime());
|
||||
const fromDate = parseDate(from as string);
|
||||
const toDate = parseDate(to as string);
|
||||
|
||||
let query = {};
|
||||
|
||||
if (isDate(fromDate) && isDate(toDate)) {
|
||||
if (fromDate && toDate) {
|
||||
query = {
|
||||
date: {
|
||||
$gte: fromDate,
|
||||
@@ -57,6 +56,11 @@ export const getMetrics = async (req: Request, res: Response) => {
|
||||
metrics = await createMetricModel(selectedMetric).find(query).lean();
|
||||
}
|
||||
|
||||
// Process include/exclude filters if provided
|
||||
if (include || exclude) {
|
||||
metrics = metrics.map(metric => filterFields(metric, include, exclude));
|
||||
}
|
||||
|
||||
console.log(metrics);
|
||||
res.json(metrics);
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,21 +3,20 @@ import { Request, Response } from 'express';
|
||||
import { IngestData } from '../models/IngestData';
|
||||
import { IngestResponse } from '../models/IngestResponse';
|
||||
import { RouteModel, WorkoutModel, mapWorkoutData, mapRoute } from '../models/Workout';
|
||||
import { filterFields, parseDate } from '../utils';
|
||||
|
||||
export const getWorkouts = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
const { startDate, endDate, include, exclude } = req.query;
|
||||
|
||||
const fromDate = new Date(Number(startDate));
|
||||
const toDate = new Date(Number(endDate));
|
||||
const fromDate = parseDate(startDate as string);
|
||||
const toDate = parseDate(endDate as string);
|
||||
|
||||
console.log(fromDate, toDate);
|
||||
|
||||
const isDate = (date: Date) => !isNaN(date.getTime());
|
||||
|
||||
let query = {};
|
||||
|
||||
if (isDate(fromDate) && isDate(toDate)) {
|
||||
if (fromDate && toDate) {
|
||||
query = {
|
||||
start: {
|
||||
$gte: fromDate,
|
||||
@@ -49,8 +48,14 @@ export const getWorkouts = async (req: Request, res: Response) => {
|
||||
return mappedWorkouts;
|
||||
});
|
||||
|
||||
// Process include/exclude filters if provided
|
||||
let processedWorkouts = workouts;
|
||||
if (include || exclude) {
|
||||
processedWorkouts = workouts.map(workout => filterFields(workout, include, exclude));
|
||||
}
|
||||
|
||||
console.log(`${workouts.length} workouts fetched`);
|
||||
res.status(200).json(workouts);
|
||||
res.status(200).json(processedWorkouts);
|
||||
} catch (error) {
|
||||
console.error('Error fetching workouts:', error);
|
||||
res.status(500).json({ error: 'Error fetching workouts' });
|
||||
@@ -60,6 +65,7 @@ export const getWorkouts = async (req: Request, res: Response) => {
|
||||
export const getWorkout = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { include, exclude } = req.query;
|
||||
|
||||
console.log('Fetching workout details for ID:', id);
|
||||
const workoutMetadata = await WorkoutModel.findOne({ workoutId: id })
|
||||
@@ -102,7 +108,12 @@ export const getWorkout = async (req: Request, res: Response) => {
|
||||
});
|
||||
});
|
||||
|
||||
const ret = { ...workoutMetadata, route: route || [] };
|
||||
let ret = { ...workoutMetadata, route: route || [] };
|
||||
|
||||
// Process include/exclude filters if provided
|
||||
if (include || exclude) {
|
||||
ret = filterFields(ret, include, exclude);
|
||||
}
|
||||
|
||||
console.log(`Workout ${id} fetched with ${route?.length ?? 0} locations`);
|
||||
res.status(200).json(ret);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
/**
|
||||
* Authentication middleware for read access
|
||||
*/
|
||||
export const requireReadAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
const token = req.headers['api-key'] as string;
|
||||
|
||||
if (!token || !token.startsWith('sk-') || token !== process.env.READ_TOKEN) {
|
||||
return res.status(401).json({ error: 'Unauthorized: Invalid read token' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Authentication middleware for write access
|
||||
*/
|
||||
export const requireWriteAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
const token = req.headers['api-key'] as string;
|
||||
|
||||
if (!token || !token.startsWith('sk-') || token !== process.env.WRITE_TOKEN) {
|
||||
return res.status(401).json({ error: 'Unauthorized: Invalid write token' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Helper function to filter object fields based on include/exclude params
|
||||
* @param obj Object to filter fields from
|
||||
* @param include Raw include query parameter (string or undefined)
|
||||
* @param exclude Raw exclude query parameter (string or undefined)
|
||||
* @returns Filtered object
|
||||
*/
|
||||
export const filterFields = (obj: any, include?: any, exclude?: any) => {
|
||||
// Parse parameters if they exist
|
||||
const includeFields = include ? (include as string).split(',') : undefined;
|
||||
const excludeFields = exclude ? (exclude as string).split(',') : undefined;
|
||||
|
||||
if (!includeFields && !excludeFields) return obj;
|
||||
|
||||
if (includeFields && includeFields.length > 0) {
|
||||
const filtered: any = {};
|
||||
includeFields.forEach(field => {
|
||||
if (obj[field] !== undefined) {
|
||||
filtered[field] = obj[field];
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
if (excludeFields && excludeFields.length > 0) {
|
||||
const filtered = { ...obj };
|
||||
excludeFields.forEach(field => {
|
||||
delete filtered[field];
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a date string or timestamp into a Date object
|
||||
* Supports formats:
|
||||
* - Unix timestamp (number or string)
|
||||
* - "YYYY/MM/DD"
|
||||
* - "YYYY-MM-DD"
|
||||
* - "YYYY-MM-DD HH:MM:SS"
|
||||
*
|
||||
* @param dateInput Date input as string or number
|
||||
* @returns Date object or null if invalid
|
||||
*/
|
||||
export const parseDate = (dateInput: string | number | undefined): Date | null => {
|
||||
if (!dateInput) return null;
|
||||
|
||||
// Try parsing as number (timestamp)
|
||||
if (typeof dateInput === 'number' || !isNaN(Number(dateInput))) {
|
||||
const date = new Date(Number(dateInput));
|
||||
return isValidDate(date) ? date : null;
|
||||
}
|
||||
|
||||
// Try parsing "YYYY/MM/DD" format
|
||||
if (dateInput.includes('/')) {
|
||||
const date = new Date(dateInput);
|
||||
return isValidDate(date) ? date : null;
|
||||
}
|
||||
|
||||
// Try parsing "YYYY-MM-DD" or "YYYY-MM-DD HH:MM:SS" format
|
||||
if (dateInput.includes('-')) {
|
||||
const date = new Date(dateInput);
|
||||
return isValidDate(date) ? date : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a date is valid
|
||||
* @param date Date to validate
|
||||
* @returns boolean indicating if date is valid
|
||||
*/
|
||||
const isValidDate = (date: Date): boolean => {
|
||||
return !isNaN(date.getTime());
|
||||
};
|
||||
Reference in new issue
Block a user