{"id":48488040,"url":"https://github.com/gpappsoft/ort","last_synced_at":"2026-04-07T11:00:26.971Z","repository":{"id":349727295,"uuid":"1203634922","full_name":"gpappsoft/ORT","owner":"gpappsoft","description":"A self-hosted GPS track manager REST API built with FastAPI and PostgreSQL/PostGIS.","archived":false,"fork":false,"pushed_at":"2026-04-07T08:58:31.000Z","size":84,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-07T10:22:06.819Z","etag":null,"topics":["api-rest","car-tracker","car-tracking","docker","fastapi-sqlalchemy","gps","gps-location","gps-logger","gps-tracker","gps-tracking","gpstrack","gpstracker","gpstracking","hiking-trails","pedantic","self-hosted","track-manager","tracking"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gpappsoft.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-07T08:14:32.000Z","updated_at":"2026-04-07T09:17:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"cea2669a-5024-47f2-985c-e5c1b639a663","html_url":"https://github.com/gpappsoft/ORT","commit_stats":null,"previous_names":["gpappsoft/ort"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/gpappsoft/ORT","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gpappsoft%2FORT","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gpappsoft%2FORT/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gpappsoft%2FORT/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gpappsoft%2FORT/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gpappsoft","download_url":"https://codeload.github.com/gpappsoft/ORT/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gpappsoft%2FORT/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31509941,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T03:10:19.677Z","status":"ssl_error","status_checked_at":"2026-04-07T03:10:13.982Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-rest","car-tracker","car-tracking","docker","fastapi-sqlalchemy","gps","gps-location","gps-logger","gps-tracker","gps-tracking","gpstrack","gpstracker","gpstracking","hiking-trails","pedantic","self-hosted","track-manager","tracking"],"created_at":"2026-04-07T11:00:16.641Z","updated_at":"2026-04-07T11:00:26.950Z","avatar_url":"https://github.com/gpappsoft.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ORT - The Open Route Tracker\n\nA self-hosted GPS track manager REST API built with FastAPI and PostgreSQL/PostGIS.\n\n---\n\n## Table of Contents\n\n- [Requirements](#requirements)\n- [Docker](#docker)\n  - [Build the image](#build-the-image)\n  - [Run the container](#run-the-container)\n  - [Environment variables](#environment-variables)\n- [Authentication](#authentication)\n  - [Register a new user](#register-a-new-user)\n  - [Login and obtain a token](#login-and-obtain-a-token)\n  - [Authenticate with Bearer token](#authenticate-with-bearer-token)\n  - [OAuth2 password flow](#oauth2-password-flow)\n- [API Reference](#api-reference)\n  - [Users](#users)\n  - [Tracks](#tracks)\n  - [Images](#images)\n- [Interactive API Docs](#interactive-api-docs)\n\n---\n\n## Requirements\n\n- Docker (for containerised deployment), **or** Python ≥ 3.11 + Poetry\n- PostgreSQL ≥ 14 with the **PostGIS** extension enabled\n- *(Optional)* Redis for distributed caching\n\n---\n\n## Docker\n\n### Build the image\n\n```bash\ndocker build -t ort .\n```\n\nThe Dockerfile uses a multi-stage Chainguard Wolfi base image and runs the application as a non-root user on port **5000**.\n\n### Run the container\n\nMinimum required environment variables are `DATABASE_URI`, `TOKEN_URL`, `SECRET_KEY`, and `IMAGE_PATH`.\n\n```bash\ndocker run --name ort \\\n  -e DATABASE_URI=\"postgresql+asyncpg://ort:ort@db-host:5432/ort\" \\\n  -e TOKEN_URL=\"http://localhost:8000/auth/login\" \\\n  -e SECRET_KEY=\"$(openssl rand -hex 32)\" \\\n  -e IMAGE_PATH=\"/tmp\" \\\n  -p 8000:5000 \\\n  ort:latest\n```\n\nThe API is then reachable at `http://localhost:8000`.\n\n### Environment variables\n\nCopy `.env_example` to `.env` and adjust the values before running locally without Docker.\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `DATABASE_URI` | yes | — | Async PostgreSQL connection string (`postgresql+asyncpg://…`) |\n| `TOKEN_URL` | yes | — | Full URL of the login endpoint, e.g. `http://localhost:8000/auth/login` |\n| `SECRET_KEY` | yes | — | Random secret used to sign JWTs. Generate with `openssl rand -hex 32` |\n| `IMAGE_PATH` | yes | — | Directory where uploaded images are stored |\n| `ALGORITHM` | no | `HS256` | JWT signing algorithm |\n| `ACCESS_TOKEN_EXPIRE_MINUTES` | no | `60` | Token validity in minutes |\n| `LOG_LEVEL` | no | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |\n| `SQL_ECHO` | no | `False` | Log all SQL statements (useful for debugging) |\n| `CORS_ORIGINS` | no | `[]` | JSON array of allowed CORS origins, e.g. `[\"https://app.example.com\"]` |\n| `REGISTRATION_ENABLED` | no | `true` | Set to `false` to disable public registration |\n| `EMAIL_CONFIRMATION` | no | `false` | Require e-mail verification before login |\n| `MAX_IMAGE_SIZE` | no | `2097152` | Maximum upload size in bytes (default 2 MB) |\n| `CACHE_ENABLED` | no | `true` | Enable response caching |\n| `CACHE_TYPE` | no | `local` | `local` (in-memory TTLCache) or `redis` |\n| `CACHE_TTL` | no | `3600` | Cache time-to-live in seconds |\n| `CACHE_MAXSIZE` | no | `1000` | Maximum entries for the local cache |\n| `REDIS_HOST` | no | `127.0.0.1` | Redis hostname (only when `CACHE_TYPE=redis`) |\n| `REDIS_PORT` | no | `6379` | Redis port |\n| `REDIS_DB` | no | `0` | Redis database index |\n| `REDIS_PASSWORD` | no | — | Redis password |\n| `REDIS_USERNAME` | no | — | Redis username |\n\n---\n\n## Authentication\n\nORT uses **JWT Bearer tokens** issued via an OAuth2 Password flow. The typical sequence is:\n\n1. Register a user account\n2. Log in to receive an access token\n3. Include the token in every subsequent request\n\n### Register a new user\n\nRegistration is rate-limited to **5 requests per 5 minutes** per IP.\n\n```bash\ncurl -X POST http://localhost:8000/users/register \\\n  -F \"username=johndoe\" \\\n  -F \"email=john@example.com\" \\\n  -F \"password=supersecret\" \\\n  -F \"firstname=John\" \\\n  -F \"lastname=Doe\"\n```\n\n**Response `201 Created`:**\n\n```json\n{\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"username\": \"johndoe\",\n  \"email\": \"john@example.com\",\n  \"firstname\": \"John\",\n  \"lastname\": \"Doe\"\n}\n```\n\n\u003e If `EMAIL_CONFIRMATION=true` the account is disabled until the e-mail address is verified.\n\n### Login and obtain a token\n\nLogin is rate-limited to **10 requests per minute** per IP.\n\n```bash\ncurl -X POST http://localhost:8000/auth/login \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"username=john@example.com\u0026password=supersecret\"\n```\n\nYou may use either the **username** or the **e-mail address** in the `username` field.\n\n**Response `200 OK`:**\n\n```json\n{\n  \"access_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n  \"token_type\": \"bearer\"\n}\n```\n\nTokens expire after `ACCESS_TOKEN_EXPIRE_MINUTES` minutes (default 60). Re-authenticate to obtain a fresh token.\n\n### Authenticate with Bearer token\n\nPass the token in the `Authorization` header on every protected request:\n\n```bash\nexport TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\"\n\ncurl http://localhost:8000/users/ \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\n### OAuth2 password flow\n\nORT implements the standard [OAuth2 Password Grant](https://oauth.net/2/grant-types/password/) flow, making it compatible with any OAuth2-aware client.\n\n**Token endpoint:** `POST /auth/login`\n\n| Field | Value |\n|---|---|\n| `grant_type` | `password` |\n| `username` | user e-mail or username |\n| `password` | account password |\n| `scope` | *(optional)* `user` or `admin` |\n\nExample using an OAuth2 library (Python `httpx`):\n\n```python\nimport httpx\n\nresponse = httpx.post(\n    \"http://localhost:8000/auth/login\",\n    data={\n        \"grant_type\": \"password\",\n        \"username\": \"john@example.com\",\n        \"password\": \"supersecret\",\n    },\n)\ntoken = response.json()[\"access_token\"]\n\n# Use the token\nclient = httpx.Client(headers={\"Authorization\": f\"Bearer {token}\"})\nme = client.get(\"http://localhost:8000/users/\")\n```\n\n---\n\n## API Reference\n\nAll protected endpoints require the `Authorization: Bearer \u003ctoken\u003e` header.\n\n### Users\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `POST` | `/users/register` | No | Register a new user |\n| `GET` | `/users/` | Yes | Get the current user's profile |\n\n### Tracks\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `POST` | `/tracks/` | Yes | Upload a GPX track file |\n| `GET` | `/tracks/` | Yes | List tracks (paginated, max 200) |\n| `GET` | `/tracks/download` | Yes | Download all tracks as a ZIP archive |\n| `GET` | `/tracks/{track_id}` | Yes | Get track summary |\n| `GET` | `/tracks/{track_id}/details` | Yes | Get track with waypoints, comments, and images |\n| `GET` | `/tracks/{track_id}/points/` | Yes | Get all track points as GeoJSON |\n| `GET` | `/tracks/{track_id}/linestring` | Yes | Get track geometry as a GeoJSON LineString |\n| `GET` | `/tracks/{track_id}/download` | Yes | Download a single track as GPX |\n| `DELETE` | `/tracks/{track_id}` | Yes | Delete a track |\n\n**Upload a GPX file:**\n\n```bash\ncurl -X POST http://localhost:8000/tracks/ \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"file=@my_track.gpx\"\n```\n\n**List tracks (with pagination):**\n\n```bash\ncurl \"http://localhost:8000/tracks/?limit=50\u0026offset=0\" \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n\n### Images\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `POST` | `/images/{track_id}` | Yes | Upload an image for a track (EXIF GPS extracted automatically) |\n| `GET` | `/images/` | Yes | List images (paginated, max 200) |\n| `GET` | `/images/{image_id}` | Yes | Get image details and comments (by ID or MD5 hash) |\n| `GET` | `/images/track/{track_id}` | Yes | List all images for a track |\n| `GET` | `/images/track/{track_id}/details` | Yes | Get images with comments for a track |\n| `PUT` | `/images/{image_id}` | Yes | Update image metadata |\n| `DELETE` | `/images/{image_id}` | Yes | Delete an image |\n\n**Upload an image:**\n\n```bash\ncurl -X POST http://localhost:8000/images/{track_id} \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -F \"file=@photo.jpg\"\n```\n\n---\n\n## Interactive API Docs\n\nORT ships with Swagger UI. Open the following URL in your browser while the server is running:\n\n```\nhttp://localhost:8000/api/docs\n```\n\nYou can authorise directly in the UI by clicking **Authorize** and entering your Bearer token, or by using the built-in OAuth2 password form.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgpappsoft%2Fort","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgpappsoft%2Fort","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgpappsoft%2Fort/lists"}