mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
feat(go-backend): implement complete API layer with handlers, services, and repositories
- Add DTOs for user, organization, target, engine, pagination, and response handling - Implement repository layer for user, organization, target, and engine entities - Implement service layer with business logic for all core modules - Implement HTTP handlers for user, organization, target, and engine endpoints - Add complete CRUD API routes with soft delete support for organizations and targets - Add environment configuration file with database, Redis, and logging settings - Add docker-compose.dev.yml for PostgreSQL and Redis development dependencies - Add comprehensive README.md with migration progress, API endpoints, and tech stack - Update main.go to wire repositories, services, and handlers with dependency injection - Update config.go to support .env file loading with environment variable priority - Update database.go to initialize all repositories and services
This commit is contained in:
24
go-backend/.env
Normal file
24
go-backend/.env
Normal file
@@ -0,0 +1,24 @@
|
||||
# Server Configuration
|
||||
SERVER_PORT=8888
|
||||
GIN_MODE=debug
|
||||
|
||||
# Database Configuration (PostgreSQL)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_NAME=xingrin
|
||||
DB_SSLMODE=disable
|
||||
DB_MAX_OPEN_CONNS=25
|
||||
DB_MAX_IDLE_CONNS=5
|
||||
DB_CONN_MAX_LIFETIME=300
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=debug
|
||||
LOG_FORMAT=json
|
||||
90
go-backend/README.md
Normal file
90
go-backend/README.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Go Backend
|
||||
|
||||
Python Django 后端的 Go 重写版本。
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 组件 | 选择 |
|
||||
|------|------|
|
||||
| Web 框架 | Gin |
|
||||
| ORM | GORM |
|
||||
| 配置 | Viper |
|
||||
| 日志 | Zap |
|
||||
| 认证 | JWT (bcrypt) |
|
||||
|
||||
## 迁移进度
|
||||
|
||||
### ✅ 已完成
|
||||
|
||||
| 模块 | 说明 |
|
||||
|------|------|
|
||||
| 项目基础 | 目录结构、配置管理、数据库连接、日志 |
|
||||
| 数据模型 | 全部 33 个模型,含索引和约束 |
|
||||
| JWT 认证 | 登录、刷新、中间件 |
|
||||
| 用户 API | 创建、列表、修改密码 |
|
||||
| 组织 API | 完整 CRUD(软删除) |
|
||||
| 目标 API | 完整 CRUD(软删除、类型自动检测) |
|
||||
| 引擎 API | 完整 CRUD |
|
||||
|
||||
### 🚧 待实现
|
||||
|
||||
| 模块 | 优先级 | 说明 |
|
||||
|------|--------|------|
|
||||
| Scan API | 高 | 扫描管理(发起、状态、结果) |
|
||||
| Asset API | 高 | 资产查询(子域名、端口、漏洞等) |
|
||||
| Worker | 高 | 扫描任务执行(核心逻辑) |
|
||||
| 定时任务 | 中 | 定时扫描 |
|
||||
| 通知 | 低 | 扫描完成通知 |
|
||||
| 统计 | 低 | 资产统计 |
|
||||
|
||||
### ⏳ 技术债务
|
||||
|
||||
| 项目 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| Context 传递 | Repository/Service 加 context 参数 | 中 |
|
||||
| 单元测试 | Handler/Service 层测试 | 中 |
|
||||
| 接口抽象 | Repository 接口化(便于 mock) | 低 |
|
||||
| 泛型重构 | 通用 Repository(等模块多了再做) | 低 |
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
# 开发
|
||||
make run
|
||||
|
||||
# 测试
|
||||
make test
|
||||
|
||||
# 构建
|
||||
make build
|
||||
```
|
||||
|
||||
## API 端点
|
||||
|
||||
```
|
||||
POST /api/auth/login # 登录
|
||||
POST /api/auth/refresh # 刷新 token
|
||||
GET /api/auth/me # 当前用户
|
||||
|
||||
POST /api/users # 创建用户
|
||||
GET /api/users # 用户列表
|
||||
PUT /api/users/password # 修改密码
|
||||
|
||||
GET /api/organizations # 组织列表
|
||||
POST /api/organizations # 创建组织
|
||||
GET /api/organizations/:id # 获取组织
|
||||
PUT /api/organizations/:id # 更新组织
|
||||
DELETE /api/organizations/:id # 删除组织
|
||||
|
||||
GET /api/targets # 目标列表
|
||||
POST /api/targets # 创建目标
|
||||
GET /api/targets/:id # 获取目标
|
||||
PUT /api/targets/:id # 更新目标
|
||||
DELETE /api/targets/:id # 删除目标
|
||||
|
||||
GET /api/engines # 引擎列表
|
||||
POST /api/engines # 创建引擎
|
||||
GET /api/engines/:id # 获取引擎
|
||||
PUT /api/engines/:id # 更新引擎
|
||||
DELETE /api/engines/:id # 删除引擎
|
||||
```
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"github.com/xingrin/go-backend/internal/handler"
|
||||
"github.com/xingrin/go-backend/internal/middleware"
|
||||
"github.com/xingrin/go-backend/internal/pkg"
|
||||
"github.com/xingrin/go-backend/internal/repository"
|
||||
"github.com/xingrin/go-backend/internal/service"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -95,9 +97,25 @@ func main() {
|
||||
router.Use(middleware.Recovery())
|
||||
router.Use(middleware.Logger())
|
||||
|
||||
// Create repositories
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
orgRepo := repository.NewOrganizationRepository(db)
|
||||
targetRepo := repository.NewTargetRepository(db)
|
||||
engineRepo := repository.NewEngineRepository(db)
|
||||
|
||||
// Create services
|
||||
userSvc := service.NewUserService(userRepo)
|
||||
orgSvc := service.NewOrganizationService(orgRepo)
|
||||
targetSvc := service.NewTargetService(targetRepo)
|
||||
engineSvc := service.NewEngineService(engineRepo)
|
||||
|
||||
// Create handlers
|
||||
healthHandler := handler.NewHealthHandler(db, redisClient)
|
||||
authHandler := handler.NewAuthHandler(db, jwtManager)
|
||||
userHandler := handler.NewUserHandler(userSvc)
|
||||
orgHandler := handler.NewOrganizationHandler(orgSvc)
|
||||
targetHandler := handler.NewTargetHandler(targetSvc)
|
||||
engineHandler := handler.NewEngineHandler(engineSvc)
|
||||
|
||||
// Register health routes
|
||||
router.GET("/health", healthHandler.Check)
|
||||
@@ -118,7 +136,34 @@ func main() {
|
||||
protected := api.Group("")
|
||||
protected.Use(middleware.AuthMiddleware(jwtManager))
|
||||
{
|
||||
// Auth
|
||||
protected.GET("/auth/me", authHandler.GetCurrentUser)
|
||||
|
||||
// Users
|
||||
protected.POST("/users", userHandler.Create)
|
||||
protected.GET("/users", userHandler.List)
|
||||
protected.PUT("/users/password", userHandler.UpdatePassword)
|
||||
|
||||
// Organizations
|
||||
protected.POST("/organizations", orgHandler.Create)
|
||||
protected.GET("/organizations", orgHandler.List)
|
||||
protected.GET("/organizations/:id", orgHandler.GetByID)
|
||||
protected.PUT("/organizations/:id", orgHandler.Update)
|
||||
protected.DELETE("/organizations/:id", orgHandler.Delete)
|
||||
|
||||
// Targets
|
||||
protected.POST("/targets", targetHandler.Create)
|
||||
protected.GET("/targets", targetHandler.List)
|
||||
protected.GET("/targets/:id", targetHandler.GetByID)
|
||||
protected.PUT("/targets/:id", targetHandler.Update)
|
||||
protected.DELETE("/targets/:id", targetHandler.Delete)
|
||||
|
||||
// Engines
|
||||
protected.POST("/engines", engineHandler.Create)
|
||||
protected.GET("/engines", engineHandler.List)
|
||||
protected.GET("/engines/:id", engineHandler.GetByID)
|
||||
protected.PUT("/engines/:id", engineHandler.Update)
|
||||
protected.DELETE("/engines/:id", engineHandler.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
go-backend/docker-compose.dev.yml
Normal file
35
go-backend/docker-compose.dev.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
# Go 后端开发环境依赖
|
||||
# 启动: docker compose -f docker-compose.dev.yml up -d
|
||||
# 停止: docker compose -f docker-compose.dev.yml down
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: xingrin
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -57,14 +57,28 @@ type JWTConfig struct {
|
||||
RefreshExpire time.Duration `mapstructure:"JWT_REFRESH_EXPIRE"`
|
||||
}
|
||||
|
||||
// Load reads configuration from environment variables
|
||||
// Load reads configuration from .env file and environment variables
|
||||
// Priority: environment variables > .env file > defaults
|
||||
func Load() (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
// Set default values
|
||||
setDefaults(v)
|
||||
|
||||
// Read from environment variables
|
||||
// Try to read .env file (optional, won't fail if not found)
|
||||
v.SetConfigName(".env")
|
||||
v.SetConfigType("env")
|
||||
v.AddConfigPath(".") // Current directory (go-backend/)
|
||||
v.AddConfigPath("./go-backend") // When running from project root
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
// .env file not found is OK, we'll use env vars or defaults
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
// Only return error if it's not a "file not found" error
|
||||
return nil, fmt.Errorf("error reading config file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Environment variables override .env file
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ func NewDatabase(cfg *config.DatabaseConfig) (*gorm.DB, error) {
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
SingularTable: true, // Use singular table names (e.g., "target" not "targets")
|
||||
},
|
||||
// Disable auto-migration to prevent creating new tables
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
|
||||
24
go-backend/internal/dto/engine.go
Normal file
24
go-backend/internal/dto/engine.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// CreateEngineRequest represents create engine request
|
||||
type CreateEngineRequest struct {
|
||||
Name string `json:"name" binding:"required,max=200"`
|
||||
Configuration string `json:"configuration"`
|
||||
}
|
||||
|
||||
// UpdateEngineRequest represents update engine request
|
||||
type UpdateEngineRequest struct {
|
||||
Name string `json:"name" binding:"required,max=200"`
|
||||
Configuration string `json:"configuration"`
|
||||
}
|
||||
|
||||
// EngineResponse represents engine response
|
||||
type EngineResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Configuration string `json:"configuration"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
23
go-backend/internal/dto/organization.go
Normal file
23
go-backend/internal/dto/organization.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// CreateOrganizationRequest represents create organization request
|
||||
type CreateOrganizationRequest struct {
|
||||
Name string `json:"name" binding:"required,max=300"`
|
||||
Description string `json:"description" binding:"max=1000"`
|
||||
}
|
||||
|
||||
// UpdateOrganizationRequest represents update organization request
|
||||
type UpdateOrganizationRequest struct {
|
||||
Name string `json:"name" binding:"required,max=300"`
|
||||
Description string `json:"description" binding:"max=1000"`
|
||||
}
|
||||
|
||||
// OrganizationResponse represents organization response
|
||||
type OrganizationResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
49
go-backend/internal/dto/pagination.go
Normal file
49
go-backend/internal/dto/pagination.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package dto
|
||||
|
||||
// PaginationQuery represents pagination query parameters
|
||||
type PaginationQuery struct {
|
||||
Page int `form:"page" binding:"omitempty,min=1"`
|
||||
PageSize int `form:"pageSize" binding:"omitempty,min=1,max=100"`
|
||||
}
|
||||
|
||||
// GetPage returns page number with default
|
||||
func (p *PaginationQuery) GetPage() int {
|
||||
if p.Page <= 0 {
|
||||
return 1
|
||||
}
|
||||
return p.Page
|
||||
}
|
||||
|
||||
// GetPageSize returns page size with default
|
||||
func (p *PaginationQuery) GetPageSize() int {
|
||||
if p.PageSize <= 0 {
|
||||
return 20
|
||||
}
|
||||
if p.PageSize > 100 {
|
||||
return 100
|
||||
}
|
||||
return p.PageSize
|
||||
}
|
||||
|
||||
// GetOffset returns offset for database query
|
||||
func (p *PaginationQuery) GetOffset() int {
|
||||
return (p.GetPage() - 1) * p.GetPageSize()
|
||||
}
|
||||
|
||||
// PaginatedResponse represents a paginated response
|
||||
type PaginatedResponse struct {
|
||||
Data interface{} `json:"data"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
|
||||
// NewPaginatedResponse creates a new paginated response
|
||||
func NewPaginatedResponse(data interface{}, total int64, page, pageSize int) *PaginatedResponse {
|
||||
return &PaginatedResponse{
|
||||
Data: data,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
}
|
||||
62
go-backend/internal/dto/response.go
Normal file
62
go-backend/internal/dto/response.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Response represents a standard API response
|
||||
type Response struct {
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Success sends a success response
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Data: data,
|
||||
Message: "success",
|
||||
})
|
||||
}
|
||||
|
||||
// Created sends a created response
|
||||
func Created(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusCreated, Response{
|
||||
Data: data,
|
||||
Message: "created",
|
||||
})
|
||||
}
|
||||
|
||||
// NoContent sends a no content response
|
||||
func NoContent(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Error sends an error response
|
||||
func Error(c *gin.Context, status int, message string) {
|
||||
c.JSON(status, Response{
|
||||
Error: message,
|
||||
})
|
||||
}
|
||||
|
||||
// BadRequest sends a bad request error
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
Error(c, http.StatusBadRequest, message)
|
||||
}
|
||||
|
||||
// NotFound sends a not found error
|
||||
func NotFound(c *gin.Context, message string) {
|
||||
Error(c, http.StatusNotFound, message)
|
||||
}
|
||||
|
||||
// InternalError sends an internal server error
|
||||
func InternalError(c *gin.Context, message string) {
|
||||
Error(c, http.StatusInternalServerError, message)
|
||||
}
|
||||
|
||||
// Paginated sends a paginated response
|
||||
func Paginated(c *gin.Context, data interface{}, total int64, page, pageSize int) {
|
||||
c.JSON(http.StatusOK, NewPaginatedResponse(data, total, page, pageSize))
|
||||
}
|
||||
31
go-backend/internal/dto/target.go
Normal file
31
go-backend/internal/dto/target.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// CreateTargetRequest represents create target request
|
||||
type CreateTargetRequest struct {
|
||||
Name string `json:"name" binding:"required,max=300"`
|
||||
Type string `json:"type" binding:"omitempty,oneof=domain ip cidr"`
|
||||
}
|
||||
|
||||
// UpdateTargetRequest represents update target request
|
||||
type UpdateTargetRequest struct {
|
||||
Name string `json:"name" binding:"required,max=300"`
|
||||
Type string `json:"type" binding:"omitempty,oneof=domain ip cidr"`
|
||||
}
|
||||
|
||||
// TargetListQuery represents target list query parameters
|
||||
type TargetListQuery struct {
|
||||
PaginationQuery
|
||||
Type string `form:"type" binding:"omitempty,oneof=domain ip cidr"`
|
||||
Search string `form:"search"`
|
||||
}
|
||||
|
||||
// TargetResponse represents target response
|
||||
type TargetResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastScannedAt *time.Time `json:"lastScannedAt"`
|
||||
}
|
||||
27
go-backend/internal/dto/user.go
Normal file
27
go-backend/internal/dto/user.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// CreateUserRequest represents create user request
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=150"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
}
|
||||
|
||||
// UpdatePasswordRequest represents update password request
|
||||
type UpdatePasswordRequest struct {
|
||||
OldPassword string `json:"oldPassword" binding:"required"`
|
||||
NewPassword string `json:"newPassword" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
// UserResponse represents user response
|
||||
type UserResponse struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
IsActive bool `json:"isActive"`
|
||||
IsSuperuser bool `json:"isSuperuser"`
|
||||
DateJoined time.Time `json:"dateJoined"`
|
||||
LastLogin *time.Time `json:"lastLogin"`
|
||||
}
|
||||
165
go-backend/internal/handler/engine_handler.go
Normal file
165
go-backend/internal/handler/engine_handler.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/xingrin/go-backend/internal/dto"
|
||||
"github.com/xingrin/go-backend/internal/service"
|
||||
)
|
||||
|
||||
// EngineHandler handles engine endpoints
|
||||
type EngineHandler struct {
|
||||
svc *service.EngineService
|
||||
}
|
||||
|
||||
// NewEngineHandler creates a new engine handler
|
||||
func NewEngineHandler(svc *service.EngineService) *EngineHandler {
|
||||
return &EngineHandler{svc: svc}
|
||||
}
|
||||
|
||||
// Create creates a new engine
|
||||
// POST /api/engines
|
||||
func (h *EngineHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateEngineRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
dto.BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
engine, err := h.svc.Create(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrEngineExists) {
|
||||
dto.BadRequest(c, "Engine name already exists")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to create engine")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Created(c, dto.EngineResponse{
|
||||
ID: engine.ID,
|
||||
Name: engine.Name,
|
||||
Configuration: engine.Configuration,
|
||||
CreatedAt: engine.CreatedAt,
|
||||
UpdatedAt: engine.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// List returns paginated engines
|
||||
// GET /api/engines
|
||||
func (h *EngineHandler) List(c *gin.Context) {
|
||||
var query dto.PaginationQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
dto.BadRequest(c, "Invalid query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
engines, total, err := h.svc.List(&query)
|
||||
if err != nil {
|
||||
dto.InternalError(c, "Failed to list engines")
|
||||
return
|
||||
}
|
||||
|
||||
var resp []dto.EngineResponse
|
||||
for _, e := range engines {
|
||||
resp = append(resp, dto.EngineResponse{
|
||||
ID: e.ID,
|
||||
Name: e.Name,
|
||||
Configuration: e.Configuration,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
dto.Paginated(c, resp, total, query.GetPage(), query.GetPageSize())
|
||||
}
|
||||
|
||||
// GetByID returns an engine by ID
|
||||
// GET /api/engines/:id
|
||||
func (h *EngineHandler) GetByID(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
dto.BadRequest(c, "Invalid engine ID")
|
||||
return
|
||||
}
|
||||
|
||||
engine, err := h.svc.GetByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrEngineNotFound) {
|
||||
dto.NotFound(c, "Engine not found")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to get engine")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Success(c, dto.EngineResponse{
|
||||
ID: engine.ID,
|
||||
Name: engine.Name,
|
||||
Configuration: engine.Configuration,
|
||||
CreatedAt: engine.CreatedAt,
|
||||
UpdatedAt: engine.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Update updates an engine
|
||||
// PUT /api/engines/:id
|
||||
func (h *EngineHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
dto.BadRequest(c, "Invalid engine ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateEngineRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
dto.BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
engine, err := h.svc.Update(id, &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrEngineNotFound) {
|
||||
dto.NotFound(c, "Engine not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrEngineExists) {
|
||||
dto.BadRequest(c, "Engine name already exists")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to update engine")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Success(c, dto.EngineResponse{
|
||||
ID: engine.ID,
|
||||
Name: engine.Name,
|
||||
Configuration: engine.Configuration,
|
||||
CreatedAt: engine.CreatedAt,
|
||||
UpdatedAt: engine.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes an engine
|
||||
// DELETE /api/engines/:id
|
||||
func (h *EngineHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
dto.BadRequest(c, "Invalid engine ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.svc.Delete(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrEngineNotFound) {
|
||||
dto.NotFound(c, "Engine not found")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to delete engine")
|
||||
return
|
||||
}
|
||||
|
||||
dto.NoContent(c)
|
||||
}
|
||||
161
go-backend/internal/handler/organization_handler.go
Normal file
161
go-backend/internal/handler/organization_handler.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/xingrin/go-backend/internal/dto"
|
||||
"github.com/xingrin/go-backend/internal/service"
|
||||
)
|
||||
|
||||
// OrganizationHandler handles organization endpoints
|
||||
type OrganizationHandler struct {
|
||||
svc *service.OrganizationService
|
||||
}
|
||||
|
||||
// NewOrganizationHandler creates a new organization handler
|
||||
func NewOrganizationHandler(svc *service.OrganizationService) *OrganizationHandler {
|
||||
return &OrganizationHandler{svc: svc}
|
||||
}
|
||||
|
||||
// Create creates a new organization
|
||||
// POST /api/organizations
|
||||
func (h *OrganizationHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateOrganizationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
dto.BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
org, err := h.svc.Create(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrOrganizationExists) {
|
||||
dto.BadRequest(c, "Organization name already exists")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to create organization")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Created(c, dto.OrganizationResponse{
|
||||
ID: org.ID,
|
||||
Name: org.Name,
|
||||
Description: org.Description,
|
||||
CreatedAt: org.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// List returns paginated organizations
|
||||
// GET /api/organizations
|
||||
func (h *OrganizationHandler) List(c *gin.Context) {
|
||||
var query dto.PaginationQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
dto.BadRequest(c, "Invalid query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
orgs, total, err := h.svc.List(&query)
|
||||
if err != nil {
|
||||
dto.InternalError(c, "Failed to list organizations")
|
||||
return
|
||||
}
|
||||
|
||||
var resp []dto.OrganizationResponse
|
||||
for _, o := range orgs {
|
||||
resp = append(resp, dto.OrganizationResponse{
|
||||
ID: o.ID,
|
||||
Name: o.Name,
|
||||
Description: o.Description,
|
||||
CreatedAt: o.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
dto.Paginated(c, resp, total, query.GetPage(), query.GetPageSize())
|
||||
}
|
||||
|
||||
// GetByID returns an organization by ID
|
||||
// GET /api/organizations/:id
|
||||
func (h *OrganizationHandler) GetByID(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
dto.BadRequest(c, "Invalid organization ID")
|
||||
return
|
||||
}
|
||||
|
||||
org, err := h.svc.GetByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrOrganizationNotFound) {
|
||||
dto.NotFound(c, "Organization not found")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to get organization")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Success(c, dto.OrganizationResponse{
|
||||
ID: org.ID,
|
||||
Name: org.Name,
|
||||
Description: org.Description,
|
||||
CreatedAt: org.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Update updates an organization
|
||||
// PUT /api/organizations/:id
|
||||
func (h *OrganizationHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
dto.BadRequest(c, "Invalid organization ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateOrganizationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
dto.BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
org, err := h.svc.Update(id, &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrOrganizationNotFound) {
|
||||
dto.NotFound(c, "Organization not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrOrganizationExists) {
|
||||
dto.BadRequest(c, "Organization name already exists")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to update organization")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Success(c, dto.OrganizationResponse{
|
||||
ID: org.ID,
|
||||
Name: org.Name,
|
||||
Description: org.Description,
|
||||
CreatedAt: org.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete soft deletes an organization
|
||||
// DELETE /api/organizations/:id
|
||||
func (h *OrganizationHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
dto.BadRequest(c, "Invalid organization ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.svc.Delete(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrOrganizationNotFound) {
|
||||
dto.NotFound(c, "Organization not found")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to delete organization")
|
||||
return
|
||||
}
|
||||
|
||||
dto.NoContent(c)
|
||||
}
|
||||
173
go-backend/internal/handler/target_handler.go
Normal file
173
go-backend/internal/handler/target_handler.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/xingrin/go-backend/internal/dto"
|
||||
"github.com/xingrin/go-backend/internal/service"
|
||||
)
|
||||
|
||||
// TargetHandler handles target endpoints
|
||||
type TargetHandler struct {
|
||||
svc *service.TargetService
|
||||
}
|
||||
|
||||
// NewTargetHandler creates a new target handler
|
||||
func NewTargetHandler(svc *service.TargetService) *TargetHandler {
|
||||
return &TargetHandler{svc: svc}
|
||||
}
|
||||
|
||||
// Create creates a new target
|
||||
// POST /api/targets
|
||||
func (h *TargetHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateTargetRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
dto.BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
target, err := h.svc.Create(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTargetExists) {
|
||||
dto.BadRequest(c, "Target name already exists")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrInvalidTarget) {
|
||||
dto.BadRequest(c, "Invalid target format")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to create target")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Created(c, dto.TargetResponse{
|
||||
ID: target.ID,
|
||||
Name: target.Name,
|
||||
Type: target.Type,
|
||||
CreatedAt: target.CreatedAt,
|
||||
LastScannedAt: target.LastScannedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// List returns paginated targets
|
||||
// GET /api/targets
|
||||
func (h *TargetHandler) List(c *gin.Context) {
|
||||
var query dto.TargetListQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
dto.BadRequest(c, "Invalid query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
targets, total, err := h.svc.List(&query)
|
||||
if err != nil {
|
||||
dto.InternalError(c, "Failed to list targets")
|
||||
return
|
||||
}
|
||||
|
||||
var resp []dto.TargetResponse
|
||||
for _, t := range targets {
|
||||
resp = append(resp, dto.TargetResponse{
|
||||
ID: t.ID,
|
||||
Name: t.Name,
|
||||
Type: t.Type,
|
||||
CreatedAt: t.CreatedAt,
|
||||
LastScannedAt: t.LastScannedAt,
|
||||
})
|
||||
}
|
||||
|
||||
dto.Paginated(c, resp, total, query.GetPage(), query.GetPageSize())
|
||||
}
|
||||
|
||||
// GetByID returns a target by ID
|
||||
// GET /api/targets/:id
|
||||
func (h *TargetHandler) GetByID(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
dto.BadRequest(c, "Invalid target ID")
|
||||
return
|
||||
}
|
||||
|
||||
target, err := h.svc.GetByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTargetNotFound) {
|
||||
dto.NotFound(c, "Target not found")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to get target")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Success(c, dto.TargetResponse{
|
||||
ID: target.ID,
|
||||
Name: target.Name,
|
||||
Type: target.Type,
|
||||
CreatedAt: target.CreatedAt,
|
||||
LastScannedAt: target.LastScannedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Update updates a target
|
||||
// PUT /api/targets/:id
|
||||
func (h *TargetHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
dto.BadRequest(c, "Invalid target ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateTargetRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
dto.BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
target, err := h.svc.Update(id, &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTargetNotFound) {
|
||||
dto.NotFound(c, "Target not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrTargetExists) {
|
||||
dto.BadRequest(c, "Target name already exists")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrInvalidTarget) {
|
||||
dto.BadRequest(c, "Invalid target format")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to update target")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Success(c, dto.TargetResponse{
|
||||
ID: target.ID,
|
||||
Name: target.Name,
|
||||
Type: target.Type,
|
||||
CreatedAt: target.CreatedAt,
|
||||
LastScannedAt: target.LastScannedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete soft deletes a target
|
||||
// DELETE /api/targets/:id
|
||||
func (h *TargetHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
dto.BadRequest(c, "Invalid target ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.svc.Delete(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrTargetNotFound) {
|
||||
dto.NotFound(c, "Target not found")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to delete target")
|
||||
return
|
||||
}
|
||||
|
||||
dto.NoContent(c)
|
||||
}
|
||||
115
go-backend/internal/handler/user_handler.go
Normal file
115
go-backend/internal/handler/user_handler.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/xingrin/go-backend/internal/dto"
|
||||
"github.com/xingrin/go-backend/internal/middleware"
|
||||
"github.com/xingrin/go-backend/internal/service"
|
||||
)
|
||||
|
||||
// UserHandler handles user endpoints
|
||||
type UserHandler struct {
|
||||
svc *service.UserService
|
||||
}
|
||||
|
||||
// NewUserHandler creates a new user handler
|
||||
func NewUserHandler(svc *service.UserService) *UserHandler {
|
||||
return &UserHandler{svc: svc}
|
||||
}
|
||||
|
||||
// Create creates a new user
|
||||
// POST /api/users
|
||||
func (h *UserHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
dto.BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.svc.Create(&req)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrUsernameExists) {
|
||||
dto.BadRequest(c, "Username already exists")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to create user")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Created(c, dto.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
IsActive: user.IsActive,
|
||||
IsSuperuser: user.IsSuperuser,
|
||||
DateJoined: user.DateJoined,
|
||||
LastLogin: user.LastLogin,
|
||||
})
|
||||
}
|
||||
|
||||
// List returns paginated users
|
||||
// GET /api/users
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
var query dto.PaginationQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
dto.BadRequest(c, "Invalid query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
users, total, err := h.svc.List(&query)
|
||||
if err != nil {
|
||||
dto.InternalError(c, "Failed to list users")
|
||||
return
|
||||
}
|
||||
|
||||
var resp []dto.UserResponse
|
||||
for _, u := range users {
|
||||
resp = append(resp, dto.UserResponse{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
IsActive: u.IsActive,
|
||||
IsSuperuser: u.IsSuperuser,
|
||||
DateJoined: u.DateJoined,
|
||||
LastLogin: u.LastLogin,
|
||||
})
|
||||
}
|
||||
|
||||
dto.Paginated(c, resp, total, query.GetPage(), query.GetPageSize())
|
||||
}
|
||||
|
||||
// UpdatePassword updates user password
|
||||
// PUT /api/users/:id/password
|
||||
func (h *UserHandler) UpdatePassword(c *gin.Context) {
|
||||
// Get current user from context
|
||||
claims, ok := middleware.GetUserClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdatePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
dto.BadRequest(c, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.svc.UpdatePassword(claims.UserID, &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrUserNotFound) {
|
||||
dto.NotFound(c, "User not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrInvalidPassword) {
|
||||
dto.BadRequest(c, "Invalid old password")
|
||||
return
|
||||
}
|
||||
dto.InternalError(c, "Failed to update password")
|
||||
return
|
||||
}
|
||||
|
||||
dto.Success(c, gin.H{"message": "Password updated"})
|
||||
}
|
||||
65
go-backend/internal/repository/engine_repository.go
Normal file
65
go-backend/internal/repository/engine_repository.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/xingrin/go-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// EngineRepository handles scan engine database operations
|
||||
type EngineRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewEngineRepository creates a new engine repository
|
||||
func NewEngineRepository(db *gorm.DB) *EngineRepository {
|
||||
return &EngineRepository{db: db}
|
||||
}
|
||||
|
||||
// Create creates a new engine
|
||||
func (r *EngineRepository) Create(engine *model.ScanEngine) error {
|
||||
return r.db.Create(engine).Error
|
||||
}
|
||||
|
||||
// FindByID finds an engine by ID
|
||||
func (r *EngineRepository) FindByID(id int) (*model.ScanEngine, error) {
|
||||
var engine model.ScanEngine
|
||||
err := r.db.First(&engine, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &engine, nil
|
||||
}
|
||||
|
||||
// FindAll finds all engines with pagination
|
||||
func (r *EngineRepository) FindAll(offset, limit int) ([]model.ScanEngine, int64, error) {
|
||||
var engines []model.ScanEngine
|
||||
var total int64
|
||||
|
||||
if err := r.db.Model(&model.ScanEngine{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := r.db.Offset(offset).Limit(limit).Order("created_at DESC").Find(&engines).Error
|
||||
return engines, total, err
|
||||
}
|
||||
|
||||
// Update updates an engine
|
||||
func (r *EngineRepository) Update(engine *model.ScanEngine) error {
|
||||
return r.db.Save(engine).Error
|
||||
}
|
||||
|
||||
// Delete deletes an engine
|
||||
func (r *EngineRepository) Delete(id int) error {
|
||||
return r.db.Delete(&model.ScanEngine{}, id).Error
|
||||
}
|
||||
|
||||
// ExistsByName checks if engine name exists
|
||||
func (r *EngineRepository) ExistsByName(name string, excludeID ...int) (bool, error) {
|
||||
var count int64
|
||||
query := r.db.Model(&model.ScanEngine{}).Where("name = ?", name)
|
||||
if len(excludeID) > 0 {
|
||||
query = query.Where("id != ?", excludeID[0])
|
||||
}
|
||||
err := query.Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
70
go-backend/internal/repository/organization_repository.go
Normal file
70
go-backend/internal/repository/organization_repository.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/xingrin/go-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// OrganizationRepository handles organization database operations
|
||||
type OrganizationRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewOrganizationRepository creates a new organization repository
|
||||
func NewOrganizationRepository(db *gorm.DB) *OrganizationRepository {
|
||||
return &OrganizationRepository{db: db}
|
||||
}
|
||||
|
||||
// Create creates a new organization
|
||||
func (r *OrganizationRepository) Create(org *model.Organization) error {
|
||||
return r.db.Create(org).Error
|
||||
}
|
||||
|
||||
// FindByID finds an organization by ID (excluding soft deleted)
|
||||
func (r *OrganizationRepository) FindByID(id int) (*model.Organization, error) {
|
||||
var org model.Organization
|
||||
err := r.db.Where("id = ? AND deleted_at IS NULL", id).First(&org).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &org, nil
|
||||
}
|
||||
|
||||
// FindAll finds all organizations with pagination (excluding soft deleted)
|
||||
func (r *OrganizationRepository) FindAll(offset, limit int) ([]model.Organization, int64, error) {
|
||||
var orgs []model.Organization
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.Organization{}).Where("deleted_at IS NULL")
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := query.Offset(offset).Limit(limit).Order("created_at DESC").Find(&orgs).Error
|
||||
return orgs, total, err
|
||||
}
|
||||
|
||||
// Update updates an organization
|
||||
func (r *OrganizationRepository) Update(org *model.Organization) error {
|
||||
return r.db.Save(org).Error
|
||||
}
|
||||
|
||||
// SoftDelete soft deletes an organization
|
||||
func (r *OrganizationRepository) SoftDelete(id int) error {
|
||||
now := time.Now()
|
||||
return r.db.Model(&model.Organization{}).Where("id = ?", id).Update("deleted_at", now).Error
|
||||
}
|
||||
|
||||
// ExistsByName checks if organization name exists (excluding soft deleted)
|
||||
func (r *OrganizationRepository) ExistsByName(name string, excludeID ...int) (bool, error) {
|
||||
var count int64
|
||||
query := r.db.Model(&model.Organization{}).Where("name = ? AND deleted_at IS NULL", name)
|
||||
if len(excludeID) > 0 {
|
||||
query = query.Where("id != ?", excludeID[0])
|
||||
}
|
||||
err := query.Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
77
go-backend/internal/repository/target_repository.go
Normal file
77
go-backend/internal/repository/target_repository.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/xingrin/go-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TargetRepository handles target database operations
|
||||
type TargetRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewTargetRepository creates a new target repository
|
||||
func NewTargetRepository(db *gorm.DB) *TargetRepository {
|
||||
return &TargetRepository{db: db}
|
||||
}
|
||||
|
||||
// Create creates a new target
|
||||
func (r *TargetRepository) Create(target *model.Target) error {
|
||||
return r.db.Create(target).Error
|
||||
}
|
||||
|
||||
// FindByID finds a target by ID (excluding soft deleted)
|
||||
func (r *TargetRepository) FindByID(id int) (*model.Target, error) {
|
||||
var target model.Target
|
||||
err := r.db.Where("id = ? AND deleted_at IS NULL", id).First(&target).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &target, nil
|
||||
}
|
||||
|
||||
// FindAll finds all targets with pagination and filters (excluding soft deleted)
|
||||
func (r *TargetRepository) FindAll(offset, limit int, targetType, search string) ([]model.Target, int64, error) {
|
||||
var targets []model.Target
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.Target{}).Where("deleted_at IS NULL")
|
||||
|
||||
if targetType != "" {
|
||||
query = query.Where("type = ?", targetType)
|
||||
}
|
||||
if search != "" {
|
||||
query = query.Where("name ILIKE ?", "%"+search+"%")
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := query.Offset(offset).Limit(limit).Order("created_at DESC").Find(&targets).Error
|
||||
return targets, total, err
|
||||
}
|
||||
|
||||
// Update updates a target
|
||||
func (r *TargetRepository) Update(target *model.Target) error {
|
||||
return r.db.Save(target).Error
|
||||
}
|
||||
|
||||
// SoftDelete soft deletes a target
|
||||
func (r *TargetRepository) SoftDelete(id int) error {
|
||||
now := time.Now()
|
||||
return r.db.Model(&model.Target{}).Where("id = ?", id).Update("deleted_at", now).Error
|
||||
}
|
||||
|
||||
// ExistsByName checks if target name exists (excluding soft deleted)
|
||||
func (r *TargetRepository) ExistsByName(name string, excludeID ...int) (bool, error) {
|
||||
var count int64
|
||||
query := r.db.Model(&model.Target{}).Where("name = ? AND deleted_at IS NULL", name)
|
||||
if len(excludeID) > 0 {
|
||||
query = query.Where("id != ?", excludeID[0])
|
||||
}
|
||||
err := query.Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
66
go-backend/internal/repository/user_repository.go
Normal file
66
go-backend/internal/repository/user_repository.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/xingrin/go-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserRepository handles user database operations
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserRepository creates a new user repository
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
// Create creates a new user
|
||||
func (r *UserRepository) Create(user *model.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
// FindByID finds a user by ID
|
||||
func (r *UserRepository) FindByID(id int) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.First(&user, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByUsername finds a user by username
|
||||
func (r *UserRepository) FindByUsername(username string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindAll finds all users with pagination
|
||||
func (r *UserRepository) FindAll(offset, limit int) ([]model.User, int64, error) {
|
||||
var users []model.User
|
||||
var total int64
|
||||
|
||||
if err := r.db.Model(&model.User{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := r.db.Offset(offset).Limit(limit).Order("id DESC").Find(&users).Error
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
// Update updates a user
|
||||
func (r *UserRepository) Update(user *model.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
// ExistsByUsername checks if username exists
|
||||
func (r *UserRepository) ExistsByUsername(username string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.User{}).Where("username = ?", username).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
108
go-backend/internal/service/engine_service.go
Normal file
108
go-backend/internal/service/engine_service.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/xingrin/go-backend/internal/dto"
|
||||
"github.com/xingrin/go-backend/internal/model"
|
||||
"github.com/xingrin/go-backend/internal/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEngineNotFound = errors.New("engine not found")
|
||||
ErrEngineExists = errors.New("engine name already exists")
|
||||
)
|
||||
|
||||
// EngineService handles engine business logic
|
||||
type EngineService struct {
|
||||
repo *repository.EngineRepository
|
||||
}
|
||||
|
||||
// NewEngineService creates a new engine service
|
||||
func NewEngineService(repo *repository.EngineRepository) *EngineService {
|
||||
return &EngineService{repo: repo}
|
||||
}
|
||||
|
||||
// Create creates a new engine
|
||||
func (s *EngineService) Create(req *dto.CreateEngineRequest) (*model.ScanEngine, error) {
|
||||
exists, err := s.repo.ExistsByName(req.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrEngineExists
|
||||
}
|
||||
|
||||
engine := &model.ScanEngine{
|
||||
Name: req.Name,
|
||||
Configuration: req.Configuration,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(engine); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// List returns paginated engines
|
||||
func (s *EngineService) List(query *dto.PaginationQuery) ([]model.ScanEngine, int64, error) {
|
||||
return s.repo.FindAll(query.GetOffset(), query.GetPageSize())
|
||||
}
|
||||
|
||||
// GetByID returns an engine by ID
|
||||
func (s *EngineService) GetByID(id int) (*model.ScanEngine, error) {
|
||||
engine, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEngineNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// Update updates an engine
|
||||
func (s *EngineService) Update(id int, req *dto.UpdateEngineRequest) (*model.ScanEngine, error) {
|
||||
engine, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEngineNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check name uniqueness if changed
|
||||
if engine.Name != req.Name {
|
||||
exists, err := s.repo.ExistsByName(req.Name, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrEngineExists
|
||||
}
|
||||
}
|
||||
|
||||
engine.Name = req.Name
|
||||
engine.Configuration = req.Configuration
|
||||
|
||||
if err := s.repo.Update(engine); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// Delete deletes an engine
|
||||
func (s *EngineService) Delete(id int) error {
|
||||
_, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrEngineNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return s.repo.Delete(id)
|
||||
}
|
||||
109
go-backend/internal/service/organization_service.go
Normal file
109
go-backend/internal/service/organization_service.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/xingrin/go-backend/internal/dto"
|
||||
"github.com/xingrin/go-backend/internal/model"
|
||||
"github.com/xingrin/go-backend/internal/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrOrganizationNotFound = errors.New("organization not found")
|
||||
ErrOrganizationExists = errors.New("organization name already exists")
|
||||
)
|
||||
|
||||
// OrganizationService handles organization business logic
|
||||
type OrganizationService struct {
|
||||
repo *repository.OrganizationRepository
|
||||
}
|
||||
|
||||
// NewOrganizationService creates a new organization service
|
||||
func NewOrganizationService(repo *repository.OrganizationRepository) *OrganizationService {
|
||||
return &OrganizationService{repo: repo}
|
||||
}
|
||||
|
||||
// Create creates a new organization
|
||||
func (s *OrganizationService) Create(req *dto.CreateOrganizationRequest) (*model.Organization, error) {
|
||||
exists, err := s.repo.ExistsByName(req.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrOrganizationExists
|
||||
}
|
||||
|
||||
org := &model.Organization{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(org); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return org, nil
|
||||
}
|
||||
|
||||
// List returns paginated organizations
|
||||
func (s *OrganizationService) List(query *dto.PaginationQuery) ([]model.Organization, int64, error) {
|
||||
return s.repo.FindAll(query.GetOffset(), query.GetPageSize())
|
||||
}
|
||||
|
||||
// GetByID returns an organization by ID
|
||||
func (s *OrganizationService) GetByID(id int) (*model.Organization, error) {
|
||||
org, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrOrganizationNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return org, nil
|
||||
}
|
||||
|
||||
// Update updates an organization
|
||||
func (s *OrganizationService) Update(id int, req *dto.UpdateOrganizationRequest) (*model.Organization, error) {
|
||||
org, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrOrganizationNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check name uniqueness if changed
|
||||
if org.Name != req.Name {
|
||||
exists, err := s.repo.ExistsByName(req.Name, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrOrganizationExists
|
||||
}
|
||||
}
|
||||
|
||||
org.Name = req.Name
|
||||
org.Description = req.Description
|
||||
|
||||
if err := s.repo.Update(org); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return org, nil
|
||||
}
|
||||
|
||||
// Delete soft deletes an organization
|
||||
func (s *OrganizationService) Delete(id int) error {
|
||||
// Check if exists
|
||||
_, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrOrganizationNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return s.repo.SoftDelete(id)
|
||||
}
|
||||
166
go-backend/internal/service/target_service.go
Normal file
166
go-backend/internal/service/target_service.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/xingrin/go-backend/internal/dto"
|
||||
"github.com/xingrin/go-backend/internal/model"
|
||||
"github.com/xingrin/go-backend/internal/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTargetNotFound = errors.New("target not found")
|
||||
ErrTargetExists = errors.New("target name already exists")
|
||||
ErrInvalidTarget = errors.New("invalid target format")
|
||||
)
|
||||
|
||||
// TargetService handles target business logic
|
||||
type TargetService struct {
|
||||
repo *repository.TargetRepository
|
||||
}
|
||||
|
||||
// NewTargetService creates a new target service
|
||||
func NewTargetService(repo *repository.TargetRepository) *TargetService {
|
||||
return &TargetService{repo: repo}
|
||||
}
|
||||
|
||||
// Create creates a new target
|
||||
func (s *TargetService) Create(req *dto.CreateTargetRequest) (*model.Target, error) {
|
||||
exists, err := s.repo.ExistsByName(req.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrTargetExists
|
||||
}
|
||||
|
||||
// Auto-detect type if not provided
|
||||
targetType := req.Type
|
||||
if targetType == "" {
|
||||
targetType = detectTargetType(req.Name)
|
||||
if targetType == "" {
|
||||
return nil, ErrInvalidTarget
|
||||
}
|
||||
}
|
||||
|
||||
target := &model.Target{
|
||||
Name: req.Name,
|
||||
Type: targetType,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// List returns paginated targets
|
||||
func (s *TargetService) List(query *dto.TargetListQuery) ([]model.Target, int64, error) {
|
||||
return s.repo.FindAll(query.GetOffset(), query.GetPageSize(), query.Type, query.Search)
|
||||
}
|
||||
|
||||
// GetByID returns a target by ID
|
||||
func (s *TargetService) GetByID(id int) (*model.Target, error) {
|
||||
target, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrTargetNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// Update updates a target
|
||||
func (s *TargetService) Update(id int, req *dto.UpdateTargetRequest) (*model.Target, error) {
|
||||
target, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrTargetNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check name uniqueness if changed
|
||||
if target.Name != req.Name {
|
||||
exists, err := s.repo.ExistsByName(req.Name, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrTargetExists
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect type if not provided
|
||||
targetType := req.Type
|
||||
if targetType == "" {
|
||||
targetType = detectTargetType(req.Name)
|
||||
if targetType == "" {
|
||||
return nil, ErrInvalidTarget
|
||||
}
|
||||
}
|
||||
|
||||
target.Name = req.Name
|
||||
target.Type = targetType
|
||||
|
||||
if err := s.repo.Update(target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// Delete soft deletes a target
|
||||
func (s *TargetService) Delete(id int) error {
|
||||
_, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrTargetNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return s.repo.SoftDelete(id)
|
||||
}
|
||||
|
||||
// detectTargetType auto-detects target type from name
|
||||
func detectTargetType(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
// Check CIDR
|
||||
if strings.Contains(name, "/") {
|
||||
_, _, err := net.ParseCIDR(name)
|
||||
if err == nil {
|
||||
return "cidr"
|
||||
}
|
||||
}
|
||||
|
||||
// Check IP
|
||||
if ip := net.ParseIP(name); ip != nil {
|
||||
return "ip"
|
||||
}
|
||||
|
||||
// Check domain
|
||||
if isValidDomain(name) {
|
||||
return "domain"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// isValidDomain validates domain format
|
||||
func isValidDomain(domain string) bool {
|
||||
if len(domain) == 0 || len(domain) > 253 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Simple domain regex
|
||||
domainRegex := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
|
||||
return domainRegex.MatchString(domain)
|
||||
}
|
||||
100
go-backend/internal/service/user_service.go
Normal file
100
go-backend/internal/service/user_service.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/xingrin/go-backend/internal/auth"
|
||||
"github.com/xingrin/go-backend/internal/dto"
|
||||
"github.com/xingrin/go-backend/internal/model"
|
||||
"github.com/xingrin/go-backend/internal/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUsernameExists = errors.New("username already exists")
|
||||
ErrInvalidPassword = errors.New("invalid password")
|
||||
)
|
||||
|
||||
// UserService handles user business logic
|
||||
type UserService struct {
|
||||
repo *repository.UserRepository
|
||||
}
|
||||
|
||||
// NewUserService creates a new user service
|
||||
func NewUserService(repo *repository.UserRepository) *UserService {
|
||||
return &UserService{repo: repo}
|
||||
}
|
||||
|
||||
// Create creates a new user
|
||||
func (s *UserService) Create(req *dto.CreateUserRequest) (*model.User, error) {
|
||||
// Check if username exists
|
||||
exists, err := s.repo.ExistsByUsername(req.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrUsernameExists
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := auth.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
Username: req.Username,
|
||||
Password: hashedPassword,
|
||||
Email: req.Email,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// List returns paginated users
|
||||
func (s *UserService) List(query *dto.PaginationQuery) ([]model.User, int64, error) {
|
||||
return s.repo.FindAll(query.GetOffset(), query.GetPageSize())
|
||||
}
|
||||
|
||||
// GetByID returns a user by ID
|
||||
func (s *UserService) GetByID(id int) (*model.User, error) {
|
||||
user, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UpdatePassword updates user password
|
||||
func (s *UserService) UpdatePassword(id int, req *dto.UpdatePasswordRequest) error {
|
||||
user, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify old password
|
||||
if !auth.VerifyPassword(req.OldPassword, user.Password) {
|
||||
return ErrInvalidPassword
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, err := auth.HashPassword(req.NewPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Password = hashedPassword
|
||||
return s.repo.Update(user)
|
||||
}
|
||||
Reference in New Issue
Block a user