From 846725a81e556db2f3fe7e25ecdecfb0bff30007 Mon Sep 17 00:00:00 2001 From: sstent Date: Mon, 22 Dec 2025 07:24:47 -0800 Subject: [PATCH] feat(plan): create implementation plan for persisted auth Adds the technical plan, data model, API contracts, and research for the persisted Garmin authentication feature. --- contracts/garmin_auth_session.json | 111 ++++++++++++++++++ .../data-model.md | 39 ++++++ specs/010-specification-overview-the/plan.md | 61 ++++++++++ .../quickstart.md | 51 ++++++++ .../research.md | 36 ++++++ 5 files changed, 298 insertions(+) create mode 100644 contracts/garmin_auth_session.json create mode 100644 specs/010-specification-overview-the/data-model.md create mode 100644 specs/010-specification-overview-the/plan.md create mode 100644 specs/010-specification-overview-the/quickstart.md create mode 100644 specs/010-specification-overview-the/research.md diff --git a/contracts/garmin_auth_session.json b/contracts/garmin_auth_session.json new file mode 100644 index 0000000..6112ea3 --- /dev/null +++ b/contracts/garmin_auth_session.json @@ -0,0 +1,111 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Garmin Sync Authentication API", + "version": "1.0.0", + "description": "API for managing Garmin Connect authentication and session persistence." + }, + "paths": { + "/api/v1/garmin/session/login": { + "post": { + "summary": "Initiate Garmin Connect Login", + "operationId": "login_garmin_session_login_post", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" }, + "password": { "type": "string", "format": "password" } + }, + "required": ["username", "password"] + } + } + } + }, + "responses": { + "200": { + "description": "Login successful or MFA required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "string", "enum": ["SUCCESS", "MFA_REQUIRED"] }, + "message": { "type": "string" } + } + } + } + } + }, + "401": { + "description": "Invalid credentials" + } + } + } + }, + "/api/v1/garmin/session/mfa": { + "post": { + "summary": "Submit MFA Code", + "operationId": "mfa_garmin_session_mfa_post", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "mfa_code": { "type": "string" } + }, + "required": ["mfa_code"] + } + } + } + }, + "responses": { + "200": { + "description": "MFA submission successful, session persisted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "string", "example": "SUCCESS" }, + "message": { "type": "string" } + } + } + } + } + }, + "400": { + "description": "Invalid MFA code or no pending login" + } + } + } + }, + "/api/v1/garmin/session/status": { + "get": { + "summary": "Get Garmin Session Status", + "operationId": "status_garmin_session_status_get", + "responses": { + "200": { + "description": "Current status of the persisted session", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "string", "enum": ["VALID", "MISSING", "EXPIRED", "MFA_PENDING"] }, + "last_validated": { "type": "string", "format": "date-time" } + } + } + } + } + } + } + } + } + } +} diff --git a/specs/010-specification-overview-the/data-model.md b/specs/010-specification-overview-the/data-model.md new file mode 100644 index 0000000..02c2436 --- /dev/null +++ b/specs/010-specification-overview-the/data-model.md @@ -0,0 +1,39 @@ +# Data Model: Garmin Authentication State + +**Date**: 2025-12-22 + +This document defines the data model for storing a user's persisted Garmin Connect authentication state, as required by the `Persist Garmin Authentication for Stateless Sync` feature. + +## Entity: `GarminAuthenticationState` + +This entity represents a user's authenticated session with Garmin Connect. It is designed to be stored in the CentralDB and is associated with a single application user. + +### Fields + +| Field Name | Data Type | Nullable | Description | +| ----------------- | ------------- | -------- | -------------------------------------------------------------------------------------------------------- | +| `user_id` | Foreign Key | False | The unique identifier for the application user this authentication state belongs to. | +| `session_data` | Text / Blob | True | The serialized, possibly encrypted, session data from the `garth` library. Null if no session is stored. | +| `mfa_pending` | Boolean | False | A flag indicating if the authentication process is currently paused, awaiting an MFA code from the user. | +| `last_validated` | Timestamp | True | The timestamp when the session was last successfully used to communicate with the Garmin API. | +| `created_at` | Timestamp | False | The timestamp when this record was created. | +| `updated_at` | Timestamp | False | The timestamp when this record was last updated. | + +### Relationships + +- **Belongs to**: `User`. A one-to-one relationship exists between a `User` and their `GarminAuthenticationState`. + +### State Transitions + +The `GarminAuthenticationState` entity can transition through several states: + +1. **Non-existent**: No record exists for the user. This is the initial state. +2. **MFA Pending**: A record exists with `mfa_pending = true` and `session_data` is likely `null`. This occurs after an initial login attempt triggers an MFA challenge. +3. **Active / Persisted**: A record exists with `mfa_pending = false` and `session_data` is populated. This is the state for a successfully authenticated user, allowing for stateless background syncs. +4. **Invalid / Stale**: The `session_data` is present but no longer valid for authentication with Garmin's servers, and it could not be refreshed. This state is not explicitly stored but is determined at runtime, leading to the clearing of the `session_data`. + +### Validation Rules + +- `user_id` must correspond to an existing user in the `users` table. +- `session_data` should be stored in an encrypted format to protect sensitive session information. [NEEDS CLARIFICATION: The encryption method and key management strategy needs to be defined during implementation.] +- `last_validated` should be updated after every successful background sync or API call to Garmin Connect. diff --git a/specs/010-specification-overview-the/plan.md b/specs/010-specification-overview-the/plan.md new file mode 100644 index 0000000..e1f5015 --- /dev/null +++ b/specs/010-specification-overview-the/plan.md @@ -0,0 +1,61 @@ +# Implementation Plan: Persist Garmin Authentication for Stateless Sync + +**Feature Spec**: [/home/sstent/Projects/FitTrack/GarminSync/specs/010-specification-overview-the/spec.md](/home/sstent/Projects/FitTrack/GarminSync/specs/010-specification-overview-the/spec.md) +**Branch**: `010-specification-overview-the` + +## Technical Context + +This section outlines the technologies and architectural decisions for implementing the feature. + +- **Authentication Library**: `garth` will be used for handling the Garmin Connect authentication lifecycle, including login, MFA, and session serialization. +- **API Framework**: FastAPI will be used to build the new REST API endpoints, with Pydantic for data modeling. +- **Database**: The CentralDB (PostgreSQL/SQLite) will store the persisted session data. +- **ORM**: SQLAlchemy will be used to define the `GarminAuthenticationState` model and interact with the database. +- **Session Storage**: The serialized `garth` session will be stored as a Text/Blob field in the `GarminAuthenticationState` table. +- **Background Jobs**: Existing background job services will be modified to load the session from the DB, use it, and update it if refreshed. + +## Constitution Check + +This feature plan is evaluated against the project's constitution. + +- [x] **I. Python Modernization**: All new code will use Python 3.13+ with type hints. +- [x] **II. Virtual Environment Isolation**: Development will occur within the existing `.venv`. +- [x] **III. Test-Driven Development**: Tests will be created for the new services and endpoints. +- [x] **V. Project Structure Standards**: New code will be placed in the appropriate `src/api`, `src/models`, and `src/services` directories. +- [x] **VI. Service-Specific Standards**: + - `centraldb_service`: The plan uses the mandated SQLAlchemy 2.0+ and FastAPI. + - `garminsync_service`: The plan directly addresses OAuth flows for Garmin Connect. +- [x] **X. API Standards**: A new FastAPI router will be created, and the OpenAPI contract will be published. + +**Result**: The plan is fully compliant with the project constitution. + +--- + +## Phase 0: Outline & Research + +The research phase focused on validating the use of `garth` and confirming its integration with the existing technology stack. + +- **`research.md`**: [/home/sstent/Projects/FitTrack/GarminSync/specs/010-specification-overview-the/research.md](/home/sstent/Projects/FitTrack/GarminSync/specs/010-specification-overview-the/research.md) + +All technical unknowns have been resolved. The chosen stack is `FastAPI` + `SQLAlchemy` + `garth`. + +--- + +## Phase 1: Design & Contracts + +This phase defines the data structures and API contracts for the feature. + +- **`data-model.md`**: [/home/sstent/Projects/FitTrack/GarminSync/specs/010-specification-overview-the/data-model.md](/home/sstent/Projects/FitTrack/GarminSync/specs/010-specification-overview-the/data-model.md) +- **API Contracts**: + - [/home/sstent/Projects/FitTrack/GarminSync/contracts/garmin_auth_session.json](/home/sstent/Projects/FitTrack/GarminSync/contracts/garmin_auth_session.json) +- **`quickstart.md`**: [/home/sstent/Projects/FitTrack/GarminSync/specs/010-specification-overview-the/quickstart.md](/home/sstent/Projects/FitTrack/GarminSync/specs/010-specification-overview-the/quickstart.md) + +### Agent Context Update + +No new technologies are being introduced that are not already listed in the agent's context (`GEMINI.md`). The plan uses the existing stack (`FastAPI`, `garth`, `SQLAlchemy`, `httpx`, `pydantic`). Therefore, no update to the agent context is required at this time. + +--- + +## Phase 2: Implementation Tasks + +This phase will be detailed in the next step (`/speckit.tasks`) and will involve creating the actual Python code based on the design artifacts from Phase 1. \ No newline at end of file diff --git a/specs/010-specification-overview-the/quickstart.md b/specs/010-specification-overview-the/quickstart.md new file mode 100644 index 0000000..f4d8f1f --- /dev/null +++ b/specs/010-specification-overview-the/quickstart.md @@ -0,0 +1,51 @@ +# Quickstart: Implementing Persisted Garmin Authentication + +**Date**: 2025-12-22 + +This guide provides a high-level overview for developers implementing the `Persist Garmin Authentication for Stateless Sync` feature. + +## 1. Database Model + +- **Action**: Implement the `GarminAuthenticationState` model as defined in `data-model.md`. +- **File**: Create a new model in `src/models/garmin_auth_state.py`. +- **Details**: Ensure the model includes `user_id`, `session_data`, `mfa_pending`, and `last_validated` fields. Use SQLAlchemy and create a corresponding Alembic migration script. + +## 2. API Endpoints + +- **Action**: Implement the three new endpoints defined in `contracts/garmin_auth_session.json`. +- **Files**: Add a new router in `src/api/v1/auth.py`. +- **Logic**: + - `POST /api/v1/garmin/session/login`: + - Initialize `garth`. + - Call `garth.login(username, password)`. + - If `garth.MFARequired` is raised, create/update the `GarminAuthenticationState` record with `mfa_pending=True` and return `{"status": "MFA_REQUIRED"}`. + - If successful, `dumps()` the session, save it to `session_data`, set `mfa_pending=False`, and return `{"status": "SUCCESS"}`. + - `POST /api/v1/garmin/session/mfa`: + - Load the pending `garth` client state. + - Call `garth.enter_mfa(mfa_code)`. + - On success, `dumps()` the completed session and persist it to the database. + - `GET /api/v1/garmin/session/status`: + - Query the `GarminAuthenticationState` for the current user. + - Perform a lightweight validation call with the loaded session (e.g., `garth.connectapi.get_user_settings()`). + - Return the status (`VALID`, `MISSING`, `EXPIRED`, `MFA_PENDING`). + +## 3. Update Background Sync Services + +- **Action**: Modify the existing background sync services (`GarminActivityService`, `GarminHealthService`) to use the persisted session. +- **Files**: Update the relevant service files in `src/services/`. +- **Pattern: Load-Use-Update**: + 1. **Load**: At the start of the job, fetch the `session_data` from the database. + 2. **Initialize**: Call `garth.client.loads(session_data)` to initialize the client. + 3. **Use**: Perform the sync operations. `garth` will handle automatic token refreshes. + 4. **Update**: Before the job finishes, check if the session was modified (refreshed). If so, `dumps()` the new session and update the `session_data` field in the database. This is critical for maintaining a fresh session for the next job. + +## 4. Error Handling + +- Implement logic to catch `garth` exceptions for invalid sessions. +- If a session is invalid and cannot be refreshed, update the sync job status to `AUTH_EXPIRED` and clear the `session_data` from the database. + +## 5. Testing + +- Write unit tests for the new API endpoints and the Load-Use-Update pattern. +- Write integration tests that cover the full login (including MFA) and background sync flow. +- Mock the `garth` library to simulate different scenarios: successful login, MFA required, invalid session, and token refresh. diff --git a/specs/010-specification-overview-the/research.md b/specs/010-specification-overview-the/research.md new file mode 100644 index 0000000..010c55f --- /dev/null +++ b/specs/010-specification-overview-the/research.md @@ -0,0 +1,36 @@ +# Research: Persist Garmin Authentication for Stateless Sync + +**Date**: 2025-12-22 + +This document outlines the decisions made regarding the technical implementation for persisting Garmin authentication sessions. + +## 1. Authentication Library + +- **Decision**: Use the `garth` library for handling Garmin Connect authentication. +- **Rationale**: The initial feature description explicitly mentions `garth`. This library is purpose-built for interacting with the Garmin Connect API, handles MFA, and most importantly, supports session export (`dumps`) and import (`loads`). This is the cornerstone of the "stateless sync" requirement, allowing us to persist the session state. It aligns with the existing project dependencies mentioned in `GEMINI.md`. +- **Alternatives considered**: + - `garminconnect`: Another popular library. While it's great for fetching data, its session management is not as explicitly designed for serialization and deserialization as `garth`'s, which is critical for this feature. We will continue to use it for data fetching, but `garth` will manage the authentication lifecycle. + +## 2. Session Data Storage + +- **Decision**: Store the serialized `garth` session in the CentralDB. +- **Rationale**: The feature spec requires a persistent store. The CentralDB (as defined in the constitution and project context) is the designated single source of truth for user-related data. Storing the session here ensures that any service or background worker can access it. The session data will be stored as a text or blob type. +- **Alternatives considered**: + - **Redis Cache**: Could be used for faster access, but it's not guaranteed to be persistent. The constitution mentions Redis for rate limiting, not for primary data storage. + - **Local File System**: Not suitable for a distributed or stateless service architecture, as different workers would not have access to the same session file. + +## 3. API Framework + +- **Decision**: Use FastAPI for the new API endpoints. +- **Rationale**: The project constitution mandates FastAPI for all API services. It's already in use in the project, ensuring consistency. Pydantic, which comes with FastAPI, will be used for request/response modeling. +- **Alternatives considered**: None, as this is a strict requirement from the project constitution. + +## 4. Database Interaction + +- **Decision**: Use SQLAlchemy to interact with the CentralDB. +- **Rationale**: The constitution specifies SQLAlchemy as the ORM. This ensures that the data model for the `GarminAuthenticationState` is managed consistently with other project models. +- **Alternatives considered**: None. This is a constitutional requirement. + +## Conclusion + +The technical approach is a straightforward integration of `garth`'s session management with the existing FastAPI and SQLAlchemy stack. All technical choices are dictated by the feature's core requirements and the project's established constitution.