chore: add ansible deploy simple logic, upgrade admin auth logic and docs
ci-agent / build (push) Failing after 1m55s

This commit is contained in:
d3m0k1d
2026-04-04 05:19:40 +03:00
parent 2a8faaa9fe
commit 10d899b50f
16 changed files with 3516 additions and 382 deletions
+17
View File
@@ -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,
+1 -1
View File
@@ -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
+859 -151
View File
File diff suppressed because it is too large Load Diff
+859 -151
View File
File diff suppressed because it is too large Load Diff
+547 -73
View File
@@ -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
+135
View File
@@ -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)
}
+62
View File
@@ -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
}
+136
View File
@@ -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
`
+5
View File
@@ -0,0 +1,5 @@
package ansible
const BaseInvTemplate = `
`
+172
View File
@@ -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,
})
}
+222
View File
@@ -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")
+202
View File
@@ -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]
}
+81
View File
@@ -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"`
}
+216 -5
View File
@@ -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
}
+2 -1
View File
@@ -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
);
`