Merge pull request #1 from john-theo/main

feat: add api-key authorization
This commit is contained in:
Lybron Sobers authored and GitHub committed 2025-09-16 21:03:20 +02:00
commit 38a7771deb
8 files changed
+155 -18

No files matched your search

+1
View File
@@ -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
View File
@@ -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!"
+2
View File
@@ -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
View File
@@ -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!' });
+10 -6
View File
@@ -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) {
+19 -8
View File
@@ -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);
+27
View File
@@ -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();
};
+78
View File
@@ -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());
};