Implement automatic Garmin token refresh and fix expiry tracking

- Add OAuth1 to OAuth2 token exchange using Garmin's exchange endpoint
- Track refresh token expiry (~30 days) instead of access token expiry (~21 hours)
- Auto-refresh access tokens in cron sync before they expire
- Update Python script to output refresh_token_expires_at
- Add garminRefreshTokenExpiresAt field to User type and database schema
- Fix token input UX: show when warning active, not just when disconnected
- Add Cache-Control headers to /api/user and /api/garmin/status to prevent stale data
- Add oauth-1.0a package for OAuth1 signature generation

The system now automatically refreshes OAuth2 tokens using the stored OAuth1 token,
so users only need to re-run the Python auth script every ~30 days (when refresh
token expires) instead of every ~21 hours (when access token expires).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-14 20:33:10 +00:00
parent 6df145d916
commit b221acee40
31 changed files with 607 additions and 92 deletions

View File

@@ -29,9 +29,15 @@ vi.mock("@/lib/pocketbase", () => ({
// Mock email sending
const mockSendDailyEmail = vi.fn().mockResolvedValue(undefined);
const mockSendTokenExpirationWarning = vi.fn().mockResolvedValue(undefined);
const mockSendPeriodConfirmationEmail = vi.fn().mockResolvedValue(undefined);
vi.mock("@/lib/email", () => ({
sendDailyEmail: (data: unknown) => mockSendDailyEmail(data),
sendTokenExpirationWarning: (...args: unknown[]) =>
mockSendTokenExpirationWarning(...args),
sendPeriodConfirmationEmail: (...args: unknown[]) =>
mockSendPeriodConfirmationEmail(...args),
}));
import { POST } from "./route";
@@ -48,6 +54,7 @@ describe("POST /api/cron/notifications", () => {
garminOauth1Token: "encrypted:oauth1-token",
garminOauth2Token: "encrypted:oauth2-token",
garminTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
garminRefreshTokenExpiresAt: null,
calendarToken: "cal-token",
lastPeriodDate: new Date("2025-01-01"),
cycleLength: 28,