feat: improve datasource query audit workflows

This commit is contained in:
Logan committed 2026-07-06 23:01:04 +08:00
1 parent 74748e4914
commit 644981eef4
32 files changed
+780 -3061

No files matched your search

+2 -2
View File
@@ -10,8 +10,8 @@ FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates sqlite3 curl && apt-get clean && rm -rf /var/lib/apt/lists/*
# Install gh-ost for online DDL
RUN GH_OST_VERSION=1.1.9 && \
curl -fsSL "https://github.com/github/gh-ost/releases/download/v${GH_OST_VERSION}/gh-ost-binary-linux-amd64-*.tar.gz" \
| tar xz -C /usr/local/bin gh-ost && chmod +x /usr/local/bin/gh-ost
curl -fsSL -o /usr/local/bin/gh-ost "https://github.com/github/gh-ost/releases/download/v${GH_OST_VERSION}/gh-ost" && \
chmod +x /usr/local/bin/gh-ost
WORKDIR /app
COPY --from=build /app/opsxsql .
COPY --from=build /app/cmd/server/seed.go /app/
@@ -117,7 +117,7 @@ func testPostgreSQL(ctx context.Context, host string, port int, user, pass, dbna
dbname = "postgres"
}
dsn := fmt.Sprintf("postgres://%s:***@%s:%d/%s?sslmode=disable&connect_timeout=10",
user, pass, host, port, dbname)
user, host, port, dbname)
db, err := sql.Open("pgx", dsn)
if err != nil {
return false, fmt.Sprintf("Connection failed: %v", err)
@@ -141,12 +141,12 @@ func testRedis(ctx context.Context, host string, port int, username, pass string
redisUsername = ""
}
rdb := redis.NewClient(&redis.Options{
Addr: addr,
Username: redisUsername,
Password: pass,
DB: 0,
DialTimeout: 10 * time.Second,
ReadTimeout: 10 * time.Second,
Addr: addr,
Username: redisUsername,
Password: pass,
DB: 0,
DialTimeout: 10 * time.Second,
ReadTimeout: 10 * time.Second,
})
defer rdb.Close()
@@ -541,7 +541,7 @@ func getDBConnection(instance *model.Instance, dbName string) (*sql.DB, error) {
dbName = "postgres"
}
dsn := fmt.Sprintf("postgres://%s:***@%s:%d/%s?sslmode=disable&connect_timeout=10",
instance.Username, password, instance.Host, instance.Port, dbName)
instance.Username, instance.Host, instance.Port, dbName)
return sql.Open("pgx", dsn)
default:
return nil, fmt.Errorf("unsupported database type: %s", instance.DBType)
+16 -16
View File
@@ -14,10 +14,10 @@ import (
// GET /api/v1/instances
func ListInstances(c *gin.Context) {
userID, _, role := middleware.GetCurrentUser(c)
var instances []model.Instance
query := model.DB.Order("id")
// developer only sees instances in their groups
if role == "developer" {
var groupIDs []int
@@ -33,12 +33,12 @@ func ListInstances(c *gin.Context) {
}
}
}
if err := query.Find(&instances).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
// mask passwords in response
for i := range instances {
instances[i].Password = ""
@@ -86,8 +86,8 @@ func CreateInstance(c *gin.Context) {
if result.Version != "" {
updates["version"] = result.Version
}
if result.ClusterMode {
updates["cluster_mode"] = true
if instance.DBType == "redis" {
updates["cluster_mode"] = result.ClusterMode
}
} else {
updates["status"] = "error"
@@ -108,13 +108,13 @@ func UpdateInstance(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// if password is empty or masked placeholder, don't update it
if pw, ok := input["password"].(string); ok {
pw = strings.TrimSpace(pw)
@@ -128,12 +128,12 @@ func UpdateInstance(c *gin.Context) {
}
}
}
if err := model.DB.Model(&instance).Updates(input).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
model.DB.First(&instance, id)
instance.Password = "******"
c.JSON(http.StatusOK, instance)
@@ -157,15 +157,15 @@ func TestConnection(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
return
}
result := testDBConnectionFull(&instance)
// Update version & cluster_mode in DB if detected
updates := map[string]interface{}{}
if result.Success && result.Version != "" {
updates["version"] = result.Version
}
if result.ClusterMode {
updates["cluster_mode"] = true
if instance.DBType == "redis" {
updates["cluster_mode"] = result.ClusterMode
}
if len(updates) > 0 {
model.DB.Model(&model.Instance{}).Where("id = ?", instance.ID).Updates(updates)
@@ -181,16 +181,16 @@ func TestConnection(c *gin.Context) {
func InstanceStats(c *gin.Context) {
var total int64
model.DB.Model(&model.Instance{}).Count(&total)
type typeCount struct {
DBType string `json:"db_type"`
Count int64 `json:"count"`
}
var byType []typeCount
model.DB.Model(&model.Instance{}).Select("db_type, count(*) as count").Group("db_type").Scan(&byType)
c.JSON(http.StatusOK, gin.H{
"total": total,
"total": total,
"by_type": byType,
})
}
+90 -12
View File
@@ -7,20 +7,20 @@ import (
"sql-platform/internal/model"
"github.com/gin-gonic/gin"
"time"
"context"
"sql-platform/internal/middleware"
"strings"
"strconv"
"sort"
"encoding/json"
"bytes"
"regexp"
"context"
"encoding/json"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"go.mongodb.org/mongo-driver/v2/mongo/readpref"
"regexp"
"sort"
"sql-platform/internal/middleware"
"strconv"
"strings"
"time"
)
// GET /api/v1/instances/:id/detail/databases
@@ -523,7 +523,6 @@ func ExecuteQuery(c *gin.Context) {
}
// Read query limits from platform settings
maxLimit := getSettingInt("query_limit_max", 1000)
autoLimit := getSetting("query_auto_limit", "true") == "true"
if input.Limit <= 0 {
input.Limit = maxLimit
if input.Limit > 1000 {
@@ -547,7 +546,6 @@ func ExecuteQuery(c *gin.Context) {
}
}
}
_ = autoLimit
var instance model.Instance
if err := model.DB.First(&instance, input.InstanceID).Error; err != nil {
@@ -619,6 +617,9 @@ func executeSQLQuery(instance *model.Instance, dbName string, sqlStr string, lim
_ = ctx
upperSQL := strings.ToUpper(strings.TrimSpace(sqlStr))
if ok, reason := isReadOnlySQL(sqlStr); !ok {
return nil, fmt.Errorf("查询入口仅允许只读语句:%s。数据变更请提交工单", reason)
}
// Count total (only when no user LIMIT and offset=0)
total := 0
@@ -643,7 +644,8 @@ func executeSQLQuery(instance *model.Instance, dbName string, sqlStr string, lim
// Inject LIMIT/OFFSET if not already present — only for SELECT statements
// SHOW/DESC/EXPLAIN/CREATE/ALTER/DROP/etc do not support LIMIT syntax
needsLimit := !hasLimit && strings.HasPrefix(upperSQL, "SELECT")
autoLimit := getSetting("query_auto_limit", "true") == "true"
needsLimit := autoLimit && !hasLimit && isLimitableReadSQL(upperSQL)
if needsLimit {
sqlStr = fmt.Sprintf("%s LIMIT %d OFFSET %d", sqlStr, limit, offset)
}
@@ -689,6 +691,82 @@ func executeSQLQuery(instance *model.Instance, dbName string, sqlStr string, lim
}, nil
}
func isReadOnlySQL(sqlStr string) (bool, string) {
cleaned := strings.TrimSpace(stripLeadingSQLComments(sqlStr))
if cleaned == "" {
return false, "空语句"
}
if hasMultipleSQLStatements(cleaned) {
return false, "不允许一次执行多条语句"
}
upper := strings.ToUpper(cleaned)
readPrefixes := []string{"SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN", "WITH"}
allowed := false
for _, prefix := range readPrefixes {
if strings.HasPrefix(upper, prefix+" ") || upper == prefix {
allowed = true
break
}
}
if !allowed {
return false, "只允许 SELECT / SHOW / DESCRIBE / EXPLAIN"
}
blockedPatterns := []string{
" FOR UPDATE", " LOCK IN SHARE MODE", " INTO OUTFILE", " INTO DUMPFILE",
"EXPLAIN ANALYZE", "DELETE ", "UPDATE ", "INSERT ", "REPLACE ",
"CREATE ", "ALTER ", "DROP ", "TRUNCATE ", "RENAME ", "GRANT ", "REVOKE ",
"CALL ", "DO ", "LOAD DATA", "COPY ",
}
for _, pattern := range blockedPatterns {
if strings.Contains(upper, pattern) {
return false, strings.TrimSpace(pattern)
}
}
return true, ""
}
func isLimitableReadSQL(upperSQL string) bool {
return strings.HasPrefix(upperSQL, "SELECT") || strings.HasPrefix(upperSQL, "WITH")
}
func hasMultipleSQLStatements(sqlStr string) bool {
trimmed := strings.TrimSpace(sqlStr)
if strings.HasSuffix(trimmed, ";") {
trimmed = strings.TrimSpace(strings.TrimSuffix(trimmed, ";"))
}
return strings.Contains(trimmed, ";")
}
func stripLeadingSQLComments(sqlStr string) string {
s := strings.TrimSpace(sqlStr)
for {
switch {
case strings.HasPrefix(s, "--"):
if idx := strings.Index(s, "\n"); idx >= 0 {
s = strings.TrimSpace(s[idx+1:])
continue
}
return ""
case strings.HasPrefix(s, "#"):
if idx := strings.Index(s, "\n"); idx >= 0 {
s = strings.TrimSpace(s[idx+1:])
continue
}
return ""
case strings.HasPrefix(s, "/*"):
if idx := strings.Index(s, "*/"); idx >= 0 {
s = strings.TrimSpace(s[idx+2:])
continue
}
return s
default:
return s
}
}
}
// parseCommandParts splits a command string into parts, respecting quoted strings.
// e.g. HGET "my key" "my field" => ["HGET", "my key", "my field"]
func parseCommandParts(input string) []string {
+166 -28
View File
@@ -13,6 +13,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)
// Helper: get Redis client from instance (auto-detect standalone vs cluster)
type redisClientWrapper struct {
Client *redis.Client
@@ -21,7 +22,6 @@ type redisClientWrapper struct {
}
func (w *redisClientWrapper) Close() {
// Always close the single-node Client (cluster uses single-node now)
if w.Client != nil {
w.Client.Close()
}
@@ -30,19 +30,24 @@ func (w *redisClientWrapper) Close() {
}
}
// Ping, Info, DBSize, Scan, Type, TTL, Get, LRange, SMembers, ZRangeWithScores, HGetAll,
// SlowLogGet, ClientList, Do, Set, RPush, SAdd, HSet, Expire, Del — unified interface via wrapper methods
// NOTE: All methods use Client (single-node). isCluster flag is only for ScanAllKeys behavior.
func (w *redisClientWrapper) Ping(ctx context.Context) *redis.StatusCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.Ping(ctx)
}
return w.Client.Ping(ctx)
}
func (w *redisClientWrapper) Info(ctx context.Context, sections ...string) *redis.StringCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.Info(ctx, sections...)
}
return w.Client.Info(ctx, sections...)
}
func (w *redisClientWrapper) DBSize(ctx context.Context) *redis.IntCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.DBSize(ctx)
}
return w.Client.DBSize(ctx)
}
@@ -50,9 +55,27 @@ func (w *redisClientWrapper) Scan(ctx context.Context, cursor uint64, match stri
return w.Client.Scan(ctx, cursor, match, count)
}
// ScanAllKeys scans keys on the connected node.
// For clusters, this only scans keys on the single connected node (not all masters).
func (w *redisClientWrapper) ScanAllKeys(ctx context.Context, match string, count int) ([]string, error) {
if w.isCluster && w.ClusterClient != nil {
var allKeys []string
err := w.ClusterClient.ForEachMaster(ctx, func(ctx context.Context, shard *redis.Client) error {
cursor := uint64(0)
for {
keys, nextCursor, err := shard.Scan(ctx, cursor, match, int64(count)).Result()
if err != nil {
return err
}
allKeys = append(allKeys, keys...)
cursor = nextCursor
if cursor == 0 {
break
}
}
return nil
})
return allKeys, err
}
var allKeys []string
cursor := uint64(0)
for {
@@ -70,30 +93,51 @@ func (w *redisClientWrapper) ScanAllKeys(ctx context.Context, match string, coun
}
func (w *redisClientWrapper) Type(ctx context.Context, key string) *redis.StatusCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.Type(ctx, key)
}
return w.Client.Type(ctx, key)
}
func (w *redisClientWrapper) TTL(ctx context.Context, key string) *redis.DurationCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.TTL(ctx, key)
}
return w.Client.TTL(ctx, key)
}
func (w *redisClientWrapper) Get(ctx context.Context, key string) *redis.StringCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.Get(ctx, key)
}
return w.Client.Get(ctx, key)
}
func (w *redisClientWrapper) LRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.LRange(ctx, key, start, stop)
}
return w.Client.LRange(ctx, key, start, stop)
}
func (w *redisClientWrapper) SMembers(ctx context.Context, key string) *redis.StringSliceCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.SMembers(ctx, key)
}
return w.Client.SMembers(ctx, key)
}
func (w *redisClientWrapper) ZRangeWithScores(ctx context.Context, key string, start, stop int64) *redis.ZSliceCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.ZRangeWithScores(ctx, key, start, stop)
}
return w.Client.ZRangeWithScores(ctx, key, start, stop)
}
func (w *redisClientWrapper) HGetAll(ctx context.Context, key string) *redis.MapStringStringCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.HGetAll(ctx, key)
}
return w.Client.HGetAll(ctx, key)
}
@@ -106,42 +150,72 @@ func (w *redisClientWrapper) ClientList(ctx context.Context) *redis.StringCmd {
}
func (w *redisClientWrapper) Do(ctx context.Context, args ...interface{}) *redis.Cmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.Do(ctx, args...)
}
return w.Client.Do(ctx, args...)
}
func (w *redisClientWrapper) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.Set(ctx, key, value, expiration)
}
return w.Client.Set(ctx, key, value, expiration)
}
func (w *redisClientWrapper) RPush(ctx context.Context, key string, values ...interface{}) *redis.IntCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.RPush(ctx, key, values...)
}
return w.Client.RPush(ctx, key, values...)
}
func (w *redisClientWrapper) SAdd(ctx context.Context, key string, members ...interface{}) *redis.IntCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.SAdd(ctx, key, members...)
}
return w.Client.SAdd(ctx, key, members...)
}
func (w *redisClientWrapper) HSet(ctx context.Context, key string, values ...interface{}) *redis.IntCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.HSet(ctx, key, values...)
}
return w.Client.HSet(ctx, key, values...)
}
func (w *redisClientWrapper) Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.Expire(ctx, key, expiration)
}
return w.Client.Expire(ctx, key, expiration)
}
func (w *redisClientWrapper) Del(ctx context.Context, keys ...string) *redis.IntCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.Del(ctx, keys...)
}
return w.Client.Del(ctx, keys...)
}
func (w *redisClientWrapper) LLen(ctx context.Context, key string) *redis.IntCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.LLen(ctx, key)
}
return w.Client.LLen(ctx, key)
}
func (w *redisClientWrapper) SCard(ctx context.Context, key string) *redis.IntCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.SCard(ctx, key)
}
return w.Client.SCard(ctx, key)
}
func (w *redisClientWrapper) SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *redis.ScanCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.SScan(ctx, key, cursor, match, count)
}
return w.Client.SScan(ctx, key, cursor, match, count)
}
@@ -162,10 +236,6 @@ func getRedisClient(instance *model.Instance, db int) (*redisClientWrapper, erro
redisUsername = "" // go-redis treats empty Username as no ACL auth
}
// Always use single-node connection for query execution.
// ClusterClient does topology discovery (CLUSTER SLOTS) which fails when
// internal cluster IPs are unreachable from the client (e.g., Docker networks).
// Single-node mode works for all read-only commands on cluster nodes.
client := redis.NewClient(&redis.Options{
Addr: addr,
Username: redisUsername,
@@ -180,7 +250,48 @@ func getRedisClient(instance *model.Instance, db int) (*redisClientWrapper, erro
// Check cluster mode flag for wrapper behavior (ScanAllKeys needs to know)
isCluster := instance.ClusterMode != nil && *instance.ClusterMode
return &redisClientWrapper{Client: client, isCluster: isCluster}, nil
wrapper := &redisClientWrapper{Client: client, isCluster: isCluster}
if !isCluster {
return wrapper, nil
}
// Redis Cluster routes key commands by slot. Build the address list from
// CLUSTER SLOTS, but replace advertised internal node IPs with the configured
// host so Docker/NAT deployments can still reach the mapped node ports.
addrs := []string{addr}
if slots, err := client.ClusterSlots(ctx).Result(); err == nil && len(slots) > 0 {
seen := map[string]bool{}
addrs = addrs[:0]
for _, slot := range slots {
if len(slot.Nodes) == 0 {
continue
}
nodeAddr := slot.Nodes[0].Addr
if idx := strings.LastIndex(nodeAddr, ":"); idx >= 0 {
nodeAddr = instance.Host + nodeAddr[idx:]
}
if !seen[nodeAddr] {
seen[nodeAddr] = true
addrs = append(addrs, nodeAddr)
}
}
if len(addrs) == 0 {
addrs = []string{addr}
}
}
clusterClient := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: addrs,
Username: redisUsername,
Password: password,
})
if err := clusterClient.Ping(ctx).Err(); err != nil {
clusterClient.Close()
client.Close()
return nil, fmt.Errorf("Redis cluster connection failed: %v", err)
}
wrapper.ClusterClient = clusterClient
return wrapper, nil
}
// GET /api/v1/redis/:id/info
@@ -265,11 +376,12 @@ func RedisDbsize(c *gin.Context) {
func RedisScanKeys(c *gin.Context) {
id := c.Param("id")
dbParam := c.Query("db")
pattern := c.DefaultQuery("pattern", "*")
// Auto-append * if pattern has no wildcard (user-friendly fuzzy search)
if !strings.Contains(pattern, "*") && !strings.Contains(pattern, "?") && !strings.Contains(pattern, "[") {
pattern = pattern + "*"
rawPattern := strings.TrimSpace(c.DefaultQuery("pattern", "*"))
if rawPattern == "" {
rawPattern = "*"
}
pattern := rawPattern
hasWildcard := strings.ContainsAny(pattern, "*?[")
count, _ := strconv.Atoi(c.DefaultQuery("count", "100"))
db := 0
@@ -291,11 +403,6 @@ func RedisScanKeys(c *gin.Context) {
defer client.Close()
ctx := context.Background()
allKeys, err := client.ScanAllKeys(ctx, pattern, count)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
// Get types for each key
type KeyInfo struct {
@@ -303,6 +410,37 @@ func RedisScanKeys(c *gin.Context) {
Type string `json:"type"`
TTL int64 `json:"ttl"`
}
// Redis Cluster 精确 key 查询应优先走槽位路由直查。全量 SCAN 需要访问各 master,
// 在 NAT/代理暴露单入口时容易受节点通告地址影响。
if !hasWildcard && pattern != "*" {
keyType, err := client.Type(ctx, pattern).Result()
if err == nil && keyType != "none" {
ttl, _ := client.TTL(ctx, pattern).Result()
ttlMs := int64(-1)
if ttl > 0 {
ttlMs = ttl.Milliseconds()
}
c.JSON(http.StatusOK, gin.H{
"keys": []KeyInfo{{Key: pattern, Type: keyType, TTL: ttlMs}},
"cursor": 0,
"total": 1,
})
return
}
}
// Auto-append * if pattern has no wildcard (user-friendly fuzzy search)
if !hasWildcard {
pattern = pattern + "*"
}
allKeys, err := client.ScanAllKeys(ctx, pattern, count)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
var keyInfos []KeyInfo
for _, key := range allKeys {
keyType, _ := client.Type(ctx, key).Result()
@@ -498,7 +636,7 @@ func RedisExecuteCommand(c *gin.Context) {
ctx := context.Background()
cmd := strings.ToUpper(input.Command)
// Safe read-only commands
readOnly := map[string]bool{
"GET": true, "MGET": true, "HGET": true, "HGETALL": true,
@@ -508,7 +646,7 @@ func RedisExecuteCommand(c *gin.Context) {
"STRLEN": true, "LLEN": true, "SCARD": true, "ZCARD": true,
"HLEN": true, "GETRANGE": true,
}
if !readOnly[cmd] {
c.JSON(http.StatusForbidden, gin.H{"detail": "Only read-only commands allowed via query interface"})
return
@@ -547,10 +685,10 @@ func RedisCreateKey(c *gin.Context) {
}
var input struct {
Key string `json:"key" binding:"required"`
Type string `json:"type" binding:"required"`
Value interface{} `json:"value"`
TTL int `json:"ttl"`
Key string `json:"key" binding:"required"`
Type string `json:"type" binding:"required"`
Value interface{} `json:"value"`
TTL int `json:"ttl"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
@@ -571,7 +709,7 @@ func RedisCreateKey(c *gin.Context) {
defer client.Close()
ctx := context.Background()
switch input.Type {
case "string":
client.Set(ctx, input.Key, input.Value, 0)
+13 -5
View File
@@ -10,6 +10,14 @@ import (
"sql-platform/internal/model"
)
func stringPtr(value string) *string {
return &value
}
func boolPtr(value bool) *bool {
return &value
}
type AuthService struct {
DB *gorm.DB
}
@@ -25,7 +33,7 @@ func (s *AuthService) Login(username, password string) (*model.User, string, err
return nil, "", errors.New("invalid username or password")
}
if !user.IsActive {
if user.IsActive != nil && !*user.IsActive {
return nil, "", errors.New("account is disabled")
}
@@ -33,7 +41,7 @@ func (s *AuthService) Login(username, password string) (*model.User, string, err
return nil, "", errors.New("invalid username or password")
}
token, err := middleware.GenerateToken(user.ID, user.Username, user.Role)
token, err := middleware.GenerateToken(&user)
if err != nil {
return nil, "", errors.New("failed to generate token")
}
@@ -67,11 +75,11 @@ func (s *AuthService) Register(username, displayName, email, phone, password, ro
user := model.User{
Username: username,
DisplayName: displayName,
Email: email,
Phone: phone,
Email: stringPtr(email),
Phone: stringPtr(phone),
HashedPassword: string(hashedPassword),
Role: role,
IsActive: true,
IsActive: boolPtr(true),
}
if err := s.DB.Create(&user).Error; err != nil {
+6 -4
View File
@@ -19,14 +19,16 @@ func NewTicketService(db *gorm.DB) *TicketService {
// CreateTicket creates a new ticket
func (s *TicketService) CreateTicket(ticket *model.Ticket) error {
if ticket.TicketNo == "" {
ticket.TicketNo = s.generateTicketNo()
if ticket.TicketNo == nil || *ticket.TicketNo == "" {
ticketNo := s.generateTicketNo()
ticket.TicketNo = &ticketNo
}
if ticket.CreateTime.IsZero() {
ticket.CreateTime = time.Now()
}
if ticket.Status == "" {
ticket.Status = "pending"
if ticket.Status == nil || *ticket.Status == "" {
status := "pending"
ticket.Status = &status
}
return s.DB.Create(ticket).Error
}