aa8f7af55d
Add comprehensive offline synchronization support for habits and entries: ## Infrastructure (Phase 1) - Add UpdatedAt and DeletedAt timestamps to Habit and HabitEntry entities - Implement soft delete with Delete(), Touch(), and IsDeleted() methods - Create SQL migration with optimized composite indexes for sync queries - Add GetChangesSince() and SoftDelete() to both repositories - Update all Find* methods to exclude soft-deleted records - 13 comprehensive TDD tests for sync repository methods ## HTTP Endpoints (Phase 2) - GET /api/v1/sync/changes: retrieve all changes since timestamp - POST /api/v1/sync/batch: apply client changes with conflict resolution - Implement Last-Write-Wins strategy using UpdatedAt timestamps - Add authentication and rate limiting (100 req/min) - Validate user ownership for all sync operations - 9 tests for sync handlers (3 queries + 6 commands) ## Technical Details - Composite indexes: (user_id, updated_at) for optimal query performance - No pagination: atomic sync operations for data consistency - Upsert behavior: create resources if not found on server - DTOs with full entity state including timestamps - Swagger documentation updated for new endpoints All 220+ tests passing ✓
1341 lines
34 KiB
YAML
1341 lines
34 KiB
YAML
basePath: /api/v1
|
|
definitions:
|
|
http.AuthResponse:
|
|
properties:
|
|
refresh_token:
|
|
type: string
|
|
token:
|
|
type: string
|
|
user_id:
|
|
type: string
|
|
type: object
|
|
http.CreateHabitRequest:
|
|
properties:
|
|
carry_over:
|
|
type: boolean
|
|
description:
|
|
type: string
|
|
frequency:
|
|
$ref: '#/definitions/value_objects.Frequency'
|
|
is_negative:
|
|
type: boolean
|
|
name:
|
|
type: string
|
|
specific_dates:
|
|
items:
|
|
type: integer
|
|
type: array
|
|
specific_days:
|
|
items:
|
|
type: integer
|
|
type: array
|
|
target_value:
|
|
type: number
|
|
type:
|
|
$ref: '#/definitions/value_objects.HabitType'
|
|
type: object
|
|
http.EntryChangesDTO:
|
|
properties:
|
|
created:
|
|
items:
|
|
$ref: '#/definitions/http.SyncHabitEntryDTO'
|
|
type: array
|
|
deleted:
|
|
items:
|
|
type: string
|
|
type: array
|
|
updated:
|
|
items:
|
|
$ref: '#/definitions/http.SyncHabitEntryDTO'
|
|
type: array
|
|
type: object
|
|
http.ErrorResponse:
|
|
properties:
|
|
error:
|
|
type: string
|
|
type: object
|
|
http.ForgotPasswordRequest:
|
|
properties:
|
|
email:
|
|
type: string
|
|
type: object
|
|
http.GetUserHabitsResponse:
|
|
properties:
|
|
data:
|
|
items:
|
|
$ref: '#/definitions/http.UserHabitResponse'
|
|
type: array
|
|
pagination:
|
|
$ref: '#/definitions/pagination.Response'
|
|
type: object
|
|
http.HabitChangesDTO:
|
|
properties:
|
|
created:
|
|
items:
|
|
$ref: '#/definitions/http.SyncHabitDTO'
|
|
type: array
|
|
deleted:
|
|
items:
|
|
type: string
|
|
type: array
|
|
updated:
|
|
items:
|
|
$ref: '#/definitions/http.SyncHabitDTO'
|
|
type: array
|
|
type: object
|
|
http.HabitEntriesResponse:
|
|
properties:
|
|
entries:
|
|
items:
|
|
$ref: '#/definitions/http.HabitEntryResponse'
|
|
type: array
|
|
limit:
|
|
type: integer
|
|
page:
|
|
type: integer
|
|
total:
|
|
type: integer
|
|
type: object
|
|
http.HabitEntryResponse:
|
|
properties:
|
|
completed_at:
|
|
type: string
|
|
habit_id:
|
|
type: string
|
|
id:
|
|
type: string
|
|
scheduled_date:
|
|
type: string
|
|
value:
|
|
type: number
|
|
type: object
|
|
http.HealthResponse:
|
|
properties:
|
|
database:
|
|
type: string
|
|
smtp:
|
|
type: string
|
|
status:
|
|
type: string
|
|
uptime:
|
|
type: string
|
|
type: object
|
|
http.LoginRequest:
|
|
properties:
|
|
email:
|
|
type: string
|
|
password:
|
|
type: string
|
|
type: object
|
|
http.LogoutRequest:
|
|
properties:
|
|
refresh_token:
|
|
type: string
|
|
type: object
|
|
http.MarkHabitRequest:
|
|
properties:
|
|
scheduled_date:
|
|
type: string
|
|
value:
|
|
type: number
|
|
type: object
|
|
http.RefreshRequest:
|
|
properties:
|
|
refresh_token:
|
|
type: string
|
|
type: object
|
|
http.RegisterRequest:
|
|
properties:
|
|
email:
|
|
type: string
|
|
password:
|
|
type: string
|
|
type: object
|
|
http.RegisterResponse:
|
|
properties:
|
|
message:
|
|
type: string
|
|
user_id:
|
|
type: string
|
|
type: object
|
|
http.ResendVerificationRequest:
|
|
properties:
|
|
email:
|
|
type: string
|
|
type: object
|
|
http.ResetPasswordRequest:
|
|
properties:
|
|
new_password:
|
|
type: string
|
|
token:
|
|
type: string
|
|
type: object
|
|
http.SyncBatchRequest:
|
|
properties:
|
|
entries:
|
|
$ref: '#/definitions/http.EntryChangesDTO'
|
|
habits:
|
|
$ref: '#/definitions/http.HabitChangesDTO'
|
|
type: object
|
|
http.SyncChangesResponse:
|
|
properties:
|
|
entries:
|
|
$ref: '#/definitions/http.EntryChangesDTO'
|
|
habits:
|
|
$ref: '#/definitions/http.HabitChangesDTO'
|
|
type: object
|
|
http.SyncHabitDTO:
|
|
properties:
|
|
archived_at:
|
|
type: string
|
|
carry_over:
|
|
type: boolean
|
|
created_at:
|
|
type: string
|
|
description:
|
|
type: string
|
|
frequency:
|
|
$ref: '#/definitions/value_objects.Frequency'
|
|
id:
|
|
type: string
|
|
is_negative:
|
|
type: boolean
|
|
name:
|
|
type: string
|
|
specific_dates:
|
|
items:
|
|
type: integer
|
|
type: array
|
|
specific_days:
|
|
items:
|
|
type: integer
|
|
type: array
|
|
target_value:
|
|
type: number
|
|
type:
|
|
$ref: '#/definitions/value_objects.HabitType'
|
|
updated_at:
|
|
type: string
|
|
user_id:
|
|
type: string
|
|
type: object
|
|
http.SyncHabitEntryDTO:
|
|
properties:
|
|
completed_at:
|
|
type: string
|
|
habit_id:
|
|
type: string
|
|
id:
|
|
type: string
|
|
scheduled_date:
|
|
type: string
|
|
updated_at:
|
|
type: string
|
|
value:
|
|
type: number
|
|
type: object
|
|
http.TodaysHabitEntryResponse:
|
|
properties:
|
|
completed_at:
|
|
type: string
|
|
id:
|
|
type: string
|
|
value:
|
|
type: number
|
|
type: object
|
|
http.TodaysHabitResponse:
|
|
properties:
|
|
entry:
|
|
$ref: '#/definitions/http.TodaysHabitEntryResponse'
|
|
id:
|
|
type: string
|
|
is_carried_over:
|
|
type: boolean
|
|
is_negative:
|
|
type: boolean
|
|
name:
|
|
type: string
|
|
scheduled_date:
|
|
type: string
|
|
target_value:
|
|
type: number
|
|
type:
|
|
$ref: '#/definitions/value_objects.HabitType'
|
|
type: object
|
|
http.UpdateHabitRequest:
|
|
properties:
|
|
carry_over:
|
|
type: boolean
|
|
description:
|
|
type: string
|
|
name:
|
|
type: string
|
|
specific_dates:
|
|
items:
|
|
type: integer
|
|
type: array
|
|
specific_days:
|
|
items:
|
|
type: integer
|
|
type: array
|
|
target_value:
|
|
type: number
|
|
type: object
|
|
http.UserHabitResponse:
|
|
properties:
|
|
carry_over:
|
|
type: boolean
|
|
frequency:
|
|
$ref: '#/definitions/value_objects.Frequency'
|
|
id:
|
|
type: string
|
|
is_negative:
|
|
type: boolean
|
|
name:
|
|
type: string
|
|
specific_days:
|
|
items:
|
|
type: integer
|
|
type: array
|
|
target_value:
|
|
type: number
|
|
type:
|
|
$ref: '#/definitions/value_objects.HabitType'
|
|
type: object
|
|
http.ValidationErrorResponse:
|
|
properties:
|
|
error:
|
|
type: string
|
|
field:
|
|
type: string
|
|
type: object
|
|
http.VerifyEmailRequest:
|
|
properties:
|
|
token:
|
|
type: string
|
|
type: object
|
|
pagination.Response:
|
|
properties:
|
|
page:
|
|
type: integer
|
|
page_size:
|
|
type: integer
|
|
total_items:
|
|
type: integer
|
|
total_pages:
|
|
type: integer
|
|
type: object
|
|
queries.ExportEntryDTO:
|
|
properties:
|
|
completed_at:
|
|
type: string
|
|
habit_id:
|
|
type: string
|
|
id:
|
|
type: string
|
|
scheduled_date:
|
|
type: string
|
|
value:
|
|
type: number
|
|
type: object
|
|
queries.ExportHabitDTO:
|
|
properties:
|
|
archived_at:
|
|
type: string
|
|
carry_over:
|
|
type: boolean
|
|
created_at:
|
|
type: string
|
|
description:
|
|
type: string
|
|
frequency:
|
|
$ref: '#/definitions/value_objects.Frequency'
|
|
id:
|
|
type: string
|
|
is_negative:
|
|
type: boolean
|
|
name:
|
|
type: string
|
|
specific_dates:
|
|
items:
|
|
type: integer
|
|
type: array
|
|
specific_days:
|
|
items:
|
|
type: integer
|
|
type: array
|
|
target_value:
|
|
type: number
|
|
type:
|
|
$ref: '#/definitions/value_objects.HabitType'
|
|
type: object
|
|
queries.ExportUserDataResult:
|
|
properties:
|
|
entries:
|
|
items:
|
|
$ref: '#/definitions/queries.ExportEntryDTO'
|
|
type: array
|
|
exported_at:
|
|
type: string
|
|
habits:
|
|
items:
|
|
$ref: '#/definitions/queries.ExportHabitDTO'
|
|
type: array
|
|
type: object
|
|
queries.HabitStatsDTO:
|
|
properties:
|
|
completion_rate:
|
|
type: number
|
|
completions_this_month:
|
|
type: integer
|
|
completions_this_week:
|
|
type: integer
|
|
current_streak:
|
|
type: integer
|
|
habit_id:
|
|
type: string
|
|
habit_name:
|
|
type: string
|
|
longest_streak:
|
|
type: integer
|
|
total_completions:
|
|
type: integer
|
|
type: object
|
|
value_objects.Frequency:
|
|
enum:
|
|
- DAILY
|
|
- WEEKLY
|
|
- MONTHLY
|
|
type: string
|
|
x-enum-varnames:
|
|
- FrequencyDaily
|
|
- FrequencyWeekly
|
|
- FrequencyMonthly
|
|
value_objects.HabitType:
|
|
enum:
|
|
- BOOLEAN
|
|
- COUNTER
|
|
- VALUE
|
|
type: string
|
|
x-enum-varnames:
|
|
- HabitTypeBoolean
|
|
- HabitTypeCounter
|
|
- HabitTypeValue
|
|
info:
|
|
contact:
|
|
email: contact@apocapoc.app
|
|
name: API Support
|
|
url: https://github.com/davidfolch/apocapoc-api
|
|
description: Self-hosted habit tracking service
|
|
license:
|
|
name: MIT
|
|
url: https://opensource.org/licenses/MIT
|
|
termsOfService: http://swagger.io/terms/
|
|
title: Apocapoc API
|
|
paths:
|
|
/auth/forgot-password:
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: Request a password reset email with a reset token
|
|
parameters:
|
|
- description: User email
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.ForgotPasswordRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: Reset email sent successfully
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"400":
|
|
description: Invalid email
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"403":
|
|
description: Email not verified
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: User not found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal server error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
summary: Request password reset
|
|
tags:
|
|
- auth
|
|
/auth/login:
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: Authenticate user with email and password. Returns both access
|
|
token and refresh token. The access token is used for API requests, the refresh
|
|
token is used to obtain new access tokens when they expire.
|
|
parameters:
|
|
- description: Login credentials
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.LoginRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: Returns access token, refresh token, and user ID
|
|
schema:
|
|
$ref: '#/definitions/http.AuthResponse'
|
|
"400":
|
|
description: Invalid request body
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"401":
|
|
description: Invalid email or password
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"403":
|
|
description: Email not verified
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal server error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
summary: Login user
|
|
tags:
|
|
- auth
|
|
/auth/logout:
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: Revoke the refresh token to invalidate the user session. After
|
|
logout, the refresh token cannot be used to obtain new access tokens. The
|
|
user will need to login again. Always call this endpoint before clearing tokens
|
|
from client storage to ensure proper session termination.
|
|
parameters:
|
|
- description: Refresh token to revoke
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.LogoutRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: Successfully logged out - the refresh token is now invalid
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"400":
|
|
description: Invalid request body or missing refresh token
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: Refresh token not found (already revoked or never existed)
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal server error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
summary: Logout user
|
|
tags:
|
|
- auth
|
|
/auth/refresh:
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: 'Exchange a valid refresh token for a new access token and refresh
|
|
token pair. IMPORTANT: The old refresh token is automatically revoked and
|
|
you receive a NEW refresh token - always update both tokens in storage. Use
|
|
this endpoint when the access token expires to maintain the user session without
|
|
requiring re-login.'
|
|
parameters:
|
|
- description: Current refresh token
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.RefreshRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: Returns NEW access token and NEW refresh token - the old refresh
|
|
token is now invalid
|
|
schema:
|
|
$ref: '#/definitions/http.AuthResponse'
|
|
"400":
|
|
description: Invalid request body
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"401":
|
|
description: Invalid or expired refresh token
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal server error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
summary: Refresh access token
|
|
tags:
|
|
- auth
|
|
/auth/register:
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: Create a new user account. If email verification is enabled, you
|
|
will receive a verification email. Otherwise, you can login immediately.
|
|
parameters:
|
|
- description: 'Registration data (password requires: min 8 chars, uppercase,
|
|
lowercase, digit, special char)'
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.RegisterRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"201":
|
|
description: Returns user ID and message about next steps
|
|
schema:
|
|
$ref: '#/definitions/http.RegisterResponse'
|
|
"400":
|
|
description: 'Invalid input: email format or password requirements'
|
|
schema:
|
|
$ref: '#/definitions/http.ValidationErrorResponse'
|
|
"403":
|
|
description: Registration is closed
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"409":
|
|
description: Email already registered
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal server error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
summary: Register a new user
|
|
tags:
|
|
- auth
|
|
/auth/resend-verification:
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: Resend the email verification link to the user's email address
|
|
parameters:
|
|
- description: User email
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.ResendVerificationRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: Verification email sent
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"400":
|
|
description: Invalid email
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: User not found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"409":
|
|
description: Email already verified
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal server error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
summary: Resend verification email
|
|
tags:
|
|
- auth
|
|
/auth/reset-password:
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: Reset user password using the reset token from email
|
|
parameters:
|
|
- description: Reset token and new password
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.ResetPasswordRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: Password reset successfully
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"400":
|
|
description: Invalid token or password requirements not met
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: User not found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal server error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
summary: Reset password
|
|
tags:
|
|
- auth
|
|
/auth/verify-email:
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: Verify user email address using the token sent via email
|
|
parameters:
|
|
- description: Verification token
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.VerifyEmailRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: Email verified successfully
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"400":
|
|
description: Invalid or expired token
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"409":
|
|
description: Email already verified
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal server error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
summary: Verify email address
|
|
tags:
|
|
- auth
|
|
/export:
|
|
get:
|
|
description: Export all user habits and entries in JSON format with gzip compression.
|
|
Limited to 1 export per hour.
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: Compressed JSON export
|
|
schema:
|
|
$ref: '#/definitions/queries.ExportUserDataResult'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"429":
|
|
description: Rate limit exceeded
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal server error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Export user data
|
|
tags:
|
|
- export
|
|
/habits:
|
|
get:
|
|
description: Get all active habits for the authenticated user with optional
|
|
pagination and filters
|
|
parameters:
|
|
- description: 'Page number (default: 1)'
|
|
in: query
|
|
name: page
|
|
type: integer
|
|
- description: 'Page size (default: 50, max: 100)'
|
|
in: query
|
|
name: page_size
|
|
type: integer
|
|
- description: Filter by type (BOOLEAN, COUNTER, VALUE)
|
|
in: query
|
|
name: type
|
|
type: string
|
|
- description: Filter by frequency (DAILY, WEEKLY, MONTHLY)
|
|
in: query
|
|
name: frequency
|
|
type: string
|
|
- description: 'Include archived habits (default: false)'
|
|
in: query
|
|
name: archived
|
|
type: boolean
|
|
- description: Search by name or description
|
|
in: query
|
|
name: search
|
|
type: string
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/http.GetUserHabitsResponse'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Get all user habits
|
|
tags:
|
|
- habits
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: Create a new habit for the authenticated user
|
|
parameters:
|
|
- description: Habit data
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.CreateHabitRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"400":
|
|
description: Bad Request
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Create a new habit
|
|
tags:
|
|
- habits
|
|
/habits/{id}:
|
|
delete:
|
|
description: Archive (soft delete) a habit
|
|
parameters:
|
|
- description: Habit ID
|
|
in: path
|
|
name: id
|
|
required: true
|
|
type: string
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"403":
|
|
description: Forbidden
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: Not Found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Archive habit
|
|
tags:
|
|
- habits
|
|
get:
|
|
description: Get a specific habit by ID
|
|
parameters:
|
|
- description: Habit ID
|
|
in: path
|
|
name: id
|
|
required: true
|
|
type: string
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/http.UserHabitResponse'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"403":
|
|
description: Forbidden
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: Not Found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Get habit by ID
|
|
tags:
|
|
- habits
|
|
put:
|
|
consumes:
|
|
- application/json
|
|
description: Update an existing habit
|
|
parameters:
|
|
- description: Habit ID
|
|
in: path
|
|
name: id
|
|
required: true
|
|
type: string
|
|
- description: Update data
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.UpdateHabitRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"400":
|
|
description: Bad Request
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"403":
|
|
description: Forbidden
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: Not Found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Update habit
|
|
tags:
|
|
- habits
|
|
/habits/{id}/entries:
|
|
get:
|
|
description: Get entries (completion history) for a habit with optional date
|
|
filtering and pagination
|
|
parameters:
|
|
- description: Habit ID
|
|
in: path
|
|
name: id
|
|
required: true
|
|
type: string
|
|
- description: Start date (YYYY-MM-DD)
|
|
in: query
|
|
name: from
|
|
type: string
|
|
- description: End date (YYYY-MM-DD)
|
|
in: query
|
|
name: to
|
|
type: string
|
|
- description: Page number
|
|
in: query
|
|
name: page
|
|
type: integer
|
|
- description: Page size (max 100)
|
|
in: query
|
|
name: limit
|
|
type: integer
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/http.HabitEntriesResponse'
|
|
"400":
|
|
description: Bad Request
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"403":
|
|
description: Forbidden
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: Not Found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Get habit entries
|
|
tags:
|
|
- habits
|
|
/habits/{id}/entries/{date}:
|
|
delete:
|
|
description: Delete a habit entry (unmark completion)
|
|
parameters:
|
|
- description: Habit ID
|
|
in: path
|
|
name: id
|
|
required: true
|
|
type: string
|
|
- description: Date (YYYY-MM-DD)
|
|
in: path
|
|
name: date
|
|
required: true
|
|
type: string
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"400":
|
|
description: Bad Request
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"403":
|
|
description: Forbidden
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: Not Found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Unmark habit
|
|
tags:
|
|
- habits
|
|
/habits/{id}/mark:
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: Mark a habit as completed for a specific date
|
|
parameters:
|
|
- description: Habit ID
|
|
in: path
|
|
name: id
|
|
required: true
|
|
type: string
|
|
- description: Mark data
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.MarkHabitRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"400":
|
|
description: Bad Request
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: Not Found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"409":
|
|
description: Conflict
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Mark habit as complete
|
|
tags:
|
|
- habits
|
|
/habits/today:
|
|
get:
|
|
description: Get all habits scheduled for today for the authenticated user.
|
|
Includes the entry for today if it exists. Requires timezone as query parameter
|
|
(e.g., ?timezone=America/New_York).
|
|
parameters:
|
|
- description: IANA timezone (e.g., 'America/New_York', 'Europe/Madrid', 'UTC')
|
|
in: query
|
|
name: timezone
|
|
required: true
|
|
type: string
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
items:
|
|
$ref: '#/definitions/http.TodaysHabitResponse'
|
|
type: array
|
|
"400":
|
|
description: Invalid or missing timezone
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Get today's habits
|
|
tags:
|
|
- habits
|
|
/health:
|
|
get:
|
|
description: Get API health status including database connectivity and uptime
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/http.HealthResponse'
|
|
"503":
|
|
description: Service Unavailable
|
|
schema:
|
|
$ref: '#/definitions/http.HealthResponse'
|
|
summary: Health check
|
|
tags:
|
|
- system
|
|
/stats/habits/{id}:
|
|
get:
|
|
description: Get statistics for a specific habit including streaks and completion
|
|
rates
|
|
parameters:
|
|
- description: Habit ID
|
|
in: path
|
|
name: id
|
|
required: true
|
|
type: string
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/queries.HabitStatsDTO'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"403":
|
|
description: Forbidden
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: Not Found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Get habit statistics
|
|
tags:
|
|
- stats
|
|
/sync/batch:
|
|
post:
|
|
consumes:
|
|
- application/json
|
|
description: Apply a batch of changes from the client for offline sync (Last-Write-Wins)
|
|
parameters:
|
|
- description: Sync batch data
|
|
in: body
|
|
name: request
|
|
required: true
|
|
schema:
|
|
$ref: '#/definitions/http.SyncBatchRequest'
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"400":
|
|
description: Bad Request
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Apply sync batch
|
|
tags:
|
|
- sync
|
|
/sync/changes:
|
|
get:
|
|
consumes:
|
|
- application/json
|
|
description: Get all changes (habits and entries) since a given timestamp for
|
|
offline sync
|
|
parameters:
|
|
- description: ISO 8601 timestamp (e.g., 2025-01-01T00:00:00Z)
|
|
in: query
|
|
name: since
|
|
required: true
|
|
type: string
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/http.SyncChangesResponse'
|
|
"400":
|
|
description: Bad Request
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"401":
|
|
description: Unauthorized
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal Server Error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Get sync changes
|
|
tags:
|
|
- sync
|
|
/users/me:
|
|
delete:
|
|
description: Permanently delete the authenticated user's account and all associated
|
|
data (habits, entries, tokens). This action cannot be undone.
|
|
produces:
|
|
- application/json
|
|
responses:
|
|
"200":
|
|
description: Account deleted successfully
|
|
schema:
|
|
additionalProperties:
|
|
type: string
|
|
type: object
|
|
"401":
|
|
description: Unauthorized - invalid or missing token
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"404":
|
|
description: User not found
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
"500":
|
|
description: Internal server error
|
|
schema:
|
|
$ref: '#/definitions/http.ErrorResponse'
|
|
security:
|
|
- BearerAuth: []
|
|
summary: Delete user account
|
|
tags:
|
|
- users
|
|
securityDefinitions:
|
|
BearerAuth:
|
|
description: Type "Bearer" followed by a space and JWT token.
|
|
in: header
|
|
name: Authorization
|
|
type: apiKey
|
|
swagger: "2.0"
|