diff --git a/backend/docs/docs.go b/backend/docs/docs.go
index 9da7489..eb5e1dd 100644
--- a/backend/docs/docs.go
+++ b/backend/docs/docs.go
@@ -38,6 +38,257 @@ const docTemplate = `{
"responses": {}
}
},
+ "/auth/changePassword": {
+ "post": {
+ "security": [
+ {
+ "JWT": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Set new password using the old password",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.ChangePasswordRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Password successfully changed"
+ },
+ "403": {
+ "description": "Invalid old password"
+ }
+ }
+ }
+ },
+ "/auth/login": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Acquire tokens via login credentials (and 2FA code if needed)",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.LoginRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": " ",
+ "schema": {
+ "$ref": "#/definitions/models.LoginResponse"
+ }
+ },
+ "403": {
+ "description": "Invalid login credentials"
+ }
+ }
+ }
+ },
+ "/auth/passwordResetBegin": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Request password reset email",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.PasswordResetBeginRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Reset code sent to the email if it is attached to an account"
+ },
+ "429": {
+ "description": "Too many recent requests for this email"
+ }
+ }
+ }
+ },
+ "/auth/passwordResetComplete": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Complete password reset via email code",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.PasswordResetCompleteRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": " ",
+ "schema": {
+ "$ref": "#/definitions/models.PasswordResetCompleteResponse"
+ }
+ },
+ "403": {
+ "description": "Wrong verification code or username"
+ }
+ }
+ }
+ },
+ "/auth/refresh": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Receive new tokens via refresh token",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.RefreshRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": " ",
+ "schema": {
+ "$ref": "#/definitions/models.RefreshResponse"
+ }
+ },
+ "401": {
+ "description": "Invalid refresh token"
+ }
+ }
+ }
+ },
+ "/auth/registrationBegin": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Register an account",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.RegistrationBeginRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Account is created and awaiting verification"
+ },
+ "409": {
+ "description": "Username or email is already taken"
+ },
+ "429": {
+ "description": "Too many recent registration attempts for this email"
+ }
+ }
+ }
+ },
+ "/auth/registrationComplete": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Confirm with code, finish creating the account",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.RegistrationCompleteRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": " ",
+ "schema": {
+ "$ref": "#/definitions/models.RegistrationCompleteResponse"
+ }
+ },
+ "403": {
+ "description": "Invalid email or verification code"
+ }
+ }
+ }
+ },
"/profile": {
"patch": {
"security": [
@@ -144,6 +395,208 @@ const docTemplate = `{
],
"responses": {}
}
+ },
+ "/service/health": {
+ "get": {
+ "description": "Used internally for checking service health",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Service"
+ ],
+ "summary": "Get health status",
+ "responses": {
+ "200": {
+ "description": "Says whether it's healthy or not",
+ "schema": {
+ "$ref": "#/definitions/models.HealthStatusResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "models.ChangePasswordRequest": {
+ "type": "object",
+ "required": [
+ "old_password",
+ "password"
+ ],
+ "properties": {
+ "old_password": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ },
+ "totp": {
+ "type": "string"
+ }
+ }
+ },
+ "models.HealthStatusResponse": {
+ "type": "object",
+ "properties": {
+ "healthy": {
+ "type": "boolean"
+ }
+ }
+ },
+ "models.LoginRequest": {
+ "type": "object",
+ "required": [
+ "password",
+ "username"
+ ],
+ "properties": {
+ "password": {
+ "type": "string",
+ "maxLength": 100
+ },
+ "totp": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string",
+ "maxLength": 20,
+ "minLength": 3
+ }
+ }
+ },
+ "models.LoginResponse": {
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "models.PasswordResetBeginRequest": {
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "properties": {
+ "email": {
+ "type": "string"
+ }
+ }
+ },
+ "models.PasswordResetCompleteRequest": {
+ "type": "object",
+ "required": [
+ "email",
+ "password",
+ "verification_code"
+ ],
+ "properties": {
+ "email": {
+ "type": "string"
+ },
+ "log_out_sessions": {
+ "type": "boolean"
+ },
+ "password": {
+ "type": "string"
+ },
+ "verification_code": {
+ "type": "string"
+ }
+ }
+ },
+ "models.PasswordResetCompleteResponse": {
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "models.RefreshRequest": {
+ "type": "object",
+ "required": [
+ "refresh_token"
+ ],
+ "properties": {
+ "refresh_token": {
+ "type": "string",
+ "maxLength": 2000
+ }
+ }
+ },
+ "models.RefreshResponse": {
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "models.RegistrationBeginRequest": {
+ "type": "object",
+ "required": [
+ "email",
+ "password",
+ "username"
+ ],
+ "properties": {
+ "email": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ },
+ "models.RegistrationCompleteRequest": {
+ "type": "object",
+ "required": [
+ "name",
+ "username",
+ "verification_code"
+ ],
+ "properties": {
+ "birthday": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "verification_code": {
+ "type": "string"
+ }
+ }
+ },
+ "models.RegistrationCompleteResponse": {
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ }
+ }
}
},
"securityDefinitions": {
diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json
index c2c6d2a..4f37e72 100644
--- a/backend/docs/swagger.json
+++ b/backend/docs/swagger.json
@@ -34,6 +34,257 @@
"responses": {}
}
},
+ "/auth/changePassword": {
+ "post": {
+ "security": [
+ {
+ "JWT": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Set new password using the old password",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.ChangePasswordRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Password successfully changed"
+ },
+ "403": {
+ "description": "Invalid old password"
+ }
+ }
+ }
+ },
+ "/auth/login": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Acquire tokens via login credentials (and 2FA code if needed)",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.LoginRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": " ",
+ "schema": {
+ "$ref": "#/definitions/models.LoginResponse"
+ }
+ },
+ "403": {
+ "description": "Invalid login credentials"
+ }
+ }
+ }
+ },
+ "/auth/passwordResetBegin": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Request password reset email",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.PasswordResetBeginRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Reset code sent to the email if it is attached to an account"
+ },
+ "429": {
+ "description": "Too many recent requests for this email"
+ }
+ }
+ }
+ },
+ "/auth/passwordResetComplete": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Complete password reset via email code",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.PasswordResetCompleteRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": " ",
+ "schema": {
+ "$ref": "#/definitions/models.PasswordResetCompleteResponse"
+ }
+ },
+ "403": {
+ "description": "Wrong verification code or username"
+ }
+ }
+ }
+ },
+ "/auth/refresh": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Receive new tokens via refresh token",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.RefreshRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": " ",
+ "schema": {
+ "$ref": "#/definitions/models.RefreshResponse"
+ }
+ },
+ "401": {
+ "description": "Invalid refresh token"
+ }
+ }
+ }
+ },
+ "/auth/registrationBegin": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Register an account",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.RegistrationBeginRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Account is created and awaiting verification"
+ },
+ "409": {
+ "description": "Username or email is already taken"
+ },
+ "429": {
+ "description": "Too many recent registration attempts for this email"
+ }
+ }
+ }
+ },
+ "/auth/registrationComplete": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Confirm with code, finish creating the account",
+ "parameters": [
+ {
+ "description": " ",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/models.RegistrationCompleteRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": " ",
+ "schema": {
+ "$ref": "#/definitions/models.RegistrationCompleteResponse"
+ }
+ },
+ "403": {
+ "description": "Invalid email or verification code"
+ }
+ }
+ }
+ },
"/profile": {
"patch": {
"security": [
@@ -140,6 +391,208 @@
],
"responses": {}
}
+ },
+ "/service/health": {
+ "get": {
+ "description": "Used internally for checking service health",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Service"
+ ],
+ "summary": "Get health status",
+ "responses": {
+ "200": {
+ "description": "Says whether it's healthy or not",
+ "schema": {
+ "$ref": "#/definitions/models.HealthStatusResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "models.ChangePasswordRequest": {
+ "type": "object",
+ "required": [
+ "old_password",
+ "password"
+ ],
+ "properties": {
+ "old_password": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ },
+ "totp": {
+ "type": "string"
+ }
+ }
+ },
+ "models.HealthStatusResponse": {
+ "type": "object",
+ "properties": {
+ "healthy": {
+ "type": "boolean"
+ }
+ }
+ },
+ "models.LoginRequest": {
+ "type": "object",
+ "required": [
+ "password",
+ "username"
+ ],
+ "properties": {
+ "password": {
+ "type": "string",
+ "maxLength": 100
+ },
+ "totp": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string",
+ "maxLength": 20,
+ "minLength": 3
+ }
+ }
+ },
+ "models.LoginResponse": {
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "models.PasswordResetBeginRequest": {
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "properties": {
+ "email": {
+ "type": "string"
+ }
+ }
+ },
+ "models.PasswordResetCompleteRequest": {
+ "type": "object",
+ "required": [
+ "email",
+ "password",
+ "verification_code"
+ ],
+ "properties": {
+ "email": {
+ "type": "string"
+ },
+ "log_out_sessions": {
+ "type": "boolean"
+ },
+ "password": {
+ "type": "string"
+ },
+ "verification_code": {
+ "type": "string"
+ }
+ }
+ },
+ "models.PasswordResetCompleteResponse": {
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "models.RefreshRequest": {
+ "type": "object",
+ "required": [
+ "refresh_token"
+ ],
+ "properties": {
+ "refresh_token": {
+ "type": "string",
+ "maxLength": 2000
+ }
+ }
+ },
+ "models.RefreshResponse": {
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "models.RegistrationBeginRequest": {
+ "type": "object",
+ "required": [
+ "email",
+ "password",
+ "username"
+ ],
+ "properties": {
+ "email": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ },
+ "models.RegistrationCompleteRequest": {
+ "type": "object",
+ "required": [
+ "name",
+ "username",
+ "verification_code"
+ ],
+ "properties": {
+ "birthday": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "verification_code": {
+ "type": "string"
+ }
+ }
+ },
+ "models.RegistrationCompleteResponse": {
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ }
+ }
}
},
"securityDefinitions": {
diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml
index ba28971..39f35c1 100644
--- a/backend/docs/swagger.yaml
+++ b/backend/docs/swagger.yaml
@@ -1,4 +1,123 @@
basePath: /api/
+definitions:
+ models.ChangePasswordRequest:
+ properties:
+ old_password:
+ type: string
+ password:
+ type: string
+ totp:
+ type: string
+ required:
+ - old_password
+ - password
+ type: object
+ models.HealthStatusResponse:
+ properties:
+ healthy:
+ type: boolean
+ type: object
+ models.LoginRequest:
+ properties:
+ password:
+ maxLength: 100
+ type: string
+ totp:
+ type: string
+ username:
+ maxLength: 20
+ minLength: 3
+ type: string
+ required:
+ - password
+ - username
+ type: object
+ models.LoginResponse:
+ properties:
+ access_token:
+ type: string
+ refresh_token:
+ type: string
+ type: object
+ models.PasswordResetBeginRequest:
+ properties:
+ email:
+ type: string
+ required:
+ - email
+ type: object
+ models.PasswordResetCompleteRequest:
+ properties:
+ email:
+ type: string
+ log_out_sessions:
+ type: boolean
+ password:
+ type: string
+ verification_code:
+ type: string
+ required:
+ - email
+ - password
+ - verification_code
+ type: object
+ models.PasswordResetCompleteResponse:
+ properties:
+ access_token:
+ type: string
+ refresh_token:
+ type: string
+ type: object
+ models.RefreshRequest:
+ properties:
+ refresh_token:
+ maxLength: 2000
+ type: string
+ required:
+ - refresh_token
+ type: object
+ models.RefreshResponse:
+ properties:
+ access_token:
+ type: string
+ refresh_token:
+ type: string
+ type: object
+ models.RegistrationBeginRequest:
+ properties:
+ email:
+ type: string
+ password:
+ type: string
+ username:
+ type: string
+ required:
+ - email
+ - password
+ - username
+ type: object
+ models.RegistrationCompleteRequest:
+ properties:
+ birthday:
+ type: string
+ name:
+ type: string
+ username:
+ type: string
+ verification_code:
+ type: string
+ required:
+ - name
+ - username
+ - verification_code
+ type: object
+ models.RegistrationCompleteResponse:
+ properties:
+ access_token:
+ type: string
+ refresh_token:
+ type: string
+ type: object
info:
contact: {}
description: Easy and feature-rich wishlist.
@@ -19,6 +138,165 @@ paths:
summary: Change account password
tags:
- Account
+ /auth/changePassword:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: ' '
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/models.ChangePasswordRequest'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Password successfully changed
+ "403":
+ description: Invalid old password
+ security:
+ - JWT: []
+ summary: Set new password using the old password
+ tags:
+ - Auth
+ /auth/login:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: ' '
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/models.LoginRequest'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: ' '
+ schema:
+ $ref: '#/definitions/models.LoginResponse'
+ "403":
+ description: Invalid login credentials
+ summary: Acquire tokens via login credentials (and 2FA code if needed)
+ tags:
+ - Auth
+ /auth/passwordResetBegin:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: ' '
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/models.PasswordResetBeginRequest'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Reset code sent to the email if it is attached to an account
+ "429":
+ description: Too many recent requests for this email
+ summary: Request password reset email
+ tags:
+ - Auth
+ /auth/passwordResetComplete:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: ' '
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/models.PasswordResetCompleteRequest'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: ' '
+ schema:
+ $ref: '#/definitions/models.PasswordResetCompleteResponse'
+ "403":
+ description: Wrong verification code or username
+ summary: Complete password reset via email code
+ tags:
+ - Auth
+ /auth/refresh:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: ' '
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/models.RefreshRequest'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: ' '
+ schema:
+ $ref: '#/definitions/models.RefreshResponse'
+ "401":
+ description: Invalid refresh token
+ summary: Receive new tokens via refresh token
+ tags:
+ - Auth
+ /auth/registrationBegin:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: ' '
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/models.RegistrationBeginRequest'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Account is created and awaiting verification
+ "409":
+ description: Username or email is already taken
+ "429":
+ description: Too many recent registration attempts for this email
+ summary: Register an account
+ tags:
+ - Auth
+ /auth/registrationComplete:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: ' '
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/models.RegistrationCompleteRequest'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: ' '
+ schema:
+ $ref: '#/definitions/models.RegistrationCompleteResponse'
+ "403":
+ description: Invalid email or verification code
+ summary: Confirm with code, finish creating the account
+ tags:
+ - Auth
/profile:
patch:
consumes:
@@ -84,6 +362,21 @@ paths:
summary: Update profile privacy settings
tags:
- Profile
+ /service/health:
+ get:
+ consumes:
+ - application/json
+ description: Used internally for checking service health
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Says whether it's healthy or not
+ schema:
+ $ref: '#/definitions/models.HealthStatusResponse'
+ summary: Get health status
+ tags:
+ - Service
schemes:
- http
securityDefinitions:
diff --git a/backend/internal/controllers/account.go b/backend/internal/controllers/account.go
deleted file mode 100644
index 22e3862..0000000
--- a/backend/internal/controllers/account.go
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) 2025 Nikolai Papin
-//
-// This file is part of Easywish
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
-// the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-package controllers
-
-import (
- "net/http"
-
- "github.com/gin-gonic/gin"
-)
-
-// @Summary Change account password
-// @Tags Account
-// @Accept json
-// @Produce json
-// @Security JWT
-// @Router /account/changePassword [put]
-func ChangePassword(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
-}
diff --git a/backend/internal/controllers/auth.go b/backend/internal/controllers/auth.go
index d21cc37..0c264fc 100644
--- a/backend/internal/controllers/auth.go
+++ b/backend/internal/controllers/auth.go
@@ -30,258 +30,270 @@ import (
"go.uber.org/zap"
)
+type AuthController struct {
+ auth services.AuthService
+ log *zap.Logger
+}
+
func NewAuthController(log *zap.Logger, auth services.AuthService) Controller {
+ ctrl := &AuthController{auth: auth, log: log}
+
return &controllerImpl{
Path: "/auth",
Middleware: []gin.HandlerFunc{},
Methods: []ControllerMethod{
-
- // @Summary Register an account
- // @Tags Auth
- // @Accept json
- // @Produce json
- // @Param request body models.RegistrationBeginRequest true " "
- // @Success 200 "Account is created and awaiting verification"
- // @Failure 409 "Username or email is already taken"
- // @Failure 429 "Too many recent registration attempts for this email"
- // @Router /auth/registrationBegin [post]
{
- HttpMethod: POST,
- Path: "/registrationBegin",
+ HttpMethod: POST,
+ Path: "/registrationBegin",
Authorization: enums.GuestRole,
- Middleware: []gin.HandlerFunc{},
- Function: func(c *gin.Context) {
-
- request, err := GetRequest[models.RegistrationBeginRequest](c); if err != nil {
- return
- }
-
- _, err = auth.RegistrationBegin(request.Body); if err != nil {
- if errors.Is(err, errs.ErrUsernameTaken) || errors.Is(err, errs.ErrEmailTaken) {
- c.Status(http.StatusConflict)
- } else {
- c.Status(http.StatusInternalServerError)
- }
- return
- }
-
- c.Status(http.StatusOK)
- return
- },
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.registrationBeginHandler,
},
-
- // @Summary Confirm with code, finish creating the account
- // @Tags Auth
- // @Accept json
- // @Produce json
- // @Param request body models.RegistrationCompleteRequest true " "
- // @Success 200 {object} models.RegistrationCompleteResponse " "
- // @Failure 403 "Invalid email or verification code"
- // @Router /auth/registrationComplete [post]
{
- HttpMethod: POST,
- Path: "/registrationComplete",
+ HttpMethod: POST,
+ Path: "/registrationComplete",
Authorization: enums.GuestRole,
- Middleware: []gin.HandlerFunc{},
- Function: func(c *gin.Context) {
-
- request, err := GetRequest[models.RegistrationCompleteRequest](c); if err != nil {
- return
- }
-
- response, err := auth.RegistrationComplete(request.User, request.Body)
-
- if err != nil {
- if errors.Is(err, errs.ErrForbidden) {
- c.Status(http.StatusForbidden)
- } else if errors.Is(err, errs.ErrUnauthorized) {
- c.Status(http.StatusUnauthorized)
- } else {
- c.Status(http.StatusInternalServerError)
- }
- return
- }
-
- c.JSON(http.StatusOK, response)
- },
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.registrationCompleteHandler,
},
-
- // @Summary Acquire tokens via login credentials (and 2FA code if needed)
- // @Tags Auth
- // @Accept json
- // @Produce json
- // @Param request body models.LoginRequest true " "
- // @Success 200 {object} models.LoginResponse " "
- // @Failure 403 "Invalid login credentials"
- // @Router /auth/login [post]
{
- HttpMethod: POST,
- Path: "/login",
+ HttpMethod: POST,
+ Path: "/login",
Authorization: enums.GuestRole,
- Middleware: []gin.HandlerFunc{},
- Function: func(c *gin.Context) {
- request, err := GetRequest[models.LoginRequest](c); if err != nil {
- return
- }
-
- response, err := auth.Login(request.User, request.Body)
-
- if err != nil {
- if errors.Is(err, errs.ErrForbidden) {
- c.Status(http.StatusForbidden)
- } else {
- c.Status(http.StatusInternalServerError)
- }
- return
- }
-
- c.JSON(http.StatusOK, response)
- },
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.loginHandler,
},
-
- // @Summary Receive new tokens via refresh token
- // @Tags Auth
- // @Accept json
- // @Produce json
- // @Param request body models.RefreshRequest true " "
- // @Router /auth/refresh [post]
- // @Success 200 {object} models.RefreshResponse " "
- // @Failure 401 "Invalid refresh token"
{
- HttpMethod: POST,
- Path: "/refresh",
+ HttpMethod: POST,
+ Path: "/refresh",
Authorization: enums.GuestRole,
- Middleware: []gin.HandlerFunc{},
- Function: func(c *gin.Context) {
-
- request, err := GetRequest[models.RefreshRequest](c); if err != nil {
- return
- }
-
- response, err := auth.Refresh(request.Body)
- if err != nil {
- if utils.ErrorIsOneOf(
- err,
- errs.ErrTokenExpired,
- errs.ErrTokenInvalid,
- errs.ErrInvalidToken,
- errs.ErrWrongTokenType,
- errs.ErrSessionNotFound,
- errs.ErrSessionTerminated,
- ) {
- c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
- } else {
- c.JSON(http.StatusInternalServerError, err.Error())
- }
- return
- }
-
- c.JSON(http.StatusOK, response)
- },
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.refreshHandler,
},
-
- // @Summary Request password reset email
- // @Tags Auth
- // @Accept json
- // @Produce json
- // @Param request body models.PasswordResetBeginRequest true " "
- // @Router /auth/passwordResetBegin [post]
- // @Success 200 "Reset code sent to the email if it is attached to an account"
- // @Failure 429 "Too many recent requests for this email"
{
- HttpMethod: POST,
- Path: "/passwordResetBegin",
+ HttpMethod: POST,
+ Path: "/passwordResetBegin",
Authorization: enums.GuestRole,
- Middleware: []gin.HandlerFunc{},
- Function: func(c *gin.Context) {
-
- request, err := GetRequest[models.PasswordResetBeginRequest](c); if err != nil {
- return
- }
-
- response, err := auth.PasswordResetBegin(request.Body)
- if err != nil {
- if errors.Is(err, errs.ErrTooManyRequests) {
- c.Status(http.StatusTooManyRequests)
- } else {
- c.Status(http.StatusInternalServerError)
- }
- return
- }
-
- c.JSON(http.StatusOK, response)
- },
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.passwordResetBeginHandler,
},
-
-
- // @Summary Complete password reset via email code
- // @Tags Auth
- // @Accept json
- // @Produce json
- // @Param request body models.PasswordResetCompleteRequest true " "
- // @Router /auth/passwordResetComplete [post]
- // @Success 200 {object} models.PasswordResetCompleteResponse " "
- // @Success 403 "Wrong verification code or username"
{
- HttpMethod: POST,
- Path: "/passwordResetComplete",
+ HttpMethod: POST,
+ Path: "/passwordResetComplete",
Authorization: enums.GuestRole,
- Middleware: []gin.HandlerFunc{},
- Function: func(c *gin.Context) {
-
- request, err := GetRequest[models.PasswordResetCompleteRequest](c); if err != nil {
- return
- }
-
- response, err := auth.PasswordResetComplete(request.Body)
- if err != nil {
- if errors.Is(err, errs.ErrForbidden) {
- c.Status(http.StatusForbidden)
- } else {
- c.Status(http.StatusInternalServerError)
- }
- return
- }
-
- c.JSON(http.StatusOK, response)
- },
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.passwordResetCompleteHandler,
},
-
- // @Summary Set new password using the old password
- // @Tags Auth
- // @Accept json
- // @Produce json
- // @Security JWT
- // @Param request body models.ChangePasswordRequest true " "
- // @Success 200 "Password successfully changed"
- // @Failure 403 "Invalid old password"
- // @Router /auth/changePassword [post]
{
- HttpMethod: POST,
- Path: "/changePassword",
+ HttpMethod: POST,
+ Path: "/changePassword",
Authorization: enums.UserRole,
- Middleware: []gin.HandlerFunc{},
- Function: func(c *gin.Context) {
-
- request, err := GetRequest[models.ChangePasswordRequest](c); if err != nil {
- return
- }
-
- response, err := auth.ChangePassword(request.Body, request.User)
-
- if err != nil {
- if errors.Is(err, errs.ErrForbidden) {
- c.Status(http.StatusForbidden)
- } else {
- c.Status(http.StatusInternalServerError)
- }
- return
- }
-
- c.JSON(http.StatusOK, response)
- },
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.changePasswordHandler,
},
-
},
}
}
+
+// @Summary Register an account
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.RegistrationBeginRequest true " "
+// @Success 200 "Account is created and awaiting verification"
+// @Failure 409 "Username or email is already taken"
+// @Failure 429 "Too many recent registration attempts for this email"
+// @Router /auth/registrationBegin [post]
+func (ctrl *AuthController) registrationBeginHandler(c *gin.Context) {
+ request, err := GetRequest[models.RegistrationBeginRequest](c)
+ if err != nil {
+ return
+ }
+
+ _, err = ctrl.auth.RegistrationBegin(request.Body)
+ if err != nil {
+ if errors.Is(err, errs.ErrUsernameTaken) || errors.Is(err, errs.ErrEmailTaken) {
+ c.Status(http.StatusConflict)
+ } else if errors.Is(err, errs.ErrTooManyRequests) {
+ c.Status(http.StatusTooManyRequests)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ }
+ return
+ }
+
+ c.Status(http.StatusOK)
+}
+
+// @Summary Confirm with code, finish creating the account
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.RegistrationCompleteRequest true " "
+// @Success 200 {object} models.RegistrationCompleteResponse " "
+// @Failure 403 "Invalid email or verification code"
+// @Router /auth/registrationComplete [post]
+func (ctrl *AuthController) registrationCompleteHandler(c *gin.Context) {
+ request, err := GetRequest[models.RegistrationCompleteRequest](c)
+ if err != nil {
+ return
+ }
+
+ response, err := ctrl.auth.RegistrationComplete(request.User, request.Body)
+ if err != nil {
+ if errors.Is(err, errs.ErrForbidden) {
+ c.Status(http.StatusForbidden)
+ } else if errors.Is(err, errs.ErrUnauthorized) {
+ c.Status(http.StatusUnauthorized)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ }
+ return
+ }
+
+ c.JSON(http.StatusOK, response)
+}
+
+// @Summary Acquire tokens via login credentials (and 2FA code if needed)
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.LoginRequest true " "
+// @Success 200 {object} models.LoginResponse " "
+// @Failure 403 "Invalid login credentials"
+// @Router /auth/login [post]
+func (ctrl *AuthController) loginHandler(c *gin.Context) {
+ request, err := GetRequest[models.LoginRequest](c)
+ if err != nil {
+ return
+ }
+
+ response, err := ctrl.auth.Login(request.User, request.Body)
+ if err != nil {
+ if errors.Is(err, errs.ErrForbidden) {
+ c.Status(http.StatusForbidden)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ }
+ return
+ }
+
+ c.JSON(http.StatusOK, response)
+}
+
+// @Summary Receive new tokens via refresh token
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.RefreshRequest true " "
+// @Router /auth/refresh [post]
+// @Success 200 {object} models.RefreshResponse " "
+// @Failure 401 "Invalid refresh token"
+func (ctrl *AuthController) refreshHandler(c *gin.Context) {
+ request, err := GetRequest[models.RefreshRequest](c)
+ if err != nil {
+ return
+ }
+
+ response, err := ctrl.auth.Refresh(request.Body)
+ if err != nil {
+ if utils.ErrorIsOneOf(
+ err,
+ errs.ErrTokenExpired,
+ errs.ErrTokenInvalid,
+ errs.ErrInvalidToken,
+ errs.ErrWrongTokenType,
+ errs.ErrSessionNotFound,
+ errs.ErrSessionTerminated,
+ ) {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
+ } else {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ }
+ return
+ }
+
+ c.JSON(http.StatusOK, response)
+}
+
+// @Summary Request password reset email
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.PasswordResetBeginRequest true " "
+// @Router /auth/passwordResetBegin [post]
+// @Success 200 "Reset code sent to the email if it is attached to an account"
+// @Failure 429 "Too many recent requests for this email"
+func (ctrl *AuthController) passwordResetBeginHandler(c *gin.Context) {
+ request, err := GetRequest[models.PasswordResetBeginRequest](c)
+ if err != nil {
+ return
+ }
+
+ _, err = ctrl.auth.PasswordResetBegin(request.Body)
+ if err != nil {
+ if errors.Is(err, errs.ErrTooManyRequests) {
+ c.Status(http.StatusTooManyRequests)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ }
+ return
+ }
+
+ c.Status(http.StatusOK)
+}
+
+// @Summary Complete password reset via email code
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.PasswordResetCompleteRequest true " "
+// @Router /auth/passwordResetComplete [post]
+// @Success 200 {object} models.PasswordResetCompleteResponse " "
+// @Success 403 "Wrong verification code or username"
+func (ctrl *AuthController) passwordResetCompleteHandler(c *gin.Context) {
+ request, err := GetRequest[models.PasswordResetCompleteRequest](c)
+ if err != nil {
+ return
+ }
+
+ response, err := ctrl.auth.PasswordResetComplete(request.Body)
+ if err != nil {
+ if errors.Is(err, errs.ErrForbidden) {
+ c.Status(http.StatusForbidden)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ }
+ return
+ }
+
+ c.JSON(http.StatusOK, response)
+}
+
+// @Summary Set new password using the old password
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Security JWT
+// @Param request body models.ChangePasswordRequest true " "
+// @Success 200 "Password successfully changed"
+// @Failure 403 "Invalid old password"
+// @Router /auth/changePassword [post]
+func (ctrl *AuthController) changePasswordHandler(c *gin.Context) {
+ request, err := GetRequest[models.ChangePasswordRequest](c)
+ if err != nil {
+ return
+ }
+
+ _, err = ctrl.auth.ChangePassword(request.Body, request.User)
+ if err != nil {
+ if errors.Is(err, errs.ErrForbidden) {
+ c.Status(http.StatusForbidden)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ }
+ return
+ }
+
+ c.Status(http.StatusOK)
+}
diff --git a/backend/internal/controllers/controller.go b/backend/internal/controllers/controller.go
index 63de177..e9d3a85 100644
--- a/backend/internal/controllers/controller.go
+++ b/backend/internal/controllers/controller.go
@@ -103,10 +103,10 @@ func GetRequest[ModelT any](c *gin.Context) (*dto.Request[ModelT], error) {
gin.H{"error": "Client info was not found"})
panic("No client_info found in gin context. Does the handler use AuthMiddleware?")
}
- cinfo := cinfoFromCtx.(*dto.ClientInfo)
+ cinfo := cinfoFromCtx.(dto.ClientInfo)
return &dto.Request[ModelT]{
Body: body,
- User: *cinfo,
+ User: cinfo,
}, nil
}
diff --git a/backend/internal/controllers/profile.go b/backend/internal/controllers/profile.go
deleted file mode 100644
index 9d3d1cc..0000000
--- a/backend/internal/controllers/profile.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright (c) 2025 Nikolai Papin
-//
-// This file is part of Easywish
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
-// the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-package controllers
-
-import (
- "net/http"
-
- "github.com/gin-gonic/gin"
-)
-
-type ProfileController interface {
- GetProfile(c *gin.Context)
- GetOwnProfile(c *gin.Context)
- UpdateProfile(c *gin.Context)
- GetPrivacySettings(c *gin.Context)
- UpdatePrivacySettings(c *gin.Context)
- Router
-}
-
-type profileControllerImpl struct {
-}
-
-func NewProfileController() ProfileController {
- return &profileControllerImpl{}
-}
-
-// @Summary Get someone's profile details
-// @Tags Profile
-// @Accept json
-// @Produce json
-// @Param username path string true "Username"
-// @Security JWT
-// @Router /profile/{username} [get]
-func (p *profileControllerImpl) GetProfile(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
-}
-
-// @Summary Get own profile when authorized
-// @Tags Profile
-// @Accept json
-// @Produce json
-// @Security JWT
-// @Router /profile/me [get]
-func (p *profileControllerImpl) GetOwnProfile(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
-}
-
-// @Summary Update profile
-// @Tags Profile
-// @Accept json
-// @Produce json
-// @Security JWT
-// @Router /profile [patch]
-func (p *profileControllerImpl) UpdateProfile(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
-}
-
-// @Summary Get profile privacy settings
-// @Tags Profile
-// @Accept json
-// @Produce json
-// @Security JWT
-// @Router /profile/privacy [get]
-func (p *profileControllerImpl) GetPrivacySettings(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
-}
-
-// @Summary Update profile privacy settings
-// @Tags Profile
-// @Accept json
-// @Produce json
-// @Security JWT
-// @Router /profile/privacy [patch]
-func (p *profileControllerImpl) UpdatePrivacySettings(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
-}
-
-func (p *profileControllerImpl) RegisterRoutes(group *gin.RouterGroup) {
-}
diff --git a/backend/internal/controllers/router.go b/backend/internal/controllers/router.go
deleted file mode 100644
index 20731b5..0000000
--- a/backend/internal/controllers/router.go
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) 2025 Nikolai Papin
-//
-// This file is part of Easywish
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
-// the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-package controllers
-
-import (
- "github.com/gin-gonic/gin"
-)
-
-type Router interface {
- RegisterRoutes(group *gin.RouterGroup)
-}
diff --git a/backend/internal/controllers/service.go b/backend/internal/controllers/service.go
index c19be38..38b959a 100644
--- a/backend/internal/controllers/service.go
+++ b/backend/internal/controllers/service.go
@@ -25,32 +25,36 @@ import (
"github.com/gin-gonic/gin"
)
+type ServiceController struct {}
+
func NewServiceController() Controller {
+
+ ctrl := &ServiceController{}
+
return &controllerImpl{
Path: "/service",
Middleware: []gin.HandlerFunc{},
Methods: []ControllerMethod{
- // Health godoc
- // @Summary Get health status
- // @Description Used internally for checking service health
- // @Tags Service
- // @Accept json
- // @Produce json
- // @Success 200 {object} HealthStatus "Says whether it's healthy or not"
- // @Router /service/health [get]
{
HttpMethod: GET,
Path: "/health",
Authorization: enums.GuestRole,
Middleware: []gin.HandlerFunc{},
- Function: func(c *gin.Context) {
-
- c.JSON(http.StatusOK, models.HealthStatusResponse{Healthy: true,})
-
- },
+ Function: ctrl.healthHandler,
},
},
}
}
+
+// @Summary Get health status
+// @Description Used internally for checking service health
+// @Tags Service
+// @Accept json
+// @Produce json
+// @Success 200 {object} models.HealthStatusResponse "Says whether it's healthy or not"
+// @Router /service/health [get]
+func (ctrl *ServiceController) healthHandler(c *gin.Context) {
+ c.JSON(http.StatusOK, models.HealthStatusResponse{Healthy: true,})
+}