feat: add swagger docs
Docker Build and Push / Build and Push Docker Image (push) Successful in 17m14s

This commit is contained in:
2026-02-22 18:36:47 +07:00
parent 140671445c
commit de821b2762
10 changed files with 3075 additions and 0 deletions
+1056
View File
File diff suppressed because it is too large Load Diff
+1032
View File
File diff suppressed because it is too large Load Diff
+675
View File
@@ -0,0 +1,675 @@
basePath: /
definitions:
handler.AnswerInput:
properties:
answer:
type: string
question_id:
type: string
type: object
handler.AnswerResponse:
properties:
answer:
type: string
question_id:
type: string
type: object
handler.Auth:
properties:
email:
type: string
password:
type: string
type: object
handler.ChangePasswordRequest:
properties:
new_password:
type: string
old_password:
type: string
type: object
handler.CreateFormRequest:
properties:
description:
type: string
questions:
items:
$ref: '#/definitions/handler.QuestionInput'
type: array
title:
type: string
type: object
handler.FormResponse:
properties:
created_at:
type: string
description:
type: string
id:
type: string
questions:
items:
$ref: '#/definitions/handler.QuestionResponse'
type: array
response_count:
type: integer
title:
type: string
updated_at:
type: string
user_id:
type: string
type: object
handler.LogoutRequest:
properties:
refresh_token:
type: string
type: object
handler.QuestionInput:
properties:
options:
items:
$ref: '#/definitions/handler.QuestionOptionInput'
type: array
position:
type: integer
required:
type: boolean
title:
type: string
type:
type: string
type: object
handler.QuestionOptionInput:
properties:
label:
type: string
position:
type: integer
type: object
handler.QuestionOptionResponse:
properties:
id:
type: integer
label:
type: string
position:
type: integer
type: object
handler.QuestionResponse:
properties:
id:
type: string
options:
items:
$ref: '#/definitions/handler.QuestionOptionResponse'
type: array
position:
type: integer
required:
type: boolean
title:
type: string
type:
type: string
type: object
handler.RefreshRequest:
properties:
refresh_token:
type: string
type: object
handler.SubmitResponseRequest:
properties:
answers:
items:
$ref: '#/definitions/handler.AnswerInput'
type: array
type: object
handler.SubmitResponseResponse:
properties:
answers:
items:
$ref: '#/definitions/handler.AnswerResponse'
type: array
form_id:
type: string
id:
type: string
submitted_at:
type: string
type: object
handler.UpdateFormRequest:
properties:
description:
type: string
questions:
items:
$ref: '#/definitions/handler.QuestionInput'
type: array
title:
type: string
type: object
host: localhost:8080
info:
contact: {}
description: REST API for Ristek Task Backend
title: Ristek Task API
version: "1.0"
paths:
/api/auth/login:
post:
consumes:
- application/json
description: Authenticate with email and password to receive access and refresh
tokens
parameters:
- description: Login credentials
in: body
name: body
required: true
schema:
$ref: '#/definitions/handler.Auth'
produces:
- application/json
responses:
"200":
description: access_token, refresh_token, expires_in
schema:
additionalProperties: true
type: object
"400":
description: Bad request
schema:
type: string
"401":
description: Unauthorized (invalid credentials)
schema:
type: string
"500":
description: Internal server error
schema:
type: string
summary: Login
tags:
- auth
/api/auth/logout:
post:
consumes:
- application/json
description: Invalidate the given refresh token to log out the current session
parameters:
- description: Refresh token to invalidate
in: body
name: body
required: true
schema:
$ref: '#/definitions/handler.LogoutRequest'
produces:
- application/json
responses:
"204":
description: No Content
"400":
description: Bad request
schema:
type: string
"500":
description: Internal server error
schema:
type: string
summary: Logout
tags:
- auth
/api/auth/logout/all:
delete:
description: Invalidate all refresh tokens for the authenticated user
produces:
- application/json
responses:
"204":
description: No Content
"401":
description: Unauthorized
schema:
type: string
"500":
description: Internal server error
schema:
type: string
security:
- BearerAuth: []
summary: Logout all sessions
tags:
- auth
/api/auth/me:
get:
description: Return profile information for the authenticated user
produces:
- application/json
responses:
"200":
description: id, email, created_at, updated_at
schema:
additionalProperties: true
type: object
"401":
description: Unauthorized
schema:
type: string
"500":
description: Internal server error
schema:
type: string
security:
- BearerAuth: []
summary: Get current user
tags:
- auth
/api/auth/me/password:
patch:
consumes:
- application/json
description: Update the password of the currently authenticated user
parameters:
- description: Old and new password
in: body
name: body
required: true
schema:
$ref: '#/definitions/handler.ChangePasswordRequest'
produces:
- application/json
responses:
"204":
description: No Content
"400":
description: Bad request (e.g. incorrect old password)
schema:
type: string
"401":
description: Unauthorized
schema:
type: string
"500":
description: Internal server error
schema:
type: string
security:
- BearerAuth: []
summary: Change password
tags:
- auth
/api/auth/refresh:
post:
consumes:
- application/json
description: Exchange a valid refresh token for a new access token and refresh
token pair
parameters:
- description: Refresh token payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/handler.RefreshRequest'
produces:
- application/json
responses:
"200":
description: access_token, refresh_token, expires_in
schema:
additionalProperties: true
type: object
"400":
description: Bad request
schema:
type: string
"401":
description: Unauthorized (invalid or expired refresh token)
schema:
type: string
"500":
description: Internal server error
schema:
type: string
summary: Refresh access token
tags:
- auth
/api/auth/register:
post:
consumes:
- application/json
description: Create a new user account with email and password
parameters:
- description: Register credentials
in: body
name: body
required: true
schema:
$ref: '#/definitions/handler.Auth'
produces:
- application/json
responses:
"201":
description: Created
"400":
description: Bad request (e.g. email already exists, password too short)
schema:
type: string
"500":
description: Internal server error
schema:
type: string
summary: Register a new user
tags:
- auth
/api/form:
post:
consumes:
- application/json
description: Create a new form with title, description, and questions for the
authenticated user
parameters:
- description: Form creation payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/handler.CreateFormRequest'
produces:
- application/json
responses:
"201":
description: Created form
schema:
$ref: '#/definitions/handler.FormResponse'
"400":
description: Bad request (e.g. title is required)
schema:
type: string
"401":
description: Unauthorized
schema:
type: string
"500":
description: Internal server error
schema:
type: string
security:
- BearerAuth: []
summary: Create a form
tags:
- forms
/api/form/{id}:
delete:
description: Delete a form by ID. Only the form owner can delete it. Deletion
is blocked if the form already has responses
parameters:
- description: Form ID (UUID)
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"204":
description: No Content
"400":
description: Invalid form ID
schema:
type: string
"401":
description: Unauthorized
schema:
type: string
"403":
description: Forbidden (not the form owner)
schema:
type: string
"404":
description: Form not found
schema:
type: string
"409":
description: Conflict (form already has responses)
schema:
type: string
"500":
description: Internal server error
schema:
type: string
security:
- BearerAuth: []
summary: Delete a form
tags:
- forms
get:
description: Retrieve a form and its questions by form ID (publicly accessible)
parameters:
- description: Form ID (UUID)
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Form details
schema:
$ref: '#/definitions/handler.FormResponse'
"400":
description: Invalid form ID
schema:
type: string
"404":
description: Form not found
schema:
type: string
"500":
description: Internal server error
schema:
type: string
summary: Get a form
tags:
- forms
put:
consumes:
- application/json
description: Replace a form's title, description, and questions. Only the form
owner can update it
parameters:
- description: Form ID (UUID)
in: path
name: id
required: true
type: string
- description: Form update payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/handler.UpdateFormRequest'
produces:
- application/json
responses:
"200":
description: Updated form
schema:
$ref: '#/definitions/handler.FormResponse'
"400":
description: Bad request (e.g. title is required)
schema:
type: string
"401":
description: Unauthorized
schema:
type: string
"403":
description: Forbidden (not the form owner)
schema:
type: string
"404":
description: Form not found
schema:
type: string
"500":
description: Internal server error
schema:
type: string
security:
- BearerAuth: []
summary: Update a form
tags:
- forms
/api/form/{id}/response:
post:
consumes:
- application/json
description: Submit answers to a form's questions. Authentication is optional
(anonymous submissions allowed). Required questions must be answered. Choice
answers must match valid options
parameters:
- description: Form ID (UUID)
in: path
name: id
required: true
type: string
- description: Response answers payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/handler.SubmitResponseRequest'
produces:
- application/json
responses:
"201":
description: Submitted response
schema:
$ref: '#/definitions/handler.SubmitResponseResponse'
"400":
description: Bad request (e.g. invalid answer, missing required question)
schema:
type: string
"404":
description: Form not found
schema:
type: string
"500":
description: Internal server error
schema:
type: string
security:
- BearerAuth: []
summary: Submit a form response
tags:
- forms
/api/form/{id}/responses:
get:
description: Retrieve all submitted responses for a form. Only the form owner
can access this
parameters:
- description: Form ID (UUID)
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: List of form responses with answers
schema:
items:
$ref: '#/definitions/handler.SubmitResponseResponse'
type: array
"400":
description: Invalid form ID
schema:
type: string
"401":
description: Unauthorized
schema:
type: string
"403":
description: Forbidden (not the form owner)
schema:
type: string
"404":
description: Form not found
schema:
type: string
"500":
description: Internal server error
schema:
type: string
security:
- BearerAuth: []
summary: Get form responses
tags:
- forms
/api/forms:
get:
description: Retrieve all forms belonging to the authenticated user, with optional
search, status filter, and sort options
parameters:
- description: Filter by title (case-insensitive)
in: query
name: search
type: string
- description: 'Filter by response status: has_responses | no_responses'
in: query
name: status
type: string
- description: 'Sort field: created_at (default) | updated_at'
in: query
name: sort_by
type: string
- description: 'Sort direction: newest (default) | oldest'
in: query
name: sort_dir
type: string
produces:
- application/json
responses:
"200":
description: List of forms
schema:
items:
$ref: '#/definitions/handler.FormResponse'
type: array
"401":
description: Unauthorized
schema:
type: string
"500":
description: Internal server error
schema:
type: string
security:
- BearerAuth: []
summary: List forms
tags:
- forms
/health:
get:
description: Returns 200 OK if the server is running
produces:
- text/plain
responses:
"200":
description: OK
schema:
type: string
summary: Health check
tags:
- health
securityDefinitions:
BearerAuth:
description: 'Enter your bearer token in the format: Bearer {token}'
in: header
name: Authorization
type: apiKey
swagger: "2.0"
+17
View File
@@ -11,20 +11,37 @@ require (
) )
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/goccy/go-json v0.10.3 // indirect github.com/goccy/go-json v0.10.3 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/segmentio/asm v1.2.1 // indirect github.com/segmentio/asm v1.2.1 // indirect
github.com/swaggo/files v1.0.1 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/swaggo/http-swagger/v2 v2.0.2 // indirect
github.com/swaggo/swag v1.16.6 // indirect
github.com/valyala/fastjson v1.6.7 // indirect github.com/valyala/fastjson v1.6.7 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )
+83
View File
@@ -1,8 +1,29 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -17,6 +38,12 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
@@ -31,27 +58,83 @@ github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0
github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU= github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+86
View File
@@ -30,6 +30,18 @@ func isDuplicateError(err error) bool {
return false return false
} }
// RegisterPost registers a new user account
//
// @Summary Register a new user
// @Description Create a new user account with email and password
// @Tags auth
// @Accept json
// @Produce json
// @Param body body Auth true "Register credentials"
// @Success 201
// @Failure 400 {string} string "Bad request (e.g. email already exists, password too short)"
// @Failure 500 {string} string "Internal server error"
// @Router /api/auth/register [post]
func (h *Handler) RegisterPost(w http.ResponseWriter, r *http.Request) { func (h *Handler) RegisterPost(w http.ResponseWriter, r *http.Request) {
var register Auth var register Auth
if err := json.NewDecoder(r.Body).Decode(&register); err != nil { if err := json.NewDecoder(r.Body).Decode(&register); err != nil {
@@ -83,6 +95,19 @@ type RefreshRequest struct {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
} }
// RefreshPost refreshes the access token using a refresh token
//
// @Summary Refresh access token
// @Description Exchange a valid refresh token for a new access token and refresh token pair
// @Tags auth
// @Accept json
// @Produce json
// @Param body body RefreshRequest true "Refresh token payload"
// @Success 200 {object} map[string]interface{} "access_token, refresh_token, expires_in"
// @Failure 400 {string} string "Bad request"
// @Failure 401 {string} string "Unauthorized (invalid or expired refresh token)"
// @Failure 500 {string} string "Internal server error"
// @Router /api/auth/refresh [post]
func (h *Handler) RefreshPost(w http.ResponseWriter, r *http.Request) { func (h *Handler) RefreshPost(w http.ResponseWriter, r *http.Request) {
var req RefreshRequest var req RefreshRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -164,6 +189,18 @@ type LogoutRequest struct {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
} }
// LogoutPost logs out the current session by invalidating the refresh token
//
// @Summary Logout
// @Description Invalidate the given refresh token to log out the current session
// @Tags auth
// @Accept json
// @Produce json
// @Param body body LogoutRequest true "Refresh token to invalidate"
// @Success 204
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /api/auth/logout [post]
func (h *Handler) LogoutPost(w http.ResponseWriter, r *http.Request) { func (h *Handler) LogoutPost(w http.ResponseWriter, r *http.Request) {
var req LogoutRequest var req LogoutRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -189,6 +226,17 @@ func (h *Handler) LogoutPost(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} }
// LogoutAllDelete logs out all sessions for the authenticated user
//
// @Summary Logout all sessions
// @Description Invalidate all refresh tokens for the authenticated user
// @Tags auth
// @Produce json
// @Security BearerAuth
// @Success 204
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /api/auth/logout/all [delete]
func (h *Handler) LogoutAllDelete(w http.ResponseWriter, r *http.Request) { func (h *Handler) LogoutAllDelete(w http.ResponseWriter, r *http.Request) {
userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string) userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string)
if !ok || userIDStr == "" { if !ok || userIDStr == "" {
@@ -214,6 +262,17 @@ func (h *Handler) LogoutAllDelete(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} }
// MeGet returns the profile of the currently authenticated user
//
// @Summary Get current user
// @Description Return profile information for the authenticated user
// @Tags auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{} "id, email, created_at, updated_at"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /api/auth/me [get]
func (h *Handler) MeGet(w http.ResponseWriter, r *http.Request) { func (h *Handler) MeGet(w http.ResponseWriter, r *http.Request) {
userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string) userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string)
if !ok || userIDStr == "" { if !ok || userIDStr == "" {
@@ -256,6 +315,20 @@ type ChangePasswordRequest struct {
NewPassword string `json:"new_password"` NewPassword string `json:"new_password"`
} }
// MePasswordPatch changes the password of the authenticated user
//
// @Summary Change password
// @Description Update the password of the currently authenticated user
// @Tags auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body ChangePasswordRequest true "Old and new password"
// @Success 204
// @Failure 400 {string} string "Bad request (e.g. incorrect old password)"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /api/auth/me/password [patch]
func (h *Handler) MePasswordPatch(w http.ResponseWriter, r *http.Request) { func (h *Handler) MePasswordPatch(w http.ResponseWriter, r *http.Request) {
userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string) userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string)
if !ok || userIDStr == "" { if !ok || userIDStr == "" {
@@ -320,6 +393,19 @@ func (h *Handler) MePasswordPatch(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} }
// LoginPost authenticates a user and returns tokens
//
// @Summary Login
// @Description Authenticate with email and password to receive access and refresh tokens
// @Tags auth
// @Accept json
// @Produce json
// @Param body body Auth true "Login credentials"
// @Success 200 {object} map[string]interface{} "access_token, refresh_token, expires_in"
// @Failure 400 {string} string "Bad request"
// @Failure 401 {string} string "Unauthorized (invalid credentials)"
// @Failure 500 {string} string "Internal server error"
// @Router /api/auth/login [post]
func (h *Handler) LoginPost(w http.ResponseWriter, r *http.Request) { func (h *Handler) LoginPost(w http.ResponseWriter, r *http.Request) {
var login Auth var login Auth
if err := json.NewDecoder(r.Body).Decode(&login); err != nil { if err := json.NewDecoder(r.Body).Decode(&login); err != nil {
+104
View File
@@ -137,6 +137,21 @@ func isChoiceType(t repository.QuestionType) bool {
return false return false
} }
// FormsGet returns all forms owned by the authenticated user
//
// @Summary List forms
// @Description Retrieve all forms belonging to the authenticated user, with optional search, status filter, and sort options
// @Tags forms
// @Produce json
// @Security BearerAuth
// @Param search query string false "Filter by title (case-insensitive)"
// @Param status query string false "Filter by response status: has_responses | no_responses"
// @Param sort_by query string false "Sort field: created_at (default) | updated_at"
// @Param sort_dir query string false "Sort direction: newest (default) | oldest"
// @Success 200 {array} FormResponse "List of forms"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /api/forms [get]
func (h *Handler) FormsGet(w http.ResponseWriter, r *http.Request) { func (h *Handler) FormsGet(w http.ResponseWriter, r *http.Request) {
userID, ok := h.currentUserID(r) userID, ok := h.currentUserID(r)
if !ok { if !ok {
@@ -223,6 +238,20 @@ func (h *Handler) FormsGet(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(items) _ = json.NewEncoder(w).Encode(items)
} }
// FormsPost creates a new form
//
// @Summary Create a form
// @Description Create a new form with title, description, and questions for the authenticated user
// @Tags forms
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body CreateFormRequest true "Form creation payload"
// @Success 201 {object} FormResponse "Created form"
// @Failure 400 {string} string "Bad request (e.g. title is required)"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /api/form [post]
func (h *Handler) FormsPost(w http.ResponseWriter, r *http.Request) { func (h *Handler) FormsPost(w http.ResponseWriter, r *http.Request) {
userID, ok := h.currentUserID(r) userID, ok := h.currentUserID(r)
if !ok { if !ok {
@@ -270,6 +299,18 @@ func (h *Handler) FormsPost(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(buildFormResponse(form, questions, options)) _ = json.NewEncoder(w).Encode(buildFormResponse(form, questions, options))
} }
// FormGet retrieves a single form by ID
//
// @Summary Get a form
// @Description Retrieve a form and its questions by form ID (publicly accessible)
// @Tags forms
// @Produce json
// @Param id path string true "Form ID (UUID)"
// @Success 200 {object} FormResponse "Form details"
// @Failure 400 {string} string "Invalid form ID"
// @Failure 404 {string} string "Form not found"
// @Failure 500 {string} string "Internal server error"
// @Router /api/form/{id} [get]
func (h *Handler) FormGet(w http.ResponseWriter, r *http.Request) { func (h *Handler) FormGet(w http.ResponseWriter, r *http.Request) {
formID, err := uuid.Parse(r.PathValue("id")) formID, err := uuid.Parse(r.PathValue("id"))
if err != nil { if err != nil {
@@ -310,6 +351,23 @@ func (h *Handler) FormGet(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(buildFormResponse(form, questions, options)) _ = json.NewEncoder(w).Encode(buildFormResponse(form, questions, options))
} }
// FormPut updates an existing form
//
// @Summary Update a form
// @Description Replace a form's title, description, and questions. Only the form owner can update it
// @Tags forms
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Form ID (UUID)"
// @Param body body UpdateFormRequest true "Form update payload"
// @Success 200 {object} FormResponse "Updated form"
// @Failure 400 {string} string "Bad request (e.g. title is required)"
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "Forbidden (not the form owner)"
// @Failure 404 {string} string "Form not found"
// @Failure 500 {string} string "Internal server error"
// @Router /api/form/{id} [put]
func (h *Handler) FormPut(w http.ResponseWriter, r *http.Request) { func (h *Handler) FormPut(w http.ResponseWriter, r *http.Request) {
userID, ok := h.currentUserID(r) userID, ok := h.currentUserID(r)
if !ok { if !ok {
@@ -389,6 +447,22 @@ func (h *Handler) FormPut(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(buildFormResponse(updated, questions, options)) _ = json.NewEncoder(w).Encode(buildFormResponse(updated, questions, options))
} }
// FormDelete deletes a form
//
// @Summary Delete a form
// @Description Delete a form by ID. Only the form owner can delete it. Deletion is blocked if the form already has responses
// @Tags forms
// @Produce json
// @Security BearerAuth
// @Param id path string true "Form ID (UUID)"
// @Success 204
// @Failure 400 {string} string "Invalid form ID"
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "Forbidden (not the form owner)"
// @Failure 404 {string} string "Form not found"
// @Failure 409 {string} string "Conflict (form already has responses)"
// @Failure 500 {string} string "Internal server error"
// @Router /api/form/{id} [delete]
func (h *Handler) FormDelete(w http.ResponseWriter, r *http.Request) { func (h *Handler) FormDelete(w http.ResponseWriter, r *http.Request) {
userID, ok := h.currentUserID(r) userID, ok := h.currentUserID(r)
if !ok { if !ok {
@@ -508,6 +582,21 @@ type SubmitResponseResponse struct {
Answers []AnswerResponse `json:"answers"` Answers []AnswerResponse `json:"answers"`
} }
// FormResponsesPost submits a response to a form
//
// @Summary Submit a form response
// @Description Submit answers to a form's questions. Authentication is optional (anonymous submissions allowed). Required questions must be answered. Choice answers must match valid options
// @Tags forms
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Form ID (UUID)"
// @Param body body SubmitResponseRequest true "Response answers payload"
// @Success 201 {object} SubmitResponseResponse "Submitted response"
// @Failure 400 {string} string "Bad request (e.g. invalid answer, missing required question)"
// @Failure 404 {string} string "Form not found"
// @Failure 500 {string} string "Internal server error"
// @Router /api/form/{id}/response [post]
func (h *Handler) FormResponsesPost(w http.ResponseWriter, r *http.Request) { func (h *Handler) FormResponsesPost(w http.ResponseWriter, r *http.Request) {
formID, err := uuid.Parse(r.PathValue("id")) formID, err := uuid.Parse(r.PathValue("id"))
if err != nil { if err != nil {
@@ -668,6 +757,21 @@ func (h *Handler) FormResponsesPost(w http.ResponseWriter, r *http.Request) {
}) })
} }
// FormResponsesGet retrieves all responses for a form
//
// @Summary Get form responses
// @Description Retrieve all submitted responses for a form. Only the form owner can access this
// @Tags forms
// @Produce json
// @Security BearerAuth
// @Param id path string true "Form ID (UUID)"
// @Success 200 {array} SubmitResponseResponse "List of form responses with answers"
// @Failure 400 {string} string "Invalid form ID"
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "Forbidden (not the form owner)"
// @Failure 404 {string} string "Form not found"
// @Failure 500 {string} string "Internal server error"
// @Router /api/form/{id}/responses [get]
func (h *Handler) FormResponsesGet(w http.ResponseWriter, r *http.Request) { func (h *Handler) FormResponsesGet(w http.ResponseWriter, r *http.Request) {
userID, ok := h.currentUserID(r) userID, ok := h.currentUserID(r)
if !ok { if !ok {
+8
View File
@@ -31,6 +31,14 @@ func internalServerError(w http.ResponseWriter, err error) {
_, _ = w.Write([]byte(err.Error())) _, _ = w.Write([]byte(err.Error()))
} }
// HealthGet returns the health status of the server
//
// @Summary Health check
// @Description Returns 200 OK if the server is running
// @Tags health
// @Produce plain
// @Success 200 {string} string "OK"
// @Router /health [get]
func (h *Handler) HealthGet(w http.ResponseWriter, r *http.Request) { func (h *Handler) HealthGet(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK")) _, _ = w.Write([]byte("OK"))
+4
View File
@@ -3,10 +3,13 @@ package server
import ( import (
"fmt" "fmt"
"net/http" "net/http"
_ "ristek-task-be/docs"
"ristek-task-be/internal/db/sqlc/repository" "ristek-task-be/internal/db/sqlc/repository"
"ristek-task-be/internal/handler" "ristek-task-be/internal/handler"
"ristek-task-be/internal/jwt" "ristek-task-be/internal/jwt"
"ristek-task-be/internal/middleware" "ristek-task-be/internal/middleware"
httpSwagger "github.com/swaggo/http-swagger/v2"
) )
type Server struct { type Server struct {
@@ -30,6 +33,7 @@ func router(repository *repository.Queries, jwt *jwt.JWT) *http.ServeMux {
h := handler.New(repository, jwt) h := handler.New(repository, jwt)
r.HandleFunc("GET /health", h.HealthGet) r.HandleFunc("GET /health", h.HealthGet)
r.Handle("/swagger/", httpSwagger.Handler(httpSwagger.URL("/swagger/doc.json")))
authRoute := http.NewServeMux() authRoute := http.NewServeMux()
r.Handle("/api/auth/", http.StripPrefix("/api/auth", authRoute)) r.Handle("/api/auth/", http.StripPrefix("/api/auth", authRoute))
+10
View File
@@ -14,6 +14,16 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
// @title Ristek Task API
// @version 1.0
// @description REST API for Ristek Task Backend
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Enter your bearer token in the format: Bearer {token}
// @host localhost:8080
// @BasePath /
func main() { func main() {
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
log.SetFlags(log.LstdFlags | log.Lshortfile) log.SetFlags(log.LstdFlags | log.Lshortfile)