Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ internal/web/dist → Embedded Flutter web build (copied by `make web`)
| `cmd/grom/` | Main binary; example configs in `config-examples/` |
| `api/v1/` | Gin handlers, route registration, DTO/response types |
| `api/docs/` | `swag`-generated OpenAPI (`make doc`) |
| `internal/auth/` | JWT + password hashing + `AuthRequired` middleware |
| `internal/auth/` | JWT + password hashing + `AuthRequired` middleware; password reset under `internal/auth/reset/` |
| `internal/mailer/` | Outbound email (`off` / `log` / `smtp` via go-mail) |
| `internal/config/` | Viper YAML config; global `config.Cfg` |
| `internal/logging/` | `slog` setup from `logging.level` / `logging.format` |
| `internal/users/` | User repository + models |
Expand Down Expand Up @@ -156,6 +157,7 @@ TLS / federation / storage are documented in `docs/admin/configuration.md` (inst
8. **Avatars:** local users + federated author avatar cache; public federation avatar routes differ from authenticated API avatar routes.
9. **Speed chart:** pre-downsampled series (≤500 pts) written at track attach; `GET /workouts/{id}/speed` reads chart only. File driver: `speed-chart.json` blob (JSON for debuggability); bbolt driver: packed binary values in `speed_charts` / `fed_speed_charts` buckets (tracks/media stay on FS).
10. **Heart rate chart:** same pattern as speed (`heartrate-chart.json` on file; packed binary in bbolt `heart_rate_charts` / `fed_heart_rate_charts`); `GET /workouts/{id}/heartrate`; `distance_m` omitted without GPS; X axis is distance km or elapsed minutes from first HR sample.
11. **Password reset:** optional; enabled when `mailer.driver` is `log`/`smtp` and `auth.reset.public_base_url` is set. API `POST /auth/password/forgot` and `/auth/password/reset`; tokens in `reset_tokens.yaml` / bbolt `reset_tokens` (not migrated). UI: Forgot password on login + web `/reset-password` (mobile opens email link in browser). `password_reset_enabled` on `/server-info`.

## Agent do / don't

Expand Down Expand Up @@ -189,6 +191,7 @@ TLS / federation / storage are documented in `docs/admin/configuration.md` (inst
| Track parsing/stats | `internal/tracks/` |
| Flutter screen/API | `ui/grom/lib/pages/`, `api_request.dart` |
| Config / TLS listen | `internal/config/`, `internal/server/`; human docs in `docs/admin/configuration.md` |
| Password reset / mailer | `internal/auth/reset/`, `internal/mailer/`, `api/v1/auth_password.go`; docs in `docs/admin/configuration.md` |
| Logging | `internal/logging/`, `logging:` in `cmd/grom/config-examples/` |
| Human docs | `docs/README.md` (index), `docs/user/`, `docs/admin/`; keep `README.md` short |
| Version bump / release | edit `VERSION`; move `CHANGELOG.md` `[Unreleased]` → `## [X.Y.Z] - YYYY-MM-DD`; update compare links; tag `X.Y.Z` on master (CI fills release body from changelog) |
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Password reset via email: `POST /api/v1/auth/password/forgot` and `/reset`, opaque tokens (`reset_tokens.yaml` / bbolt `reset_tokens`), `mailer` config (log/smtp via go-mail, no local MTA), `auth.reset.public_base_url`, in-memory rate limits, and `password_reset_enabled` on `/server-info`
- Web UI reset page (`/reset-password`) and Forgot password flow on the login screen (mobile opens the email link in a browser)
- Mobile login/register: when the server field has no scheme or port, probe `GET /api/v1/status` over HTTPS then HTTP, update the field with the resolved URL (TLS/certificate errors still select HTTPS; if both fail, default to HTTPS as before)
- Android release allows cleartext HTTP so the client can reach local/LAN instances without TLS; iOS `Info.plist` sets `NSAllowsLocalNetworking` for the same local-HTTP case

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ Then open `http://localhost:8080/` for the web UI, or `http://localhost:8080/api
## Documentation

- **[Docs index](docs/README.md)** — user and admin guides
- [User overview](docs/user/overview.md) — client screens (workouts, likes, comments, recording, equipment)
- [User overview](docs/user/overview.md) — client screens (workouts, likes, comments, password reset, recording, equipment)
- [Install and run](docs/admin/install.md) — build and start the server
- [Configuration](docs/admin/configuration.md) — TLS, storage, federation, logging
- [Configuration](docs/admin/configuration.md) — TLS, storage, federation, logging, mailer / password reset
- API docs — `/api/docs/` on a running server (OpenAPI sources in [`api/docs/`](api/docs/))

## License
Expand Down
128 changes: 128 additions & 0 deletions api/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,104 @@ const docTemplate = `{
}
}
},
"/auth/password/forgot": {
"post": {
"description": "Sends a password reset email if the account exists. Always returns 204 when reset is enabled (except rate limits).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Request password reset",
"parameters": [
{
"description": "Account email",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.forgotPasswordRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
},
"429": {
"description": "Too Many Requests",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
}
}
}
},
"/auth/password/reset": {
"post": {
"description": "Sets a new password using a one-time token from the reset email",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Reset password with token",
"parameters": [
{
"description": "Reset token and new password",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.resetPasswordRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
},
"429": {
"description": "Too Many Requests",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
}
}
}
},
"/auth/register": {
"post": {
"description": "Create a new user account",
Expand Down Expand Up @@ -3199,6 +3297,36 @@ const docTemplate = `{
"example": "2026-07-05T14:30:01Z"
}
}
},
"v1.forgotPasswordRequest": {
"type": "object",
"required": [
"email"
],
"properties": {
"email": {
"type": "string",
"example": "solarwind.palm@gmail.com"
}
}
},
"v1.resetPasswordRequest": {
"type": "object",
"required": [
"password",
"token"
],
"properties": {
"password": {
"type": "string",
"minLength": 8,
"example": "secret123"
},
"token": {
"type": "string",
"example": "abc123"
}
}
}
},
"securityDefinitions": {
Expand Down
128 changes: 128 additions & 0 deletions api/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,104 @@
}
}
},
"/auth/password/forgot": {
"post": {
"description": "Sends a password reset email if the account exists. Always returns 204 when reset is enabled (except rate limits).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Request password reset",
"parameters": [
{
"description": "Account email",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.forgotPasswordRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
},
"429": {
"description": "Too Many Requests",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
}
}
}
},
"/auth/password/reset": {
"post": {
"description": "Sets a new password using a one-time token from the reset email",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Reset password with token",
"parameters": [
{
"description": "Reset token and new password",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.resetPasswordRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
},
"429": {
"description": "Too Many Requests",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/v1.ErrorResponse"
}
}
}
}
},
"/auth/register": {
"post": {
"description": "Create a new user account",
Expand Down Expand Up @@ -3193,6 +3291,36 @@
"example": "2026-07-05T14:30:01Z"
}
}
},
"v1.forgotPasswordRequest": {
"type": "object",
"required": [
"email"
],
"properties": {
"email": {
"type": "string",
"example": "solarwind.palm@gmail.com"
}
}
},
"v1.resetPasswordRequest": {
"type": "object",
"required": [
"password",
"token"
],
"properties": {
"password": {
"type": "string",
"minLength": 8,
"example": "secret123"
},
"token": {
"type": "string",
"example": "abc123"
}
}
}
},
"securityDefinitions": {
Expand Down
Loading