init
This commit is contained in:
@@ -2,7 +2,7 @@ import { GarminConnect } from "garmin-connect";
|
||||
import type { IActivity } from "garmin-connect/dist/garmin/types/activity";
|
||||
import type { IOauth1Token } from "garmin-connect/dist/garmin/types";
|
||||
import type { RoutePoint, RunMetrics, RunningActivityInput } from "@/lib/models/running";
|
||||
import { getSavedOauth1Token } from "@/lib/models/garmin-auth";
|
||||
import { getGarminCredentials, getSavedOauth1Token } from "@/lib/models/garmin-auth";
|
||||
import { completeMfaAndGetTicket, loginAndGetTicket, type GarminPendingMfa } from "@/lib/garmin/sso";
|
||||
|
||||
const FETCH_LIMIT = 50;
|
||||
@@ -13,6 +13,12 @@ export class GarminLoginRequiredError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class GarminCredentialsMissingError extends Error {
|
||||
constructor() {
|
||||
super("Brak danych logowania do Garmin Connect. Skonfiguruj je w Ustawieniach.");
|
||||
}
|
||||
}
|
||||
|
||||
function parseGarminDate(value: string): Date {
|
||||
return new Date(`${value.replace(" ", "T")}Z`);
|
||||
}
|
||||
@@ -154,7 +160,6 @@ export async function fetchActivityRunMetrics(
|
||||
const step = Math.max(1, Math.floor(rows.length / MAX));
|
||||
const sampled = rows.filter((_, i) => i % step === 0 || i === rows.length - 1);
|
||||
|
||||
// Try several known Garmin distance metric keys
|
||||
const distKey = ["directDistance", "sumDistance", "directCumulativeDistance"].find(
|
||||
(k) => sampled.some((row) => get(row, k) !== null && get(row, k)! > 0)
|
||||
);
|
||||
@@ -172,24 +177,21 @@ export async function fetchActivityRunMetrics(
|
||||
return values.some((v) => v > 0) ? values : undefined;
|
||||
};
|
||||
|
||||
const speedSeries = series("directSpeed", 3);
|
||||
const paceSec = speedSeries
|
||||
? speedSeries.map((v) => (v > 0.5 ? Math.round(1000 / v) : 0))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
distanceKm,
|
||||
hrBpm: series("directHeartRate"),
|
||||
cadenceSpm: series("directDoubleCadence"),
|
||||
gctMs: series("directGroundContactTime"),
|
||||
gcbLeftPct: series("directGroundContactBalanceLeft", 1),
|
||||
paceSec,
|
||||
};
|
||||
}
|
||||
|
||||
function getCredentials(): { username: string; password: string } {
|
||||
const username = process.env.GARMIN_EMAIL;
|
||||
const password = process.env.GARMIN_PASSWORD;
|
||||
if (!username || !password) {
|
||||
throw new Error("Brak danych logowania do Garmin Connect (GARMIN_EMAIL / GARMIN_PASSWORD).");
|
||||
}
|
||||
return { username, password };
|
||||
}
|
||||
|
||||
async function exchangeOauth1Token(client: GarminConnect, oauth1Token: IOauth1Token): Promise<void> {
|
||||
const http = client.client;
|
||||
if (!http.OAUTH_CONSUMER) {
|
||||
@@ -205,12 +207,8 @@ async function exchangeOauth1Token(client: GarminConnect, oauth1Token: IOauth1To
|
||||
await http.exchange({ oauth, token: oauth1Token });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a client authenticated using a previously saved OAuth1 token
|
||||
* (long-lived, survives across syncs) - no MFA needed if it's still valid.
|
||||
*/
|
||||
export async function getAuthorizedClient(): Promise<GarminConnect> {
|
||||
const saved = await getSavedOauth1Token();
|
||||
export async function getAuthorizedClient(userId: string): Promise<GarminConnect> {
|
||||
const saved = await getSavedOauth1Token(userId);
|
||||
if (!saved) {
|
||||
throw new GarminLoginRequiredError();
|
||||
}
|
||||
@@ -232,16 +230,16 @@ async function establishClientFromTicket(ticket: string): Promise<{ client: Garm
|
||||
return { client, oauth1Token: oauth1.token };
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a fresh SSO login using env credentials. If the account requires
|
||||
* MFA, returns the pending state needed to complete it via
|
||||
* `completeGarminMfaLogin` once the user supplies the emailed code.
|
||||
*/
|
||||
export async function beginGarminLogin(): Promise<
|
||||
{ client: GarminConnect; oauth1Token: IOauth1Token } | { mfaRequired: true; pendingState: GarminPendingMfa }
|
||||
export async function beginGarminLogin(
|
||||
userId: string
|
||||
): Promise<
|
||||
| { client: GarminConnect; oauth1Token: IOauth1Token }
|
||||
| { mfaRequired: true; pendingState: GarminPendingMfa }
|
||||
> {
|
||||
const { username, password } = getCredentials();
|
||||
const result = await loginAndGetTicket(username, password);
|
||||
const creds = await getGarminCredentials(userId);
|
||||
if (!creds) throw new GarminCredentialsMissingError();
|
||||
|
||||
const result = await loginAndGetTicket(creds.email, creds.password);
|
||||
if ("mfaRequired" in result) return result;
|
||||
return establishClientFromTicket(result.ticket);
|
||||
}
|
||||
@@ -254,14 +252,7 @@ export async function completeGarminMfaLogin(
|
||||
return establishClientFromTicket(ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all recent running activities (mapped), regardless of `since` -
|
||||
* callers should upsert all of them so previously-synced activities get
|
||||
* backfilled with newly added metric fields, but can use `since` to decide
|
||||
* which ones are "new" for reporting purposes.
|
||||
*/
|
||||
export async function fetchRunningActivities(client: GarminConnect): Promise<RunningActivityInput[]> {
|
||||
const activities = await client.getActivities(0, FETCH_LIMIT);
|
||||
|
||||
return activities.filter(isRunningActivity).map(mapActivity);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user