diff --git a/backend/?mode=rwc&pragma=journal_mode(wal)&pragma=synchronous(normal)&pragma=busy_timeout(30000) b/backend/?mode=rwc&pragma=journal_mode(wal)&pragma=synchronous(normal)&pragma=busy_timeout(30000) new file mode 100644 index 0000000..15e7dfc Binary files /dev/null and b/backend/?mode=rwc&pragma=journal_mode(wal)&pragma=synchronous(normal)&pragma=busy_timeout(30000) differ diff --git a/backend/cmd/main.go b/backend/cmd/main.go index a45ab83..9674fed 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -74,6 +74,7 @@ func main() { PermissionView: true, PermissionManage: true, PermissionAdmin: true, + IsActive: true, // Admin user is active by default }) if err != nil { log.Printf("Warning: failed to create admin user: %v", err) @@ -108,6 +109,17 @@ func main() { authTokenGroup.GET("/tokens", handlers.RequireAdmin(), auth.ListTokens) authTokenGroup.DELETE("/token", auth.DeleteMyToken) authTokenGroup.DELETE("/tokens/:login", handlers.RequireAdmin(), auth.DeleteToken) + + // User management (admin only) - Full CRUD + authTokenGroup.GET("/users/:login", handlers.RequireAdmin(), auth.GetUser) + authTokenGroup.PUT("/users/:login", handlers.RequireAdmin(), auth.UpdateUser) + authTokenGroup.PUT("/users/:login/permissions", handlers.RequireAdmin(), auth.UpdateUserPermissions) + authTokenGroup.PUT("/users/:login/password", handlers.RequireAdmin(), auth.ResetUserPassword) + + // User activation management (admin only) + authTokenGroup.POST("/users/:login/activate", handlers.RequireAdmin(), auth.ActivateUser) + authTokenGroup.POST("/users/:login/deactivate", handlers.RequireAdmin(), auth.DeactivateUser) + authTokenGroup.GET("/users/inactive", handlers.RequireAdmin(), auth.ListInactiveUsers) } // Agents (requires manage_agent permission) @@ -126,12 +138,17 @@ func main() { agentRegTokenGroup.Use(auth.AuthMiddleware(), handlers.RequireManageAgent()) { agentRegTokenGroup.POST("/register-token", agentReg.CreateRegistrationToken) + agentRegTokenGroup.POST("/deploy", agentDeploy.DeployAgents) } // Logs (requires view permission) logsGroup := v1.Group("/logs") logsGroup.Use(auth.AuthMiddleware(), handlers.RequireView()) { + // Mock logs endpoint (always available, no ClickHouse required) + mockLogHandlers := handlers.NewLogHandlers(nil) + logsGroup.GET("/mock", mockLogHandlers.GetMockLogs) + if cfg.Database.Clickhouse_host != "" { chConn, err := storage.OpenClickHouse(storage.ClickHouseConfig{ Host: cfg.Database.Clickhouse_host, diff --git a/backend/dockerfile b/backend/dockerfile index 4e9e2ca..13ed416 100644 --- a/backend/dockerfile +++ b/backend/dockerfile @@ -14,7 +14,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \ FROM alpine:3.23.0 -RUN apk add --no-cache curl openssl bash +RUN apk add --no-cache curl openssl bash ansible COPY --from=builder /app/backend/backend . COPY --from=builder /app/backend/scripts /etc/hellreign/scripts diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 60f82ea..8dd0dd9 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -31,7 +31,64 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/internal_handlers.AgentInfo" + "$ref": "#/definitions/handlers.AgentInfo" + } + } + } + } + } + }, + "/agents/deploy": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Deploy HellreigN agents to multiple servers using Ansible playbooks. Supports Docker and Binary deployment types.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "agents" + ], + "summary": "Deploy agents to multiple servers via Ansible", + "parameters": [ + { + "description": "Deployment configuration for servers", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repository.DeployAgentsRequest" + } + } + ], + "responses": { + "200": { + "description": "Deployment results with tokens for each server", + "schema": { + "$ref": "#/definitions/repository.DeployResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" } } } @@ -57,7 +114,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_handlers.RegisterRequest" + "$ref": "#/definitions/handlers.RegisterRequest" } } ], @@ -65,7 +122,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_handlers.RegisterResponse" + "$ref": "#/definitions/handlers.RegisterResponse" } } } @@ -95,7 +152,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest" + "$ref": "#/definitions/repository.RegistrationRequest" } } ], @@ -129,7 +186,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginRequest" + "$ref": "#/definitions/repository.LoginRequest" } } ], @@ -137,7 +194,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginResponse" + "$ref": "#/definitions/repository.LoginResponse" } }, "400": { @@ -157,6 +214,15 @@ const docTemplate = `{ "type": "string" } } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } } } } @@ -178,7 +244,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate" + "$ref": "#/definitions/repository.TokenCreate" } } ], @@ -274,7 +340,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens" + "$ref": "#/definitions/repository.Tokens" } } }, @@ -337,6 +403,408 @@ const docTemplate = `{ } } }, + "/auth/users/:login": { + "get": { + "description": "Returns a user by their login (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get user by login", + "parameters": [ + { + "type": "string", + "description": "Login of the user", + "name": "login", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repository.Tokens" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Updates a user's name and last name (admin only)", + "consumes": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Update user", + "parameters": [ + { + "type": "string", + "description": "Login of the user", + "name": "login", + "in": "path", + "required": true + }, + { + "description": "User data to update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repository.TokenUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/users/:login/activate": { + "post": { + "description": "Activates a user account by login (admin only)", + "tags": [ + "auth" + ], + "summary": "Activate user", + "parameters": [ + { + "type": "string", + "description": "Login of the user to activate", + "name": "login", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/users/:login/deactivate": { + "post": { + "description": "Deactivates a user account by login (admin only)", + "tags": [ + "auth" + ], + "summary": "Deactivate user", + "parameters": [ + { + "type": "string", + "description": "Login of the user to deactivate", + "name": "login", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/users/:login/password": { + "put": { + "description": "Resets a user's password to a new value (admin only)", + "consumes": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Reset user password", + "parameters": [ + { + "type": "string", + "description": "Login of the user", + "name": "login", + "in": "path", + "required": true + }, + { + "description": "New password", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repository.TokenPasswordReset" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/users/:login/permissions": { + "put": { + "description": "Updates a user's permissions and activation status (admin only)", + "consumes": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Update user permissions", + "parameters": [ + { + "type": "string", + "description": "Login of the user", + "name": "login", + "in": "path", + "required": true + }, + { + "description": "Permissions to update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repository.TokenUpdatePermissions" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/users/inactive": { + "get": { + "description": "Returns list of all users waiting for activation", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "List inactive users", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/repository.Tokens" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/auth/validate": { "get": { "description": "Check if the provided Bearer token is valid and return its permissions", @@ -351,7 +819,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens" + "$ref": "#/definitions/repository.Tokens" } }, "401": { @@ -428,7 +896,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_storage.LogEntry" + "$ref": "#/definitions/storage.LogEntry" } } } @@ -453,7 +921,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_handlers.InsertLogRequest" + "$ref": "#/definitions/handlers.InsertLogRequest" } } ], @@ -513,7 +981,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_handlers.InsertLogsRequest" + "$ref": "#/definitions/handlers.InsertLogsRequest" } } ], @@ -553,6 +1021,63 @@ const docTemplate = `{ } } }, + "/logs/mock": { + "get": { + "description": "Returns 100 mock log entries for frontend development (no ClickHouse required)", + "produces": [ + "application/json" + ], + "tags": [ + "logs" + ], + "summary": "Get mock logs", + "parameters": [ + { + "type": "string", + "description": "Filter by level", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "Filter by service", + "name": "service", + "in": "query" + }, + { + "type": "string", + "description": "Filter by agent", + "name": "agent", + "in": "query" + }, + { + "type": "integer", + "default": 100, + "description": "Limit results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset results", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/storage.LogEntry" + } + } + } + } + } + }, "/logs/services": { "get": { "description": "Returns list of all unique service names in logs", @@ -578,140 +1103,7 @@ const docTemplate = `{ } }, "definitions": { - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginRequest": { - "type": "object", - "required": [ - "login", - "password" - ], - "properties": { - "login": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginResponse": { - "type": "object", - "properties": { - "last_name": { - "type": "string" - }, - "login": { - "type": "string" - }, - "name": { - "type": "string" - }, - "permission_admin": { - "type": "boolean" - }, - "permission_manage_agent": { - "type": "boolean" - }, - "permission_view": { - "type": "boolean" - }, - "token": { - "type": "string" - } - } - }, - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - } - }, - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate": { - "type": "object", - "required": [ - "last_name", - "login", - "name", - "password" - ], - "properties": { - "last_name": { - "type": "string" - }, - "login": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "permission_admin": { - "type": "boolean" - }, - "permission_manage_agent": { - "type": "boolean" - }, - "permission_view": { - "type": "boolean" - } - } - }, - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "last_name": { - "type": "string" - }, - "login": { - "type": "string" - }, - "name": { - "type": "string" - }, - "permission_admin": { - "type": "boolean" - }, - "permission_manage_agent": { - "type": "boolean" - }, - "permission_view": { - "type": "boolean" - }, - "token": { - "type": "string" - } - } - }, - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_storage.LogEntry": { - "type": "object", - "properties": { - "agent": { - "type": "string" - }, - "level": { - "type": "string" - }, - "message": { - "type": "string" - }, - "service": { - "type": "string" - }, - "timestamp": { - "type": "string" - } - } - }, - "internal_handlers.AgentInfo": { + "handlers.AgentInfo": { "type": "object", "properties": { "label": { @@ -728,7 +1120,7 @@ const docTemplate = `{ } } }, - "internal_handlers.InsertLogRequest": { + "handlers.InsertLogRequest": { "type": "object", "required": [ "agent", @@ -754,7 +1146,7 @@ const docTemplate = `{ } } }, - "internal_handlers.InsertLogsRequest": { + "handlers.InsertLogsRequest": { "type": "object", "required": [ "logs" @@ -763,12 +1155,12 @@ const docTemplate = `{ "logs": { "type": "array", "items": { - "$ref": "#/definitions/internal_handlers.InsertLogRequest" + "$ref": "#/definitions/handlers.InsertLogRequest" } } } }, - "internal_handlers.RegisterRequest": { + "handlers.RegisterRequest": { "type": "object", "required": [ "csr", @@ -783,7 +1175,7 @@ const docTemplate = `{ } } }, - "internal_handlers.RegisterResponse": { + "handlers.RegisterResponse": { "type": "object", "properties": { "ca_cert": { @@ -793,6 +1185,322 @@ const docTemplate = `{ "type": "string" } } + }, + "repository.AgentDeployConfig": { + "description": "Configuration for deploying HellreigN agent to a single server", + "type": "object", + "required": [ + "agentLabel", + "authMethod", + "deployType", + "ip", + "user" + ], + "properties": { + "agentLabel": { + "type": "string", + "example": "production-server-1" + }, + "authMethod": { + "allOf": [ + { + "$ref": "#/definitions/repository.AuthMethod" + } + ], + "example": "key" + }, + "deployType": { + "allOf": [ + { + "$ref": "#/definitions/repository.DeployType" + } + ], + "example": "docker" + }, + "ip": { + "type": "string", + "example": "192.168.1.100" + }, + "password": { + "type": "string", + "example": "secret" + }, + "port": { + "type": "integer", + "example": 22 + }, + "sshKey": { + "type": "string", + "example": "-----BEGIN OPENSSH PRIVATE KEY-----" + }, + "user": { + "type": "string", + "example": "admin" + } + } + }, + "repository.AuthMethod": { + "description": "SSH authentication method: key or password", + "type": "string", + "enum": [ + "key", + "password" + ], + "x-enum-varnames": [ + "AuthMethodKey", + "AuthMethodPassword" + ] + }, + "repository.DeployAgentsRequest": { + "description": "Request to deploy HellreigN agents to multiple servers", + "type": "object", + "required": [ + "servers" + ], + "properties": { + "servers": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/repository.AgentDeployConfig" + } + } + } + }, + "repository.DeployResponse": { + "description": "Response containing deployment results and registration tokens", + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Deployment completed" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/repository.DeployResult" + } + } + } + }, + "repository.DeployResult": { + "description": "Result of deploying to a single server", + "type": "object", + "properties": { + "agent_label": { + "type": "string", + "example": "production-server-1" + }, + "error": { + "type": "string", + "example": "" + }, + "ip": { + "type": "string", + "example": "192.168.1.100" + }, + "success": { + "type": "boolean", + "example": true + }, + "token": { + "type": "string", + "example": "abc123..." + } + } + }, + "repository.DeployType": { + "description": "Type of deployment: docker or binary", + "type": "string", + "enum": [ + "docker", + "binary" + ], + "x-enum-varnames": [ + "DeployTypeDocker", + "DeployTypeBinary" + ] + }, + "repository.LoginRequest": { + "type": "object", + "required": [ + "login", + "password" + ], + "properties": { + "login": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "repository.LoginResponse": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean" + }, + "last_name": { + "type": "string" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "permission_admin": { + "type": "boolean" + }, + "permission_manage_agent": { + "type": "boolean" + }, + "permission_view": { + "type": "boolean" + }, + "token": { + "type": "string" + } + } + }, + "repository.RegistrationRequest": { + "type": "object", + "required": [ + "label" + ], + "properties": { + "label": { + "type": "string" + } + } + }, + "repository.TokenCreate": { + "type": "object", + "required": [ + "last_name", + "login", + "name", + "password" + ], + "properties": { + "is_active": { + "type": "boolean" + }, + "last_name": { + "type": "string" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "permission_admin": { + "type": "boolean" + }, + "permission_manage_agent": { + "type": "boolean" + }, + "permission_view": { + "type": "boolean" + } + } + }, + "repository.TokenPasswordReset": { + "type": "object", + "required": [ + "new_password" + ], + "properties": { + "new_password": { + "type": "string" + } + } + }, + "repository.TokenUpdate": { + "type": "object", + "properties": { + "last_name": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "repository.TokenUpdatePermissions": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean" + }, + "permission_admin": { + "type": "boolean" + }, + "permission_manage_agent": { + "type": "boolean" + }, + "permission_view": { + "type": "boolean" + } + } + }, + "repository.Tokens": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "last_name": { + "type": "string" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "permission_admin": { + "type": "boolean" + }, + "permission_manage_agent": { + "type": "boolean" + }, + "permission_view": { + "type": "boolean" + }, + "token": { + "type": "string" + } + } + }, + "storage.LogEntry": { + "type": "object", + "properties": { + "agent": { + "type": "string" + }, + "level": { + "type": "string" + }, + "message": { + "type": "string" + }, + "service": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 758c170..8f04013 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -20,7 +20,64 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/internal_handlers.AgentInfo" + "$ref": "#/definitions/handlers.AgentInfo" + } + } + } + } + } + }, + "/agents/deploy": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Deploy HellreigN agents to multiple servers using Ansible playbooks. Supports Docker and Binary deployment types.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "agents" + ], + "summary": "Deploy agents to multiple servers via Ansible", + "parameters": [ + { + "description": "Deployment configuration for servers", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repository.DeployAgentsRequest" + } + } + ], + "responses": { + "200": { + "description": "Deployment results with tokens for each server", + "schema": { + "$ref": "#/definitions/repository.DeployResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" } } } @@ -46,7 +103,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_handlers.RegisterRequest" + "$ref": "#/definitions/handlers.RegisterRequest" } } ], @@ -54,7 +111,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_handlers.RegisterResponse" + "$ref": "#/definitions/handlers.RegisterResponse" } } } @@ -84,7 +141,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest" + "$ref": "#/definitions/repository.RegistrationRequest" } } ], @@ -118,7 +175,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginRequest" + "$ref": "#/definitions/repository.LoginRequest" } } ], @@ -126,7 +183,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginResponse" + "$ref": "#/definitions/repository.LoginResponse" } }, "400": { @@ -146,6 +203,15 @@ "type": "string" } } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } } } } @@ -167,7 +233,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate" + "$ref": "#/definitions/repository.TokenCreate" } } ], @@ -263,7 +329,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens" + "$ref": "#/definitions/repository.Tokens" } } }, @@ -326,6 +392,408 @@ } } }, + "/auth/users/:login": { + "get": { + "description": "Returns a user by their login (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get user by login", + "parameters": [ + { + "type": "string", + "description": "Login of the user", + "name": "login", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repository.Tokens" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Updates a user's name and last name (admin only)", + "consumes": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Update user", + "parameters": [ + { + "type": "string", + "description": "Login of the user", + "name": "login", + "in": "path", + "required": true + }, + { + "description": "User data to update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repository.TokenUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/users/:login/activate": { + "post": { + "description": "Activates a user account by login (admin only)", + "tags": [ + "auth" + ], + "summary": "Activate user", + "parameters": [ + { + "type": "string", + "description": "Login of the user to activate", + "name": "login", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/users/:login/deactivate": { + "post": { + "description": "Deactivates a user account by login (admin only)", + "tags": [ + "auth" + ], + "summary": "Deactivate user", + "parameters": [ + { + "type": "string", + "description": "Login of the user to deactivate", + "name": "login", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/users/:login/password": { + "put": { + "description": "Resets a user's password to a new value (admin only)", + "consumes": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Reset user password", + "parameters": [ + { + "type": "string", + "description": "Login of the user", + "name": "login", + "in": "path", + "required": true + }, + { + "description": "New password", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repository.TokenPasswordReset" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/users/:login/permissions": { + "put": { + "description": "Updates a user's permissions and activation status (admin only)", + "consumes": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Update user permissions", + "parameters": [ + { + "type": "string", + "description": "Login of the user", + "name": "login", + "in": "path", + "required": true + }, + { + "description": "Permissions to update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repository.TokenUpdatePermissions" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/users/inactive": { + "get": { + "description": "Returns list of all users waiting for activation", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "List inactive users", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/repository.Tokens" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/auth/validate": { "get": { "description": "Check if the provided Bearer token is valid and return its permissions", @@ -340,7 +808,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens" + "$ref": "#/definitions/repository.Tokens" } }, "401": { @@ -417,7 +885,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_storage.LogEntry" + "$ref": "#/definitions/storage.LogEntry" } } } @@ -442,7 +910,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_handlers.InsertLogRequest" + "$ref": "#/definitions/handlers.InsertLogRequest" } } ], @@ -502,7 +970,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_handlers.InsertLogsRequest" + "$ref": "#/definitions/handlers.InsertLogsRequest" } } ], @@ -542,6 +1010,63 @@ } } }, + "/logs/mock": { + "get": { + "description": "Returns 100 mock log entries for frontend development (no ClickHouse required)", + "produces": [ + "application/json" + ], + "tags": [ + "logs" + ], + "summary": "Get mock logs", + "parameters": [ + { + "type": "string", + "description": "Filter by level", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "Filter by service", + "name": "service", + "in": "query" + }, + { + "type": "string", + "description": "Filter by agent", + "name": "agent", + "in": "query" + }, + { + "type": "integer", + "default": 100, + "description": "Limit results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset results", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/storage.LogEntry" + } + } + } + } + } + }, "/logs/services": { "get": { "description": "Returns list of all unique service names in logs", @@ -567,140 +1092,7 @@ } }, "definitions": { - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginRequest": { - "type": "object", - "required": [ - "login", - "password" - ], - "properties": { - "login": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginResponse": { - "type": "object", - "properties": { - "last_name": { - "type": "string" - }, - "login": { - "type": "string" - }, - "name": { - "type": "string" - }, - "permission_admin": { - "type": "boolean" - }, - "permission_manage_agent": { - "type": "boolean" - }, - "permission_view": { - "type": "boolean" - }, - "token": { - "type": "string" - } - } - }, - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - } - }, - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate": { - "type": "object", - "required": [ - "last_name", - "login", - "name", - "password" - ], - "properties": { - "last_name": { - "type": "string" - }, - "login": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "permission_admin": { - "type": "boolean" - }, - "permission_manage_agent": { - "type": "boolean" - }, - "permission_view": { - "type": "boolean" - } - } - }, - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "last_name": { - "type": "string" - }, - "login": { - "type": "string" - }, - "name": { - "type": "string" - }, - "permission_admin": { - "type": "boolean" - }, - "permission_manage_agent": { - "type": "boolean" - }, - "permission_view": { - "type": "boolean" - }, - "token": { - "type": "string" - } - } - }, - "gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_storage.LogEntry": { - "type": "object", - "properties": { - "agent": { - "type": "string" - }, - "level": { - "type": "string" - }, - "message": { - "type": "string" - }, - "service": { - "type": "string" - }, - "timestamp": { - "type": "string" - } - } - }, - "internal_handlers.AgentInfo": { + "handlers.AgentInfo": { "type": "object", "properties": { "label": { @@ -717,7 +1109,7 @@ } } }, - "internal_handlers.InsertLogRequest": { + "handlers.InsertLogRequest": { "type": "object", "required": [ "agent", @@ -743,7 +1135,7 @@ } } }, - "internal_handlers.InsertLogsRequest": { + "handlers.InsertLogsRequest": { "type": "object", "required": [ "logs" @@ -752,12 +1144,12 @@ "logs": { "type": "array", "items": { - "$ref": "#/definitions/internal_handlers.InsertLogRequest" + "$ref": "#/definitions/handlers.InsertLogRequest" } } } }, - "internal_handlers.RegisterRequest": { + "handlers.RegisterRequest": { "type": "object", "required": [ "csr", @@ -772,7 +1164,7 @@ } } }, - "internal_handlers.RegisterResponse": { + "handlers.RegisterResponse": { "type": "object", "properties": { "ca_cert": { @@ -782,6 +1174,322 @@ "type": "string" } } + }, + "repository.AgentDeployConfig": { + "description": "Configuration for deploying HellreigN agent to a single server", + "type": "object", + "required": [ + "agentLabel", + "authMethod", + "deployType", + "ip", + "user" + ], + "properties": { + "agentLabel": { + "type": "string", + "example": "production-server-1" + }, + "authMethod": { + "allOf": [ + { + "$ref": "#/definitions/repository.AuthMethod" + } + ], + "example": "key" + }, + "deployType": { + "allOf": [ + { + "$ref": "#/definitions/repository.DeployType" + } + ], + "example": "docker" + }, + "ip": { + "type": "string", + "example": "192.168.1.100" + }, + "password": { + "type": "string", + "example": "secret" + }, + "port": { + "type": "integer", + "example": 22 + }, + "sshKey": { + "type": "string", + "example": "-----BEGIN OPENSSH PRIVATE KEY-----" + }, + "user": { + "type": "string", + "example": "admin" + } + } + }, + "repository.AuthMethod": { + "description": "SSH authentication method: key or password", + "type": "string", + "enum": [ + "key", + "password" + ], + "x-enum-varnames": [ + "AuthMethodKey", + "AuthMethodPassword" + ] + }, + "repository.DeployAgentsRequest": { + "description": "Request to deploy HellreigN agents to multiple servers", + "type": "object", + "required": [ + "servers" + ], + "properties": { + "servers": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/repository.AgentDeployConfig" + } + } + } + }, + "repository.DeployResponse": { + "description": "Response containing deployment results and registration tokens", + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Deployment completed" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/repository.DeployResult" + } + } + } + }, + "repository.DeployResult": { + "description": "Result of deploying to a single server", + "type": "object", + "properties": { + "agent_label": { + "type": "string", + "example": "production-server-1" + }, + "error": { + "type": "string", + "example": "" + }, + "ip": { + "type": "string", + "example": "192.168.1.100" + }, + "success": { + "type": "boolean", + "example": true + }, + "token": { + "type": "string", + "example": "abc123..." + } + } + }, + "repository.DeployType": { + "description": "Type of deployment: docker or binary", + "type": "string", + "enum": [ + "docker", + "binary" + ], + "x-enum-varnames": [ + "DeployTypeDocker", + "DeployTypeBinary" + ] + }, + "repository.LoginRequest": { + "type": "object", + "required": [ + "login", + "password" + ], + "properties": { + "login": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "repository.LoginResponse": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean" + }, + "last_name": { + "type": "string" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "permission_admin": { + "type": "boolean" + }, + "permission_manage_agent": { + "type": "boolean" + }, + "permission_view": { + "type": "boolean" + }, + "token": { + "type": "string" + } + } + }, + "repository.RegistrationRequest": { + "type": "object", + "required": [ + "label" + ], + "properties": { + "label": { + "type": "string" + } + } + }, + "repository.TokenCreate": { + "type": "object", + "required": [ + "last_name", + "login", + "name", + "password" + ], + "properties": { + "is_active": { + "type": "boolean" + }, + "last_name": { + "type": "string" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "permission_admin": { + "type": "boolean" + }, + "permission_manage_agent": { + "type": "boolean" + }, + "permission_view": { + "type": "boolean" + } + } + }, + "repository.TokenPasswordReset": { + "type": "object", + "required": [ + "new_password" + ], + "properties": { + "new_password": { + "type": "string" + } + } + }, + "repository.TokenUpdate": { + "type": "object", + "properties": { + "last_name": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "repository.TokenUpdatePermissions": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean" + }, + "permission_admin": { + "type": "boolean" + }, + "permission_manage_agent": { + "type": "boolean" + }, + "permission_view": { + "type": "boolean" + } + } + }, + "repository.Tokens": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "last_name": { + "type": "string" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "permission_admin": { + "type": "boolean" + }, + "permission_manage_agent": { + "type": "boolean" + }, + "permission_view": { + "type": "boolean" + }, + "token": { + "type": "string" + } + } + }, + "storage.LogEntry": { + "type": "object", + "properties": { + "agent": { + "type": "string" + }, + "level": { + "type": "string" + }, + "message": { + "type": "string" + }, + "service": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index c017e44..e8d189b 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,5 +1,155 @@ definitions: - gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginRequest: + handlers.AgentInfo: + properties: + label: + type: string + services: + items: + type: string + type: array + token: + type: string + type: object + handlers.InsertLogRequest: + properties: + agent: + type: string + level: + type: string + message: + type: string + service: + type: string + timestamp: + type: string + required: + - agent + - level + - message + - service + type: object + handlers.InsertLogsRequest: + properties: + logs: + items: + $ref: '#/definitions/handlers.InsertLogRequest' + type: array + required: + - logs + type: object + handlers.RegisterRequest: + properties: + csr: + type: string + token: + type: string + required: + - csr + - token + type: object + handlers.RegisterResponse: + properties: + ca_cert: + type: string + client_cert: + type: string + type: object + repository.AgentDeployConfig: + description: Configuration for deploying HellreigN agent to a single server + properties: + agentLabel: + example: production-server-1 + type: string + authMethod: + allOf: + - $ref: '#/definitions/repository.AuthMethod' + example: key + deployType: + allOf: + - $ref: '#/definitions/repository.DeployType' + example: docker + ip: + example: 192.168.1.100 + type: string + password: + example: secret + type: string + port: + example: 22 + type: integer + sshKey: + example: '-----BEGIN OPENSSH PRIVATE KEY-----' + type: string + user: + example: admin + type: string + required: + - agentLabel + - authMethod + - deployType + - ip + - user + type: object + repository.AuthMethod: + description: 'SSH authentication method: key or password' + enum: + - key + - password + type: string + x-enum-varnames: + - AuthMethodKey + - AuthMethodPassword + repository.DeployAgentsRequest: + description: Request to deploy HellreigN agents to multiple servers + properties: + servers: + items: + $ref: '#/definitions/repository.AgentDeployConfig' + minItems: 1 + type: array + required: + - servers + type: object + repository.DeployResponse: + description: Response containing deployment results and registration tokens + properties: + message: + example: Deployment completed + type: string + results: + items: + $ref: '#/definitions/repository.DeployResult' + type: array + type: object + repository.DeployResult: + description: Result of deploying to a single server + properties: + agent_label: + example: production-server-1 + type: string + error: + example: "" + type: string + ip: + example: 192.168.1.100 + type: string + success: + example: true + type: boolean + token: + example: abc123... + type: string + type: object + repository.DeployType: + description: 'Type of deployment: docker or binary' + enum: + - docker + - binary + type: string + x-enum-varnames: + - DeployTypeDocker + - DeployTypeBinary + repository.LoginRequest: properties: login: type: string @@ -9,8 +159,10 @@ definitions: - login - password type: object - gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginResponse: + repository.LoginResponse: properties: + is_active: + type: boolean last_name: type: string login: @@ -26,15 +178,17 @@ definitions: token: type: string type: object - gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest: + repository.RegistrationRequest: properties: label: type: string required: - label type: object - gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate: + repository.TokenCreate: properties: + is_active: + type: boolean last_name: type: string login: @@ -55,10 +209,37 @@ definitions: - name - password type: object - gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens: + repository.TokenPasswordReset: + properties: + new_password: + type: string + required: + - new_password + type: object + repository.TokenUpdate: + properties: + last_name: + type: string + name: + type: string + type: object + repository.TokenUpdatePermissions: + properties: + is_active: + type: boolean + permission_admin: + type: boolean + permission_manage_agent: + type: boolean + permission_view: + type: boolean + type: object + repository.Tokens: properties: id: type: integer + is_active: + type: boolean last_name: type: string login: @@ -74,7 +255,7 @@ definitions: token: type: string type: object - gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_storage.LogEntry: + storage.LogEntry: properties: agent: type: string @@ -87,61 +268,6 @@ definitions: timestamp: type: string type: object - internal_handlers.AgentInfo: - properties: - label: - type: string - services: - items: - type: string - type: array - token: - type: string - type: object - internal_handlers.InsertLogRequest: - properties: - agent: - type: string - level: - type: string - message: - type: string - service: - type: string - timestamp: - type: string - required: - - agent - - level - - message - - service - type: object - internal_handlers.InsertLogsRequest: - properties: - logs: - items: - $ref: '#/definitions/internal_handlers.InsertLogRequest' - type: array - required: - - logs - type: object - internal_handlers.RegisterRequest: - properties: - csr: - type: string - token: - type: string - required: - - csr - - token - type: object - internal_handlers.RegisterResponse: - properties: - ca_cert: - type: string - client_cert: - type: string - type: object info: contact: {} paths: @@ -155,11 +281,48 @@ paths: description: OK schema: items: - $ref: '#/definitions/internal_handlers.AgentInfo' + $ref: '#/definitions/handlers.AgentInfo' type: array summary: Get connected agents tags: - agents + /agents/deploy: + post: + consumes: + - application/json + description: Deploy HellreigN agents to multiple servers using Ansible playbooks. + Supports Docker and Binary deployment types. + parameters: + - description: Deployment configuration for servers + in: body + name: request + required: true + schema: + $ref: '#/definitions/repository.DeployAgentsRequest' + produces: + - application/json + responses: + "200": + description: Deployment results with tokens for each server + schema: + $ref: '#/definitions/repository.DeployResponse' + "400": + description: Invalid request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + security: + - Bearer: [] + summary: Deploy agents to multiple servers via Ansible + tags: + - agents /agents/register: post: consumes: @@ -170,14 +333,14 @@ paths: name: request required: true schema: - $ref: '#/definitions/internal_handlers.RegisterRequest' + $ref: '#/definitions/handlers.RegisterRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_handlers.RegisterResponse' + $ref: '#/definitions/handlers.RegisterResponse' summary: Register agent tags: - agents @@ -191,7 +354,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest' + $ref: '#/definitions/repository.RegistrationRequest' produces: - application/json responses: @@ -217,12 +380,12 @@ paths: name: request required: true schema: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginRequest' + $ref: '#/definitions/repository.LoginRequest' responses: "200": description: OK schema: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.LoginResponse' + $ref: '#/definitions/repository.LoginResponse' "400": description: Bad Request schema: @@ -235,6 +398,12 @@ paths: additionalProperties: type: string type: object + "403": + description: Forbidden + schema: + additionalProperties: + type: string + type: object summary: Login tags: - auth @@ -273,7 +442,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate' + $ref: '#/definitions/repository.TokenCreate' responses: "200": description: OK @@ -312,7 +481,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens' + $ref: '#/definitions/repository.Tokens' type: array "500": description: Internal Server Error @@ -354,6 +523,272 @@ paths: summary: Delete user tags: - auth + /auth/users/:login: + get: + description: Returns a user by their login (admin only) + parameters: + - description: Login of the user + in: path + name: login + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/repository.Tokens' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Get user by login + tags: + - auth + put: + consumes: + - application/json + description: Updates a user's name and last name (admin only) + parameters: + - description: Login of the user + in: path + name: login + required: true + type: string + - description: User data to update + in: body + name: request + required: true + schema: + $ref: '#/definitions/repository.TokenUpdate' + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Update user + tags: + - auth + /auth/users/:login/activate: + post: + description: Activates a user account by login (admin only) + parameters: + - description: Login of the user to activate + in: path + name: login + required: true + type: string + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Activate user + tags: + - auth + /auth/users/:login/deactivate: + post: + description: Deactivates a user account by login (admin only) + parameters: + - description: Login of the user to deactivate + in: path + name: login + required: true + type: string + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Deactivate user + tags: + - auth + /auth/users/:login/password: + put: + consumes: + - application/json + description: Resets a user's password to a new value (admin only) + parameters: + - description: Login of the user + in: path + name: login + required: true + type: string + - description: New password + in: body + name: request + required: true + schema: + $ref: '#/definitions/repository.TokenPasswordReset' + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Reset user password + tags: + - auth + /auth/users/:login/permissions: + put: + consumes: + - application/json + description: Updates a user's permissions and activation status (admin only) + parameters: + - description: Login of the user + in: path + name: login + required: true + type: string + - description: Permissions to update + in: body + name: request + required: true + schema: + $ref: '#/definitions/repository.TokenUpdatePermissions' + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Update user permissions + tags: + - auth + /auth/users/inactive: + get: + description: Returns list of all users waiting for activation + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/repository.Tokens' + type: array + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: List inactive users + tags: + - auth /auth/validate: get: description: Check if the provided Bearer token is valid and return its permissions @@ -363,7 +798,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.Tokens' + $ref: '#/definitions/repository.Tokens' "401": description: Unauthorized schema: @@ -414,7 +849,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_storage.LogEntry' + $ref: '#/definitions/storage.LogEntry' type: array summary: Search logs tags: @@ -429,7 +864,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_handlers.InsertLogRequest' + $ref: '#/definitions/handlers.InsertLogRequest' produces: - application/json responses: @@ -468,7 +903,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_handlers.InsertLogsRequest' + $ref: '#/definitions/handlers.InsertLogsRequest' produces: - application/json responses: @@ -496,6 +931,45 @@ paths: summary: Get distinct log levels tags: - logs + /logs/mock: + get: + description: Returns 100 mock log entries for frontend development (no ClickHouse + required) + parameters: + - description: Filter by level + in: query + name: level + type: string + - description: Filter by service + in: query + name: service + type: string + - description: Filter by agent + in: query + name: agent + type: string + - default: 100 + description: Limit results + in: query + name: limit + type: integer + - default: 0 + description: Offset results + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/storage.LogEntry' + type: array + summary: Get mock logs + tags: + - logs /logs/services: get: description: Returns list of all unique service names in logs diff --git a/backend/internal/ansible/executor.go b/backend/internal/ansible/executor.go new file mode 100644 index 0000000..29294fc --- /dev/null +++ b/backend/internal/ansible/executor.go @@ -0,0 +1,135 @@ +package ansible + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "sync" +) + +// Executor handles running Ansible playbooks +type Executor struct { + workDir string + grpcServerHost string + grpcServerPort string + backendURL string +} + +// ExecutorConfig holds configuration for the Executor +type ExecutorConfig struct { + WorkDir string + GRPCServerHost string + GRPCServerPort string + BackendURL string +} + +// NewExecutor creates a new Ansible executor +func NewExecutor(cfg ExecutorConfig) *Executor { + return &Executor{ + workDir: cfg.WorkDir, + grpcServerHost: cfg.GRPCServerHost, + grpcServerPort: cfg.GRPCServerPort, + backendURL: cfg.BackendURL, + } +} + +// DeployResult holds the result of a deployment +type DeployResult struct { + Host string + Success bool + Stdout string + Stderr string + Err error +} + +// WorkDir returns the work directory path +func (e *Executor) WorkDir() string { + return e.workDir +} + +// Deploy runs Ansible playbook for the given inventory +func (e *Executor) Deploy(ctx context.Context, inventoryPath string, deployType string) ([]DeployResult, error) { + playbookName := "binary_deploy.yml" + if deployType == "docker" { + playbookName = "docker_deploy.yml" + } + + playbookPath := filepath.Join(e.workDir, playbookName) + + cmd := exec.CommandContext(ctx, "ansible-playbook", + "-i", inventoryPath, + "-e", fmt.Sprintf("backend_url=%s", e.backendURL), + playbookPath, + ) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + runErr := cmd.Run() + + // Parse results per host (simplified - returns single result for all) + return []DeployResult{ + { + Host: "all", + Success: runErr == nil, + Stdout: stdout.String(), + Stderr: stderr.String(), + Err: runErr, + }, + }, nil +} + +// DeployParallel runs Ansible playbook for multiple inventories in parallel +func (e *Executor) DeployParallel(ctx context.Context, inventoryPaths []string, deployType string) (map[string][]DeployResult, error) { + var wg sync.WaitGroup + results := make(map[string][]DeployResult) + errCh := make(chan error, len(inventoryPaths)) + + for _, path := range inventoryPaths { + wg.Add(1) + go func(p string) { + defer wg.Done() + res, err := e.Deploy(ctx, p, deployType) + if err != nil { + errCh <- err + } + results[p] = res + }(path) + } + + wg.Wait() + close(errCh) + + // Collect errors + var errs []error + for err := range errCh { + errs = append(errs, err) + } + + if len(errs) > 0 { + return results, fmt.Errorf("some deployments failed: %v", errs) + } + + return results, nil +} + +// WritePlaybook writes a playbook to the work directory +func (e *Executor) WritePlaybook(name string, content string) error { + path := filepath.Join(e.workDir, name) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + return os.WriteFile(path, []byte(content), 0644) +} + +// WriteAllPlaybooks writes all playbooks to the work directory +func (e *Executor) WriteAllPlaybooks() error { + if err := e.WritePlaybook("binary_deploy.yml", BinaryDeployPlaybook); err != nil { + return err + } + return e.WritePlaybook("docker_deploy.yml", DockerDeployPlaybook) +} diff --git a/backend/internal/ansible/inventory.go b/backend/internal/ansible/inventory.go new file mode 100644 index 0000000..d69a9f3 --- /dev/null +++ b/backend/internal/ansible/inventory.go @@ -0,0 +1,62 @@ +package ansible + +import ( + "fmt" + "os" + "path/filepath" + "text/template" +) + +// InventoryHost represents a single host in the inventory +type InventoryHost struct { + Name string + IP string + Port int + User string + AuthMethod string + SSHKey string + Password string + DeployType string + Token string +} + +// Inventory represents an Ansible inventory file +type Inventory struct { + Hosts []InventoryHost +} + +const inventoryTemplateText = `{{ range .Hosts }} +{{ .Name }} ansible_host={{ .IP }} ansible_port={{ .Port }} ansible_user={{ .User }} ansible_connection=ssh +{{ if eq .AuthMethod "key" }}ansible_ssh_private_key_file={{ .SSHKey }}{{ end }} +{{ if eq .AuthMethod "password" }}ansible_ssh_pass={{ .Password }}{{ end }} +deploy_type={{ .DeployType }} +agent_token={{ .Token }} +agent_label={{ .Name }} + +{{ end }}` + +// GenerateInventory generates an Ansible inventory file from the given hosts +func GenerateInventory(hosts []InventoryHost, outputPath string) error { + tmpl, err := template.New("inventory").Parse(inventoryTemplateText) + if err != nil { + return fmt.Errorf("failed to parse inventory template: %w", err) + } + + // Ensure directory exists + dir := filepath.Dir(outputPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create inventory directory: %w", err) + } + + file, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create inventory file: %w", err) + } + defer file.Close() + + if err := tmpl.Execute(file, Inventory{Hosts: hosts}); err != nil { + return fmt.Errorf("failed to execute inventory template: %w", err) + } + + return nil +} diff --git a/backend/internal/ansible/playbooks.go b/backend/internal/ansible/playbooks.go new file mode 100644 index 0000000..dbb0d40 --- /dev/null +++ b/backend/internal/ansible/playbooks.go @@ -0,0 +1,136 @@ +package ansible + +// BinaryDeployPlaybook returns the Ansible playbook for binary deployment +const BinaryDeployPlaybook = `--- +- name: Deploy HellreigN Agent (Binary) + hosts: all + become: yes + vars: + agent_label: "{{ agent_label }}" + agent_token: "{{ agent_token }}" + backend_url: "{{ backend_url }}" + install_dir: /opt/hellreign + bin_name: hellreign-agent + service_name: hellreign-agent + cert_dir: "{{ install_dir }}/certs" + + tasks: + - name: Create installation directory + file: + path: "{{ install_dir }}" + state: directory + mode: '0755' + + - name: Create certificates directory + file: + path: "{{ cert_dir }}" + state: directory + mode: '0755' + + - name: Download HellreigN Agent binary + get_url: + url: "https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN/releases/latest/download/{{ bin_name }}" + dest: "{{ install_dir }}/{{ bin_name }}" + mode: '0755' + + - name: Create agent configuration + copy: + content: | + backend_url: "{{ backend_url }}" + label: "{{ agent_label }}" + registration_token: "{{ agent_token }}" + cert_dir: "{{ cert_dir }}" + dest: "{{ install_dir }}/config.yml" + mode: '0644' + + - name: Create systemd service file + copy: + content: | + [Unit] + Description=HellreigN Agent + After=network.target + + [Service] + Type=simple + ExecStart={{ install_dir }}/{{ bin_name }} + Restart=always + RestartSec=5 + Environment=CONFIG_FILE={{ install_dir }}/config.yml + StandardOutput=journal + StandardError=journal + + [Install] + WantedBy=multi-user.target + dest: /etc/systemd/system/{{ service_name }}.service + mode: '0644' + + - name: Reload systemd daemon + systemd: + daemon_reload: yes + + - name: Enable and start HellreigN Agent service + systemd: + name: "{{ service_name }}" + enabled: yes + state: started +` + +// DockerDeployPlaybook returns the Ansible playbook for Docker deployment +const DockerDeployPlaybook = `--- +- name: Deploy HellreigN Agent (Docker) + hosts: all + become: yes + vars: + agent_label: "{{ agent_label }}" + agent_token: "{{ agent_token }}" + backend_url: "{{ backend_url }}" + container_name: hellreign-agent-{{ agent_label }} + image: "gitea.d3m0k1d.ru/d3m0k1d/hellreign-agent:latest" + cert_dir: /etc/hellreign-agent/certs + + tasks: + - name: Install Docker (if not present) + block: + - name: Check if Docker is installed + command: docker --version + register: docker_check + ignore_errors: yes + changed_when: false + + - name: Install Docker + shell: | + curl -fsSL https://get.docker.com | sh + when: docker_check.rc != 0 + + - name: Create certificates directory + file: + path: "{{ cert_dir }}" + state: directory + mode: '0755' + + - name: Pull HellreigN Agent image + community.docker.docker_image: + name: "{{ image }}" + source: pull + + - name: Create agent configuration + copy: + content: | + backend_url: "{{ backend_url }}" + label: "{{ agent_label }}" + registration_token: "{{ agent_token }}" + cert_dir: "{{ cert_dir }}" + dest: "{{ cert_dir }}/config.yml" + mode: '0644' + + - name: Create and run HellreigN Agent container + community.docker.docker_container: + name: "{{ container_name }}" + image: "{{ image }}" + state: started + restart_policy: always + volumes: + - "{{ cert_dir }}:/etc/hellreign-agent/certs" + env: + CONFIG_FILE: /etc/hellreign-agent/certs/config.yml +` diff --git a/backend/internal/ansible/templates.go b/backend/internal/ansible/templates.go new file mode 100644 index 0000000..dc1a823 --- /dev/null +++ b/backend/internal/ansible/templates.go @@ -0,0 +1,5 @@ +package ansible + +const BaseInvTemplate = ` + + ` diff --git a/backend/internal/handlers/agent_deploy.go b/backend/internal/handlers/agent_deploy.go new file mode 100644 index 0000000..1352d73 --- /dev/null +++ b/backend/internal/handlers/agent_deploy.go @@ -0,0 +1,172 @@ +package handlers + +import ( + "context" + "fmt" + "net/http" + "os" + "path/filepath" + "time" + + "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/ansible" + "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository" + "github.com/gin-gonic/gin" +) + +type AgentDeployGroup struct { + *Handlers + executor *ansible.Executor +} + +func NewAgentDeployGroup(h *Handlers) *AgentDeployGroup { + workDir := os.Getenv("ANSIBLE_WORK_DIR") + if workDir == "" { + workDir = "/tmp/hellreign/ansible" + } + + grpcPort := os.Getenv("GRPC_PORT") + if grpcPort == "" { + grpcPort = "9001" + } + + backendURL := os.Getenv("BACKEND_URL") + if backendURL == "" { + backendURL = "http://localhost:8080" + } + + exec := ansible.NewExecutor(ansible.ExecutorConfig{ + WorkDir: workDir, + GRPCServerHost: "0.0.0.0", // TODO: make configurable + GRPCServerPort: grpcPort, + BackendURL: backendURL, + }) + + // Write playbooks on init + if err := exec.WriteAllPlaybooks(); err != nil { + // Log but don't fail - playbooks can be written later + _ = err + } + + return &AgentDeployGroup{ + Handlers: h, + executor: exec, + } +} + +// DeployAgents deploys agents to multiple servers +// @Summary Deploy agents to multiple servers via Ansible +// @Description Deploy HellreigN agents to multiple servers using Ansible playbooks. Supports Docker and Binary deployment types. +// @Tags agents +// @Accept json +// @Produce json +// @Param request body repository.DeployAgentsRequest true "Deployment configuration for servers" +// @Success 200 {object} repository.DeployResponse "Deployment results with tokens for each server" +// @Failure 400 {object} map[string]string "Invalid request" +// @Failure 500 {object} map[string]string "Internal server error" +// @Security Bearer +// @Router /agents/deploy [post] +func (adg *AgentDeployGroup) DeployAgents(c *gin.Context) { + var req repository.DeployAgentsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Create work directory + workDir := adg.executor.WorkDir() + if err := os.MkdirAll(workDir, 0755); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create work directory"}) + return + } + + // Generate registration tokens for each server + results := make([]repository.DeployResult, 0, len(req.Servers)) + timestamp := time.Now().UnixMilli() + + ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Minute) + defer cancel() + + for i, server := range req.Servers { + // Create registration token + token, err := adg.Repo.CreateRegistrationToken(server.AgentLabel) + if err != nil { + results = append(results, repository.DeployResult{ + IP: server.IP, + AgentLabel: server.AgentLabel, + Success: false, + Error: fmt.Sprintf("failed to create token: %v", err), + }) + continue + } + + // Set default port + port := server.Port + if port == 0 { + port = 22 + } + + // Generate inventory for this single server + inventoryHosts := []ansible.InventoryHost{ + { + Name: server.AgentLabel, + IP: server.IP, + Port: port, + User: server.User, + AuthMethod: string(server.AuthMethod), + SSHKey: server.SSHKey, + Password: server.Password, + DeployType: string(server.DeployType), + Token: token, + }, + } + + inventoryPath := filepath.Join(workDir, fmt.Sprintf("inventory_%d_%d", timestamp, i)) + if err := ansible.GenerateInventory(inventoryHosts, inventoryPath); err != nil { + results = append(results, repository.DeployResult{ + IP: server.IP, + AgentLabel: server.AgentLabel, + Token: token, + Success: false, + Error: fmt.Sprintf("failed to generate inventory: %v", err), + }) + continue + } + + // Run Ansible playbook for this server + deployResults, err := adg.executor.Deploy(ctx, inventoryPath, string(server.DeployType)) + + // Clean up inventory file + os.Remove(inventoryPath) + + if err != nil { + results = append(results, repository.DeployResult{ + IP: server.IP, + AgentLabel: server.AgentLabel, + Token: token, + Success: false, + Error: fmt.Sprintf("deployment failed: %v", err), + }) + continue + } + + success := true + errMsg := "" + if len(deployResults) > 0 && !deployResults[0].Success { + success = false + errMsg = deployResults[0].Stderr + } + + results = append(results, repository.DeployResult{ + IP: server.IP, + AgentLabel: server.AgentLabel, + Token: token, + Success: success, + Error: errMsg, + }) + } + + c.JSON(http.StatusOK, repository.DeployResponse{ + Message: "Deployment completed", + Results: results, + }) +} diff --git a/backend/internal/handlers/auth.go b/backend/internal/handlers/auth.go index 1ba27ef..7ff3ebd 100644 --- a/backend/internal/handlers/auth.go +++ b/backend/internal/handlers/auth.go @@ -23,6 +23,7 @@ type AuthGroup struct { // @Success 200 {object} repository.LoginResponse // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string +// @Failure 403 {object} map[string]string // @Router /auth/login [post] func (ag *AuthGroup) Login(c *gin.Context) { var req repository.LoginRequest @@ -37,6 +38,10 @@ func (ag *AuthGroup) Login(c *gin.Context) { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } + if errors.Is(err, repository.ErrAccountInactive) { + c.JSON(http.StatusForbidden, gin.H{"error": "account is not activated by admin"}) + return + } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to authenticate"}) return } @@ -168,6 +173,223 @@ func (ag *AuthGroup) DeleteMyToken(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "account deleted"}) } +// ActivateUser activates a user by login. +// @Summary Activate user +// @Description Activates a user account by login (admin only) +// @Tags auth +// @Param login path string true "Login of the user to activate" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /auth/users/:login/activate [post] +func (ag *AuthGroup) ActivateUser(c *gin.Context) { + login := c.Param("login") + if login == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "login required"}) + return + } + + if err := ag.Repo.ActivateUserByLogin(login); err != nil { + if errors.Is(err, repository.ErrNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to activate user"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "user activated"}) +} + +// DeactivateUser deactivates a user by login. +// @Summary Deactivate user +// @Description Deactivates a user account by login (admin only) +// @Tags auth +// @Param login path string true "Login of the user to deactivate" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /auth/users/:login/deactivate [post] +func (ag *AuthGroup) DeactivateUser(c *gin.Context) { + login := c.Param("login") + if login == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "login required"}) + return + } + + if err := ag.Repo.DeactivateUserByLogin(login); err != nil { + if errors.Is(err, repository.ErrNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to deactivate user"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "user deactivated"}) +} + +// ListInactiveUsers returns all users that are not activated. +// @Summary List inactive users +// @Description Returns list of all users waiting for activation +// @Tags auth +// @Produce json +// @Success 200 {array} repository.Tokens +// @Failure 500 {object} map[string]string +// @Router /auth/users/inactive [get] +func (ag *AuthGroup) ListInactiveUsers(c *gin.Context) { + tokens, err := ag.Repo.ListInactiveTokens() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list inactive users"}) + return + } + c.JSON(http.StatusOK, tokens) +} + +// GetUser returns a user by login. +// @Summary Get user by login +// @Description Returns a user by their login (admin only) +// @Tags auth +// @Produce json +// @Param login path string true "Login of the user" +// @Success 200 {object} repository.Tokens +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /auth/users/:login [get] +func (ag *AuthGroup) GetUser(c *gin.Context) { + login := c.Param("login") + if login == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "login required"}) + return + } + + user, err := ag.Repo.GetTokenByLogin(login) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get user"}) + return + } + + c.JSON(http.StatusOK, user) +} + +// UpdateUser updates user's name and last name. +// @Summary Update user +// @Description Updates a user's name and last name (admin only) +// @Tags auth +// @Accept json +// @Param login path string true "Login of the user" +// @Param request body repository.TokenUpdate true "User data to update" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /auth/users/:login [put] +func (ag *AuthGroup) UpdateUser(c *gin.Context) { + login := c.Param("login") + if login == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "login required"}) + return + } + + var update repository.TokenUpdate + if err := c.ShouldBindJSON(&update); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + return + } + + if err := ag.Repo.UpdateToken(login, update); err != nil { + if errors.Is(err, repository.ErrNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update user"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "user updated"}) +} + +// UpdateUserPermissions updates user's permissions and activation status. +// @Summary Update user permissions +// @Description Updates a user's permissions and activation status (admin only) +// @Tags auth +// @Accept json +// @Param login path string true "Login of the user" +// @Param request body repository.TokenUpdatePermissions true "Permissions to update" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /auth/users/:login/permissions [put] +func (ag *AuthGroup) UpdateUserPermissions(c *gin.Context) { + login := c.Param("login") + if login == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "login required"}) + return + } + + var update repository.TokenUpdatePermissions + if err := c.ShouldBindJSON(&update); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + return + } + + if err := ag.Repo.UpdatePermissions(login, update); err != nil { + if errors.Is(err, repository.ErrNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update permissions"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "permissions updated"}) +} + +// ResetUserPassword resets a user's password. +// @Summary Reset user password +// @Description Resets a user's password to a new value (admin only) +// @Tags auth +// @Accept json +// @Param login path string true "Login of the user" +// @Param request body repository.TokenPasswordReset true "New password" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /auth/users/:login/password [put] +func (ag *AuthGroup) ResetUserPassword(c *gin.Context) { + login := c.Param("login") + if login == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "login required"}) + return + } + + var req repository.TokenPasswordReset + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + return + } + + if err := ag.Repo.UpdatePassword(login, req.NewPassword); err != nil { + if errors.Is(err, repository.ErrNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reset password"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "password reset"}) +} + // getTokenFromHeader extracts the Bearer token from the Authorization header. func getTokenFromHeader(c *gin.Context) string { auth := c.GetHeader("Authorization") diff --git a/backend/internal/handlers/logs_mock.go b/backend/internal/handlers/logs_mock.go new file mode 100644 index 0000000..01e30f7 --- /dev/null +++ b/backend/internal/handlers/logs_mock.go @@ -0,0 +1,202 @@ +package handlers + +import ( + "math/rand" + "net/http" + "strconv" + "time" + + "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage" + "github.com/gin-gonic/gin" +) + +// GetMockLogs returns 100 mock log entries for frontend development +// @Summary Get mock logs +// @Description Returns 100 mock log entries for frontend development (no ClickHouse required) +// @Tags logs +// @Produce json +// @Param level query string false "Filter by level" +// @Param service query string false "Filter by service" +// @Param agent query string false "Filter by agent" +// @Param limit query int false "Limit results" default(100) +// @Param offset query int false "Offset results" default(0) +// @Success 200 {array} storage.LogEntry +// @Router /logs/mock [get] +func (lh *LogHandlers) GetMockLogs(c *gin.Context) { + levelFilter := c.Query("level") + serviceFilter := c.Query("service") + agentFilter := c.Query("agent") + + limit := 100 + offset := 0 + + if l := c.Query("limit"); l != "" { + if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { + limit = parsed + } + } + if o := c.Query("offset"); o != "" { + if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { + offset = parsed + } + } + + logs := generateMockLogs(100) + + // Apply filters + var filtered []storage.LogEntry + for _, log := range logs { + if levelFilter != "" && log.Level != levelFilter { + continue + } + if serviceFilter != "" && log.Service != serviceFilter { + continue + } + if agentFilter != "" && log.Agent != agentFilter { + continue + } + filtered = append(filtered, log) + } + + // Apply pagination + end := offset + limit + if end > len(filtered) { + end = len(filtered) + } + if offset > len(filtered) { + filtered = []storage.LogEntry{} + } else { + filtered = filtered[offset:end] + } + + c.JSON(http.StatusOK, filtered) +} + +func generateMockLogs(count int) []storage.LogEntry { + services := []string{ + "auth-service", + "user-service", + "agent-service", + "gateway", + "scheduler", + "notification-service", + "metrics-collector", + "deployment-service", + } + + agents := []string{ + "agent-prod-01", + "agent-prod-02", + "agent-staging-01", + "agent-dev-01", + "agent-dev-02", + "agent-monitoring-01", + "agent-backup-01", + "agent-ci-runner-01", + } + + levels := []string{"INFO", "WARNING", "ERROR", "FATAL", "DEBUG"} + levelWeights := []int{50, 20, 15, 5, 10} // weighted distribution + + messages := map[string][]string{ + "INFO": { + "Service started successfully", + "Health check passed", + "Configuration loaded", + "Connection established to database", + "Cache refreshed successfully", + "Request processed in 45ms", + "User login successful", + "Agent registered successfully", + "Deployment completed for 3 servers", + "Metrics exported to storage", + "Backup completed successfully", + "SSL certificate valid for 89 days", + "Task scheduled: cleanup-temp-files", + "Webhook delivered successfully", + "Session created for user admin", + }, + "WARNING": { + "High memory usage detected: 85%", + "Slow query detected: 2.3s", + "Rate limit approaching for client 192.168.1.50", + "Certificate expires in 7 days", + "Retry attempt 2/3 for request", + "Disk usage above threshold: 78%", + "Connection pool nearly exhausted: 45/50", + "Deprecated API endpoint called: /api/v1/legacy", + "Response time exceeded SLA: 1.2s > 1s", + "Agent heartbeat delayed by 5s", + }, + "ERROR": { + "Failed to connect to database: timeout after 30s", + "Authentication failed for user test_user", + "Agent deployment failed: SSH connection refused", + "Failed to send notification: SMTP server unavailable", + "Request failed with status 500", + "File not found: /etc/hellreign/config.yml", + "Invalid token provided", + "Permission denied for user viewer", + "Failed to parse configuration: invalid YAML", + "Agent unreachable: connection timeout", + }, + "FATAL": { + "Out of memory: cannot allocate 512MB", + "Database connection lost, all retries exhausted", + "Critical: SSL certificate expired", + "Unrecoverable error: data corruption detected", + "Service crashed: segmentation fault", + }, + "DEBUG": { + "Processing request payload: 2.3KB", + "Cache hit ratio: 78%", + "Executing query: SELECT * FROM logs WHERE...", + "HTTP request headers: {Content-Type: application/json}", + "Agent status check: 8 agents online", + "Memory allocation: 256MB used of 1024MB", + "Thread pool size: 12 active, 4 idle", + "GC pause: 15ms", + }, + } + + r := rand.New(rand.NewSource(42)) // fixed seed for reproducibility + + var logs []storage.LogEntry + now := time.Now() + + for i := 0; i < count; i++ { + level := weightedRandom(r, levels, levelWeights) + service := services[r.Intn(len(services))] + agent := agents[r.Intn(len(agents))] + msgs := messages[level] + message := msgs[r.Intn(len(msgs))] + + // Spread logs over the last 24 hours + timestamp := now.Add(-time.Duration(count-i) * time.Minute * 15) + + logs = append(logs, storage.LogEntry{ + Timestamp: timestamp, + Level: level, + Service: service, + Agent: agent, + Message: message, + }) + } + + return logs +} + +func weightedRandom(r *rand.Rand, items []string, weights []int) string { + total := 0 + for _, w := range weights { + total += w + } + n := r.Intn(total) + for i, w := range weights { + n -= w + if n < 0 { + return items[i] + } + } + return items[len(items)-1] +} diff --git a/backend/internal/repository/models.go b/backend/internal/repository/models.go index c7cf39f..84e107c 100644 --- a/backend/internal/repository/models.go +++ b/backend/internal/repository/models.go @@ -10,6 +10,7 @@ type Tokens struct { PermissionView bool `json:"permission_view"` PermissionManage bool `json:"permission_manage_agent"` PermissionAdmin bool `json:"permission_admin"` + IsActive bool `json:"is_active"` } // TokenCreate is the request body for creating a new user. @@ -21,6 +22,31 @@ type TokenCreate struct { PermissionView bool `json:"permission_view"` PermissionManage bool `json:"permission_manage_agent"` PermissionAdmin bool `json:"permission_admin"` + IsActive bool `json:"is_active"` +} + +// TokenUpdate is the request body for updating an existing user. +type TokenUpdate struct { + Name string `json:"name"` + LastName string `json:"last_name"` +} + +// TokenUpdatePermissions is the request body for updating user permissions. +type TokenUpdatePermissions struct { + PermissionView *bool `json:"permission_view"` + PermissionManage *bool `json:"permission_manage_agent"` + PermissionAdmin *bool `json:"permission_admin"` + IsActive *bool `json:"is_active"` +} + +// TokenPasswordReset is the request body for resetting a user's password. +type TokenPasswordReset struct { + NewPassword string `json:"new_password" binding:"required"` +} + +// BatchActionRequest is the request body for batch activate/deactivate users. +type BatchActionRequest struct { + Logins []string `json:"logins" binding:"required,min=1"` } // LoginRequest is the request body for login. @@ -38,6 +64,7 @@ type LoginResponse struct { PermissionView bool `json:"permission_view"` PermissionManage bool `json:"permission_manage_agent"` PermissionAdmin bool `json:"permission_admin"` + IsActive bool `json:"is_active"` } // RegistrationToken represents a one-time agent registration token. @@ -60,3 +87,57 @@ type RegistrationResponse struct { CACert string `json:"ca_cert"` ClientCert string `json:"client_cert"` } + +// DeployType represents the type of agent deployment +// @Description Type of deployment: docker or binary +type DeployType string + +const ( + DeployTypeDocker DeployType = "docker" + DeployTypeBinary DeployType = "binary" +) + +// AuthMethod represents the SSH authentication method +// @Description SSH authentication method: key or password +type AuthMethod string + +const ( + AuthMethodKey AuthMethod = "key" + AuthMethodPassword AuthMethod = "password" +) + +// AgentDeployConfig represents the configuration for deploying an agent to a server +// @Description Configuration for deploying HellreigN agent to a single server +type AgentDeployConfig struct { + User string `json:"user" binding:"required" example:"admin" description:"SSH username"` + IP string `json:"ip" binding:"required" example:"192.168.1.100" description:"Server IP address"` + Port int `json:"port" example:"22" description:"SSH port (default: 22)"` + AuthMethod AuthMethod `json:"authMethod" binding:"required" example:"key" description:"SSH auth method: key or password"` + SSHKey string `json:"sshKey,omitempty" example:"-----BEGIN OPENSSH PRIVATE KEY-----" description:"SSH private key (required if authMethod=key)"` + Password string `json:"password,omitempty" example:"secret" description:"SSH password (required if authMethod=password)"` + DeployType DeployType `json:"deployType" binding:"required" example:"docker" description:"Deployment type: docker or binary"` + AgentLabel string `json:"agentLabel" binding:"required" example:"production-server-1" description:"Unique label for the agent"` +} + +// DeployAgentsRequest represents the request body for deploying agents to multiple servers +// @Description Request to deploy HellreigN agents to multiple servers +type DeployAgentsRequest struct { + Servers []AgentDeployConfig `json:"servers" binding:"required,min=1,dive" description:"List of server configurations"` +} + +// DeployResponse represents the response after deploying agents +// @Description Response containing deployment results and registration tokens +type DeployResponse struct { + Message string `json:"message" example:"Deployment completed"` + Results []DeployResult `json:"results" description:"Deployment results for each server"` +} + +// DeployResult represents the result of deploying to a single server +// @Description Result of deploying to a single server +type DeployResult struct { + IP string `json:"ip" example:"192.168.1.100" description:"Server IP address"` + AgentLabel string `json:"agent_label" example:"production-server-1" description:"Agent label"` + Token string `json:"token" example:"abc123..." description:"Registration token for agent registration"` + Success bool `json:"success" example:"true" description:"Whether deployment succeeded"` + Error string `json:"error,omitempty" example:"" description:"Error message if deployment failed"` +} diff --git a/backend/internal/repository/repository.go b/backend/internal/repository/repository.go index 7d1da55..49492f1 100644 --- a/backend/internal/repository/repository.go +++ b/backend/internal/repository/repository.go @@ -21,6 +21,7 @@ func New(db *sql.DB) *Repository { } var ErrNotFound = errors.New("not found") +var ErrAccountInactive = errors.New("account is not activated") // Init creates the tokens table if it does not exist. func (r *Repository) Init() error { @@ -29,6 +30,7 @@ func (r *Repository) Init() error { } // CreateToken inserts a new user record with hashed password and generated token. +// New users are created with is_active=false by default. func (r *Repository) CreateToken(tc TokenCreate) (string, error) { hashed, err := bcrypt.GenerateFromPassword([]byte(tc.Password), bcrypt.DefaultCost) if err != nil { @@ -41,10 +43,10 @@ func (r *Repository) CreateToken(tc TokenCreate) (string, error) { } result, err := r.DB.Exec( - `INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, tc.Name, tc.LastName, tc.Login, string(hashed), token, - tc.PermissionView, tc.PermissionManage, tc.PermissionAdmin, + tc.PermissionView, tc.PermissionManage, tc.PermissionAdmin, false, ) if err != nil { return "", err @@ -63,11 +65,11 @@ func (r *Repository) Login(login, password string) (*LoginResponse, error) { var hashedPassword string err := r.DB.QueryRow( - `SELECT id, name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin + `SELECT id, name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin, is_active FROM tokens WHERE login = ?`, login, ).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &hashedPassword, &t.Token, - &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin) + &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -80,6 +82,10 @@ func (r *Repository) Login(login, password string) (*LoginResponse, error) { return nil, ErrNotFound } + if !t.IsActive { + return nil, ErrAccountInactive + } + // Generate new token on each login newToken, err := utils.RandomToken() if err != nil { @@ -99,6 +105,7 @@ func (r *Repository) Login(login, password string) (*LoginResponse, error) { PermissionView: t.PermissionView, PermissionManage: t.PermissionManage, PermissionAdmin: t.PermissionAdmin, + IsActive: t.IsActive, }, nil } @@ -244,3 +251,207 @@ func (r *Repository) MarkRegistrationTokenUsed(token string) error { } return nil } + +// ActivateToken activates a user by token value. +func (r *Repository) ActivateToken(token string) error { + result, err := r.DB.Exec( + `UPDATE tokens SET is_active = 1 WHERE token = ?`, + token, + ) + if err != nil { + return err + } + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return ErrNotFound + } + return nil +} + +// DeactivateToken deactivates a user by token value. +func (r *Repository) DeactivateToken(token string) error { + result, err := r.DB.Exec( + `UPDATE tokens SET is_active = 0 WHERE token = ?`, + token, + ) + if err != nil { + return err + } + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return ErrNotFound + } + return nil +} + +// ActivateUserByLogin activates a user by login. +func (r *Repository) ActivateUserByLogin(login string) error { + result, err := r.DB.Exec( + `UPDATE tokens SET is_active = 1 WHERE login = ?`, + login, + ) + if err != nil { + return err + } + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return ErrNotFound + } + return nil +} + +// DeactivateUserByLogin deactivates a user by login. +func (r *Repository) DeactivateUserByLogin(login string) error { + result, err := r.DB.Exec( + `UPDATE tokens SET is_active = 0 WHERE login = ?`, + login, + ) + if err != nil { + return err + } + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return ErrNotFound + } + return nil +} + +// ListInactiveTokens returns all users that are not activated. +func (r *Repository) ListInactiveTokens() ([]Tokens, error) { + rows, err := r.DB.Query( + `SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin, is_active + FROM tokens WHERE is_active = 0`, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var tokens []Tokens + for rows.Next() { + var t Tokens + if err := rows.Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token, + &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive); err != nil { + return nil, err + } + tokens = append(tokens, t) + } + return tokens, rows.Err() +} + +// GetTokenByLogin retrieves a user by login. +func (r *Repository) GetTokenByLogin(login string) (*Tokens, error) { + var t Tokens + err := r.DB.QueryRow( + `SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin, is_active + FROM tokens WHERE login = ?`, + login, + ).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token, + &t.PermissionView, &t.PermissionManage, &t.PermissionAdmin, &t.IsActive) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return nil, err + } + return &t, nil +} + +// UpdateToken updates name and last_name for a user by login. +func (r *Repository) UpdateToken(login string, update TokenUpdate) error { + result, err := r.DB.Exec( + `UPDATE tokens SET name = ?, last_name = ? WHERE login = ?`, + update.Name, update.LastName, login, + ) + if err != nil { + return err + } + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return ErrNotFound + } + return nil +} + +// UpdatePermissions updates permissions and is_active for a user by login. +func (r *Repository) UpdatePermissions(login string, update TokenUpdatePermissions) error { + user, err := r.GetTokenByLogin(login) + if err != nil { + return err + } + + // Use existing values if not provided + newView := user.PermissionView + newManage := user.PermissionManage + newAdmin := user.PermissionAdmin + newActive := user.IsActive + + if update.PermissionView != nil { + newView = *update.PermissionView + } + if update.PermissionManage != nil { + newManage = *update.PermissionManage + } + if update.PermissionAdmin != nil { + newAdmin = *update.PermissionAdmin + } + if update.IsActive != nil { + newActive = *update.IsActive + } + + result, err := r.DB.Exec( + `UPDATE tokens SET permission_view = ?, permission_manage_agent = ?, permission_admin = ?, is_active = ? WHERE login = ?`, + newView, newManage, newAdmin, newActive, login, + ) + if err != nil { + return err + } + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return ErrNotFound + } + return nil +} + +// UpdatePassword updates the password for a user by login. +func (r *Repository) UpdatePassword(login string, newPassword string) error { + hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + + result, err := r.DB.Exec( + `UPDATE tokens SET password = ? WHERE login = ?`, + string(hashed), login, + ) + if err != nil { + return err + } + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return ErrNotFound + } + return nil +} diff --git a/backend/internal/storage/migrations.go b/backend/internal/storage/migrations.go index 1698570..93568c3 100644 --- a/backend/internal/storage/migrations.go +++ b/backend/internal/storage/migrations.go @@ -10,7 +10,8 @@ const CreateSqlite = ` token TEXT NOT NULL UNIQUE, permission_view BOOL NOT NULL, permission_manage_agent BOOL NOT NULL, - permission_admin BOOL NOT NULL + permission_admin BOOL NOT NULL, + is_active BOOL NOT NULL DEFAULT 0 ); `