2026-06-16 09:43:48 +02:00
|
|
|
"use server";
|
|
|
|
|
|
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
|
|
|
import type { GarminConnect } from "garmin-connect";
|
|
|
|
|
import {
|
|
|
|
|
GarminLoginRequiredError,
|
2026-06-18 11:02:31 +02:00
|
|
|
GarminCredentialsMissingError,
|
2026-06-16 09:43:48 +02:00
|
|
|
beginGarminLogin,
|
|
|
|
|
completeGarminMfaLogin,
|
|
|
|
|
fetchActivityRoutePoints,
|
|
|
|
|
fetchRunningActivities,
|
|
|
|
|
getAuthorizedClient,
|
|
|
|
|
} from "@/lib/garmin/client";
|
|
|
|
|
import {
|
|
|
|
|
getLastSyncAt,
|
|
|
|
|
getRunningActivity,
|
|
|
|
|
setLastSyncAt,
|
|
|
|
|
setRunningActivityRoutePoints,
|
|
|
|
|
upsertRunningActivity,
|
|
|
|
|
} from "@/lib/models/running";
|
|
|
|
|
import {
|
|
|
|
|
clearPendingMfaState,
|
|
|
|
|
getPendingMfaState,
|
|
|
|
|
saveOauth1Token,
|
|
|
|
|
savePendingMfaState,
|
|
|
|
|
} from "@/lib/models/garmin-auth";
|
2026-06-18 11:02:31 +02:00
|
|
|
import { getCurrentUserId } from "@/lib/session";
|
2026-06-16 09:43:48 +02:00
|
|
|
|
|
|
|
|
export type SyncGarminState = { error: string } | { success: string } | { mfaRequired: true } | null;
|
|
|
|
|
|
2026-06-18 11:02:31 +02:00
|
|
|
async function syncWithClient(userId: string, client: GarminConnect): Promise<SyncGarminState> {
|
|
|
|
|
const since = await getLastSyncAt(userId);
|
2026-06-16 09:43:48 +02:00
|
|
|
const activities = await fetchRunningActivities(client);
|
|
|
|
|
const newCount = activities.filter((activity) => !since || activity.startTime > since).length;
|
|
|
|
|
|
|
|
|
|
for (const activity of activities) {
|
2026-06-18 11:02:31 +02:00
|
|
|
await upsertRunningActivity(userId, activity);
|
2026-06-16 09:43:48 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-18 11:02:31 +02:00
|
|
|
await setLastSyncAt(userId, new Date());
|
2026-06-16 09:43:48 +02:00
|
|
|
|
|
|
|
|
revalidatePath("/running");
|
|
|
|
|
revalidatePath("/settings");
|
|
|
|
|
revalidatePath("/");
|
|
|
|
|
|
|
|
|
|
return { success: `Zsynchronizowano ${newCount} nowych aktywności (zaktualizowano ${activities.length}).` };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function syncGarminActivities(): Promise<SyncGarminState> {
|
2026-06-18 11:02:31 +02:00
|
|
|
const userId = await getCurrentUserId();
|
|
|
|
|
|
2026-06-16 09:43:48 +02:00
|
|
|
try {
|
2026-06-18 11:02:31 +02:00
|
|
|
const client = await getAuthorizedClient(userId);
|
|
|
|
|
return await syncWithClient(userId, client);
|
2026-06-16 09:43:48 +02:00
|
|
|
} catch (error) {
|
2026-06-18 11:02:31 +02:00
|
|
|
if (error instanceof GarminCredentialsMissingError) {
|
|
|
|
|
return { error: error.message };
|
|
|
|
|
}
|
2026-06-16 09:43:48 +02:00
|
|
|
if (!(error instanceof GarminLoginRequiredError)) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : "Synchronizacja z Garmin nie powiodła się." };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-06-18 11:02:31 +02:00
|
|
|
const result = await beginGarminLogin(userId);
|
2026-06-16 09:43:48 +02:00
|
|
|
if ("mfaRequired" in result) {
|
2026-06-18 11:02:31 +02:00
|
|
|
await savePendingMfaState(userId, result.pendingState);
|
2026-06-16 09:43:48 +02:00
|
|
|
return { mfaRequired: true };
|
|
|
|
|
}
|
2026-06-18 11:02:31 +02:00
|
|
|
await saveOauth1Token(userId, result.oauth1Token);
|
|
|
|
|
return await syncWithClient(userId, result.client);
|
2026-06-16 09:43:48 +02:00
|
|
|
} catch (error) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : "Logowanie do Garmin nie powiodło się." };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function submitGarminMfaCode(code: string): Promise<SyncGarminState> {
|
2026-06-18 11:02:31 +02:00
|
|
|
const userId = await getCurrentUserId();
|
|
|
|
|
const pending = await getPendingMfaState(userId);
|
2026-06-16 09:43:48 +02:00
|
|
|
if (!pending) {
|
|
|
|
|
return { error: "Sesja logowania do Garmin wygasła. Kliknij \"Synchronizuj z Garmin\" ponownie." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await completeGarminMfaLogin(pending, code);
|
2026-06-18 11:02:31 +02:00
|
|
|
await saveOauth1Token(userId, result.oauth1Token);
|
|
|
|
|
await clearPendingMfaState(userId);
|
|
|
|
|
return await syncWithClient(userId, result.client);
|
2026-06-16 09:43:48 +02:00
|
|
|
} catch (error) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : "Weryfikacja kodu MFA nie powiodła się." };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type LoadRouteState = { error: string } | { success: true } | null;
|
|
|
|
|
|
|
|
|
|
export async function loadActivityRoute(activityMongoId: string): Promise<LoadRouteState> {
|
2026-06-18 11:02:31 +02:00
|
|
|
const userId = await getCurrentUserId();
|
|
|
|
|
const activity = await getRunningActivity(userId, activityMongoId);
|
2026-06-16 09:43:48 +02:00
|
|
|
if (!activity) return { error: "Nie znaleziono aktywności." };
|
|
|
|
|
|
|
|
|
|
let client: GarminConnect;
|
|
|
|
|
try {
|
2026-06-18 11:02:31 +02:00
|
|
|
client = await getAuthorizedClient(userId);
|
2026-06-16 09:43:48 +02:00
|
|
|
} catch {
|
|
|
|
|
return { error: "Brak połączenia z Garmin Connect. Wykonaj synchronizację." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-06-18 09:43:25 +02:00
|
|
|
const result = await fetchActivityRoutePoints(client, activity.garminActivityId);
|
|
|
|
|
if (!result) return { error: "Brak danych GPS dla tej aktywności." };
|
2026-06-18 11:02:31 +02:00
|
|
|
await setRunningActivityRoutePoints(userId, activity.garminActivityId, result.points, result.elevationProfile);
|
2026-06-16 09:43:48 +02:00
|
|
|
revalidatePath(`/running/${activityMongoId}`);
|
|
|
|
|
return { success: true };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : "Nie udało się pobrać mapy trasy." };
|
|
|
|
|
}
|
|
|
|
|
}
|