commit 584dbb81dc98df049bb41fb70bc5bf7352846809 Author: 110316118 <110316118@gms.tcu.edu.tw> Date: Wed Feb 19 15:01:38 2025 +0800 init diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..9a4b859 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,8 @@ +.idea +markdowns + +*.db +manager_backend.tar +usermgmt_service +main +app \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..6375369 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,34 @@ +# Use the Go image for building +FROM golang:1.21-alpine AS builder + +WORKDIR /app + +# Install dependencies +RUN apk add --no-cache gcc musl-dev sqlite sqlite-dev + +# Copy source code and Go module files +COPY go.mod go.sum ./ +RUN go mod download +COPY . . + +# Build the application +RUN CGO_ENABLED=1 GOOS=linux go build -o app + +# Use a minimal image for the final stage +FROM alpine:latest +WORKDIR /app + +RUN apk add --no-cache gcc musl-dev sqlite sqlite-dev + +# Copy the compiled app and necessary files from the builder stage +COPY --from=builder /app/app . +#COPY --from=builder /app/data /data + +# Set environment variables +ENV DB_PATH=/data/admin.db + +# Expose the application port +EXPOSE 8080 + +# Command to run the application +CMD ["./app"] diff --git a/backend/auth/authService.go b/backend/auth/authService.go new file mode 100644 index 0000000..c94bd84 --- /dev/null +++ b/backend/auth/authService.go @@ -0,0 +1,131 @@ +package auth + +import ( + "database/sql" + "errors" + _ "github.com/mattn/go-sqlite3" + "golang.org/x/crypto/bcrypt" + "time" +) + +type Admin struct { + ID int `json:"id"` + Username string `json:"username"` + Password string `json:"-"` // 不返回密碼 + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type AuthService struct { + db *sql.DB +} + +func NewAuthService(dbPath string) (*AuthService, error) { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, err + } + + // 創建管理員表 + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS admins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + is_activate BOOLEAN NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) + if err != nil { + return nil, err + } + + return &AuthService{db: db}, nil +} + +// 創建管理員 +func (s *AuthService) CreateAdmin(username, password string) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return err + } + + _, err = s.db.Exec( + "INSERT INTO admins (username, password) VALUES (?, ?)", + username, string(hashedPassword), + ) + return err +} + +// 驗證管理員 +func (s *AuthService) ValidateAdmin(username, password string) (*Admin, error) { + admin := &Admin{} + err := s.db.QueryRow( + "SELECT id, username, password, is_active FROM admins WHERE username = ?", + username, + ).Scan(&admin.ID, &admin.Username, &admin.Password, &admin.IsActive) + + if !admin.IsActive { + return nil, errors.New("user is not a admin user") + } + + if err != nil { + return nil, err + } + + err = bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(password)) + if err != nil { + return nil, err + } + + return admin, nil +} + +// 獲取所有管理員 +func (s *AuthService) GetAdmins() ([]Admin, error) { + rows, err := s.db.Query("SELECT id, username, created_at, updated_at, is_active FROM admins") + if err != nil { + return nil, err + } + defer rows.Close() + + var admins []Admin + for rows.Next() { + var admin Admin + err := rows.Scan(&admin.ID, &admin.Username, &admin.CreatedAt, &admin.UpdatedAt, &admin.IsActive) + if err != nil { + return nil, err + } + admins = append(admins, admin) + } + return admins, nil +} + +// 更新管理員密碼 +func (s *AuthService) UpdateAdminPassword(id string, newPassword string) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + + _, err = s.db.Exec( + "UPDATE admins SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", + string(hashedPassword), id, + ) + return err +} + +func (s *AuthService) UpdateAdminActivate(id string, isActive bool) error { + query := `UPDATE admins SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?` + _, err := s.db.Exec(query, isActive, id) + + return err +} + +// 刪除管理員 +func (s *AuthService) DeleteAdmin(id string) error { + _, err := s.db.Exec("DELETE FROM admins WHERE id = ?", id) + return err +} diff --git a/backend/auth/jwt-middleware.go b/backend/auth/jwt-middleware.go new file mode 100644 index 0000000..ada8496 --- /dev/null +++ b/backend/auth/jwt-middleware.go @@ -0,0 +1,63 @@ +package auth + +import ( + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" + "net/http" + "strings" + "time" +) + +var JWTSecret = []byte("your-secret-key") // 在實際使用時應該從環境變量獲取 + +type Claims struct { + Username string `json:"username"` + jwt.StandardClaims +} + +// 生成 JWT token +func GenerateToken(username string) (string, error) { + claims := Claims{ + username, + jwt.StandardClaims{ + ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), + IssuedAt: time.Now().Unix(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(JWTSecret) +} + +// JWT 認證中間件 +func JWTAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) + c.Abort() + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if !(len(parts) == 2 && parts[0] == "Bearer") { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization format"}) + c.Abort() + return + } + + claims := &Claims{} + token, err := jwt.ParseWithClaims(parts[1], claims, func(token *jwt.Token) (interface{}, error) { + return JWTSecret, nil + }) + + if err != nil || !token.Valid { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) + c.Abort() + return + } + + c.Set("username", claims.Username) + c.Next() + } +} diff --git a/backend/docker-compose-local.yml b/backend/docker-compose-local.yml new file mode 100644 index 0000000..eb160ec --- /dev/null +++ b/backend/docker-compose-local.yml @@ -0,0 +1,18 @@ +#version: '3.8' +services: + backend: + image: golang:1.21-alpine # 使用官方 Go 映像 + ports: + - "8080:8080" # 映射端口 + volumes: + - ./data:/data # 綁定數據文件夾 + - ./:/app # 綁定代碼文件夾 + - /var/run:/var/run + working_dir: /app # 設置工作目錄 + environment: + - DB_PATH=/data/admin.db # 設定數據庫路徑環境變量 + command: sh -c " + apk add --no-cache gcc musl-dev sqlite sqlite-dev && + go mod download && + CGO_ENABLED=1 GOOS=linux go build -o app && + ./app" diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..8b05c92 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.8' +services: + backend: + image: backend-backend-x86 + restart: always + ports: + - "8080:8080" # Map port 8080 + volumes: + - ./data:/data # Bind the remote server's data folder + - /var/run:/var/run + environment: + - DB_PATH=/data/admin.db # Set database path as an environment variable diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..164ea76 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,43 @@ +// go.mod +module user-management + +go 1.21.0 + +toolchain go1.22.2 + +require ( + github.com/gin-contrib/cors v1.7.3 + github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v4 v4.5.1 + github.com/mattn/go-sqlite3 v1.14.24 + golang.org/x/crypto v0.31.0 +) + +require ( + github.com/bytedance/sonic v1.12.6 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.7 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.36.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..08df2df --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,99 @@ +github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= +github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns= +github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..8fdba71 --- /dev/null +++ b/backend/main.go @@ -0,0 +1,413 @@ +package main + +import ( + "encoding/json" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "log" + "net/http" + "os" + "path/filepath" + "time" + "user-management/auth" + "user-management/system" +) + +func main() { + // 初始化認證服務 + dbPath := os.Getenv("DB_PATH") + if dbPath == "" { + dbPath = "/data/admin.db" + } + authService, err := auth.NewAuthService(dbPath) + if err != nil { + log.Fatal(err) + } + // 獲取 socket 路徑 + socketPath := os.Getenv("SOCKET_PATH") + if socketPath == "" { + socketPath = "/var/run/usermgmt.sock" + } + + // 創建系統客戶端 + client := system.NewSystemClient(socketPath) + + r := gin.Default() + + // CORS 中間件 + // Apply the CORS middleware + r.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, // Allow only frontend origin + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Content-Type", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + + r.Static("/static", "./markdowns") + + // Dynamic endpoint to serve markdown content + r.GET("/api/markdowns/:filename", func(c *gin.Context) { + filename := c.Param("filename") + filePath := filepath.Join("markdowns", filename) + + // Read the markdown file + content, err := os.ReadFile(filePath) + if err != nil { + // Handle file not found or read error + c.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) + return + } + + // Serve the file content + c.Data(http.StatusOK, "text/markdown; charset=utf-8", content) + }) + + // 管理員認證 API + r.POST("/api/admin/login", func(c *gin.Context) { + var req struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := c.BindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + admin, err := authService.ValidateAdmin(req.Username, req.Password) + if err != nil { + c.JSON(401, gin.H{"error": "Invalid credentials"}) + return + } + + token, err := auth.GenerateToken(admin.Username) + if err != nil { + c.JSON(500, gin.H{"error": "Failed to generate token"}) + return + } + + c.JSON(200, gin.H{ + "token": token, + "admin": admin, + }) + }) + + // 管理員 CRUD API + adminAPI := r.Group("/api/admin") + adminAPI.GET("/admins", func(c *gin.Context) { + admins, err := authService.GetAdmins() + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, admins) + }) + + adminAPI.POST("/admins", func(c *gin.Context) { + var req struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := c.BindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + err := authService.CreateAdmin(req.Username, req.Password) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"message": "Admin created successfully"}) + }) + + adminAPI.Use(auth.JWTAuthMiddleware()) + { + adminAPI.PUT("/admins/:id/password", func(c *gin.Context) { + id := c.Param("id") + var req struct { + Password string `json:"password"` + } + if err := c.BindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + err := authService.UpdateAdminPassword(id, req.Password) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"message": "Password updated successfully"}) + }) + + adminAPI.PUT("/admins/:id/active", func(c *gin.Context) { + id := c.Param("id") + var req struct { + IsActive bool `json:"is_active"` + } + if err := c.BindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + err := authService.UpdateAdminActivate(id, req.IsActive) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"message": "Admin active status updated successfully"}) + }) + + adminAPI.DELETE("/admins/:id", func(c *gin.Context) { + id := c.Param("id") + err := authService.DeleteAdmin(id) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"message": "Admin deleted successfully"}) + }) + } + + sambaConfigAPI := r.Group("/api/samba/config") + sambaConfigAPI.Use(auth.JWTAuthMiddleware()) + { + sambaConfigAPI.GET("/global_setting", func(c *gin.Context) { + settings, err := client.GetSambaGlobalSetting() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Unmarshal the JSON string into a map + var settingsMap map[string]interface{} + if err := json.Unmarshal([]byte(settings), &settingsMap); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse settings JSON"}) + return + } + + c.JSON(http.StatusOK, settingsMap["global"]) + }) + + sambaConfigAPI.GET("/section_setting", func(c *gin.Context) { + settings, err := client.GetSambaSectionSetting() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Unmarshal the JSON string into a map + var settingsMap map[string]interface{} + if err := json.Unmarshal([]byte(settings), &settingsMap); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse settings JSON"}) + return + } + + c.JSON(http.StatusOK, settingsMap["sections"]) + }) + + sambaConfigAPI.POST("/update_section_setting", func(c *gin.Context) { + // Read the request body + var reqBody struct { + SectionSettings string `json:"section_settings"` // Expecting a JSON string + } + + if err := c.BindJSON(&reqBody); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + // Call the UpdateSambaSectionSetting function + err := client.UpdateSambaSectionSetting(reqBody.SectionSettings) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Section settings updated successfully"}) + }) + + sambaConfigAPI.POST("/update_global_setting", func(c *gin.Context) { + // Read the request body + var reqBody struct { + GlobalSettings string `json:"global_settings"` // Expecting a JSON string + } + + if err := c.BindJSON(&reqBody); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + // Call the UpdateSambaGlobalSetting function + err := client.UpdateSambaGlobalSetting(reqBody.GlobalSettings) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Global settings updated successfully"}) + }) + + sambaConfigAPI.GET("/status", func(c *gin.Context) { + _, err := client.GetSambaServiceInfo() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, "{\"timestamp\": \"2025-01-26T20:21:50.949817+0800\", \"version\": \"4.19.5-Ubuntu\", \"smb_conf\": \"/etc/samba/smb.conf\", \"sessions\": {\"4116888046\": {\"session_id\": \"4116888046\", \"server_id\": {\"pid\": \"1805949\", \"task_id\": \"0\", \"vnn\": \"4294967295\", \"unique_id\": \"469723148854225877\"}, \"uid\": 1001, \"gid\": 1001, \"username\": \"jing\", \"groupname\": \"jing\", \"remote_machine\": \"192.168.0.1\", \"hostname\": \"ipv4:192.168.0.1:60343\", \"session_dialect\": \"SMB3_11\", \"encryption\": {\"cipher\": \"-\", \"degree\": \"none\"}, \"signing\": {\"cipher\": \"AES-128-GMAC\", \"degree\": \"partial\"}}}, \"tcons\": {\"2344016500\": {\"service\": \"share_data\", \"server_id\": {\"pid\": \"1805949\", \"task_id\": \"0\", \"vnn\": \"4294967295\", \"unique_id\": \"469723148854225877\"}, \"tcon_id\": \"2344016500\", \"session_id\": \"4116888046\", \"machine\": \"192.168.0.1\", \"connected_at\": \"2025-01-26T17:11:07.474663+08:00\", \"encryption\": {\"cipher\": \"-\", \"degree\": \"none\"}, \"signing\": {\"cipher\": \"-\", \"degree\": \"none\"}}, \"3876116464\": {\"service\": \"public_data\", \"server_id\": {\"pid\": \"1805949\", \"task_id\": \"0\", \"vnn\": \"4294967295\", \"unique_id\": \"469723148854225877\"}, \"tcon_id\": \"3876116464\", \"session_id\": \"4116888046\", \"machine\": \"192.168.0.1\", \"connected_at\": \"2025-01-26T17:11:07.477214+08:00\", \"encryption\": {\"cipher\": \"-\", \"degree\": \"none\"}, \"signing\": {\"cipher\": \"-\", \"degree\": \"none\"}}}, \"open_files\": {\"/samba/share_data/.\": {\"service_path\": \"/samba/share_data\", \"filename\": \".\", \"fileid\": {\"devid\": 2050, \"inode\": 43646978, \"extid\": 0}, \"num_pending_deletes\": 0, \"opens\": {\"1805949/39\": {\"server_id\": {\"pid\": \"1805949\", \"task_id\": \"0\", \"vnn\": \"4294967295\", \"unique_id\": \"469723148854225877\"}, \"uid\": 65534, \"share_file_id\": \"39\", \"sharemode\": {\"hex\": \"0x00000007\", \"READ\": true, \"WRITE\": true, \"DELETE\": true, \"text\": \"RWD\"}, \"access_mask\": {\"hex\": \"0x00100081\", \"READ_DATA\": true, \"WRITE_DATA\": false, \"APPEND_DATA\": false, \"READ_EA\": false, \"WRITE_EA\": false, \"EXECUTE\": false, \"READ_ATTRIBUTES\": true, \"WRITE_ATTRIBUTES\": false, \"DELETE_CHILD\": false, \"DELETE\": false, \"READ_CONTROL\": false, \"WRITE_DAC\": false, \"SYNCHRONIZE\": true, \"ACCESS_SYSTEM_SECURITY\": false, \"text\": \"R\"}, \"caching\": {\"READ\": false, \"WRITE\": false, \"HANDLE\": false, \"hex\": \"0x00000000\", \"text\": \"\"}, \"oplock\": {}, \"lease\": {}, \"opened_at\": \"2025-01-26T17:11:08.520410+08:00\"}, \"1805949/27\": {\"server_id\": {\"pid\": \"1805949\", \"task_id\": \"0\", \"vnn\": \"4294967295\", \"unique_id\": \"469723148854225877\"}, \"uid\": 65534, \"share_file_id\": \"27\", \"sharemode\": {\"hex\": \"0x00000007\", \"READ\": true, \"WRITE\": true, \"DELETE\": true, \"text\": \"RWD\"}, \"access_mask\": {\"hex\": \"0x00100081\", \"READ_DATA\": true, \"WRITE_DATA\": false, \"APPEND_DATA\": false, \"READ_EA\": false, \"WRITE_EA\": false, \"EXECUTE\": false, \"READ_ATTRIBUTES\": true, \"WRITE_ATTRIBUTES\": false, \"DELETE_CHILD\": false, \"DELETE\": false, \"READ_CONTROL\": false, \"WRITE_DAC\": false, \"SYNCHRONIZE\": true, \"ACCESS_SYSTEM_SECURITY\": false, \"text\": \"R\"}, \"caching\": {\"READ\": false, \"WRITE\": false, \"HANDLE\": false, \"hex\": \"0x00000000\", \"text\": \"\"}, \"oplock\": {}, \"lease\": {}, \"opened_at\": \"2025-01-26T17:11:08.458851+08:00\"}, \"1805949/13\": {\"server_id\": {\"pid\": \"1805949\", \"task_id\": \"0\", \"vnn\": \"4294967295\", \"unique_id\": \"469723148854225877\"}, \"uid\": 65534, \"share_file_id\": \"13\", \"sharemode\": {\"hex\": \"0x00000007\", \"READ\": true, \"WRITE\": true, \"DELETE\": true, \"text\": \"RWD\"}, \"access_mask\": {\"hex\": \"0x00100081\", \"READ_DATA\": true, \"WRITE_DATA\": false, \"APPEND_DATA\": false, \"READ_EA\": false, \"WRITE_EA\": false, \"EXECUTE\": false, \"READ_ATTRIBUTES\": true, \"WRITE_ATTRIBUTES\": false, \"DELETE_CHILD\": false, \"DELETE\": false, \"READ_CONTROL\": false, \"WRITE_DAC\": false, \"SYNCHRONIZE\": true, \"ACCESS_SYSTEM_SECURITY\": false, \"text\": \"R\"}, \"caching\": {\"READ\": false, \"WRITE\": false, \"HANDLE\": false, \"hex\": \"0x00000000\", \"text\": \"\"}, \"oplock\": {}, \"lease\": {}, \"opened_at\": \"2025-01-26T17:11:08.445320+08:00\"}}}, \"/samba/lab_data/.\": {\"service_path\": \"/samba/lab_data\", \"filename\": \".\", \"fileid\": {\"devid\": 2050, \"inode\": 43654374, \"extid\": 0}, \"num_pending_deletes\": 0, \"opens\": {\"1805949/3\": {\"server_id\": {\"pid\": \"1805949\", \"task_id\": \"0\", \"vnn\": \"4294967295\", \"unique_id\": \"469723148854225877\"}, \"uid\": 65534, \"share_file_id\": \"3\", \"sharemode\": {\"hex\": \"0x00000000\", \"READ\": false, \"WRITE\": false, \"DELETE\": false, \"text\": \"\"}, \"access_mask\": {\"hex\": \"0x00100080\", \"READ_DATA\": false, \"WRITE_DATA\": false, \"APPEND_DATA\": false, \"READ_EA\": false, \"WRITE_EA\": false, \"EXECUTE\": false, \"READ_ATTRIBUTES\": true, \"WRITE_ATTRIBUTES\": false, \"DELETE_CHILD\": false, \"DELETE\": false, \"READ_CONTROL\": false, \"WRITE_DAC\": false, \"SYNCHRONIZE\": true, \"ACCESS_SYSTEM_SECURITY\": false, \"text\": \"\"}, \"caching\": {\"READ\": false, \"WRITE\": false, \"HANDLE\": false, \"hex\": \"0x00000000\", \"text\": \"\"}, \"oplock\": {}, \"lease\": {}, \"opened_at\": \"2025-01-26T17:11:07.479460+08:00\"}}}}}\n") + }) + + sambaConfigAPI.POST("/restart_samba", func(c *gin.Context) { + err := client.RestartSambaService() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Success restart samba service"}) + }) + } + + // 用戶管理 API + r.GET("/api/users", func(c *gin.Context) { + users, err := client.GetUsers() + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, users) + }) + + r.POST("/api/users", func(c *gin.Context) { + var req struct { + Username string `json:"username"` + Password string `json:"password"` + Group string `json:"group"` + IsAdmin bool `json:"is_admin"` + Shell string `json:"shell"` + } + if err := c.BindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + err := client.CreateUser(req.Username, req.Password, req.Group, req.IsAdmin, req.Shell) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"message": "User created successfully"}) + }) + + r.PUT("/api/users/:username", func(c *gin.Context) { + username := c.Param("username") + var req struct { + Password string `json:"password"` + } + if err := c.BindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + err := client.ModifyUserPasswd(username, req.Password) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"message": "User modified successfully"}) + }) + + r.DELETE("/api/users/:username", func(c *gin.Context) { + username := c.Param("username") + err := client.DeleteUser(username) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"message": "User deleted successfully"}) + }) + + r.GET("/api/groups", func(c *gin.Context) { + groups, err := client.GetGroups() + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, groups) + }) + + r.GET("/api/groups/:username", func(c *gin.Context) { + username := c.Param("username") + groups, err := client.GetUserGroups(username) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, groups) + }) + + // Add user to multiple groups endpoint + r.POST("/api/groups/:username", func(c *gin.Context) { + username := c.Param("username") + var req struct { + Groups []string `json:"groups"` + } + if err := c.BindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + // Add user to each group + err := client.AddUserToGroups(username, req.Groups) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, gin.H{"message": "User added to groups successfully"}) + }) + + // Remove user from a group endpoint + r.DELETE("/api/groups/:username", func(c *gin.Context) { + username := c.Param("username") + var req struct { + Group string `json:"group"` + } + if err := c.BindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + err := client.RemoveUserFromGroup(username, req.Group) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, gin.H{"message": "Group removed successfully"}) + }) + + // Samba 連接 API + r.GET("/api/samba-connections", func(c *gin.Context) { + connections, err := client.GetSambaConnections() + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, connections) + }) + + // 啟動服務器 + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + log.Printf("Server starting on port %s", port) + if err := r.Run(":" + port); err != nil { + log.Fatal(err) + } +} diff --git a/backend/system/client.go b/backend/system/client.go new file mode 100644 index 0000000..e736970 --- /dev/null +++ b/backend/system/client.go @@ -0,0 +1,519 @@ +// system/client.go +package system + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "strings" +) + +type SystemClient struct { + socketPath string +} + +type Command struct { + Action string `json:"action"` + Params map[string]interface{} `json:"params"` +} + +type Response struct { + Success bool `json:"success"` + Data interface{} `json:"data"` + Error string `json:"error,omitempty"` +} + +type User struct { + Username string `json:"username"` + UID string `json:"uid"` + GID string `json:"gid"` + HomeDir string `json:"home_dir"` + Shell string `json:"shell"` +} + +type Group struct { + GroupName string `json:"groupName"` +} + +type SambaConnection struct { + PID string `json:"pid"` + Username string `json:"username"` + Group string `json:"group"` + Machine string `json:"machine"` + Protocol string `json:"protocol"` + Folder string `json:"folder"` +} + +func NewSystemClient(socketPath string) *SystemClient { + return &SystemClient{ + socketPath: socketPath, + } +} + +func (c *SystemClient) sendCommand(cmd Command) (*Response, error) { + // 連接到 Unix socket + conn, err := net.Dial("unix", c.socketPath) + if err != nil { + return nil, fmt.Errorf("failed to connect to socket: %v", err) + } + defer conn.Close() + + // 發送命令 + encoder := json.NewEncoder(conn) + if err := encoder.Encode(cmd); err != nil { + return nil, fmt.Errorf("failed to encode command: %v", err) + } + + // 讀取響應 + decoder := json.NewDecoder(conn) + var response Response + if err := decoder.Decode(&response); err != nil { + return nil, fmt.Errorf("failed to decode response: %v", err) + } + + return &response, nil +} + +func (c *SystemClient) GetUsers() ([]User, error) { + resp, err := c.sendCommand(Command{ + Action: "get_users", + Params: nil, + }) + if err != nil { + return nil, err + } + + if !resp.Success { + return nil, fmt.Errorf(resp.Error) + } + + var users []User + dataStr, ok := resp.Data.(string) + if !ok { + fmt.Println("Error: Data is not a string") + return users, nil + } + scanner := bufio.NewScanner(strings.NewReader(dataStr)) + + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, ":") + if len(parts) < 7 { + continue // Skip malformed lines + } + + username := parts[0] + uid := parts[2] + gid := parts[3] + homeDir := parts[5] + shell := parts[6] + + user := User{ + Username: username, + UID: uid, + GID: gid, + HomeDir: homeDir, + Shell: shell, + } + + users = append(users, user) + } + + if err := scanner.Err(); err != nil { + fmt.Println("Error reading command output:", err) + return users, nil + } + + // Marshal to JSON and print + _, err = json.MarshalIndent(users, "", " ") + if err != nil { + fmt.Println("Error marshalling to JSON:", err) + return users, nil + } + + return users, nil +} + +func (c *SystemClient) GetGroups() ([]Group, error) { + resp, err := c.sendCommand(Command{ + Action: "get_groups", + Params: nil, + }) + if err != nil { + return nil, err + } + + if !resp.Success { + return nil, fmt.Errorf(resp.Error) + } + + var groups []Group + dataStr, ok := resp.Data.(string) + if !ok { + fmt.Println("Error: Data is not a string") + return groups, nil + } + scanner := bufio.NewScanner(strings.NewReader(dataStr)) + + for scanner.Scan() { + line := scanner.Text() + + group := Group{ + GroupName: line, + } + + groups = append(groups, group) + } + + if err := scanner.Err(); err != nil { + fmt.Println("Error reading command output:", err) + return groups, nil + } + + // Marshal to JSON and print + _, err = json.MarshalIndent(groups, "", " ") + if err != nil { + fmt.Println("Error marshalling to JSON:", err) + return groups, nil + } + + return groups, nil +} + +func (c *SystemClient) GetUserGroups(username string) ([]Group, error) { + resp, err := c.sendCommand(Command{ + Action: "get_user_groups", + Params: map[string]interface{}{ + "username": username, + }, + }) + if err != nil { + return nil, err + } + + if !resp.Success { + return nil, fmt.Errorf(resp.Error) + } + + var groups []Group + dataStr, ok := resp.Data.([]interface{}) + if !ok { + fmt.Println("Error: Data is not a string") + return groups, nil + } + + for _, data := range dataStr { + str, ok := data.(string) + if !ok { + return nil, fmt.Errorf("data contains non-string element") + } + group := Group{ + GroupName: str, + } + + groups = append(groups, group) + } + + // Marshal to JSON and print + _, err = json.MarshalIndent(groups, "", " ") + if err != nil { + fmt.Println("Error marshalling to JSON:", err) + return groups, nil + } + + return groups, nil +} + +func (c *SystemClient) AddUserToGroups(username string, groups []string) error { + // Loop through the groups and add the user to each group + for _, group := range groups { + // Implement logic to add the user to the group + resp, err := c.sendCommand(Command{ + Action: "add_user_to_group", + Params: map[string]interface{}{ + "username": username, + "group": group, + }, + }) + if err != nil { + return err + } + + if !resp.Success { + return fmt.Errorf(resp.Error) + } + } + return nil +} + +func (c *SystemClient) RemoveUserFromGroup(username, group string) error { + resp, err := c.sendCommand(Command{ + Action: "remove_user_from_group", + Params: map[string]interface{}{ + "username": username, + "group": group, + }, + }) + if err != nil { + return err + } + + if !resp.Success { + return fmt.Errorf(resp.Error) + } + return nil +} + +func (c *SystemClient) CreateUser(username, password string, group string, isAdmin bool, shell string) error { + resp, err := c.sendCommand(Command{ + Action: "create_user", + Params: map[string]interface{}{ + "username": username, + "password": password, + "group": group, + "is_admin": isAdmin, + "shell": shell, + }, + }) + if err != nil { + return err + } + + if !resp.Success { + return fmt.Errorf(resp.Error) + } + + return nil +} + +func (c *SystemClient) ModifyUserPasswd(username string, password string) error { + resp, err := c.sendCommand(Command{ + Action: "modify_user_passwd", + Params: map[string]interface{}{ + "username": username, + "password": password, + }, + }) + if err != nil { + return err + } + + if !resp.Success { + return fmt.Errorf(resp.Error) + } + + return nil +} + +func (c *SystemClient) DeleteUser(username string) error { + resp, err := c.sendCommand(Command{ + Action: "delete_user", + Params: map[string]interface{}{ + "username": username, + }, + }) + if err != nil { + return err + } + + if !resp.Success { + return fmt.Errorf(resp.Error) + } + + return nil +} + +func (c *SystemClient) GetSambaConnections() ([]SambaConnection, error) { + resp, err := c.sendCommand(Command{ + Action: "get_samba_status", + Params: nil, + }) + if err != nil { + return nil, err + } + + if !resp.Success { + return nil, fmt.Errorf(resp.Error) + } + + var connections []SambaConnection + data, err := json.Marshal(resp.Data) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(data, &connections); err != nil { + return nil, err + } + + return connections, nil +} + +func (c *SystemClient) GetSambaGlobalSetting() (string, error) { + resp, err := c.sendCommand(Command{ + Action: "get_samba_settings", + Params: nil, + }) + if err != nil { + return "", err + } + + if !resp.Success { + return "", fmt.Errorf(resp.Error) + } + + settings_str, ok := resp.Data.(string) + if !ok { + fmt.Println("Error: Data is not a string") + return "", nil + } + + return settings_str, nil +} + +func (c *SystemClient) GetSambaSectionSetting() (string, error) { + resp, err := c.sendCommand(Command{ + Action: "get_samba_settings", + Params: nil, + }) + if err != nil { + return "", err + } + + if !resp.Success { + return "", fmt.Errorf(resp.Error) + } + + settings_str, ok := resp.Data.(string) + if !ok { + fmt.Println("Error: Data is not a string") + return "", nil + } + + return settings_str, nil +} + +func (c *SystemClient) UpdateSambaSectionSetting(fullSectionSettingsJSON string) error { + // Step 1: Get all current settings + settings, err := c.GetSambaSectionSetting() + if err != nil { + return fmt.Errorf("failed to fetch current Samba settings: %v", err) + } + + // Step 2: Parse the settings JSON string into a map + var settingsMap map[string]interface{} + if err := json.Unmarshal([]byte(settings), &settingsMap); err != nil { + return fmt.Errorf("failed to parse Samba settings JSON: %v", err) + } + + // Step 3: Parse the full new section settings JSON + var newSectionSettings map[string]interface{} + if err := json.Unmarshal([]byte(fullSectionSettingsJSON), &newSectionSettings); err != nil { + return fmt.Errorf("failed to parse new section settings JSON: %v", err) + } + + // Step 4: Replace the entire "sections" field + settingsMap["sections"] = newSectionSettings + + // Step 5: Convert updated settings back to JSON + updatedSettings, err := json.Marshal(settingsMap) + if err != nil { + return fmt.Errorf("failed to marshal updated settings to JSON: %v", err) + } + + // Step 6: Send the updated settings using `update_section_setting` + resp, err := c.sendCommand(Command{ + Action: "update_section_setting", + Params: map[string]interface{}{"config": string(updatedSettings)}, + }) + if err != nil { + return fmt.Errorf("failed to send updated settings: %v", err) + } + + if !resp.Success { + return fmt.Errorf("failed to update section settings: %v", resp.Error) + } + + return nil +} + +func (c *SystemClient) UpdateSambaGlobalSetting(fullGlobalSettingsJSON string) error { + // Step 1: Get all current settings + settings, err := c.GetSambaGlobalSetting() + if err != nil { + return fmt.Errorf("failed to fetch current Samba settings: %v", err) + } + + // Step 2: Parse the settings JSON string into a map + var settingsMap map[string]interface{} + if err := json.Unmarshal([]byte(settings), &settingsMap); err != nil { + return fmt.Errorf("failed to parse Samba settings JSON: %v", err) + } + + // Step 3: Parse the full new global settings JSON + var newGlobalSettings map[string]interface{} + if err := json.Unmarshal([]byte(fullGlobalSettingsJSON), &newGlobalSettings); err != nil { + return fmt.Errorf("failed to parse new global settings JSON: %v", err) + } + + // Step 4: Replace the entire "global" field + settingsMap["global"] = newGlobalSettings + + // Step 5: Convert updated settings back to JSON + updatedSettings, err := json.Marshal(settingsMap) + if err != nil { + return fmt.Errorf("failed to marshal updated settings to JSON: %v", err) + } + + // Step 6: Send the updated settings using `update_section_setting` + resp, err := c.sendCommand(Command{ + Action: "update_section_setting", + Params: map[string]interface{}{"config": string(updatedSettings)}, + }) + if err != nil { + return fmt.Errorf("failed to send updated settings: %v", err) + } + + if !resp.Success { + return fmt.Errorf("failed to update global settings: %v", resp.Error) + } + + return nil +} + +func (c *SystemClient) GetSambaServiceInfo() (string, error) { + resp, err := c.sendCommand(Command{ + Action: "get_samba_service_info", + Params: nil, + }) + if err != nil { + return "", err + } + + if !resp.Success { + return "", fmt.Errorf(resp.Error) + } + dataStr, ok := resp.Data.(string) + if !ok { + fmt.Println("Error: Data is not a string") + return "", nil + } + return dataStr, nil +} + +func (c *SystemClient) RestartSambaService() error { + resp, err := c.sendCommand(Command{ + Action: "restart_samba_service", + Params: nil, + }) + if err != nil { + return err + } + + if !resp.Success { + return fmt.Errorf(resp.Error) + } + + return nil +}