timetracker update

This commit is contained in:
Daniel Goc
2026-03-11 09:33:36 +01:00
parent bbf8a2c133
commit 9ef4bb219b
121 changed files with 4328 additions and 2231 deletions

View File

@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "timeTracker API",
"description": "Authentication and user management API",
"description": "Authentication, user management, and repository time tracking API",
"version": "1.0.0",
"contact": {
"name": "API Support",
@@ -22,15 +22,15 @@
},
{
"name": "Auth",
"description": "Authentication endpoints"
"description": "Authentication endpoints (under /api/v1/public/auth)"
},
{
"name": "Languages",
"description": "Language and translation endpoints"
},
{
"name": "Protected",
"description": "Protected routes requiring authentication"
"name": "Repo",
"description": "Repository time tracking data endpoints (under /api/v1/restricted/repo, requires authentication)"
},
{
"name": "Admin",
@@ -208,11 +208,11 @@
}
}
},
"/api/v1/auth/login": {
"/api/v1/public/auth/login": {
"post": {
"tags": ["Auth"],
"summary": "User login",
"description": "Authenticate a user with email and password",
"description": "Authenticate a user with email and password. Sets HTTPOnly cookies (access_token, refresh_token, is_authenticated) on success.",
"operationId": "login",
"requestBody": {
"required": true,
@@ -239,12 +239,12 @@
"schema": {
"type": "string"
},
"description": "HTTP-only cookies containing access and refresh tokens"
"description": "HTTPOnly cookies: access_token, refresh_token (opaque), is_authenticated (non-HTTPOnly flag)"
}
}
},
"400": {
"description": "Invalid request body",
"description": "Invalid request body or missing fields",
"content": {
"application/json": {
"schema": {
@@ -276,11 +276,11 @@
}
}
},
"/api/v1/auth/register": {
"/api/v1/public/auth/register": {
"post": {
"tags": ["Auth"],
"summary": "User registration",
"description": "Register a new user account",
"description": "Register a new user account. Sends a verification email. first_name and last_name are required.",
"operationId": "register",
"requestBody": {
"required": true,
@@ -310,7 +310,17 @@
}
},
"400": {
"description": "Invalid request or email already exists",
"description": "Invalid request, missing required fields, or invalid password format",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Email already exists",
"content": {
"application/json": {
"schema": {
@@ -322,11 +332,11 @@
}
}
},
"/api/v1/auth/complete-registration": {
"/api/v1/public/auth/complete-registration": {
"post": {
"tags": ["Auth"],
"summary": "Complete registration",
"description": "Complete registration after email verification",
"description": "Complete registration after email verification using the token sent by email. Sets auth cookies on success.",
"operationId": "completeRegistration",
"requestBody": {
"required": true,
@@ -347,10 +357,18 @@
"$ref": "#/components/schemas/AuthResponse"
}
}
},
"headers": {
"Set-Cookie": {
"schema": {
"type": "string"
},
"description": "HTTPOnly cookies: access_token, refresh_token, is_authenticated"
}
}
},
"400": {
"description": "Invalid token",
"description": "Invalid or expired token",
"content": {
"application/json": {
"schema": {
@@ -362,11 +380,11 @@
}
}
},
"/api/v1/auth/forgot-password": {
"/api/v1/public/auth/forgot-password": {
"post": {
"tags": ["Auth"],
"summary": "Request password reset",
"description": "Request a password reset email",
"description": "Request a password reset email. Always returns success to prevent email enumeration.",
"operationId": "forgotPassword",
"requestBody": {
"required": true,
@@ -404,7 +422,7 @@
}
},
"400": {
"description": "Invalid request",
"description": "Invalid request or missing email",
"content": {
"application/json": {
"schema": {
@@ -416,11 +434,11 @@
}
}
},
"/api/v1/auth/reset-password": {
"/api/v1/public/auth/reset-password": {
"post": {
"tags": ["Auth"],
"summary": "Reset password",
"description": "Reset password using reset token",
"description": "Reset password using reset token from email. Also revokes all existing refresh tokens for the user.",
"operationId": "resetPassword",
"requestBody": {
"required": true,
@@ -450,7 +468,7 @@
}
},
"400": {
"description": "Invalid or expired token",
"description": "Invalid or expired token, or invalid password format",
"content": {
"application/json": {
"schema": {
@@ -462,11 +480,11 @@
}
}
},
"/api/v1/auth/logout": {
"/api/v1/public/auth/logout": {
"post": {
"tags": ["Auth"],
"summary": "User logout",
"description": "Clear authentication cookies",
"description": "Revokes the refresh token from the database and clears all authentication cookies.",
"operationId": "logout",
"responses": {
"200": {
@@ -488,11 +506,11 @@
}
}
},
"/api/v1/auth/refresh": {
"/api/v1/public/auth/refresh": {
"post": {
"tags": ["Auth"],
"summary": "Refresh access token",
"description": "Get a new access token using refresh token",
"description": "Get a new access token using the refresh token. The refresh token is read from the HTTPOnly cookie first, then from the request body as fallback. Rotates the refresh token on success.",
"operationId": "refreshToken",
"requestBody": {
"content": {
@@ -502,7 +520,7 @@
"properties": {
"refresh_token": {
"type": "string",
"description": "Refresh token from login response"
"description": "Opaque refresh token (fallback if cookie not available)"
}
}
}
@@ -518,6 +536,14 @@
"$ref": "#/components/schemas/AuthResponse"
}
}
},
"headers": {
"Set-Cookie": {
"schema": {
"type": "string"
},
"description": "Rotated HTTPOnly cookies: access_token, refresh_token, is_authenticated"
}
}
},
"400": {
@@ -543,27 +569,25 @@
}
}
},
"/api/v1/protected/dashboard": {
"/api/v1/public/auth/me": {
"get": {
"tags": ["Protected"],
"summary": "Get dashboard data",
"description": "Protected route requiring authentication",
"tags": ["Auth"],
"summary": "Get current user",
"description": "Returns the currently authenticated user's session information. Requires authentication via cookie.",
"operationId": "getMe",
"security": [
{
"BearerAuth": []
"CookieAuth": []
}
],
"responses": {
"200": {
"description": "Dashboard data",
"description": "Current user info",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"user": {
"$ref": "#/components/schemas/UserSession"
}
@@ -585,28 +609,143 @@
}
}
},
"/api/v1/admin/users": {
"/api/v1/public/auth/google": {
"get": {
"tags": ["Admin"],
"summary": "Get all users",
"description": "Admin-only endpoint for user management",
"tags": ["Auth"],
"summary": "Google OAuth2 login",
"description": "Redirects the user to Google's OAuth2 consent page. Sets a short-lived oauth_state cookie for CSRF protection.",
"operationId": "googleLogin",
"responses": {
"302": {
"description": "Redirect to Google OAuth2 consent page",
"headers": {
"Location": {
"schema": {
"type": "string"
},
"description": "Google OAuth2 authorization URL"
},
"Set-Cookie": {
"schema": {
"type": "string"
},
"description": "HTTPOnly oauth_state cookie for CSRF protection (10 min expiry)"
}
}
},
"500": {
"description": "Failed to generate OAuth state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/public/auth/google/callback": {
"get": {
"tags": ["Auth"],
"summary": "Google OAuth2 callback",
"description": "Handles the OAuth2 callback from Google. Validates state, exchanges code for tokens, creates or updates user, sets auth cookies, and redirects to the app.",
"operationId": "googleCallback",
"parameters": [
{
"name": "code",
"in": "query",
"description": "Authorization code from Google",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "state",
"in": "query",
"description": "State token for CSRF validation",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"302": {
"description": "Redirect to app after successful authentication",
"headers": {
"Location": {
"schema": {
"type": "string"
},
"description": "Redirect to /{lang} (user's preferred language)"
},
"Set-Cookie": {
"schema": {
"type": "string"
},
"description": "HTTPOnly cookies: access_token, refresh_token, is_authenticated"
}
}
},
"400": {
"description": "Invalid state (CSRF) or missing authorization code",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Google OAuth callback processing error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/restricted/repo/get-repos": {
"get": {
"tags": ["Repo"],
"summary": "Get accessible repositories",
"description": "Returns a list of repository IDs that the authenticated user has access to.",
"operationId": "getRepos",
"security": [
{
"BearerAuth": []
"CookieAuth": []
}
],
"responses": {
"200": {
"description": "List of users",
"description": "List of repository IDs",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
"type": "array",
"items": {
"type": "integer",
"format": "uint"
},
"example": [1, 2, 5]
}
}
}
},
"400": {
"description": "Invalid user session",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
@@ -620,9 +759,235 @@
}
}
}
}
}
}
},
"/api/v1/restricted/repo/get-years": {
"get": {
"tags": ["Repo"],
"summary": "Get available years for a repository",
"description": "Returns a list of years for which tracked time data exists in the given repository. User must have access to the repository.",
"operationId": "getYears",
"security": [
{
"CookieAuth": []
}
],
"parameters": [
{
"name": "repoID",
"in": "query",
"description": "Repository ID",
"required": true,
"schema": {
"type": "integer",
"format": "uint"
}
}
],
"responses": {
"200": {
"description": "List of years with tracked time data",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "integer",
"format": "uint"
},
"example": [2023, 2024, 2025]
}
}
}
},
"403": {
"description": "Admin access required",
"400": {
"description": "Invalid repoID parameter or user does not have access to the repository",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/restricted/repo/get-quarters": {
"get": {
"tags": ["Repo"],
"summary": "Get quarterly time data for a repository",
"description": "Returns time tracked per quarter for the given repository and year. All 4 quarters are returned; quarters with no data have time=0. User must have access to the repository.",
"operationId": "getQuarters",
"security": [
{
"CookieAuth": []
}
],
"parameters": [
{
"name": "repoID",
"in": "query",
"description": "Repository ID",
"required": true,
"schema": {
"type": "integer",
"format": "uint"
}
},
{
"name": "year",
"in": "query",
"description": "Year to retrieve quarterly data for",
"required": true,
"schema": {
"type": "integer",
"format": "uint",
"example": 2024
}
}
],
"responses": {
"200": {
"description": "Quarterly time data",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuarterData"
}
}
}
}
},
"400": {
"description": "Invalid repoID or year parameter, or user does not have access to the repository",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/restricted/repo/get-issues": {
"get": {
"tags": ["Repo"],
"summary": "Get issues with time summaries",
"description": "Returns a paginated list of issues with time tracking summaries for the given repository, year, and quarter. User must have access to the repository.",
"operationId": "getIssues",
"security": [
{
"CookieAuth": []
}
],
"parameters": [
{
"name": "repoID",
"in": "query",
"description": "Repository ID",
"required": true,
"schema": {
"type": "integer",
"format": "uint"
}
},
{
"name": "year",
"in": "query",
"description": "Year to filter issues by",
"required": true,
"schema": {
"type": "integer",
"format": "uint",
"example": 2024
}
},
{
"name": "quarter",
"in": "query",
"description": "Quarter number (1-4) to filter issues by",
"required": true,
"schema": {
"type": "integer",
"format": "uint",
"minimum": 1,
"maximum": 4,
"example": 2
}
},
{
"name": "page_number",
"in": "query",
"description": "Page number for pagination (1-based)",
"required": true,
"schema": {
"type": "integer",
"format": "uint",
"example": 1
}
},
{
"name": "elements_per_page",
"in": "query",
"description": "Number of items per page",
"required": true,
"schema": {
"type": "integer",
"format": "uint",
"example": 30
}
}
],
"responses": {
"200": {
"description": "Paginated list of issues with time summaries",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaginatedIssues"
}
}
}
},
"400": {
"description": "Invalid parameters or user does not have access to the repository",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
@@ -675,7 +1040,13 @@
},
"RegisterRequest": {
"type": "object",
"required": ["email", "password", "confirm_password"],
"required": [
"email",
"password",
"confirm_password",
"first_name",
"last_name"
],
"properties": {
"email": {
"type": "string",
@@ -685,7 +1056,7 @@
"password": {
"type": "string",
"format": "password",
"description": "User's password (min 8 chars, uppercase, lowercase, digit)"
"description": "User's password (must meet complexity requirements: min 8 chars, uppercase, lowercase, digit)"
},
"confirm_password": {
"type": "string",
@@ -694,15 +1065,15 @@
},
"first_name": {
"type": "string",
"description": "User's first name"
"description": "User's first name (required)"
},
"last_name": {
"type": "string",
"description": "User's last name"
"description": "User's last name (required)"
},
"lang": {
"type": "string",
"description": "User's preferred language (e.g., 'en', 'pl', 'cs')"
"description": "User's preferred language ISO code (e.g., 'en', 'pl', 'cs')"
}
}
},
@@ -712,7 +1083,7 @@
"properties": {
"token": {
"type": "string",
"description": "Email verification token"
"description": "Email verification token received via email"
}
}
},
@@ -722,12 +1093,12 @@
"properties": {
"token": {
"type": "string",
"description": "Password reset token"
"description": "Password reset token received via email"
},
"password": {
"type": "string",
"format": "password",
"description": "New password"
"description": "New password (must meet complexity requirements)"
}
}
},
@@ -738,17 +1109,13 @@
"type": "string",
"description": "JWT access token"
},
"refresh_token": {
"type": "string",
"description": "JWT refresh token"
},
"token_type": {
"type": "string",
"example": "Bearer"
},
"expires_in": {
"type": "integer",
"description": "Token expiration in seconds"
"description": "Access token expiration in seconds"
},
"user": {
"$ref": "#/components/schemas/UserSession"
@@ -780,6 +1147,10 @@
},
"last_name": {
"type": "string"
},
"lang": {
"type": "string",
"description": "User's preferred language ISO code (e.g., 'en', 'pl', 'cs')"
}
}
},
@@ -788,7 +1159,7 @@
"properties": {
"error": {
"type": "string",
"description": "Error message"
"description": "Translated error message"
}
}
},
@@ -838,6 +1209,75 @@
}
}
},
"QuarterData": {
"type": "object",
"description": "Time tracked in a specific quarter",
"properties": {
"time": {
"type": "number",
"format": "double",
"description": "Total hours tracked in this quarter"
},
"quarter": {
"type": "string",
"description": "Quarter identifier in format YYYY_QN (e.g., '2024_Q1')",
"example": "2024_Q1"
}
}
},
"IssueTimeSummary": {
"type": "object",
"description": "Time tracking summary for a single issue",
"properties": {
"issue_id": {
"type": "integer",
"format": "uint",
"description": "Issue ID"
},
"issue_name": {
"type": "string",
"description": "Issue title/name"
},
"user_id": {
"type": "integer",
"format": "uint",
"description": "ID of the user who tracked time"
},
"initials": {
"type": "string",
"description": "Abbreviated initials of the user (e.g., 'J.D.')"
},
"created_date": {
"type": "string",
"format": "date",
"description": "Date when time was tracked"
},
"total_hours_spent": {
"type": "number",
"format": "double",
"description": "Total hours spent on this issue on the given date (rounded to 2 decimal places)"
}
}
},
"PaginatedIssues": {
"type": "object",
"description": "Paginated list of issue time summaries",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/IssueTimeSummary"
},
"description": "List of issue time summaries for the current page"
},
"items_count": {
"type": "integer",
"format": "uint",
"description": "Total number of items across all pages",
"example": 56
}
}
},
"SettingsResponse": {
"type": "object",
"properties": {
@@ -872,6 +1312,10 @@
"base_url": {
"type": "string",
"description": "Base URL of the application"
},
"password_regex": {
"type": "string",
"description": "Regular expression for password validation"
}
}
},
@@ -893,7 +1337,7 @@
"properties": {
"jwt_expiration": {
"type": "integer",
"description": "JWT token expiration in seconds"
"description": "JWT access token expiration in seconds"
},
"refresh_expiration": {
"type": "integer",
@@ -919,25 +1363,32 @@
"properties": {
"version": {
"type": "string",
"description": "Application version"
"description": "Application version (git tag or commit hash)"
},
"commit": {
"type": "string",
"description": "Git commit hash"
"description": "Short git commit hash"
},
"date": {
"build_date": {
"type": "string",
"description": "Build date"
"format": "date-time",
"description": "Build date in RFC3339 format"
}
}
}
},
"securitySchemes": {
"CookieAuth": {
"type": "apiKey",
"in": "cookie",
"name": "access_token",
"description": "HTTPOnly JWT access token cookie set during login, registration, or token refresh"
},
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT token obtained from login response"
"description": "JWT token obtained from login response (alternative to cookie-based auth)"
}
}
}