This commit is contained in:
Dominik Klarkowski
2026-06-18 11:02:31 +02:00
parent d00a5a42ac
commit 047e580da0
32 changed files with 735 additions and 189 deletions

View File

@@ -4,6 +4,7 @@ import { getDb } from "@/lib/db";
export type AiAnalysisTargetType = "running" | "strength" | "dashboard";
export type AiAnalysisInput = {
userId: string;
targetType: AiAnalysisTargetType;
targetId: ObjectId;
summary: string;
@@ -40,6 +41,8 @@ export function serializeAnalysis(analysis: AiAnalysis): SerializedAiAnalysis {
const COLLECTION = "ai_analyses";
const DASHBOARD_TARGET_ID = new ObjectId("000000000000000000000001");
async function getCollection() {
const db = await getDb();
return db.collection<AiAnalysis>(COLLECTION);
@@ -53,32 +56,34 @@ export async function saveAiAnalysis(input: AiAnalysisInput): Promise<AiAnalysis
}
export async function getLatestAnalysisForTarget(
userId: string,
targetType: AiAnalysisTargetType,
targetId: ObjectId
): Promise<AiAnalysis | null> {
const collection = await getCollection();
return collection.findOne({ targetType, targetId }, { sort: { createdAt: -1 } });
return collection.findOne({ userId, targetType, targetId }, { sort: { createdAt: -1 } });
}
export async function getLatestAnalysis(): Promise<AiAnalysis | null> {
const collection = await getCollection();
return collection.findOne({}, { sort: { createdAt: -1 } });
}
const DASHBOARD_TARGET_ID = new ObjectId("000000000000000000000001");
export async function getDashboardAnalysis(): Promise<AiAnalysis | null> {
export async function getDashboardAnalysis(userId: string): Promise<AiAnalysis | null> {
const collection = await getCollection();
return collection.findOne(
{ targetType: "dashboard", targetId: DASHBOARD_TARGET_ID },
{ userId, targetType: "dashboard", targetId: DASHBOARD_TARGET_ID },
{ sort: { createdAt: -1 } }
);
}
export async function saveDashboardAnalysis(
userId: string,
summary: string,
tips: string[],
model: string
): Promise<AiAnalysis> {
return saveAiAnalysis({ targetType: "dashboard", targetId: DASHBOARD_TARGET_ID, summary, tips, model });
return saveAiAnalysis({
userId,
targetType: "dashboard",
targetId: DASHBOARD_TARGET_ID,
summary,
tips,
model,
});
}

View File

@@ -4,37 +4,61 @@ import type { GarminPendingMfa } from "@/lib/garmin/sso";
const AUTH_COLLECTION = "garmin_auth";
const PENDING_COLLECTION = "garmin_login_pending";
const CREDENTIALS_COLLECTION = "garmin_credentials";
type GarminAuthDoc = { _id: "tokens"; oauth1Token: IOauth1Token; updatedAt: Date };
type GarminPendingDoc = { _id: "pending"; state: GarminPendingMfa; createdAt: Date };
type GarminAuthDoc = { _id: string; oauth1Token: IOauth1Token; updatedAt: Date };
type GarminPendingDoc = { _id: string; state: GarminPendingMfa; createdAt: Date };
type GarminCredentialsDoc = { _id: string; email: string; password: string };
export async function getSavedOauth1Token(): Promise<IOauth1Token | null> {
export async function getSavedOauth1Token(userId: string): Promise<IOauth1Token | null> {
const db = await getDb();
const doc = await db.collection<GarminAuthDoc>(AUTH_COLLECTION).findOne({ _id: "tokens" });
const doc = await db.collection<GarminAuthDoc>(AUTH_COLLECTION).findOne({ _id: userId });
return doc?.oauth1Token ?? null;
}
export async function saveOauth1Token(oauth1Token: IOauth1Token): Promise<void> {
export async function saveOauth1Token(userId: string, oauth1Token: IOauth1Token): Promise<void> {
const db = await getDb();
await db
.collection<GarminAuthDoc>(AUTH_COLLECTION)
.updateOne({ _id: "tokens" }, { $set: { oauth1Token, updatedAt: new Date() } }, { upsert: true });
.updateOne({ _id: userId }, { $set: { oauth1Token, updatedAt: new Date() } }, { upsert: true });
}
export async function savePendingMfaState(state: GarminPendingMfa): Promise<void> {
export async function savePendingMfaState(userId: string, state: GarminPendingMfa): Promise<void> {
const db = await getDb();
await db
.collection<GarminPendingDoc>(PENDING_COLLECTION)
.updateOne({ _id: "pending" }, { $set: { state, createdAt: new Date() } }, { upsert: true });
.updateOne({ _id: userId }, { $set: { state, createdAt: new Date() } }, { upsert: true });
}
export async function getPendingMfaState(): Promise<GarminPendingMfa | null> {
export async function getPendingMfaState(userId: string): Promise<GarminPendingMfa | null> {
const db = await getDb();
const doc = await db.collection<GarminPendingDoc>(PENDING_COLLECTION).findOne({ _id: "pending" });
const doc = await db.collection<GarminPendingDoc>(PENDING_COLLECTION).findOne({ _id: userId });
return doc?.state ?? null;
}
export async function clearPendingMfaState(): Promise<void> {
export async function clearPendingMfaState(userId: string): Promise<void> {
const db = await getDb();
await db.collection<GarminPendingDoc>(PENDING_COLLECTION).deleteOne({ _id: "pending" });
await db.collection<GarminPendingDoc>(PENDING_COLLECTION).deleteOne({ _id: userId });
}
export async function getGarminCredentials(
userId: string
): Promise<{ email: string; password: string } | null> {
const db = await getDb();
const doc = await db
.collection<GarminCredentialsDoc>(CREDENTIALS_COLLECTION)
.findOne({ _id: userId });
if (!doc) return null;
return { email: doc.email, password: doc.password };
}
export async function saveGarminCredentials(
userId: string,
email: string,
password: string
): Promise<void> {
const db = await getDb();
await db
.collection<GarminCredentialsDoc>(CREDENTIALS_COLLECTION)
.updateOne({ _id: userId }, { $set: { email, password } }, { upsert: true });
}

View File

@@ -40,10 +40,12 @@ export type RunMetrics = {
cadenceSpm?: number[];
gctMs?: number[];
gcbLeftPct?: number[];
paceSec?: number[];
};
export type RunningActivity = RunningActivityInput & {
_id: ObjectId;
userId: string;
createdAt: Date;
routePoints?: RoutePoint[];
elevationProfile?: number[];
@@ -56,60 +58,73 @@ const SYNC_STATE_COLLECTION = "sync_state";
async function getCollection() {
const db = await getDb();
const collection = db.collection<RunningActivity>(COLLECTION);
await collection.createIndex({ garminActivityId: 1 }, { unique: true });
await collection.createIndex({ userId: 1, garminActivityId: 1 }, { unique: true });
return collection;
}
export async function upsertRunningActivity(activity: RunningActivityInput): Promise<void> {
export async function upsertRunningActivity(
userId: string,
activity: RunningActivityInput
): Promise<void> {
const collection = await getCollection();
await collection.updateOne(
{ garminActivityId: activity.garminActivityId },
{ userId, garminActivityId: activity.garminActivityId },
{
$set: activity,
$set: { ...activity, userId },
$setOnInsert: { createdAt: new Date() },
},
{ upsert: true }
);
}
export async function listRunningActivities(): Promise<RunningActivity[]> {
export async function listRunningActivities(userId: string): Promise<RunningActivity[]> {
const collection = await getCollection();
return collection.find().sort({ startTime: -1 }).toArray();
return collection.find({ userId }).sort({ startTime: -1 }).toArray();
}
export async function getRunningActivity(id: string): Promise<RunningActivity | null> {
export async function getRunningActivity(
userId: string,
id: string
): Promise<RunningActivity | null> {
const collection = await getCollection();
return collection.findOne({ _id: new ObjectId(id) });
return collection.findOne({ _id: new ObjectId(id), userId });
}
export async function setRunningActivityMetrics(
userId: string,
garminActivityId: number,
metrics: RunMetrics
): Promise<void> {
const collection = await getCollection();
await collection.updateOne({ garminActivityId }, { $set: { runMetrics: metrics } });
await collection.updateOne({ userId, garminActivityId }, { $set: { runMetrics: metrics } });
}
export async function setRunningActivityRoutePoints(
userId: string,
garminActivityId: number,
points: RoutePoint[],
elevationProfile: number[]
): Promise<void> {
const collection = await getCollection();
await collection.updateOne({ garminActivityId }, { $set: { routePoints: points, elevationProfile } });
await collection.updateOne(
{ userId, garminActivityId },
{ $set: { routePoints: points, elevationProfile } }
);
}
type SyncState = { _id: "garmin"; lastSyncAt: Date };
type SyncState = { _id: string; lastSyncAt: Date };
export async function getLastSyncAt(): Promise<Date | null> {
export async function getLastSyncAt(userId: string): Promise<Date | null> {
const db = await getDb();
const state = await db.collection<SyncState>(SYNC_STATE_COLLECTION).findOne({ _id: "garmin" });
const state = await db
.collection<SyncState>(SYNC_STATE_COLLECTION)
.findOne({ _id: userId });
return state?.lastSyncAt ?? null;
}
export async function setLastSyncAt(date: Date): Promise<void> {
export async function setLastSyncAt(userId: string, date: Date): Promise<void> {
const db = await getDb();
await db
.collection<SyncState>(SYNC_STATE_COLLECTION)
.updateOne({ _id: "garmin" }, { $set: { lastSyncAt: date } }, { upsert: true });
.updateOne({ _id: userId }, { $set: { lastSyncAt: date } }, { upsert: true });
}

View File

@@ -29,6 +29,7 @@ export type StrengthWorkoutInput = z.infer<typeof strengthWorkoutSchema>;
export type StrengthWorkout = StrengthWorkoutInput & {
_id: ObjectId;
userId: string;
createdAt: Date;
};
@@ -37,30 +38,34 @@ const COLLECTION = "strength_workouts";
async function getCollection() {
const db = await getDb();
const collection = db.collection<StrengthWorkout>(COLLECTION);
await collection.createIndex({ sourceKey: 1 }, { unique: true });
await collection.createIndex({ userId: 1, sourceKey: 1 }, { unique: true });
return collection;
}
export async function upsertStrengthWorkout(
userId: string,
workout: StrengthWorkoutInput
): Promise<void> {
const collection = await getCollection();
await collection.updateOne(
{ sourceKey: workout.sourceKey },
{ userId, sourceKey: workout.sourceKey },
{
$set: workout,
$set: { ...workout, userId },
$setOnInsert: { createdAt: new Date() },
},
{ upsert: true }
);
}
export async function listStrengthWorkouts(): Promise<StrengthWorkout[]> {
export async function listStrengthWorkouts(userId: string): Promise<StrengthWorkout[]> {
const collection = await getCollection();
return collection.find().sort({ date: -1 }).toArray();
return collection.find({ userId }).sort({ date: -1 }).toArray();
}
export async function getStrengthWorkout(id: string): Promise<StrengthWorkout | null> {
export async function getStrengthWorkout(
userId: string,
id: string
): Promise<StrengthWorkout | null> {
const collection = await getCollection();
return collection.findOne({ _id: new ObjectId(id) });
return collection.findOne({ _id: new ObjectId(id), userId });
}