mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 12:50:11 -05:00
ea913c6c4b
Introduce a web-first AI helper with admin-configurable providers (Ollama or hosted OpenAI-compatible), server-side context building, and confirmed write actions. Expose the feature via session /api/ai/* endpoints and scoped /api/v1/ai/* endpoints. Harden security by requiring a strong SECRET_KEY for Docker Compose, adding optional settings encryption-at-rest (Fernet), and introducing TOTP-based 2FA plus password reset flows. Update admin UI, API docs, and install documentation.
624 lines
27 KiB
Python
624 lines
27 KiB
Python
"""API Documentation with Swagger UI"""
|
|
|
|
from flask import Blueprint, current_app, jsonify, render_template_string
|
|
|
|
from app.config.analytics_defaults import get_version_from_setup
|
|
from flask_swagger_ui import get_swaggerui_blueprint
|
|
|
|
# Create blueprint for serving OpenAPI spec
|
|
api_docs_bp = Blueprint("api_docs", __name__)
|
|
|
|
SWAGGER_URL = "/api/docs"
|
|
API_URL = "/api/openapi.json"
|
|
|
|
# Create Swagger UI blueprint
|
|
swaggerui_blueprint = get_swaggerui_blueprint(
|
|
SWAGGER_URL,
|
|
API_URL,
|
|
config={
|
|
"app_name": "TimeTracker REST API",
|
|
"defaultModelsExpandDepth": -1,
|
|
"displayRequestDuration": True,
|
|
"docExpansion": "list",
|
|
"filter": True,
|
|
"showExtensions": True,
|
|
"showCommonExtensions": True,
|
|
"syntaxHighlight.theme": "monokai",
|
|
},
|
|
)
|
|
|
|
|
|
@api_docs_bp.route("/api/openapi.json")
|
|
def openapi_spec():
|
|
"""Serve the OpenAPI specification"""
|
|
app_version = get_version_from_setup()
|
|
if app_version == "unknown":
|
|
app_version = current_app.config.get("APP_VERSION", "1.0.0")
|
|
|
|
spec = {
|
|
"openapi": "3.0.0",
|
|
"info": {
|
|
"title": "TimeTracker REST API",
|
|
"version": app_version,
|
|
"description": """
|
|
# TimeTracker REST API
|
|
|
|
A comprehensive REST API for time tracking, project management, and reporting.
|
|
|
|
## Two HTTP JSON surfaces
|
|
|
|
TimeTracker exposes two JSON HTTP surfaces. **This OpenAPI document describes only `/api/v1`** (paths are relative to the v1 server URL).
|
|
|
|
1. **`/api/v1` (documented here)** — Primary, versioned **REST API** for integrations (desktop, mobile, automation). Uses **API token** authentication (`Authorization: Bearer` or `X-API-Key`), scoped permissions, and stable JSON contracts.
|
|
|
|
2. **`/api/*` (not fully documented here)** — Same-origin **session** JSON used by the **logged-in web UI** (Flask-Login cookie): search, timer helpers, notifications, dashboard fragments, uploads, and similar. These routes may change with the UI. Where a v1 equivalent exists, legacy `/api` routes may return **`X-API-Deprecated: true`** and a **`Link`** header with `rel="successor-version"` pointing at the v1 path. **Integrations should not rely on `/api/*`.**
|
|
|
|
**Exception:** a few `/api` routes (for example version check/dismiss) may accept **either** session or token for admin tooling; see product docs for details.
|
|
|
|
## Authentication (paths under `/api/v1` in this spec)
|
|
|
|
All **documented** API endpoints use authentication as described below. You can obtain an API token from the admin dashboard.
|
|
|
|
### Authentication Methods
|
|
|
|
The API supports two authentication methods:
|
|
|
|
1. **Bearer Token** (Recommended):
|
|
```
|
|
Authorization: Bearer YOUR_API_TOKEN
|
|
```
|
|
|
|
2. **API Key Header**:
|
|
```
|
|
X-API-Key: YOUR_API_TOKEN
|
|
```
|
|
|
|
### Token Format
|
|
|
|
API tokens follow the format: `tt_<32_random_characters>`
|
|
|
|
Example:
|
|
```
|
|
tt_abc123def456ghi789jkl012mno345
|
|
```
|
|
|
|
## Scopes
|
|
|
|
API tokens are assigned specific scopes that define what resources they can access:
|
|
|
|
- **read:projects** - View projects
|
|
- **write:projects** - Create and update projects
|
|
- **read:time_entries** - View time entries
|
|
- **write:time_entries** - Create and update time entries
|
|
- **read:tasks** - View tasks
|
|
- **write:tasks** - Create and update tasks
|
|
- **read:clients** - View clients
|
|
- **write:clients** - Create and update clients
|
|
- **read:reports** - View reports and analytics
|
|
- **read:users** - View user information
|
|
- **read:ai** - Preview AI helper context
|
|
- **write:ai** - Chat with the AI helper and confirm proposed actions
|
|
- **admin:all** - Full administrative access
|
|
|
|
## Rate Limiting
|
|
|
|
API requests are rate-limited to prevent abuse. Current limits:
|
|
- 100 requests per minute per token
|
|
- 1000 requests per hour per token
|
|
|
|
## Pagination
|
|
|
|
List endpoints support pagination with the following query parameters:
|
|
- `page` - Page number (default: 1)
|
|
- `per_page` - Items per page (default: 50, max: 100)
|
|
|
|
List responses use a **resource-named key** plus `pagination` (e.g. `time_entries`, `projects`, `clients`). Example:
|
|
```json
|
|
{
|
|
"time_entries": [...],
|
|
"pagination": {
|
|
"page": 1,
|
|
"per_page": 50,
|
|
"total": 150,
|
|
"pages": 3,
|
|
"has_next": true,
|
|
"has_prev": false,
|
|
"next_page": 2,
|
|
"prev_page": null
|
|
}
|
|
}
|
|
```
|
|
|
|
## Error Responses
|
|
|
|
The API uses standard HTTP status codes:
|
|
|
|
- **200 OK** - Request successful
|
|
- **201 Created** - Resource created successfully
|
|
- **400 Bad Request** - Invalid input
|
|
- **401 Unauthorized** - Authentication required or invalid token
|
|
- **403 Forbidden** - Insufficient permissions
|
|
- **404 Not Found** - Resource not found
|
|
- **500 Internal Server Error** - Server error
|
|
|
|
Error responses include a JSON body with at least `error` (user-facing message) and `message`; optional `error_code` (e.g. unauthorized, forbidden, not_found, validation_error) and `errors` (field-level validation):
|
|
```json
|
|
{
|
|
"error": "Invalid token",
|
|
"message": "The provided API token is invalid or expired",
|
|
"error_code": "unauthorized"
|
|
}
|
|
```
|
|
Validation errors (400):
|
|
```json
|
|
{
|
|
"error": "Validation failed",
|
|
"message": "Validation failed",
|
|
"error_code": "validation_error",
|
|
"errors": { "field_name": ["message1", "message2"] }
|
|
}
|
|
```
|
|
|
|
## Date/Time Format
|
|
|
|
All timestamps use ISO 8601 format:
|
|
- **Date**: `YYYY-MM-DD`
|
|
- **DateTime**: `YYYY-MM-DDTHH:MM:SS` or `YYYY-MM-DDTHH:MM:SSZ`
|
|
|
|
Example: `2024-01-15T14:30:00Z`
|
|
""",
|
|
"contact": {"name": "TimeTracker API Support"},
|
|
"license": {"name": "MIT"},
|
|
},
|
|
"servers": [
|
|
{"url": "/api/v1", "description": "Versioned REST API (token auth); OpenAPI paths are relative to this base."},
|
|
{
|
|
"url": "",
|
|
"description": "Application origin only (HTML, static assets, session `/api/*`, and other non-spec routes—not covered by this document).",
|
|
},
|
|
],
|
|
"components": {
|
|
"securitySchemes": {
|
|
"BearerAuth": {
|
|
"type": "http",
|
|
"scheme": "bearer",
|
|
"bearerFormat": "API Token",
|
|
"description": "Enter your API token (format: tt_xxxxx...)",
|
|
},
|
|
"ApiKeyAuth": {
|
|
"type": "apiKey",
|
|
"in": "header",
|
|
"name": "X-API-Key",
|
|
"description": "API token in X-API-Key header",
|
|
},
|
|
},
|
|
"schemas": {
|
|
"Project": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {"type": "integer"},
|
|
"name": {"type": "string"},
|
|
"description": {"type": "string"},
|
|
"client_id": {"type": "integer", "nullable": True},
|
|
"hourly_rate": {"type": "number"},
|
|
"estimated_hours": {"type": "number", "nullable": True},
|
|
"status": {"type": "string", "enum": ["active", "archived", "on_hold"]},
|
|
"created_at": {"type": "string", "format": "date-time"},
|
|
},
|
|
},
|
|
"TimeEntry": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {"type": "integer"},
|
|
"user_id": {"type": "integer"},
|
|
"project_id": {"type": "integer"},
|
|
"task_id": {"type": "integer", "nullable": True},
|
|
"start_time": {"type": "string", "format": "date-time"},
|
|
"end_time": {"type": "string", "format": "date-time", "nullable": True},
|
|
"duration_hours": {"type": "number", "nullable": True},
|
|
"notes": {"type": "string", "nullable": True},
|
|
"tags": {"type": "string", "nullable": True},
|
|
"billable": {"type": "boolean"},
|
|
"source": {"type": "string"},
|
|
},
|
|
},
|
|
"Task": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {"type": "integer"},
|
|
"name": {"type": "string"},
|
|
"description": {"type": "string", "nullable": True},
|
|
"project_id": {"type": "integer"},
|
|
"status": {"type": "string", "enum": ["todo", "in_progress", "review", "done", "cancelled"]},
|
|
"priority": {"type": "integer"},
|
|
},
|
|
},
|
|
"Client": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {"type": "integer"},
|
|
"name": {"type": "string"},
|
|
"email": {"type": "string", "nullable": True},
|
|
"company": {"type": "string", "nullable": True},
|
|
"phone": {"type": "string", "nullable": True},
|
|
},
|
|
},
|
|
"Error": {
|
|
"type": "object",
|
|
"properties": {
|
|
"error": {"type": "string", "description": "User-facing error message"},
|
|
"message": {"type": "string", "description": "Detailed error message"},
|
|
"error_code": {
|
|
"type": "string",
|
|
"description": "Machine-readable code (e.g. unauthorized, forbidden, not_found, validation_error)",
|
|
},
|
|
"errors": {
|
|
"type": "object",
|
|
"additionalProperties": {"type": "array", "items": {"type": "string"}},
|
|
"description": "Field-level validation errors",
|
|
},
|
|
"required_scope": {"type": "string"},
|
|
"available_scopes": {"type": "array", "items": {"type": "string"}},
|
|
},
|
|
},
|
|
"Pagination": {
|
|
"type": "object",
|
|
"properties": {
|
|
"page": {"type": "integer"},
|
|
"per_page": {"type": "integer"},
|
|
"total": {"type": "integer"},
|
|
"pages": {"type": "integer"},
|
|
"has_next": {"type": "boolean"},
|
|
"has_prev": {"type": "boolean"},
|
|
"next_page": {"type": "integer", "nullable": True},
|
|
"prev_page": {"type": "integer", "nullable": True},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"security": [{"BearerAuth": []}, {"ApiKeyAuth": []}],
|
|
"tags": [
|
|
{
|
|
"name": "SessionWebApi",
|
|
"description": "Session-based JSON under `/api/*` is for the browser UI only; it is not defined in this spec. Use `/api/v1` for integrations.",
|
|
},
|
|
{"name": "System", "description": "System information and health checks"},
|
|
{"name": "Projects", "description": "Project management operations"},
|
|
{"name": "Time Entries", "description": "Time tracking operations"},
|
|
{"name": "Timer", "description": "Timer control operations"},
|
|
{"name": "Tasks", "description": "Task management operations"},
|
|
{"name": "Clients", "description": "Client management operations"},
|
|
{"name": "Reports", "description": "Reporting and analytics"},
|
|
{"name": "Users", "description": "User management operations"},
|
|
{"name": "Invoices", "description": "Invoice operations"},
|
|
{"name": "Expenses", "description": "Expense operations"},
|
|
{"name": "AI Helper", "description": "Server-side AI helper for chat, context preview, and confirmed actions"},
|
|
],
|
|
"paths": {
|
|
"/info": {
|
|
"get": {
|
|
"tags": ["System"],
|
|
"summary": "Get API information",
|
|
"description": "Returns API version and available endpoints",
|
|
"security": [],
|
|
"responses": {
|
|
"200": {
|
|
"description": "API information",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"api_version": {"type": "string"},
|
|
"app_version": {"type": "string"},
|
|
"documentation_url": {"type": "string"},
|
|
"endpoints": {"type": "object"},
|
|
},
|
|
}
|
|
}
|
|
},
|
|
}
|
|
},
|
|
}
|
|
},
|
|
"/health": {
|
|
"get": {
|
|
"tags": ["System"],
|
|
"summary": "Health check",
|
|
"description": "Check if the API is healthy and operational",
|
|
"security": [],
|
|
"responses": {"200": {"description": "API is healthy"}},
|
|
}
|
|
},
|
|
"/ai/context-preview": {
|
|
"get": {
|
|
"tags": ["AI Helper"],
|
|
"summary": "Preview AI context",
|
|
"description": "Return the compact TimeTracker context that would be sent to the AI helper.",
|
|
"responses": {"200": {"description": "Context preview"}, "401": {"description": "Unauthorized"}},
|
|
}
|
|
},
|
|
"/ai/chat": {
|
|
"post": {
|
|
"tags": ["AI Helper"],
|
|
"summary": "Chat with AI helper",
|
|
"description": "Send a prompt to the server-side AI helper. Requires the write:ai scope.",
|
|
"requestBody": {
|
|
"required": True,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"required": ["prompt"],
|
|
"properties": {
|
|
"prompt": {"type": "string"},
|
|
"history": {"type": "array", "items": {"type": "object"}},
|
|
},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"responses": {"200": {"description": "AI response"}, "400": {"description": "AI disabled or invalid input"}},
|
|
}
|
|
},
|
|
"/ai/actions/confirm": {
|
|
"post": {
|
|
"tags": ["AI Helper"],
|
|
"summary": "Confirm AI action",
|
|
"description": "Execute a user-confirmed action proposed by the AI helper.",
|
|
"requestBody": {
|
|
"required": True,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"required": ["action"],
|
|
"properties": {"action": {"type": "object"}},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"responses": {"200": {"description": "Action completed"}, "400": {"description": "Unsupported action"}},
|
|
}
|
|
},
|
|
"/projects": {
|
|
"get": {
|
|
"tags": ["Projects"],
|
|
"summary": "List projects",
|
|
"description": "Get a paginated list of projects",
|
|
"parameters": [
|
|
{
|
|
"name": "status",
|
|
"in": "query",
|
|
"schema": {"type": "string", "enum": ["active", "archived", "on_hold"]},
|
|
},
|
|
{"name": "client_id", "in": "query", "schema": {"type": "integer"}},
|
|
{"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}},
|
|
{
|
|
"name": "per_page",
|
|
"in": "query",
|
|
"schema": {"type": "integer", "default": 50, "maximum": 100},
|
|
},
|
|
],
|
|
"responses": {"200": {"description": "List of projects"}, "401": {"description": "Unauthorized"}},
|
|
},
|
|
"post": {
|
|
"tags": ["Projects"],
|
|
"summary": "Create project",
|
|
"description": "Create a new project",
|
|
"requestBody": {
|
|
"required": True,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"required": ["name"],
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"description": {"type": "string"},
|
|
"client_id": {"type": "integer"},
|
|
"hourly_rate": {"type": "number"},
|
|
"estimated_hours": {"type": "number"},
|
|
"status": {
|
|
"type": "string",
|
|
"enum": ["active", "archived", "on_hold"],
|
|
"default": "active",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"responses": {"201": {"description": "Project created"}, "400": {"description": "Invalid input"}},
|
|
},
|
|
},
|
|
"/projects/{project_id}": {
|
|
"get": {
|
|
"tags": ["Projects"],
|
|
"summary": "Get project",
|
|
"description": "Get details of a specific project",
|
|
"parameters": [
|
|
{"name": "project_id", "in": "path", "required": True, "schema": {"type": "integer"}}
|
|
],
|
|
"responses": {
|
|
"200": {"description": "Project details"},
|
|
"404": {"description": "Project not found"},
|
|
},
|
|
},
|
|
"put": {
|
|
"tags": ["Projects"],
|
|
"summary": "Update project",
|
|
"description": "Update an existing project",
|
|
"parameters": [
|
|
{"name": "project_id", "in": "path", "required": True, "schema": {"type": "integer"}}
|
|
],
|
|
"requestBody": {
|
|
"required": True,
|
|
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Project"}}},
|
|
},
|
|
"responses": {
|
|
"200": {"description": "Project updated"},
|
|
"404": {"description": "Project not found"},
|
|
},
|
|
},
|
|
"delete": {
|
|
"tags": ["Projects"],
|
|
"summary": "Archive project",
|
|
"description": "Archive a project (soft delete)",
|
|
"parameters": [
|
|
{"name": "project_id", "in": "path", "required": True, "schema": {"type": "integer"}}
|
|
],
|
|
"responses": {
|
|
"200": {"description": "Project archived"},
|
|
"404": {"description": "Project not found"},
|
|
},
|
|
},
|
|
},
|
|
"/time-entries": {
|
|
"get": {
|
|
"tags": ["Time Entries"],
|
|
"summary": "List time entries",
|
|
"description": "Get a paginated list of time entries",
|
|
"parameters": [
|
|
{"name": "project_id", "in": "query", "schema": {"type": "integer"}},
|
|
{"name": "user_id", "in": "query", "schema": {"type": "integer"}},
|
|
{"name": "start_date", "in": "query", "schema": {"type": "string", "format": "date"}},
|
|
{"name": "end_date", "in": "query", "schema": {"type": "string", "format": "date"}},
|
|
{"name": "billable", "in": "query", "schema": {"type": "boolean"}},
|
|
{"name": "page", "in": "query", "schema": {"type": "integer"}},
|
|
{"name": "per_page", "in": "query", "schema": {"type": "integer"}},
|
|
],
|
|
"responses": {"200": {"description": "List of time entries"}},
|
|
},
|
|
"post": {
|
|
"tags": ["Time Entries"],
|
|
"summary": "Create time entry",
|
|
"description": "Create a new time entry",
|
|
"requestBody": {
|
|
"required": True,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"required": ["project_id", "start_time"],
|
|
"properties": {
|
|
"project_id": {"type": "integer"},
|
|
"task_id": {"type": "integer"},
|
|
"start_time": {"type": "string", "format": "date-time"},
|
|
"end_time": {"type": "string", "format": "date-time"},
|
|
"notes": {"type": "string"},
|
|
"tags": {"type": "string"},
|
|
"billable": {"type": "boolean", "default": True},
|
|
},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"responses": {"201": {"description": "Time entry created"}},
|
|
},
|
|
},
|
|
"/timer/status": {
|
|
"get": {
|
|
"tags": ["Timer"],
|
|
"summary": "Get timer status",
|
|
"description": "Get the current timer status for the authenticated user",
|
|
"responses": {"200": {"description": "Timer status"}},
|
|
}
|
|
},
|
|
"/timer/start": {
|
|
"post": {
|
|
"tags": ["Timer"],
|
|
"summary": "Start timer",
|
|
"description": "Start a new timer for the authenticated user",
|
|
"requestBody": {
|
|
"required": True,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"required": ["project_id"],
|
|
"properties": {"project_id": {"type": "integer"}, "task_id": {"type": "integer"}},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"responses": {"201": {"description": "Timer started"}},
|
|
}
|
|
},
|
|
"/timer/stop": {
|
|
"post": {
|
|
"tags": ["Timer"],
|
|
"summary": "Stop timer",
|
|
"description": "Stop the active timer for the authenticated user",
|
|
"responses": {"200": {"description": "Timer stopped"}},
|
|
}
|
|
},
|
|
"/users/me": {
|
|
"get": {
|
|
"tags": ["Users"],
|
|
"summary": "Get current user",
|
|
"description": "Get information about the authenticated user",
|
|
"responses": {"200": {"description": "User information"}},
|
|
}
|
|
},
|
|
"/analytics/hours-by-day": {
|
|
"get": {
|
|
"tags": ["Reports"],
|
|
"summary": "Hours by day",
|
|
"description": "Get hours worked per day for a date range",
|
|
"parameters": [{"name": "days", "in": "query", "schema": {"type": "integer", "default": 30}}],
|
|
"responses": {"200": {"description": "Chart data with labels and datasets"}},
|
|
}
|
|
},
|
|
"/analytics/hours-forecast": {
|
|
"get": {
|
|
"tags": ["Reports"],
|
|
"summary": "Hours forecast",
|
|
"description": "Get forecasted hours for the next 7 days based on moving average",
|
|
"parameters": [
|
|
{"name": "days", "in": "query", "schema": {"type": "integer", "default": 30}},
|
|
{
|
|
"name": "forecast_days",
|
|
"in": "query",
|
|
"schema": {"type": "integer", "default": 7, "maximum": 14},
|
|
},
|
|
],
|
|
"responses": {"200": {"description": "Historical and forecast data"}},
|
|
}
|
|
},
|
|
"/analytics/summary-with-comparison": {
|
|
"get": {
|
|
"tags": ["Reports"],
|
|
"summary": "Summary with comparison",
|
|
"description": "Get summary metrics with comparison to previous period",
|
|
"parameters": [{"name": "days", "in": "query", "schema": {"type": "integer", "default": 30}}],
|
|
"responses": {"200": {"description": "Summary with total hours, billable, entries, changes"}},
|
|
}
|
|
},
|
|
"/invoices/{invoice_id}": {
|
|
"get": {
|
|
"tags": ["Invoices"],
|
|
"summary": "Get invoice",
|
|
"parameters": [
|
|
{"name": "invoice_id", "in": "path", "required": True, "schema": {"type": "integer"}}
|
|
],
|
|
"responses": {"200": {"description": "Invoice details"}, "404": {"description": "Not found"}},
|
|
}
|
|
},
|
|
"/expenses": {
|
|
"get": {
|
|
"tags": ["Expenses"],
|
|
"summary": "List expenses",
|
|
"parameters": [
|
|
{"name": "project_id", "in": "query", "schema": {"type": "integer"}},
|
|
{"name": "page", "in": "query", "schema": {"type": "integer"}},
|
|
{"name": "per_page", "in": "query", "schema": {"type": "integer"}},
|
|
],
|
|
"responses": {"200": {"description": "List of expenses"}},
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
return jsonify(spec)
|