From ff355ad1d9a35b9947b63223a8ea5bc6e4f1db52 Mon Sep 17 00:00:00 2001 From: Mephimeow Date: Sun, 14 Jun 2026 17:14:37 +0000 Subject: [PATCH] feat: add API versioning , translate swagger, remove rate limiter --- cmd/backend/main.go | 19 +-- docs/docs.go | 176 +++++++++++++++------------ docs/swagger.json | 175 +++++++++++++++------------ docs/swagger.yaml | 196 +++++++++++++++++++------------ internal/auth/handler.go | 100 ++++++++-------- internal/middleware/ratelimit.go | 87 -------------- internal/org/handler.go | 73 ++++++------ 7 files changed, 417 insertions(+), 409 deletions(-) delete mode 100644 internal/middleware/ratelimit.go diff --git a/cmd/backend/main.go b/cmd/backend/main.go index d0092f4..0c743dd 100644 --- a/cmd/backend/main.go +++ b/cmd/backend/main.go @@ -12,7 +12,6 @@ import ( docs "gitea.d3m0k1d.ru/HellreigN/Control-plane/docs" "gitea.d3m0k1d.ru/HellreigN/Control-plane/internal/auth" "gitea.d3m0k1d.ru/HellreigN/Control-plane/internal/config" - "gitea.d3m0k1d.ru/HellreigN/Control-plane/internal/middleware" "gitea.d3m0k1d.ru/HellreigN/Control-plane/internal/org" "github.com/gin-gonic/gin" "github.com/pressly/goose/v3" @@ -24,13 +23,16 @@ import ( // @title AegisGuard API // @version 1.0 -// @description API для AegisGuard control plane +// @description API системы управления AegisGuard. Позволяет управлять пользователями и организациями. +// @description Все защищённые эндпоинты требуют заголовок `Authorization: Bearer `. +// @description Токен получается при регистрации или входе. // @schemes http +// @BasePath /api/v1 // // @securityDefinitions.apikey Bearer // @in header // @name Authorization -// @description Type "Bearer" followed by a space and the JWT token. +// @description Введите `Bearer `, где token — access_token из ответа /auth/login или /auth/register func main() { cfg, err := config.Load() @@ -70,7 +72,6 @@ func main() { orgSvc := org.NewService(orgRepo) orgHandler := org.NewHandler(orgSvc) - loginLimiter := middleware.NewRateLimiter(10, time.Minute) authMW := auth.AuthMiddleware([]byte(cfg.JWTSecret)) go func() { @@ -91,17 +92,18 @@ func main() { docs.SwaggerInfo.Title = "AegisGuard API" docs.SwaggerInfo.Version = "1.0" - docs.SwaggerInfo.Description = "API for AegisGuard" + docs.SwaggerInfo.Description = "API системы управления AegisGuard. Позволяет управлять пользователями и организациями." docs.SwaggerInfo.Schemes = []string{"http"} + docs.SwaggerInfo.BasePath = "/api/v1" r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) - api := r.Group("/api/auth") + api := r.Group("/api/v1/auth") { api.POST("/register", handler.Register) - api.POST("/login", loginLimiter.Middleware(), handler.Login) + api.POST("/login", handler.Login) api.POST("/refresh", handler.Refresh) api.POST("/logout", handler.Logout) api.GET("/me", authMW, handler.Me) @@ -109,7 +111,7 @@ func main() { api.PUT("/password", authMW, handler.ChangePassword) } - orgs := r.Group("/api/organizations", authMW) + orgs := r.Group("/api/v1/organizations", authMW) { orgs.POST("", orgHandler.Create) orgs.GET("", orgHandler.List) @@ -144,7 +146,6 @@ func main() { log.Fatalf("server forced to shutdown: %v", err) } - loginLimiter.Stop() _ = sqlDB.Close() log.Println("server stopped") diff --git a/docs/docs.go b/docs/docs.go index c7aaf46..a7a108f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,9 +15,9 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api/auth/login": { + "/api/v1/auth/login": { "post": { - "description": "Authenticate user with email and password, returns JWT token", + "description": "Аутентификация по email и паролю. Возвращает access_token (JWT) и refresh_token.", "consumes": [ "application/json" ], @@ -27,10 +27,10 @@ const docTemplate = `{ "tags": [ "auth" ], - "summary": "Login", + "summary": "Вход", "parameters": [ { - "description": "Login credentials", + "description": "Email и пароль", "name": "request", "in": "body", "required": true, @@ -41,19 +41,19 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Успешный вход, токены в ответе", "schema": { "$ref": "#/definitions/auth.AuthResponse" } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации полей", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "401": { - "description": "Unauthorized", + "description": "Неверный email или пароль", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -61,9 +61,9 @@ const docTemplate = `{ } } }, - "/api/auth/logout": { + "/api/v1/auth/logout": { "post": { - "description": "Invalidate a refresh token (logout)", + "description": "Аннулирование refresh_token. После выхода повторное использование того же refresh_token вернёт 401.", "consumes": [ "application/json" ], @@ -73,10 +73,10 @@ const docTemplate = `{ "tags": [ "auth" ], - "summary": "Logout", + "summary": "Выход", "parameters": [ { - "description": "Refresh token to invalidate", + "description": "Refresh_token для аннулирования", "name": "request", "in": "body", "required": true, @@ -87,7 +87,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "{\"message\": \"logged out successfully\"}", "schema": { "type": "object", "additionalProperties": { @@ -96,13 +96,13 @@ const docTemplate = `{ } }, "400": { - "description": "Bad Request", + "description": "Не указан refresh_token", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "401": { - "description": "Unauthorized", + "description": "Refresh_token не найден или уже аннулирован", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -110,14 +110,14 @@ const docTemplate = `{ } } }, - "/api/auth/me": { + "/api/v1/auth/me": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get authenticated user's profile", + "description": "Получение профиля текущего авторизованного пользователя.\n**Требуется:** заголовок ` + "`" + `Authorization: Bearer \u003ctoken\u003e` + "`" + `.", "consumes": [ "application/json" ], @@ -127,16 +127,16 @@ const docTemplate = `{ "tags": [ "auth" ], - "summary": "Get current user", + "summary": "Профиль пользователя", "responses": { "200": { - "description": "OK", + "description": "Данные пользователя", "schema": { "$ref": "#/definitions/auth.UserResponse" } }, "401": { - "description": "Unauthorized", + "description": "Токен не указан или недействителен", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -149,7 +149,7 @@ const docTemplate = `{ "Bearer": [] } ], - "description": "Update current user's username", + "description": "Обновление username текущего пользователя.\n**Требуется:** заголовок ` + "`" + `Authorization: Bearer \u003ctoken\u003e` + "`" + `.", "consumes": [ "application/json" ], @@ -159,10 +159,10 @@ const docTemplate = `{ "tags": [ "auth" ], - "summary": "Update profile", + "summary": "Обновление профиля", "parameters": [ { - "description": "Profile update", + "description": "Новый username", "name": "request", "in": "body", "required": true, @@ -173,19 +173,19 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Обновлённый профиль", "schema": { "$ref": "#/definitions/auth.UserResponse" } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации: username от 3 до 30 символов", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "401": { - "description": "Unauthorized", + "description": "Токен не указан или недействителен", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -193,14 +193,14 @@ const docTemplate = `{ } } }, - "/api/auth/password": { + "/api/v1/auth/password": { "put": { "security": [ { "Bearer": [] } ], - "description": "Change current user's password", + "description": "Изменение пароля текущего пользователя. Требуется указать старый и новый пароль.\n**Требуется:** заголовок ` + "`" + `Authorization: Bearer \u003ctoken\u003e` + "`" + `.\nПароль должен содержать минимум 8 символов, заглавную букву, строчную букву и цифру.", "consumes": [ "application/json" ], @@ -210,10 +210,10 @@ const docTemplate = `{ "tags": [ "auth" ], - "summary": "Change password", + "summary": "Смена пароля", "parameters": [ { - "description": "Password change details", + "description": "Старый и новый пароль", "name": "request", "in": "body", "required": true, @@ -224,7 +224,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "{\"message\": \"password changed successfully\"}", "schema": { "type": "object", "additionalProperties": { @@ -233,13 +233,13 @@ const docTemplate = `{ } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации: неверный старый пароль, слабый новый или совпадают", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "401": { - "description": "Unauthorized", + "description": "Токен не указан или недействителен", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -247,9 +247,9 @@ const docTemplate = `{ } } }, - "/api/auth/refresh": { + "/api/v1/auth/refresh": { "post": { - "description": "Get a new access token using a refresh token", + "description": "Получение новой пары токенов по refresh_token. Старый refresh_token становится недействительным (ротация).\nЕсли refresh_token истёк или уже был использован — придёт 401.", "consumes": [ "application/json" ], @@ -259,10 +259,10 @@ const docTemplate = `{ "tags": [ "auth" ], - "summary": "Refresh token", + "summary": "Обновление токенов", "parameters": [ { - "description": "Refresh token", + "description": "Действительный refresh_token", "name": "request", "in": "body", "required": true, @@ -273,19 +273,19 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Новая пара токенов", "schema": { "$ref": "#/definitions/auth.AuthResponse" } }, "400": { - "description": "Bad Request", + "description": "Не указан refresh_token", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "401": { - "description": "Unauthorized", + "description": "Refresh_token недействителен или истёк", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -293,9 +293,9 @@ const docTemplate = `{ } } }, - "/api/auth/register": { + "/api/v1/auth/register": { "post": { - "description": "Create user account with username, email, password", + "description": "Создание новой учётной записи. После успешной регистрации сразу возвращается access_token и refresh_token.\nПароль должен содержать минимум 8 символов, заглавную букву, строчную букву и цифру.", "consumes": [ "application/json" ], @@ -305,10 +305,10 @@ const docTemplate = `{ "tags": [ "auth" ], - "summary": "Register", + "summary": "Регистрация", "parameters": [ { - "description": "Registration details", + "description": "Данные для регистрации", "name": "request", "in": "body", "required": true, @@ -319,19 +319,19 @@ const docTemplate = `{ ], "responses": { "201": { - "description": "Created", + "description": "Пользователь создан, токены в ответе", "schema": { "$ref": "#/definitions/auth.AuthResponse" } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации полей (некорректный email, слабый пароль)", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "409": { - "description": "Conflict", + "description": "Email уже зарегистрирован", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -339,14 +339,14 @@ const docTemplate = `{ } } }, - "/api/organizations": { + "/api/v1/organizations": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get all organizations", + "description": "Получение списка всех организаций с пагинацией.\n**Требуется:** заголовок ` + "`" + `Authorization: Bearer \u003ctoken\u003e` + "`" + `.", "consumes": [ "application/json" ], @@ -356,16 +356,30 @@ const docTemplate = `{ "tags": [ "organizations" ], - "summary": "List organizations", + "summary": "Список организаций", + "parameters": [ + { + "type": "integer", + "description": "Количество записей на странице (по умолчанию 20)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Смещение от начала списка (по умолчанию 0)", + "name": "offset", + "in": "query" + } + ], "responses": { "200": { - "description": "OK", + "description": "Список организаций", "schema": { "$ref": "#/definitions/org.OrgListResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Внутренняя ошибка сервера", "schema": { "$ref": "#/definitions/org.ErrorResponse" } @@ -378,7 +392,7 @@ const docTemplate = `{ "Bearer": [] } ], - "description": "Create a new organization", + "description": "Создание новой организации. slug используется в URL и должен быть уникальным.\n**Требуется:** заголовок ` + "`" + `Authorization: Bearer \u003ctoken\u003e` + "`" + `.", "consumes": [ "application/json" ], @@ -388,10 +402,10 @@ const docTemplate = `{ "tags": [ "organizations" ], - "summary": "Create organization", + "summary": "Создание организации", "parameters": [ { - "description": "Organization details", + "description": "Название и slug организации", "name": "request", "in": "body", "required": true, @@ -402,19 +416,19 @@ const docTemplate = `{ ], "responses": { "201": { - "description": "Created", + "description": "Организация создана", "schema": { "$ref": "#/definitions/org.OrgResponse" } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации полей", "schema": { "$ref": "#/definitions/org.ErrorResponse" } }, "409": { - "description": "Conflict", + "description": "Slug уже занят", "schema": { "$ref": "#/definitions/org.ErrorResponse" } @@ -422,14 +436,14 @@ const docTemplate = `{ } } }, - "/api/organizations/{id}": { + "/api/v1/organizations/{id}": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get organization details", + "description": "Получение информации об организации по её ID.\n**Требуется:** заголовок ` + "`" + `Authorization: Bearer \u003ctoken\u003e` + "`" + `.", "consumes": [ "application/json" ], @@ -439,11 +453,11 @@ const docTemplate = `{ "tags": [ "organizations" ], - "summary": "Get organization by ID", + "summary": "Получить организацию", "parameters": [ { "type": "string", - "description": "Organization ID", + "description": "UUID организации", "name": "id", "in": "path", "required": true @@ -451,13 +465,13 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Данные организации", "schema": { "$ref": "#/definitions/org.OrgResponse" } }, "404": { - "description": "Not Found", + "description": "Организация не найдена", "schema": { "$ref": "#/definitions/org.ErrorResponse" } @@ -470,7 +484,7 @@ const docTemplate = `{ "Bearer": [] } ], - "description": "Update organization name", + "description": "Обновление названия организации. slug изменить нельзя.\n**Требуется:** заголовок ` + "`" + `Authorization: Bearer \u003ctoken\u003e` + "`" + `.", "consumes": [ "application/json" ], @@ -480,17 +494,17 @@ const docTemplate = `{ "tags": [ "organizations" ], - "summary": "Update organization", + "summary": "Обновление организации", "parameters": [ { "type": "string", - "description": "Organization ID", + "description": "UUID организации", "name": "id", "in": "path", "required": true }, { - "description": "New organization details", + "description": "Новое название организации", "name": "request", "in": "body", "required": true, @@ -501,19 +515,19 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Обновлённая организация", "schema": { "$ref": "#/definitions/org.OrgResponse" } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации полей", "schema": { "$ref": "#/definitions/org.ErrorResponse" } }, "404": { - "description": "Not Found", + "description": "Организация не найдена", "schema": { "$ref": "#/definitions/org.ErrorResponse" } @@ -526,7 +540,7 @@ const docTemplate = `{ "Bearer": [] } ], - "description": "Delete an organization", + "description": "Безвозвратное удаление организации по её ID.\n**Требуется:** заголовок ` + "`" + `Authorization: Bearer \u003ctoken\u003e` + "`" + `.", "consumes": [ "application/json" ], @@ -536,11 +550,11 @@ const docTemplate = `{ "tags": [ "organizations" ], - "summary": "Delete organization", + "summary": "Удаление организации", "parameters": [ { "type": "string", - "description": "Organization ID", + "description": "UUID организации", "name": "id", "in": "path", "required": true @@ -548,7 +562,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "{\"message\": \"organization deleted\"}", "schema": { "type": "object", "additionalProperties": { @@ -557,7 +571,7 @@ const docTemplate = `{ } }, "404": { - "description": "Not Found", + "description": "Организация не найдена", "schema": { "$ref": "#/definitions/org.ErrorResponse" } @@ -747,6 +761,12 @@ const docTemplate = `{ "org.OrgListResponse": { "type": "object", "properties": { + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, "organizations": { "type": "array", "items": { @@ -803,7 +823,7 @@ const docTemplate = `{ }, "securityDefinitions": { "Bearer": { - "description": "Type \"Bearer\" followed by a space and the JWT token.", + "description": "Введите ` + "`" + `Bearer \u003ctoken\u003e` + "`" + `, где token — access_token из ответа /auth/login или /auth/register", "type": "apiKey", "name": "Authorization", "in": "header" @@ -815,10 +835,10 @@ const docTemplate = `{ var SwaggerInfo = &swag.Spec{ Version: "1.0", Host: "", - BasePath: "", + BasePath: "/api/v1", Schemes: []string{"http"}, Title: "AegisGuard API", - Description: "API for AegisGuard control plane", + Description: "API системы управления AegisGuard. Позволяет управлять пользователями и организациями.\nВсе защищённые эндпоинты требуют заголовок `Authorization: Bearer `.\nТокен получается при регистрации или входе.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index 4e5a3c9..d9731e7 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4,15 +4,16 @@ ], "swagger": "2.0", "info": { - "description": "API for AegisGuard control plane", + "description": "API системы управления AegisGuard. Позволяет управлять пользователями и организациями.\nВсе защищённые эндпоинты требуют заголовок `Authorization: Bearer \u003ctoken\u003e`.\nТокен получается при регистрации или входе.", "title": "AegisGuard API", "contact": {}, "version": "1.0" }, + "basePath": "/api/v1", "paths": { - "/api/auth/login": { + "/api/v1/auth/login": { "post": { - "description": "Authenticate user with email and password, returns JWT token", + "description": "Аутентификация по email и паролю. Возвращает access_token (JWT) и refresh_token.", "consumes": [ "application/json" ], @@ -22,10 +23,10 @@ "tags": [ "auth" ], - "summary": "Login", + "summary": "Вход", "parameters": [ { - "description": "Login credentials", + "description": "Email и пароль", "name": "request", "in": "body", "required": true, @@ -36,19 +37,19 @@ ], "responses": { "200": { - "description": "OK", + "description": "Успешный вход, токены в ответе", "schema": { "$ref": "#/definitions/auth.AuthResponse" } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации полей", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "401": { - "description": "Unauthorized", + "description": "Неверный email или пароль", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -56,9 +57,9 @@ } } }, - "/api/auth/logout": { + "/api/v1/auth/logout": { "post": { - "description": "Invalidate a refresh token (logout)", + "description": "Аннулирование refresh_token. После выхода повторное использование того же refresh_token вернёт 401.", "consumes": [ "application/json" ], @@ -68,10 +69,10 @@ "tags": [ "auth" ], - "summary": "Logout", + "summary": "Выход", "parameters": [ { - "description": "Refresh token to invalidate", + "description": "Refresh_token для аннулирования", "name": "request", "in": "body", "required": true, @@ -82,7 +83,7 @@ ], "responses": { "200": { - "description": "OK", + "description": "{\"message\": \"logged out successfully\"}", "schema": { "type": "object", "additionalProperties": { @@ -91,13 +92,13 @@ } }, "400": { - "description": "Bad Request", + "description": "Не указан refresh_token", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "401": { - "description": "Unauthorized", + "description": "Refresh_token не найден или уже аннулирован", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -105,14 +106,14 @@ } } }, - "/api/auth/me": { + "/api/v1/auth/me": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get authenticated user's profile", + "description": "Получение профиля текущего авторизованного пользователя.\n**Требуется:** заголовок `Authorization: Bearer \u003ctoken\u003e`.", "consumes": [ "application/json" ], @@ -122,16 +123,16 @@ "tags": [ "auth" ], - "summary": "Get current user", + "summary": "Профиль пользователя", "responses": { "200": { - "description": "OK", + "description": "Данные пользователя", "schema": { "$ref": "#/definitions/auth.UserResponse" } }, "401": { - "description": "Unauthorized", + "description": "Токен не указан или недействителен", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -144,7 +145,7 @@ "Bearer": [] } ], - "description": "Update current user's username", + "description": "Обновление username текущего пользователя.\n**Требуется:** заголовок `Authorization: Bearer \u003ctoken\u003e`.", "consumes": [ "application/json" ], @@ -154,10 +155,10 @@ "tags": [ "auth" ], - "summary": "Update profile", + "summary": "Обновление профиля", "parameters": [ { - "description": "Profile update", + "description": "Новый username", "name": "request", "in": "body", "required": true, @@ -168,19 +169,19 @@ ], "responses": { "200": { - "description": "OK", + "description": "Обновлённый профиль", "schema": { "$ref": "#/definitions/auth.UserResponse" } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации: username от 3 до 30 символов", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "401": { - "description": "Unauthorized", + "description": "Токен не указан или недействителен", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -188,14 +189,14 @@ } } }, - "/api/auth/password": { + "/api/v1/auth/password": { "put": { "security": [ { "Bearer": [] } ], - "description": "Change current user's password", + "description": "Изменение пароля текущего пользователя. Требуется указать старый и новый пароль.\n**Требуется:** заголовок `Authorization: Bearer \u003ctoken\u003e`.\nПароль должен содержать минимум 8 символов, заглавную букву, строчную букву и цифру.", "consumes": [ "application/json" ], @@ -205,10 +206,10 @@ "tags": [ "auth" ], - "summary": "Change password", + "summary": "Смена пароля", "parameters": [ { - "description": "Password change details", + "description": "Старый и новый пароль", "name": "request", "in": "body", "required": true, @@ -219,7 +220,7 @@ ], "responses": { "200": { - "description": "OK", + "description": "{\"message\": \"password changed successfully\"}", "schema": { "type": "object", "additionalProperties": { @@ -228,13 +229,13 @@ } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации: неверный старый пароль, слабый новый или совпадают", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "401": { - "description": "Unauthorized", + "description": "Токен не указан или недействителен", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -242,9 +243,9 @@ } } }, - "/api/auth/refresh": { + "/api/v1/auth/refresh": { "post": { - "description": "Get a new access token using a refresh token", + "description": "Получение новой пары токенов по refresh_token. Старый refresh_token становится недействительным (ротация).\nЕсли refresh_token истёк или уже был использован — придёт 401.", "consumes": [ "application/json" ], @@ -254,10 +255,10 @@ "tags": [ "auth" ], - "summary": "Refresh token", + "summary": "Обновление токенов", "parameters": [ { - "description": "Refresh token", + "description": "Действительный refresh_token", "name": "request", "in": "body", "required": true, @@ -268,19 +269,19 @@ ], "responses": { "200": { - "description": "OK", + "description": "Новая пара токенов", "schema": { "$ref": "#/definitions/auth.AuthResponse" } }, "400": { - "description": "Bad Request", + "description": "Не указан refresh_token", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "401": { - "description": "Unauthorized", + "description": "Refresh_token недействителен или истёк", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -288,9 +289,9 @@ } } }, - "/api/auth/register": { + "/api/v1/auth/register": { "post": { - "description": "Create user account with username, email, password", + "description": "Создание новой учётной записи. После успешной регистрации сразу возвращается access_token и refresh_token.\nПароль должен содержать минимум 8 символов, заглавную букву, строчную букву и цифру.", "consumes": [ "application/json" ], @@ -300,10 +301,10 @@ "tags": [ "auth" ], - "summary": "Register", + "summary": "Регистрация", "parameters": [ { - "description": "Registration details", + "description": "Данные для регистрации", "name": "request", "in": "body", "required": true, @@ -314,19 +315,19 @@ ], "responses": { "201": { - "description": "Created", + "description": "Пользователь создан, токены в ответе", "schema": { "$ref": "#/definitions/auth.AuthResponse" } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации полей (некорректный email, слабый пароль)", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } }, "409": { - "description": "Conflict", + "description": "Email уже зарегистрирован", "schema": { "$ref": "#/definitions/auth.ErrorResponse" } @@ -334,14 +335,14 @@ } } }, - "/api/organizations": { + "/api/v1/organizations": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get all organizations", + "description": "Получение списка всех организаций с пагинацией.\n**Требуется:** заголовок `Authorization: Bearer \u003ctoken\u003e`.", "consumes": [ "application/json" ], @@ -351,16 +352,30 @@ "tags": [ "organizations" ], - "summary": "List organizations", + "summary": "Список организаций", + "parameters": [ + { + "type": "integer", + "description": "Количество записей на странице (по умолчанию 20)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Смещение от начала списка (по умолчанию 0)", + "name": "offset", + "in": "query" + } + ], "responses": { "200": { - "description": "OK", + "description": "Список организаций", "schema": { "$ref": "#/definitions/org.OrgListResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Внутренняя ошибка сервера", "schema": { "$ref": "#/definitions/org.ErrorResponse" } @@ -373,7 +388,7 @@ "Bearer": [] } ], - "description": "Create a new organization", + "description": "Создание новой организации. slug используется в URL и должен быть уникальным.\n**Требуется:** заголовок `Authorization: Bearer \u003ctoken\u003e`.", "consumes": [ "application/json" ], @@ -383,10 +398,10 @@ "tags": [ "organizations" ], - "summary": "Create organization", + "summary": "Создание организации", "parameters": [ { - "description": "Organization details", + "description": "Название и slug организации", "name": "request", "in": "body", "required": true, @@ -397,19 +412,19 @@ ], "responses": { "201": { - "description": "Created", + "description": "Организация создана", "schema": { "$ref": "#/definitions/org.OrgResponse" } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации полей", "schema": { "$ref": "#/definitions/org.ErrorResponse" } }, "409": { - "description": "Conflict", + "description": "Slug уже занят", "schema": { "$ref": "#/definitions/org.ErrorResponse" } @@ -417,14 +432,14 @@ } } }, - "/api/organizations/{id}": { + "/api/v1/organizations/{id}": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get organization details", + "description": "Получение информации об организации по её ID.\n**Требуется:** заголовок `Authorization: Bearer \u003ctoken\u003e`.", "consumes": [ "application/json" ], @@ -434,11 +449,11 @@ "tags": [ "organizations" ], - "summary": "Get organization by ID", + "summary": "Получить организацию", "parameters": [ { "type": "string", - "description": "Organization ID", + "description": "UUID организации", "name": "id", "in": "path", "required": true @@ -446,13 +461,13 @@ ], "responses": { "200": { - "description": "OK", + "description": "Данные организации", "schema": { "$ref": "#/definitions/org.OrgResponse" } }, "404": { - "description": "Not Found", + "description": "Организация не найдена", "schema": { "$ref": "#/definitions/org.ErrorResponse" } @@ -465,7 +480,7 @@ "Bearer": [] } ], - "description": "Update organization name", + "description": "Обновление названия организации. slug изменить нельзя.\n**Требуется:** заголовок `Authorization: Bearer \u003ctoken\u003e`.", "consumes": [ "application/json" ], @@ -475,17 +490,17 @@ "tags": [ "organizations" ], - "summary": "Update organization", + "summary": "Обновление организации", "parameters": [ { "type": "string", - "description": "Organization ID", + "description": "UUID организации", "name": "id", "in": "path", "required": true }, { - "description": "New organization details", + "description": "Новое название организации", "name": "request", "in": "body", "required": true, @@ -496,19 +511,19 @@ ], "responses": { "200": { - "description": "OK", + "description": "Обновлённая организация", "schema": { "$ref": "#/definitions/org.OrgResponse" } }, "400": { - "description": "Bad Request", + "description": "Ошибка валидации полей", "schema": { "$ref": "#/definitions/org.ErrorResponse" } }, "404": { - "description": "Not Found", + "description": "Организация не найдена", "schema": { "$ref": "#/definitions/org.ErrorResponse" } @@ -521,7 +536,7 @@ "Bearer": [] } ], - "description": "Delete an organization", + "description": "Безвозвратное удаление организации по её ID.\n**Требуется:** заголовок `Authorization: Bearer \u003ctoken\u003e`.", "consumes": [ "application/json" ], @@ -531,11 +546,11 @@ "tags": [ "organizations" ], - "summary": "Delete organization", + "summary": "Удаление организации", "parameters": [ { "type": "string", - "description": "Organization ID", + "description": "UUID организации", "name": "id", "in": "path", "required": true @@ -543,7 +558,7 @@ ], "responses": { "200": { - "description": "OK", + "description": "{\"message\": \"organization deleted\"}", "schema": { "type": "object", "additionalProperties": { @@ -552,7 +567,7 @@ } }, "404": { - "description": "Not Found", + "description": "Организация не найдена", "schema": { "$ref": "#/definitions/org.ErrorResponse" } @@ -742,6 +757,12 @@ "org.OrgListResponse": { "type": "object", "properties": { + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, "organizations": { "type": "array", "items": { @@ -798,7 +819,7 @@ }, "securityDefinitions": { "Bearer": { - "description": "Type \"Bearer\" followed by a space and the JWT token.", + "description": "Введите `Bearer \u003ctoken\u003e`, где token — access_token из ответа /auth/login или /auth/register", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c344b46..b2b3947 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,3 +1,4 @@ +basePath: /api/v1 definitions: auth.AuthResponse: properties: @@ -125,6 +126,10 @@ definitions: type: object org.OrgListResponse: properties: + limit: + type: integer + offset: + type: integer organizations: items: $ref: '#/definitions/org.Organization' @@ -162,17 +167,21 @@ definitions: type: object info: contact: {} - description: API for AegisGuard control plane + description: |- + API системы управления AegisGuard. Позволяет управлять пользователями и организациями. + Все защищённые эндпоинты требуют заголовок `Authorization: Bearer `. + Токен получается при регистрации или входе. title: AegisGuard API version: "1.0" paths: - /api/auth/login: + /api/v1/auth/login: post: consumes: - application/json - description: Authenticate user with email and password, returns JWT token + description: Аутентификация по email и паролю. Возвращает access_token (JWT) + и refresh_token. parameters: - - description: Login credentials + - description: Email и пароль in: body name: request required: true @@ -182,27 +191,28 @@ paths: - application/json responses: "200": - description: OK + description: Успешный вход, токены в ответе schema: $ref: '#/definitions/auth.AuthResponse' "400": - description: Bad Request + description: Ошибка валидации полей schema: $ref: '#/definitions/auth.ErrorResponse' "401": - description: Unauthorized + description: Неверный email или пароль schema: $ref: '#/definitions/auth.ErrorResponse' - summary: Login + summary: Вход tags: - auth - /api/auth/logout: + /api/v1/auth/logout: post: consumes: - application/json - description: Invalidate a refresh token (logout) + description: Аннулирование refresh_token. После выхода повторное использование + того же refresh_token вернёт 401. parameters: - - description: Refresh token to invalidate + - description: Refresh_token для аннулирования in: body name: request required: true @@ -212,49 +222,53 @@ paths: - application/json responses: "200": - description: OK + description: '{"message": "logged out successfully"}' schema: additionalProperties: type: string type: object "400": - description: Bad Request + description: Не указан refresh_token schema: $ref: '#/definitions/auth.ErrorResponse' "401": - description: Unauthorized + description: Refresh_token не найден или уже аннулирован schema: $ref: '#/definitions/auth.ErrorResponse' - summary: Logout + summary: Выход tags: - auth - /api/auth/me: + /api/v1/auth/me: get: consumes: - application/json - description: Get authenticated user's profile + description: |- + Получение профиля текущего авторизованного пользователя. + **Требуется:** заголовок `Authorization: Bearer `. produces: - application/json responses: "200": - description: OK + description: Данные пользователя schema: $ref: '#/definitions/auth.UserResponse' "401": - description: Unauthorized + description: Токен не указан или недействителен schema: $ref: '#/definitions/auth.ErrorResponse' security: - Bearer: [] - summary: Get current user + summary: Профиль пользователя tags: - auth put: consumes: - application/json - description: Update current user's username + description: |- + Обновление username текущего пользователя. + **Требуется:** заголовок `Authorization: Bearer `. parameters: - - description: Profile update + - description: Новый username in: body name: request required: true @@ -264,29 +278,32 @@ paths: - application/json responses: "200": - description: OK + description: Обновлённый профиль schema: $ref: '#/definitions/auth.UserResponse' "400": - description: Bad Request + description: 'Ошибка валидации: username от 3 до 30 символов' schema: $ref: '#/definitions/auth.ErrorResponse' "401": - description: Unauthorized + description: Токен не указан или недействителен schema: $ref: '#/definitions/auth.ErrorResponse' security: - Bearer: [] - summary: Update profile + summary: Обновление профиля tags: - auth - /api/auth/password: + /api/v1/auth/password: put: consumes: - application/json - description: Change current user's password + description: |- + Изменение пароля текущего пользователя. Требуется указать старый и новый пароль. + **Требуется:** заголовок `Authorization: Bearer `. + Пароль должен содержать минимум 8 символов, заглавную букву, строчную букву и цифру. parameters: - - description: Password change details + - description: Старый и новый пароль in: body name: request required: true @@ -296,31 +313,34 @@ paths: - application/json responses: "200": - description: OK + description: '{"message": "password changed successfully"}' schema: additionalProperties: type: string type: object "400": - description: Bad Request + description: 'Ошибка валидации: неверный старый пароль, слабый новый или + совпадают' schema: $ref: '#/definitions/auth.ErrorResponse' "401": - description: Unauthorized + description: Токен не указан или недействителен schema: $ref: '#/definitions/auth.ErrorResponse' security: - Bearer: [] - summary: Change password + summary: Смена пароля tags: - auth - /api/auth/refresh: + /api/v1/auth/refresh: post: consumes: - application/json - description: Get a new access token using a refresh token + description: |- + Получение новой пары токенов по refresh_token. Старый refresh_token становится недействительным (ротация). + Если refresh_token истёк или уже был использован — придёт 401. parameters: - - description: Refresh token + - description: Действительный refresh_token in: body name: request required: true @@ -330,27 +350,29 @@ paths: - application/json responses: "200": - description: OK + description: Новая пара токенов schema: $ref: '#/definitions/auth.AuthResponse' "400": - description: Bad Request + description: Не указан refresh_token schema: $ref: '#/definitions/auth.ErrorResponse' "401": - description: Unauthorized + description: Refresh_token недействителен или истёк schema: $ref: '#/definitions/auth.ErrorResponse' - summary: Refresh token + summary: Обновление токенов tags: - auth - /api/auth/register: + /api/v1/auth/register: post: consumes: - application/json - description: Create user account with username, email, password + description: |- + Создание новой учётной записи. После успешной регистрации сразу возвращается access_token и refresh_token. + Пароль должен содержать минимум 8 символов, заглавную букву, строчную букву и цифру. parameters: - - description: Registration details + - description: Данные для регистрации in: body name: request required: true @@ -360,47 +382,60 @@ paths: - application/json responses: "201": - description: Created + description: Пользователь создан, токены в ответе schema: $ref: '#/definitions/auth.AuthResponse' "400": - description: Bad Request + description: Ошибка валидации полей (некорректный email, слабый пароль) schema: $ref: '#/definitions/auth.ErrorResponse' "409": - description: Conflict + description: Email уже зарегистрирован schema: $ref: '#/definitions/auth.ErrorResponse' - summary: Register + summary: Регистрация tags: - auth - /api/organizations: + /api/v1/organizations: get: consumes: - application/json - description: Get all organizations + description: |- + Получение списка всех организаций с пагинацией. + **Требуется:** заголовок `Authorization: Bearer `. + parameters: + - description: Количество записей на странице (по умолчанию 20) + in: query + name: limit + type: integer + - description: Смещение от начала списка (по умолчанию 0) + in: query + name: offset + type: integer produces: - application/json responses: "200": - description: OK + description: Список организаций schema: $ref: '#/definitions/org.OrgListResponse' "500": - description: Internal Server Error + description: Внутренняя ошибка сервера schema: $ref: '#/definitions/org.ErrorResponse' security: - Bearer: [] - summary: List organizations + summary: Список организаций tags: - organizations post: consumes: - application/json - description: Create a new organization + description: |- + Создание новой организации. slug используется в URL и должен быть уникальным. + **Требуется:** заголовок `Authorization: Bearer `. parameters: - - description: Organization details + - description: Название и slug организации in: body name: request required: true @@ -410,29 +445,31 @@ paths: - application/json responses: "201": - description: Created + description: Организация создана schema: $ref: '#/definitions/org.OrgResponse' "400": - description: Bad Request + description: Ошибка валидации полей schema: $ref: '#/definitions/org.ErrorResponse' "409": - description: Conflict + description: Slug уже занят schema: $ref: '#/definitions/org.ErrorResponse' security: - Bearer: [] - summary: Create organization + summary: Создание организации tags: - organizations - /api/organizations/{id}: + /api/v1/organizations/{id}: delete: consumes: - application/json - description: Delete an organization + description: |- + Безвозвратное удаление организации по её ID. + **Требуется:** заголовок `Authorization: Bearer `. parameters: - - description: Organization ID + - description: UUID организации in: path name: id required: true @@ -441,26 +478,28 @@ paths: - application/json responses: "200": - description: OK + description: '{"message": "organization deleted"}' schema: additionalProperties: type: string type: object "404": - description: Not Found + description: Организация не найдена schema: $ref: '#/definitions/org.ErrorResponse' security: - Bearer: [] - summary: Delete organization + summary: Удаление организации tags: - organizations get: consumes: - application/json - description: Get organization details + description: |- + Получение информации об организации по её ID. + **Требуется:** заголовок `Authorization: Bearer `. parameters: - - description: Organization ID + - description: UUID организации in: path name: id required: true @@ -469,29 +508,31 @@ paths: - application/json responses: "200": - description: OK + description: Данные организации schema: $ref: '#/definitions/org.OrgResponse' "404": - description: Not Found + description: Организация не найдена schema: $ref: '#/definitions/org.ErrorResponse' security: - Bearer: [] - summary: Get organization by ID + summary: Получить организацию tags: - organizations put: consumes: - application/json - description: Update organization name + description: |- + Обновление названия организации. slug изменить нельзя. + **Требуется:** заголовок `Authorization: Bearer `. parameters: - - description: Organization ID + - description: UUID организации in: path name: id required: true type: string - - description: New organization details + - description: Новое название организации in: body name: request required: true @@ -501,27 +542,28 @@ paths: - application/json responses: "200": - description: OK + description: Обновлённая организация schema: $ref: '#/definitions/org.OrgResponse' "400": - description: Bad Request + description: Ошибка валидации полей schema: $ref: '#/definitions/org.ErrorResponse' "404": - description: Not Found + description: Организация не найдена schema: $ref: '#/definitions/org.ErrorResponse' security: - Bearer: [] - summary: Update organization + summary: Обновление организации tags: - organizations schemes: - http securityDefinitions: Bearer: - description: Type "Bearer" followed by a space and the JWT token. + description: Введите `Bearer `, где token — access_token из ответа /auth/login + или /auth/register in: header name: Authorization type: apiKey diff --git a/internal/auth/handler.go b/internal/auth/handler.go index 7c2852b..a67a465 100644 --- a/internal/auth/handler.go +++ b/internal/auth/handler.go @@ -17,16 +17,17 @@ func NewHandler(service *Service) *Handler { return &Handler{service: service} } -// @Summary Register -// @Description Создание учетной записи пользователя с полями username, email, password +// @Summary Регистрация +// @Description Создание новой учётной записи. После успешной регистрации сразу возвращается access_token и refresh_token. +// @Description Пароль должен содержать минимум 8 символов, заглавную букву, строчную букву и цифру. // @Tags auth // @Accept json // @Produce json -// @Param request body RegisterRequest true "Registration details" -// @Success 201 {object} AuthResponse -// @Failure 400 {object} ErrorResponse -// @Failure 409 {object} ErrorResponse -// @Router /api/auth/register [post] +// @Param request body RegisterRequest true "Данные для регистрации" +// @Success 201 {object} AuthResponse "Пользователь создан, токены в ответе" +// @Failure 400 {object} ErrorResponse "Ошибка валидации полей (некорректный email, слабый пароль)" +// @Failure 409 {object} ErrorResponse "Email уже зарегистрирован" +// @Router /api/v1/auth/register [post] func (h *Handler) Register(c *gin.Context) { var req RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -52,16 +53,16 @@ func (h *Handler) Register(c *gin.Context) { c.JSON(http.StatusCreated, resp) } -// @Summary Login -// @Description Аунтефикация пользователя с помощью email и password, возвращает JWT token +// @Summary Вход +// @Description Аутентификация по email и паролю. Возвращает access_token (JWT) и refresh_token. // @Tags auth // @Accept json // @Produce json -// @Param request body LoginRequest true "Login credentials" -// @Success 200 {object} AuthResponse -// @Failure 400 {object} ErrorResponse -// @Failure 401 {object} ErrorResponse -// @Router /api/auth/login [post] +// @Param request body LoginRequest true "Email и пароль" +// @Success 200 {object} AuthResponse "Успешный вход, токены в ответе" +// @Failure 400 {object} ErrorResponse "Ошибка валидации полей" +// @Failure 401 {object} ErrorResponse "Неверный email или пароль" +// @Router /api/v1/auth/login [post] func (h *Handler) Login(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -83,16 +84,17 @@ func (h *Handler) Login(c *gin.Context) { c.JSON(http.StatusOK, resp) } -// @Summary Refresh token -// @Description Получение ново +// @Summary Обновление токенов +// @Description Получение новой пары токенов по refresh_token. Старый refresh_token становится недействительным (ротация). +// @Description Если refresh_token истёк или уже был использован — придёт 401. // @Tags auth // @Accept json // @Produce json -// @Param request body RefreshRequest true "Refresh token" -// @Success 200 {object} AuthResponse -// @Failure 400 {object} ErrorResponse -// @Failure 401 {object} ErrorResponse -// @Router /api/auth/refresh [post] +// @Param request body RefreshRequest true "Действительный refresh_token" +// @Success 200 {object} AuthResponse "Новая пара токенов" +// @Failure 400 {object} ErrorResponse "Не указан refresh_token" +// @Failure 401 {object} ErrorResponse "Refresh_token недействителен или истёк" +// @Router /api/v1/auth/refresh [post] func (h *Handler) Refresh(c *gin.Context) { var req RefreshRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -114,16 +116,16 @@ func (h *Handler) Refresh(c *gin.Context) { c.JSON(http.StatusOK, resp) } -// @Summary Logout -// @Description Аннулирует refresh token +// @Summary Выход +// @Description Аннулирование refresh_token. После выхода повторное использование того же refresh_token вернёт 401. // @Tags auth // @Accept json // @Produce json -// @Param request body LogoutRequest true "Refresh token to invalidate" -// @Success 200 {object} map[string]string -// @Failure 400 {object} ErrorResponse -// @Failure 401 {object} ErrorResponse -// @Router /api/auth/logout [post] +// @Param request body LogoutRequest true "Refresh_token для аннулирования" +// @Success 200 {object} map[string]string "{"message": "logged out successfully"}" +// @Failure 400 {object} ErrorResponse "Не указан refresh_token" +// @Failure 401 {object} ErrorResponse "Refresh_token не найден или уже аннулирован" +// @Router /api/v1/auth/logout [post] func (h *Handler) Logout(c *gin.Context) { var req LogoutRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -144,15 +146,16 @@ func (h *Handler) Logout(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "logged out successfully"}) } -// @Summary Get current user -// @Description Получить профиль авторизованного пользователя +// @Summary Профиль пользователя +// @Description Получение профиля текущего авторизованного пользователя. +// @Description **Требуется:** заголовок `Authorization: Bearer `. // @Tags auth // @Accept json // @Produce json // @Security Bearer -// @Success 200 {object} UserResponse -// @Failure 401 {object} ErrorResponse -// @Router /api/auth/me [get] +// @Success 200 {object} UserResponse "Данные пользователя" +// @Failure 401 {object} ErrorResponse "Токен не указан или недействителен" +// @Router /api/v1/auth/me [get] func (h *Handler) Me(c *gin.Context) { userID := api.GetUserID(c) if userID == "" { @@ -174,17 +177,19 @@ func (h *Handler) Me(c *gin.Context) { c.JSON(http.StatusOK, UserResponse{User: *user}) } -// @Summary Change password -// @Description Изменить текущий password пользователя +// @Summary Смена пароля +// @Description Изменение пароля текущего пользователя. Требуется указать старый и новый пароль. +// @Description **Требуется:** заголовок `Authorization: Bearer `. +// @Description Пароль должен содержать минимум 8 символов, заглавную букву, строчную букву и цифру. // @Tags auth // @Accept json // @Produce json // @Security Bearer -// @Param request body PasswordChangeRequest true "Password change details" -// @Success 200 {object} map[string]string -// @Failure 400 {object} ErrorResponse -// @Failure 401 {object} ErrorResponse -// @Router /api/auth/password [put] +// @Param request body PasswordChangeRequest true "Старый и новый пароль" +// @Success 200 {object} map[string]string "{"message": "password changed successfully"}" +// @Failure 400 {object} ErrorResponse "Ошибка валидации: неверный старый пароль, слабый новый или совпадают" +// @Failure 401 {object} ErrorResponse "Токен не указан или недействителен" +// @Router /api/v1/auth/password [put] func (h *Handler) ChangePassword(c *gin.Context) { userID := api.GetUserID(c) if userID == "" { @@ -216,17 +221,18 @@ func (h *Handler) ChangePassword(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "password changed successfully"}) } -// @Summary Update profile -// @Description Обновить username текущего пользователя +// @Summary Обновление профиля +// @Description Обновление username текущего пользователя. +// @Description **Требуется:** заголовок `Authorization: Bearer `. // @Tags auth // @Accept json // @Produce json // @Security Bearer -// @Param request body UpdateProfileRequest true "Profile update" -// @Success 200 {object} UserResponse -// @Failure 400 {object} ErrorResponse -// @Failure 401 {object} ErrorResponse -// @Router /api/auth/me [put] +// @Param request body UpdateProfileRequest true "Новый username" +// @Success 200 {object} UserResponse "Обновлённый профиль" +// @Failure 400 {object} ErrorResponse "Ошибка валидации: username от 3 до 30 символов" +// @Failure 401 {object} ErrorResponse "Токен не указан или недействителен" +// @Router /api/v1/auth/me [put] func (h *Handler) UpdateProfile(c *gin.Context) { userID := api.GetUserID(c) if userID == "" { diff --git a/internal/middleware/ratelimit.go b/internal/middleware/ratelimit.go deleted file mode 100644 index aa96baf..0000000 --- a/internal/middleware/ratelimit.go +++ /dev/null @@ -1,87 +0,0 @@ -package middleware - -import ( - "net/http" - "sync" - "time" - - "github.com/gin-gonic/gin" -) - -type visitor struct { - count int - lastSeen time.Time -} - -type RateLimiter struct { - mu sync.Mutex - visitors map[string]*visitor - rate int - window time.Duration - done chan struct{} -} - -func NewRateLimiter(rate int, window time.Duration) *RateLimiter { - rl := &RateLimiter{ - visitors: make(map[string]*visitor), - rate: rate, - window: window, - done: make(chan struct{}), - } - go rl.cleanup() - return rl -} - -func (rl *RateLimiter) Stop() { - close(rl.done) -} - -func (rl *RateLimiter) cleanup() { - ticker := time.NewTicker(10 * time.Minute) - defer ticker.Stop() - for { - select { - case <-rl.done: - return - case <-ticker.C: - rl.mu.Lock() - now := time.Now() - for ip, v := range rl.visitors { - if now.Sub(v.lastSeen) > rl.window*2 { - delete(rl.visitors, ip) - } - } - rl.mu.Unlock() - } - } -} - -func (rl *RateLimiter) Middleware() gin.HandlerFunc { - return func(c *gin.Context) { - ip := c.ClientIP() - - rl.mu.Lock() - v, exists := rl.visitors[ip] - now := time.Now() - - if !exists || now.Sub(v.lastSeen) > rl.window { - rl.visitors[ip] = &visitor{count: 1, lastSeen: now} - rl.mu.Unlock() - c.Next() - return - } - - v.count++ - v.lastSeen = now - - if v.count > rl.rate { - rl.mu.Unlock() - c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many requests, try again later"}) - c.Abort() - return - } - - rl.mu.Unlock() - c.Next() - } -} diff --git a/internal/org/handler.go b/internal/org/handler.go index deff358..b613d79 100644 --- a/internal/org/handler.go +++ b/internal/org/handler.go @@ -17,17 +17,18 @@ func NewHandler(service *Service) *Handler { return &Handler{service: service} } -// @Summary Create organization -// @Description Create a new organization +// @Summary Создание организации +// @Description Создание новой организации. slug используется в URL и должен быть уникальным. +// @Description **Требуется:** заголовок `Authorization: Bearer `. // @Tags organizations // @Accept json // @Produce json // @Security Bearer -// @Param request body CreateOrgRequest true "Organization details" -// @Success 201 {object} OrgResponse -// @Failure 400 {object} ErrorResponse -// @Failure 409 {object} ErrorResponse -// @Router /api/organizations [post] +// @Param request body CreateOrgRequest true "Название и slug организации" +// @Success 201 {object} OrgResponse "Организация создана" +// @Failure 400 {object} ErrorResponse "Ошибка валидации полей" +// @Failure 409 {object} ErrorResponse "Slug уже занят" +// @Router /api/v1/organizations [post] func (h *Handler) Create(c *gin.Context) { var req CreateOrgRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -49,16 +50,17 @@ func (h *Handler) Create(c *gin.Context) { c.JSON(http.StatusCreated, OrgResponse{Organization: *org}) } -// @Summary Get organization by ID -// @Description Get organization details +// @Summary Получить организацию +// @Description Получение информации об организации по её ID. +// @Description **Требуется:** заголовок `Authorization: Bearer `. // @Tags organizations // @Accept json // @Produce json // @Security Bearer -// @Param id path string true "Organization ID" -// @Success 200 {object} OrgResponse -// @Failure 404 {object} ErrorResponse -// @Router /api/organizations/{id} [get] +// @Param id path string true "UUID организации" +// @Success 200 {object} OrgResponse "Данные организации" +// @Failure 404 {object} ErrorResponse "Организация не найдена" +// @Router /api/v1/organizations/{id} [get] func (h *Handler) GetByID(c *gin.Context) { id := c.Param("id") @@ -76,17 +78,18 @@ func (h *Handler) GetByID(c *gin.Context) { c.JSON(http.StatusOK, OrgResponse{Organization: *org}) } -// @Summary List organizations -// @Description Get all organizations with pagination +// @Summary Список организаций +// @Description Получение списка всех организаций с пагинацией. +// @Description **Требуется:** заголовок `Authorization: Bearer `. // @Tags organizations // @Accept json // @Produce json // @Security Bearer -// @Param limit query int false "Page size (default 20)" -// @Param offset query int false "Offset (default 0)" -// @Success 200 {object} OrgListResponse -// @Failure 500 {object} ErrorResponse -// @Router /api/organizations [get] +// @Param limit query int false "Количество записей на странице (по умолчанию 20)" +// @Param offset query int false "Смещение от начала списка (по умолчанию 0)" +// @Success 200 {object} OrgListResponse "Список организаций" +// @Failure 500 {object} ErrorResponse "Внутренняя ошибка сервера" +// @Router /api/v1/organizations [get] func (h *Handler) List(c *gin.Context) { limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) @@ -101,18 +104,19 @@ func (h *Handler) List(c *gin.Context) { c.JSON(http.StatusOK, resp) } -// @Summary Update organization -// @Description Update organization name +// @Summary Обновление организации +// @Description Обновление названия организации. slug изменить нельзя. +// @Description **Требуется:** заголовок `Authorization: Bearer `. // @Tags organizations // @Accept json // @Produce json // @Security Bearer -// @Param id path string true "Organization ID" -// @Param request body UpdateOrgRequest true "New organization details" -// @Success 200 {object} OrgResponse -// @Failure 400 {object} ErrorResponse -// @Failure 404 {object} ErrorResponse -// @Router /api/organizations/{id} [put] +// @Param id path string true "UUID организации" +// @Param request body UpdateOrgRequest true "Новое название организации" +// @Success 200 {object} OrgResponse "Обновлённая организация" +// @Failure 400 {object} ErrorResponse "Ошибка валидации полей" +// @Failure 404 {object} ErrorResponse "Организация не найдена" +// @Router /api/v1/organizations/{id} [put] func (h *Handler) Update(c *gin.Context) { id := c.Param("id") @@ -136,16 +140,17 @@ func (h *Handler) Update(c *gin.Context) { c.JSON(http.StatusOK, OrgResponse{Organization: *org}) } -// @Summary Delete organization -// @Description Delete an organization +// @Summary Удаление организации +// @Description Безвозвратное удаление организации по её ID. +// @Description **Требуется:** заголовок `Authorization: Bearer `. // @Tags organizations // @Accept json // @Produce json // @Security Bearer -// @Param id path string true "Organization ID" -// @Success 200 {object} map[string]string -// @Failure 404 {object} ErrorResponse -// @Router /api/organizations/{id} [delete] +// @Param id path string true "UUID организации" +// @Success 200 {object} map[string]string "{"message": "organization deleted"}" +// @Failure 404 {object} ErrorResponse "Организация не найдена" +// @Router /api/v1/organizations/{id} [delete] func (h *Handler) Delete(c *gin.Context) { id := c.Param("id")