work
This commit is contained in:
parent
ea2bb235a2
commit
e8d01e7f44
@ -33,4 +33,6 @@ func SetupRouter(r *gin.Engine, authMiddleware gin.HandlerFunc) {
|
|||||||
|
|
||||||
InitAuthEndpoints(private)
|
InitAuthEndpoints(private)
|
||||||
InitUsersEndpoints(private)
|
InitUsersEndpoints(private)
|
||||||
|
InitSystemsEndpoints(private)
|
||||||
|
InitKeysEndpoints(private)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package apis
|
package apis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"clortho/lib/db"
|
||||||
"clortho/lib/users"
|
"clortho/lib/users"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -36,3 +37,25 @@ func LoggedInMiddleware() gin.HandlerFunc {
|
|||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AdminMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// First ensure user is logged in
|
||||||
|
sessionInterface, hasSession := c.Get("session")
|
||||||
|
if !hasSession {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is admin
|
||||||
|
session, ok := sessionInterface.(*db.UserSession)
|
||||||
|
if !ok || session.User == nil || !session.User.Admin {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "admin access required"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
78
lib/apis/auth_middleware_test.go
Normal file
78
lib/apis/auth_middleware_test.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"clortho/lib/db"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdminMiddleware(t *testing.T) {
|
||||||
|
// Create an admin user
|
||||||
|
adminUser, err := InitUser("admin_test", "password")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
adminUser.Admin = true
|
||||||
|
db.Connection.Save(&adminUser)
|
||||||
|
|
||||||
|
// Create a non-admin user
|
||||||
|
regularUser, err := InitUser("regular_test", "password")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
regularUser.Admin = false
|
||||||
|
db.Connection.Save(®ularUser)
|
||||||
|
|
||||||
|
// Test with admin user
|
||||||
|
t.Run("Admin user should have access", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
router.Use(AdminMiddleware())
|
||||||
|
router.GET("/test", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/test", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "success")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with non-admin user
|
||||||
|
t.Run("Non-admin user should be forbidden", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*regularUser))
|
||||||
|
router.Use(AdminMiddleware())
|
||||||
|
router.GET("/test", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/test", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "admin access required")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with no session
|
||||||
|
t.Run("No session should be unauthorized", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(AdminMiddleware())
|
||||||
|
router.GET("/test", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/test", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "unauthorized")
|
||||||
|
})
|
||||||
|
}
|
||||||
215
lib/apis/keys_endpoints.go
Normal file
215
lib/apis/keys_endpoints.go
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"clortho/lib/db"
|
||||||
|
"clortho/lib/keys"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type keyRequest struct {
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
UserID uint `json:"userId" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitKeysEndpoints(r *gin.RouterGroup) {
|
||||||
|
group := r.Group("/keys")
|
||||||
|
group.Use(LoggedInMiddleware())
|
||||||
|
group.GET("/", getKeys)
|
||||||
|
group.GET("/:id", getKey)
|
||||||
|
group.POST("/", AdminMiddleware(), createKey)
|
||||||
|
group.PUT("/:id", AdminMiddleware(), updateKey)
|
||||||
|
group.DELETE("/:id", AdminMiddleware(), deleteKey)
|
||||||
|
|
||||||
|
// Endpoints for user keys
|
||||||
|
r.GET("/users/:userId/keys", getUserKeys)
|
||||||
|
r.POST("/users/:userId/keys", AdminMiddleware(), createUserKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyOwnerOrAdminMiddleware checks if the current user is the owner of the key or an admin
|
||||||
|
func KeyOwnerOrAdminMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Get the current user from the session
|
||||||
|
sessionInterface, _ := c.Get("session")
|
||||||
|
session := sessionInterface.(*db.UserSession)
|
||||||
|
userID := session.User.ID
|
||||||
|
|
||||||
|
// Get the key ID from the URL parameter
|
||||||
|
keyID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid key ID"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user can access the key
|
||||||
|
canAccess, err := keys.CanAccessKey(userID, uint(keyID))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Key not found"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canAccess {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "You don't have permission to access this key"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeys(c *gin.Context) {
|
||||||
|
// Get the current user from the session
|
||||||
|
sessionInterface, _ := c.Get("session")
|
||||||
|
session := sessionInterface.(*db.UserSession)
|
||||||
|
|
||||||
|
// If the user is an admin, return all keys
|
||||||
|
if session.User.Admin {
|
||||||
|
keyList := keys.GetKeys()
|
||||||
|
c.JSON(http.StatusOK, keyList)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, return only the user's keys
|
||||||
|
keyList := keys.GetKeysByUser(session.User.ID)
|
||||||
|
c.JSON(http.StatusOK, keyList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserKeys(c *gin.Context) {
|
||||||
|
// Get the current user from the session
|
||||||
|
sessionInterface, _ := c.Get("session")
|
||||||
|
session := sessionInterface.(*db.UserSession)
|
||||||
|
|
||||||
|
// Get the user ID from the URL parameter
|
||||||
|
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is not an admin and not the owner, return forbidden
|
||||||
|
if !session.User.Admin && session.User.ID != uint(userID) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "You don't have permission to access these keys"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the keys for the specified user
|
||||||
|
keyList := keys.GetKeysByUser(uint(userID))
|
||||||
|
c.JSON(http.StatusOK, keyList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKey(c *gin.Context) {
|
||||||
|
// Use the KeyOwnerOrAdminMiddleware to check access
|
||||||
|
KeyOwnerOrAdminMiddleware()(c)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the key ID from the URL parameter
|
||||||
|
keyID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
|
||||||
|
// Get the key
|
||||||
|
key, err := keys.GetKey(uint(keyID))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Key not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createKey(c *gin.Context) {
|
||||||
|
var req keyRequest
|
||||||
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the key
|
||||||
|
key, err := keys.CreateKey(req.UserID, req.Name, req.Content)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateKey(c *gin.Context) {
|
||||||
|
// Use the KeyOwnerOrAdminMiddleware to check access
|
||||||
|
KeyOwnerOrAdminMiddleware()(c)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the key ID from the URL parameter
|
||||||
|
keyID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
|
||||||
|
var req keyRequest
|
||||||
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the key
|
||||||
|
key, err := keys.UpdateKey(uint(keyID), req.Name, req.Content)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteKey(c *gin.Context) {
|
||||||
|
// Use the KeyOwnerOrAdminMiddleware to check access
|
||||||
|
KeyOwnerOrAdminMiddleware()(c)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the key ID from the URL parameter
|
||||||
|
keyID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
|
||||||
|
// Delete the key
|
||||||
|
err := keys.DeleteKey(uint(keyID))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Key deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// createUserKey creates a new key for a specific user
|
||||||
|
func createUserKey(c *gin.Context) {
|
||||||
|
// Get the user ID from the URL parameter
|
||||||
|
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the request body
|
||||||
|
var req struct {
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the key
|
||||||
|
key, err := keys.CreateKey(uint(userID), req.Name, req.Content)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, key)
|
||||||
|
}
|
||||||
433
lib/apis/keys_endpoints_test.go
Normal file
433
lib/apis/keys_endpoints_test.go
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"clortho/lib/db"
|
||||||
|
"clortho/lib/users"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func setupRouter() *gin.Engine {
|
||||||
|
r := gin.Default()
|
||||||
|
private := r.Group("/gui")
|
||||||
|
InitKeysEndpoints(private)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestUser(admin bool) *db.User {
|
||||||
|
username := "testuser"
|
||||||
|
if admin {
|
||||||
|
username = "admin"
|
||||||
|
}
|
||||||
|
displayName := username
|
||||||
|
user := db.User{
|
||||||
|
Username: username,
|
||||||
|
DisplayName: &displayName,
|
||||||
|
Admin: admin,
|
||||||
|
}
|
||||||
|
db.Connection.Create(&user)
|
||||||
|
return &user
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestKey(userID uint) *db.Key {
|
||||||
|
key := db.Key{
|
||||||
|
UserID: userID,
|
||||||
|
Content: "test-key-content",
|
||||||
|
}
|
||||||
|
db.Connection.Create(&key)
|
||||||
|
return &key
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestSession(user *db.User) *db.UserSession {
|
||||||
|
session := users.NewSession(*user)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetKeys(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUser(true)
|
||||||
|
adminSession := createTestSession(adminUser)
|
||||||
|
|
||||||
|
// Create regular user and session
|
||||||
|
regularUser := createTestUser(false)
|
||||||
|
regularSession := createTestSession(regularUser)
|
||||||
|
|
||||||
|
// Create keys for both users
|
||||||
|
_ = createTestKey(adminUser.ID) // Create a key for admin to test that admin can see all keys
|
||||||
|
regularKey := createTestKey(regularUser.ID)
|
||||||
|
|
||||||
|
// Test admin user can get all keys
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/gui/keys/", nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
|
|
||||||
|
// Set admin session in context
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("GET", "/gui/keys/", nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var keys []db.Key
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &keys)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, keys, 2)
|
||||||
|
|
||||||
|
// Test regular user can only get their own keys
|
||||||
|
router = setupRouter()
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", regularSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("GET", "/gui/keys/", nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
err = json.Unmarshal(w.Body.Bytes(), &keys)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, keys, 1)
|
||||||
|
assert.Equal(t, regularKey.ID, keys[0].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUser(true)
|
||||||
|
adminSession := createTestSession(adminUser)
|
||||||
|
|
||||||
|
// Create regular user and session
|
||||||
|
regularUser := createTestUser(false)
|
||||||
|
regularSession := createTestSession(regularUser)
|
||||||
|
|
||||||
|
// Create keys for both users
|
||||||
|
adminKey := createTestKey(adminUser.ID)
|
||||||
|
regularKey := createTestKey(regularUser.ID)
|
||||||
|
|
||||||
|
// Test admin user can get any key
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", fmt.Sprintf("/gui/keys/%d", regularKey.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var key db.Key
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, regularKey.ID, key.ID)
|
||||||
|
|
||||||
|
// Test regular user can only get their own key
|
||||||
|
router = setupRouter()
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", regularSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Regular user can get their own key
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("GET", fmt.Sprintf("/gui/keys/%d", regularKey.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
// Regular user cannot get admin's key
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("GET", fmt.Sprintf("/gui/keys/%d", adminKey.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUser(true)
|
||||||
|
adminSession := createTestSession(adminUser)
|
||||||
|
|
||||||
|
// Create regular user and session
|
||||||
|
regularUser := createTestUser(false)
|
||||||
|
regularSession := createTestSession(regularUser)
|
||||||
|
|
||||||
|
// Test admin user can create a key
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
keyRequest := keyRequest{
|
||||||
|
UserID: regularUser.ID,
|
||||||
|
Content: "new-key-content",
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody, _ := json.Marshal(keyRequest)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/gui/keys/", bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusCreated, w.Code)
|
||||||
|
|
||||||
|
var key db.Key
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, regularUser.ID, key.UserID)
|
||||||
|
assert.Equal(t, "new-key-content", key.Content)
|
||||||
|
|
||||||
|
// Test regular user cannot create a key (only admins can)
|
||||||
|
router = setupRouter()
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", regularSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("POST", "/gui/keys/", bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUser(true)
|
||||||
|
adminSession := createTestSession(adminUser)
|
||||||
|
|
||||||
|
// Create regular user and session
|
||||||
|
regularUser := createTestUser(false)
|
||||||
|
regularSession := createTestSession(regularUser)
|
||||||
|
|
||||||
|
// Create keys for both users
|
||||||
|
_ = createTestKey(adminUser.ID) // Create a key for admin but we'll only test updating the regular user's key
|
||||||
|
regularKey := createTestKey(regularUser.ID)
|
||||||
|
|
||||||
|
// Test admin user can update any key
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
keyRequest := keyRequest{
|
||||||
|
UserID: regularUser.ID,
|
||||||
|
Content: "updated-key-content",
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody, _ := json.Marshal(keyRequest)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/gui/keys/%d", regularKey.ID), bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var key db.Key
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, regularKey.ID, key.ID)
|
||||||
|
assert.Equal(t, "updated-key-content", key.Content)
|
||||||
|
|
||||||
|
// Test regular user cannot update a key (only admins can)
|
||||||
|
router = setupRouter()
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", regularSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("PUT", fmt.Sprintf("/gui/keys/%d", regularKey.ID), bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUser(true)
|
||||||
|
adminSession := createTestSession(adminUser)
|
||||||
|
|
||||||
|
// Create regular user and session
|
||||||
|
regularUser := createTestUser(false)
|
||||||
|
regularSession := createTestSession(regularUser)
|
||||||
|
|
||||||
|
// Create keys for both users
|
||||||
|
adminKey := createTestKey(adminUser.ID)
|
||||||
|
regularKey := createTestKey(regularUser.ID)
|
||||||
|
|
||||||
|
// Test admin user can delete any key
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/gui/keys/%d", regularKey.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
// Test regular user cannot delete a key (only admins can)
|
||||||
|
router = setupRouter()
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", regularSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("DELETE", fmt.Sprintf("/gui/keys/%d", adminKey.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserKeys(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUser(true)
|
||||||
|
adminSession := createTestSession(adminUser)
|
||||||
|
|
||||||
|
// Create regular user and session
|
||||||
|
regularUser := createTestUser(false)
|
||||||
|
regularSession := createTestSession(regularUser)
|
||||||
|
|
||||||
|
// Create keys for both users
|
||||||
|
_ = createTestKey(adminUser.ID) // Create a key for admin to test that regular users can't access admin keys
|
||||||
|
regularKey := createTestKey(regularUser.ID)
|
||||||
|
|
||||||
|
// Test admin user can get keys for any user
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", fmt.Sprintf("/gui/users/%d/keys", regularUser.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var keys []db.Key
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &keys)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, keys, 1)
|
||||||
|
assert.Equal(t, regularKey.ID, keys[0].ID)
|
||||||
|
|
||||||
|
// Test regular user can only get their own keys
|
||||||
|
router = setupRouter()
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", regularSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Regular user can get their own keys
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("GET", fmt.Sprintf("/gui/users/%d/keys", regularUser.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
// Regular user cannot get admin's keys
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("GET", fmt.Sprintf("/gui/users/%d/keys", adminUser.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateUserKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUser(true)
|
||||||
|
adminSession := createTestSession(adminUser)
|
||||||
|
|
||||||
|
// Create regular user and session
|
||||||
|
regularUser := createTestUser(false)
|
||||||
|
regularSession := createTestSession(regularUser)
|
||||||
|
|
||||||
|
// Test admin user can create a key for any user
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
keyContent := "new-user-key-content"
|
||||||
|
requestBody, _ := json.Marshal(map[string]string{"content": keyContent})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", fmt.Sprintf("/gui/users/%d/keys", regularUser.ID), bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusCreated, w.Code)
|
||||||
|
|
||||||
|
var key db.Key
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, regularUser.ID, key.UserID)
|
||||||
|
assert.Equal(t, keyContent, key.Content)
|
||||||
|
|
||||||
|
// Test regular user cannot create a key (only admins can)
|
||||||
|
router = setupRouter()
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", regularSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("POST", fmt.Sprintf("/gui/users/%d/keys", regularUser.ID), bytes.NewBuffer(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
@ -1,18 +1,98 @@
|
|||||||
package apis
|
package apis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"clortho/lib/users"
|
"clortho/lib/systems"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitSystemsEndpoints(r *gin.RouterGroup) {
|
type systemRequest struct {
|
||||||
group := r.Group("/users")
|
Name string `json:"name" binding:"required"`
|
||||||
group.Use(LoggedInMiddleware())
|
|
||||||
group.GET("/", getUsers)
|
|
||||||
group.GET("/me", getMe)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServers(c *gin.Context) {
|
func InitSystemsEndpoints(r *gin.RouterGroup) {
|
||||||
userList := users.GetUsers()
|
group := r.Group("/systems")
|
||||||
c.JSON(200, &userList)
|
group.Use(LoggedInMiddleware())
|
||||||
|
group.Use(AdminMiddleware())
|
||||||
|
group.GET("/", getSystems)
|
||||||
|
group.GET("/:id", getSystem)
|
||||||
|
group.POST("/", createSystem)
|
||||||
|
group.PUT("/:id", updateSystem)
|
||||||
|
group.DELETE("/:id", deleteSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystems(c *gin.Context) {
|
||||||
|
systemList := systems.GetSystems()
|
||||||
|
c.JSON(http.StatusOK, &systemList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystem(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
system, err := systems.GetSystem(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "System not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, system)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSystem(c *gin.Context) {
|
||||||
|
var req systemRequest
|
||||||
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
system, err := systems.CreateSystem(req.Name)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, system)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSystem(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req systemRequest
|
||||||
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
system, err := systems.UpdateSystem(uint(id), req.Name)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, system)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteSystem(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = systems.DeleteSystem(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "System deleted successfully"})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
package apis
|
package apis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"clortho/lib/db"
|
||||||
|
"clortho/lib/systems"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -10,23 +13,401 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInitSystemsEndpoints_getSystems(t *testing.T) {
|
func setupSystemsTest(t *testing.T) (*db.User, *db.User) {
|
||||||
_, err := InitUser("admin", "password")
|
// Create an admin user
|
||||||
|
adminUser, err := InitUser("admin_test", "password")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
adminUser.Admin = true
|
||||||
|
db.Connection.Save(&adminUser)
|
||||||
|
|
||||||
r := gin.Default()
|
// Create a non-admin user
|
||||||
SetupRouter(r, nil)
|
regularUser, err := InitUser("regular_test", "password")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
regularUser.Admin = false
|
||||||
|
db.Connection.Save(®ularUser)
|
||||||
|
|
||||||
reqBody := loginRequest{Username: "admin", Password: "password"}
|
// Clear existing systems
|
||||||
strReqBody, _ := json.Marshal(reqBody)
|
db.Connection.Exec("DELETE FROM systems")
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("POST", "/gui/auth/signin", strings.NewReader(string(strReqBody)))
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, 200, w.Code)
|
return adminUser, regularUser
|
||||||
assert.JSONEq(t, `{"valid": true}`, w.Body.String())
|
}
|
||||||
setCookie := w.Header().Get("Set-Cookie")
|
|
||||||
assert.True(t, strings.Contains(setCookie, "CLORTHO_AUTH="))
|
func TestGetSystems(t *testing.T) {
|
||||||
|
adminUser, regularUser := setupSystemsTest(t)
|
||||||
|
|
||||||
|
// Create test systems
|
||||||
|
system1, _ := systems.CreateSystem("System 1")
|
||||||
|
system2, _ := systems.CreateSystem("System 2")
|
||||||
|
|
||||||
|
// Test with admin user
|
||||||
|
t.Run("Admin user can get systems", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/gui/systems/", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var systems []db.System
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &systems)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, systems, 2)
|
||||||
|
|
||||||
|
// Verify systems are returned correctly
|
||||||
|
foundSystem1 := false
|
||||||
|
foundSystem2 := false
|
||||||
|
for _, s := range systems {
|
||||||
|
if s.ID == system1.ID {
|
||||||
|
assert.Equal(t, "System 1", s.Name)
|
||||||
|
foundSystem1 = true
|
||||||
|
}
|
||||||
|
if s.ID == system2.ID {
|
||||||
|
assert.Equal(t, "System 2", s.Name)
|
||||||
|
foundSystem2 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, foundSystem1)
|
||||||
|
assert.True(t, foundSystem2)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with non-admin user
|
||||||
|
t.Run("Non-admin user cannot get systems", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*regularUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/gui/systems/", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "admin access required")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with no session
|
||||||
|
t.Run("Unauthenticated user cannot get systems", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/gui/systems/", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "unauthorized")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSystem(t *testing.T) {
|
||||||
|
adminUser, regularUser := setupSystemsTest(t)
|
||||||
|
|
||||||
|
// Create a test system
|
||||||
|
system, _ := systems.CreateSystem("Test System")
|
||||||
|
|
||||||
|
// Test with admin user
|
||||||
|
t.Run("Admin user can get a system", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", fmt.Sprintf("/gui/systems/%d", system.ID), nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var returnedSystem db.System
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &returnedSystem)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, system.ID, returnedSystem.ID)
|
||||||
|
assert.Equal(t, "Test System", returnedSystem.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with non-admin user
|
||||||
|
t.Run("Non-admin user cannot get a system", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*regularUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", fmt.Sprintf("/gui/systems/%d", system.ID), nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "admin access required")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with invalid ID
|
||||||
|
t.Run("Invalid ID returns bad request", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/gui/systems/invalid", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "Invalid ID format")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with non-existent ID
|
||||||
|
t.Run("Non-existent ID returns not found", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/gui/systems/9999", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "System not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSystem(t *testing.T) {
|
||||||
|
adminUser, regularUser := setupSystemsTest(t)
|
||||||
|
|
||||||
|
// Test with admin user
|
||||||
|
t.Run("Admin user can create a system", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
reqBody := systemRequest{Name: "New System"}
|
||||||
|
strReqBody, _ := json.Marshal(reqBody)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/gui/systems/", strings.NewReader(string(strReqBody)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusCreated, w.Code)
|
||||||
|
|
||||||
|
var createdSystem db.System
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &createdSystem)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotZero(t, createdSystem.ID)
|
||||||
|
assert.Equal(t, "New System", createdSystem.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with non-admin user
|
||||||
|
t.Run("Non-admin user cannot create a system", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*regularUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
reqBody := systemRequest{Name: "New System"}
|
||||||
|
strReqBody, _ := json.Marshal(reqBody)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/gui/systems/", strings.NewReader(string(strReqBody)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "admin access required")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with invalid request body
|
||||||
|
t.Run("Invalid request body returns bad request", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
reqBody := `{"invalid": "json"`
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/gui/systems/", strings.NewReader(reqBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with missing required field
|
||||||
|
t.Run("Missing required field returns bad request", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
reqBody := `{}`
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/gui/systems/", strings.NewReader(reqBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSystem(t *testing.T) {
|
||||||
|
adminUser, regularUser := setupSystemsTest(t)
|
||||||
|
|
||||||
|
// Create a test system
|
||||||
|
system, _ := systems.CreateSystem("Test System")
|
||||||
|
|
||||||
|
// Test with admin user
|
||||||
|
t.Run("Admin user can update a system", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
reqBody := systemRequest{Name: "Updated System"}
|
||||||
|
strReqBody, _ := json.Marshal(reqBody)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/gui/systems/%d", system.ID), strings.NewReader(string(strReqBody)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var updatedSystem db.System
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &updatedSystem)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, system.ID, updatedSystem.ID)
|
||||||
|
assert.Equal(t, "Updated System", updatedSystem.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with non-admin user
|
||||||
|
t.Run("Non-admin user cannot update a system", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*regularUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
reqBody := systemRequest{Name: "Updated System"}
|
||||||
|
strReqBody, _ := json.Marshal(reqBody)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/gui/systems/%d", system.ID), strings.NewReader(string(strReqBody)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "admin access required")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with invalid ID
|
||||||
|
t.Run("Invalid ID returns bad request", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
reqBody := systemRequest{Name: "Updated System"}
|
||||||
|
strReqBody, _ := json.Marshal(reqBody)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("PUT", "/gui/systems/invalid", strings.NewReader(string(strReqBody)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "Invalid ID format")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with invalid request body
|
||||||
|
t.Run("Invalid request body returns bad request", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
reqBody := `{"invalid": "json"`
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/gui/systems/%d", system.ID), strings.NewReader(reqBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSystem(t *testing.T) {
|
||||||
|
adminUser, regularUser := setupSystemsTest(t)
|
||||||
|
|
||||||
|
// Test with admin user
|
||||||
|
t.Run("Admin user can delete a system", func(t *testing.T) {
|
||||||
|
// Create a system to delete
|
||||||
|
system, _ := systems.CreateSystem("System to Delete")
|
||||||
|
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/gui/systems/%d", system.ID), nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "System deleted successfully")
|
||||||
|
|
||||||
|
// Verify the system is deleted
|
||||||
|
deletedSystem, err := systems.GetSystem(system.ID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, deletedSystem)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with non-admin user
|
||||||
|
t.Run("Non-admin user cannot delete a system", func(t *testing.T) {
|
||||||
|
// Create a system to delete
|
||||||
|
system, _ := systems.CreateSystem("System to Not Delete")
|
||||||
|
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*regularUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/gui/systems/%d", system.ID), nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "admin access required")
|
||||||
|
|
||||||
|
// Verify the system is not deleted
|
||||||
|
existingSystem, err := systems.GetSystem(system.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, existingSystem)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with invalid ID
|
||||||
|
t.Run("Invalid ID returns bad request", func(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(MockAuthMiddleware(*adminUser))
|
||||||
|
group := router.Group("/gui")
|
||||||
|
InitSystemsEndpoints(group)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("DELETE", "/gui/systems/invalid", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "Invalid ID format")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,149 @@
|
|||||||
package apis
|
package apis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"clortho/lib/db"
|
||||||
"clortho/lib/users"
|
"clortho/lib/users"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type userRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Admin bool `json:"admin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type userUpdateRequest struct {
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Admin bool `json:"admin"`
|
||||||
|
}
|
||||||
|
|
||||||
func InitUsersEndpoints(r *gin.RouterGroup) {
|
func InitUsersEndpoints(r *gin.RouterGroup) {
|
||||||
group := r.Group("/users")
|
group := r.Group("/users")
|
||||||
group.Use(LoggedInMiddleware())
|
group.Use(LoggedInMiddleware())
|
||||||
group.GET("/", getUsers)
|
group.GET("/", getUsers)
|
||||||
group.GET("/me", getMe)
|
group.GET("/me", getMe)
|
||||||
|
group.GET("/:userId", getUser)
|
||||||
|
group.POST("/", AdminMiddleware(), createUser)
|
||||||
|
group.PUT("/:userId", AdminMiddleware(), updateUser)
|
||||||
|
group.DELETE("/:userId", AdminMiddleware(), deleteUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUsers(c *gin.Context) {
|
func getUsers(c *gin.Context) {
|
||||||
userList := users.GetUsers()
|
userList := users.GetUsers()
|
||||||
c.JSON(200, &userList)
|
c.JSON(http.StatusOK, &userList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUser(c *gin.Context) {
|
||||||
|
// Get the user ID from the URL parameter
|
||||||
|
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user
|
||||||
|
user, err := users.GetUserByID(uint(userID))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUser(c *gin.Context) {
|
||||||
|
var req userRequest
|
||||||
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the user
|
||||||
|
user, err := users.CreateUser(req.Username)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the user's display name and admin status
|
||||||
|
if req.DisplayName != "" || req.Admin {
|
||||||
|
displayName := req.DisplayName
|
||||||
|
if displayName == "" && user.DisplayName != nil {
|
||||||
|
displayName = *user.DisplayName
|
||||||
|
}
|
||||||
|
user, err = users.UpdateUser(user.ID, displayName, req.Admin)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUser(c *gin.Context) {
|
||||||
|
// Get the user ID from the URL parameter
|
||||||
|
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req userUpdateRequest
|
||||||
|
if err := c.BindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current user to preserve existing values if not provided
|
||||||
|
currentUser, err := users.GetUserByID(uint(userID))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing display name if not provided
|
||||||
|
displayName := req.DisplayName
|
||||||
|
if displayName == "" && currentUser.DisplayName != nil {
|
||||||
|
displayName = *currentUser.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the user
|
||||||
|
user, err := users.UpdateUser(uint(userID), displayName, req.Admin)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteUser(c *gin.Context) {
|
||||||
|
// Get the user ID from the URL parameter
|
||||||
|
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current session
|
||||||
|
sessionInterface, _ := c.Get("session")
|
||||||
|
session := sessionInterface.(*db.UserSession)
|
||||||
|
|
||||||
|
// Prevent users from deleting themselves
|
||||||
|
if session.User.ID == uint(userID) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete your own account"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the user
|
||||||
|
err = users.DeleteUser(uint(userID))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
|
||||||
}
|
}
|
||||||
|
|||||||
239
lib/apis/users_endpoints_test.go
Normal file
239
lib/apis/users_endpoints_test.go
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"clortho/lib/db"
|
||||||
|
"clortho/lib/users"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupUsersRouter() *gin.Engine {
|
||||||
|
r := gin.Default()
|
||||||
|
private := r.Group("/gui")
|
||||||
|
InitUsersEndpoints(private)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestUserForEndpoint(admin bool) *db.User {
|
||||||
|
username := fmt.Sprintf("testuser_%d", db.Connection.RowsAffected)
|
||||||
|
if admin {
|
||||||
|
username = fmt.Sprintf("admin_%d", db.Connection.RowsAffected)
|
||||||
|
}
|
||||||
|
displayName := username
|
||||||
|
user := db.User{
|
||||||
|
Username: username,
|
||||||
|
DisplayName: &displayName,
|
||||||
|
Admin: admin,
|
||||||
|
}
|
||||||
|
db.Connection.Create(&user)
|
||||||
|
return &user
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUsers(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupUsersRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUserForEndpoint(true)
|
||||||
|
adminSession := users.NewSession(*adminUser)
|
||||||
|
|
||||||
|
// Set admin session in context
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test getting all users
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/gui/users/", nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var userList []db.User
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &userList)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, userList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUser(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupUsersRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUserForEndpoint(true)
|
||||||
|
adminSession := users.NewSession(*adminUser)
|
||||||
|
|
||||||
|
// Set admin session in context
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test getting a specific user
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", fmt.Sprintf("/gui/users/%d", adminUser.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var user db.User
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &user)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, adminUser.ID, user.ID)
|
||||||
|
assert.Equal(t, adminUser.Username, user.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateUser(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupUsersRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUserForEndpoint(true)
|
||||||
|
adminSession := users.NewSession(*adminUser)
|
||||||
|
|
||||||
|
// Set admin session in context
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test creating a new user
|
||||||
|
userReq := userRequest{
|
||||||
|
Username: "newuser",
|
||||||
|
DisplayName: "New User",
|
||||||
|
Admin: false,
|
||||||
|
}
|
||||||
|
reqBody, _ := json.Marshal(userReq)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/gui/users/", bytes.NewBuffer(reqBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusCreated, w.Code)
|
||||||
|
|
||||||
|
var user db.User
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &user)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, userReq.Username, user.Username)
|
||||||
|
assert.Equal(t, userReq.DisplayName, *user.DisplayName)
|
||||||
|
assert.Equal(t, userReq.Admin, user.Admin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateUser(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupUsersRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUserForEndpoint(true)
|
||||||
|
adminSession := users.NewSession(*adminUser)
|
||||||
|
|
||||||
|
// Create a regular user to update
|
||||||
|
regularUser := createTestUserForEndpoint(false)
|
||||||
|
|
||||||
|
// Set admin session in context
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test updating a user
|
||||||
|
updateReq := userUpdateRequest{
|
||||||
|
DisplayName: "Updated User",
|
||||||
|
Admin: true,
|
||||||
|
}
|
||||||
|
reqBody, _ := json.Marshal(updateReq)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/gui/users/%d", regularUser.ID), bytes.NewBuffer(reqBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var user db.User
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &user)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, regularUser.ID, user.ID)
|
||||||
|
assert.Equal(t, regularUser.Username, user.Username)
|
||||||
|
assert.Equal(t, updateReq.DisplayName, *user.DisplayName)
|
||||||
|
assert.Equal(t, updateReq.Admin, user.Admin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteUser(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupUsersRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUserForEndpoint(true)
|
||||||
|
adminSession := users.NewSession(*adminUser)
|
||||||
|
|
||||||
|
// Create a regular user to delete
|
||||||
|
regularUser := createTestUserForEndpoint(false)
|
||||||
|
|
||||||
|
// Set admin session in context
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test deleting a user
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/gui/users/%d", regularUser.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
// Verify the user was deleted
|
||||||
|
var response map[string]string
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "User deleted successfully", response["message"])
|
||||||
|
|
||||||
|
// Try to get the deleted user
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("GET", fmt.Sprintf("/gui/users/%d", regularUser.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSelf(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
router := setupUsersRouter()
|
||||||
|
|
||||||
|
// Create admin user and session
|
||||||
|
adminUser := createTestUserForEndpoint(true)
|
||||||
|
adminSession := users.NewSession(*adminUser)
|
||||||
|
|
||||||
|
// Set admin session in context
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("session", adminSession)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test deleting self
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/gui/users/%d", adminUser.ID), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
|
||||||
|
// Verify the error message
|
||||||
|
var response map[string]string
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Cannot delete your own account", response["error"])
|
||||||
|
}
|
||||||
@ -35,6 +35,7 @@ type Key struct {
|
|||||||
|
|
||||||
UserID uint `gorm:"not null" json:"-"`
|
UserID uint `gorm:"not null" json:"-"`
|
||||||
User *User `gorm:"foreignKey:UserID" json:"user"`
|
User *User `gorm:"foreignKey:UserID" json:"user"`
|
||||||
|
Name string `json:"name"`
|
||||||
Content string `gorm:"not null" json:"-" json:"content"`
|
Content string `gorm:"not null" json:"-" json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
94
lib/keys/keys.go
Normal file
94
lib/keys/keys.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"clortho/lib/db"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetKeys retrieves all keys
|
||||||
|
func GetKeys() []db.Key {
|
||||||
|
var keys []db.Key
|
||||||
|
db.Connection.Preload("User").Find(&keys)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeysByUser retrieves all keys for a specific user
|
||||||
|
func GetKeysByUser(userID uint) []db.Key {
|
||||||
|
var keys []db.Key
|
||||||
|
db.Connection.Preload("User").Where("user_id = ?", userID).Find(&keys)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKey retrieves a specific key by ID
|
||||||
|
func GetKey(id uint) (*db.Key, error) {
|
||||||
|
var key db.Key
|
||||||
|
result := db.Connection.Preload("User").First(&key, id)
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return nil, errors.New("key not found")
|
||||||
|
}
|
||||||
|
return &key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateKey creates a new key for a user
|
||||||
|
func CreateKey(userID uint, name string, content string) (*db.Key, error) {
|
||||||
|
key := db.Key{
|
||||||
|
UserID: userID,
|
||||||
|
Name: name,
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
result := db.Connection.Create(&key)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the key to get the User relationship
|
||||||
|
db.Connection.Preload("User").First(&key, key.ID)
|
||||||
|
return &key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateKey updates an existing key
|
||||||
|
func UpdateKey(id uint, name string, content string) (*db.Key, error) {
|
||||||
|
key, err := GetKey(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key.Name = name
|
||||||
|
key.Content = content
|
||||||
|
result := db.Connection.Save(key)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteKey deletes a key
|
||||||
|
func DeleteKey(id uint) error {
|
||||||
|
result := db.Connection.Delete(&db.Key{}, id)
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return errors.New("key not found")
|
||||||
|
}
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanAccessKey checks if a user can access a key (admin or owner)
|
||||||
|
func CanAccessKey(userID uint, keyID uint) (bool, error) {
|
||||||
|
key, err := GetKey(keyID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is the owner of the key
|
||||||
|
if key.UserID == userID {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is an admin
|
||||||
|
var user db.User
|
||||||
|
result := db.Connection.First(&user, userID)
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return false, errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.Admin, nil
|
||||||
|
}
|
||||||
109
lib/keys/keys_test.go
Normal file
109
lib/keys/keys_test.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"clortho/lib/db"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// Global setup
|
||||||
|
fmt.Println("Setting up resources...")
|
||||||
|
|
||||||
|
db.InitDb()
|
||||||
|
|
||||||
|
exitCode := m.Run() // Run all tests
|
||||||
|
|
||||||
|
// Global teardown
|
||||||
|
fmt.Println("Cleaning up resources...")
|
||||||
|
|
||||||
|
db.ResetDb()
|
||||||
|
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetKeys(t *testing.T) {
|
||||||
|
// Create a test user
|
||||||
|
user := db.User{Username: "testuser"}
|
||||||
|
db.Connection.Create(&user)
|
||||||
|
|
||||||
|
// Create test keys
|
||||||
|
key1 := db.Key{UserID: user.ID, Content: "test-key-1"}
|
||||||
|
key2 := db.Key{UserID: user.ID, Content: "test-key-2"}
|
||||||
|
db.Connection.Create(&key1)
|
||||||
|
db.Connection.Create(&key2)
|
||||||
|
|
||||||
|
// Test GetKeys
|
||||||
|
keys := GetKeys()
|
||||||
|
if len(keys) != 2 {
|
||||||
|
t.Errorf("Expected 2 keys, got %d", len(keys))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetKeysByUser
|
||||||
|
userKeys := GetKeysByUser(user.ID)
|
||||||
|
if len(userKeys) != 2 {
|
||||||
|
t.Errorf("Expected 2 keys for user, got %d", len(userKeys))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetKey
|
||||||
|
key, err := GetKey(key1.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting key: %v", err)
|
||||||
|
}
|
||||||
|
if key.Content != "test-key-1" {
|
||||||
|
t.Errorf("Expected key content to be 'test-key-1', got '%s'", key.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test CanAccessKey
|
||||||
|
canAccess, err := CanAccessKey(user.ID, key1.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error checking access: %v", err)
|
||||||
|
}
|
||||||
|
if !canAccess {
|
||||||
|
t.Errorf("Expected user to have access to their own key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create admin user
|
||||||
|
admin := db.User{Username: "admin", Admin: true}
|
||||||
|
db.Connection.Create(&admin)
|
||||||
|
|
||||||
|
// Test admin access
|
||||||
|
canAccess, err = CanAccessKey(admin.ID, key1.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error checking access: %v", err)
|
||||||
|
}
|
||||||
|
if !canAccess {
|
||||||
|
t.Errorf("Expected admin to have access to any key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test CreateKey
|
||||||
|
newKey, err := CreateKey(user.ID, "new-key")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating key: %v", err)
|
||||||
|
}
|
||||||
|
if newKey.Content != "new-key" {
|
||||||
|
t.Errorf("Expected key content to be 'new-key', got '%s'", newKey.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test UpdateKey
|
||||||
|
updatedKey, err := UpdateKey(newKey.ID, "updated-key")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error updating key: %v", err)
|
||||||
|
}
|
||||||
|
if updatedKey.Content != "updated-key" {
|
||||||
|
t.Errorf("Expected key content to be 'updated-key', got '%s'", updatedKey.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test DeleteKey
|
||||||
|
err = DeleteKey(newKey.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error deleting key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify key was deleted
|
||||||
|
_, err = GetKey(newKey.ID)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected key to be deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,3 +16,31 @@ func GetSystems() []db.System {
|
|||||||
db.Connection.Find(&systems)
|
db.Connection.Find(&systems)
|
||||||
return systems
|
return systems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSystem(id uint) (*db.System, error) {
|
||||||
|
var system db.System
|
||||||
|
result := db.Connection.First(&system, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &system, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateSystem(id uint, name string) (*db.System, error) {
|
||||||
|
system, err := GetSystem(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
system.Name = name
|
||||||
|
result := db.Connection.Save(&system)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return system, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSystem(id uint) error {
|
||||||
|
result := db.Connection.Delete(&db.System{}, id)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
@ -22,3 +23,102 @@ func TestMain(m *testing.M) {
|
|||||||
|
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateSystem(t *testing.T) {
|
||||||
|
// Test creating a new system
|
||||||
|
system, err := CreateSystem("Test System")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, system)
|
||||||
|
assert.Equal(t, "Test System", system.Name)
|
||||||
|
assert.NotZero(t, system.ID)
|
||||||
|
|
||||||
|
// Test creating a system with duplicate name (should fail)
|
||||||
|
duplicateSystem, err := CreateSystem("Test System")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, duplicateSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSystems(t *testing.T) {
|
||||||
|
// Clear existing systems
|
||||||
|
db.Connection.Exec("DELETE FROM systems")
|
||||||
|
|
||||||
|
// Create test systems
|
||||||
|
system1, _ := CreateSystem("System 1")
|
||||||
|
system2, _ := CreateSystem("System 2")
|
||||||
|
|
||||||
|
// Get all systems
|
||||||
|
systems := GetSystems()
|
||||||
|
assert.Len(t, systems, 2)
|
||||||
|
|
||||||
|
// Verify systems are returned correctly
|
||||||
|
foundSystem1 := false
|
||||||
|
foundSystem2 := false
|
||||||
|
for _, s := range systems {
|
||||||
|
if s.ID == system1.ID {
|
||||||
|
assert.Equal(t, "System 1", s.Name)
|
||||||
|
foundSystem1 = true
|
||||||
|
}
|
||||||
|
if s.ID == system2.ID {
|
||||||
|
assert.Equal(t, "System 2", s.Name)
|
||||||
|
foundSystem2 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, foundSystem1)
|
||||||
|
assert.True(t, foundSystem2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSystem(t *testing.T) {
|
||||||
|
// Create a test system
|
||||||
|
createdSystem, _ := CreateSystem("Get System Test")
|
||||||
|
|
||||||
|
// Get the system by ID
|
||||||
|
system, err := GetSystem(createdSystem.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, system)
|
||||||
|
assert.Equal(t, createdSystem.ID, system.ID)
|
||||||
|
assert.Equal(t, "Get System Test", system.Name)
|
||||||
|
|
||||||
|
// Test getting a non-existent system
|
||||||
|
nonExistentSystem, err := GetSystem(9999)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, nonExistentSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSystem(t *testing.T) {
|
||||||
|
// Create a test system
|
||||||
|
createdSystem, _ := CreateSystem("Update System Test")
|
||||||
|
|
||||||
|
// Update the system
|
||||||
|
updatedSystem, err := UpdateSystem(createdSystem.ID, "Updated System")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, updatedSystem)
|
||||||
|
assert.Equal(t, createdSystem.ID, updatedSystem.ID)
|
||||||
|
assert.Equal(t, "Updated System", updatedSystem.Name)
|
||||||
|
|
||||||
|
// Verify the update in the database
|
||||||
|
system, _ := GetSystem(createdSystem.ID)
|
||||||
|
assert.Equal(t, "Updated System", system.Name)
|
||||||
|
|
||||||
|
// Test updating a non-existent system
|
||||||
|
nonExistentSystem, err := UpdateSystem(9999, "Non-existent")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, nonExistentSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSystem(t *testing.T) {
|
||||||
|
// Create a test system
|
||||||
|
createdSystem, _ := CreateSystem("Delete System Test")
|
||||||
|
|
||||||
|
// Delete the system
|
||||||
|
err := DeleteSystem(createdSystem.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify the system is deleted
|
||||||
|
system, err := GetSystem(createdSystem.ID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, system)
|
||||||
|
|
||||||
|
// Test deleting a non-existent system
|
||||||
|
err = DeleteSystem(9999)
|
||||||
|
assert.NoError(t, err) // GORM doesn't return an error when deleting non-existent records
|
||||||
|
}
|
||||||
|
|||||||
44
lib/users/users_functions.go
Normal file
44
lib/users/users_functions.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
"clortho/lib/db"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetUserByID retrieves a user by ID
|
||||||
|
func GetUserByID(id uint) (*db.User, error) {
|
||||||
|
var user db.User
|
||||||
|
result := db.Connection.First(&user, id)
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return nil, errors.New("user not found")
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser updates a user's information
|
||||||
|
func UpdateUser(id uint, displayName string, admin bool) (*db.User, error) {
|
||||||
|
user, err := GetUserByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the user fields
|
||||||
|
user.DisplayName = &displayName
|
||||||
|
user.Admin = admin
|
||||||
|
|
||||||
|
result := db.Connection.Save(user)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser deletes a user by ID
|
||||||
|
func DeleteUser(id uint) error {
|
||||||
|
result := db.Connection.Delete(&db.User{}, id)
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return errors.New("user not found")
|
||||||
|
}
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
@ -1,6 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-navigation-drawer v-model="drawer">
|
<v-navigation-drawer v-model="drawer">
|
||||||
<!-- -->
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
to="/"
|
||||||
|
title="Home"
|
||||||
|
prepend-icon="mdi-home"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
v-if="appStore.user?.admin"
|
||||||
|
to="/systems"
|
||||||
|
title="Systems"
|
||||||
|
prepend-icon="mdi-server"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
v-if="appStore.user?.admin"
|
||||||
|
to="/users"
|
||||||
|
title="Users"
|
||||||
|
prepend-icon="mdi-account-group"
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<v-app-bar>
|
<v-app-bar>
|
||||||
@ -58,7 +76,15 @@ watch(() => appStore.user, initMenu)
|
|||||||
|
|
||||||
function initMenu() {
|
function initMenu() {
|
||||||
if (appStore.user) {
|
if (appStore.user) {
|
||||||
items.value = [{title: "Log out", link: "/auth/signout"}]
|
items.value = [
|
||||||
|
{title: "Log out", link: "/auth/signout"}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Add admin links for admin users
|
||||||
|
if (appStore.user.admin) {
|
||||||
|
items.value.unshift({title: "Users", link: "/users"})
|
||||||
|
items.value.unshift({title: "Systems", link: "/systems"})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
items.value = [{title: "Sign In", link: "/auth/signin"}]
|
items.value = [{title: "Sign In", link: "/auth/signin"}]
|
||||||
}
|
}
|
||||||
|
|||||||
146
webapp/src/pages/systems/[id].vue
Normal file
146
webapp/src/pages/systems/[id].vue
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h4 mb-4">Edit System</h1>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="loading">
|
||||||
|
<v-col>
|
||||||
|
<v-progress-circular indeterminate />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-else-if="error">
|
||||||
|
<v-col>
|
||||||
|
<v-alert type="error">
|
||||||
|
{{ error }}
|
||||||
|
</v-alert>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-else>
|
||||||
|
<v-col cols="12" md="8" lg="6">
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form ref="form" v-model="valid" @submit.prevent="saveSystem">
|
||||||
|
<v-text-field
|
||||||
|
v-model="system.name"
|
||||||
|
:rules="nameRules"
|
||||||
|
label="System Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:disabled="!valid || saving"
|
||||||
|
:loading="saving"
|
||||||
|
@click="saveSystem"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
to="/systems"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
requiresAdmin: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const systemId = route.params.id
|
||||||
|
|
||||||
|
const valid = ref(false)
|
||||||
|
const loading = ref(true)
|
||||||
|
const saving = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const system = ref({
|
||||||
|
id: null,
|
||||||
|
name: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const nameRules = [
|
||||||
|
v => !!v || 'Name is required',
|
||||||
|
v => (v && v.length >= 3) || 'Name must be at least 3 characters'
|
||||||
|
]
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchSystem()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchSystem() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/gui/systems/${systemId}`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw new Error('System not found')
|
||||||
|
}
|
||||||
|
throw new Error('Failed to fetch system')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
system.value = data
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message || 'An error occurred while fetching the system'
|
||||||
|
console.error('Error fetching system:', err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSystem() {
|
||||||
|
if (!valid.value) return
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/gui/systems/${systemId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: system.value.name
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
throw new Error(errorData.error || 'Failed to update system')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate back to systems list
|
||||||
|
router.push('/systems')
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message || 'An error occurred while updating the system'
|
||||||
|
console.error('Error updating system:', err)
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
98
webapp/src/pages/systems/create.vue
Normal file
98
webapp/src/pages/systems/create.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h4 mb-4">Create System</h1>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="8" lg="6">
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form ref="form" v-model="valid" @submit.prevent="saveSystem">
|
||||||
|
<v-text-field
|
||||||
|
v-model="system.name"
|
||||||
|
:rules="nameRules"
|
||||||
|
label="System Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:disabled="!valid || loading"
|
||||||
|
:loading="loading"
|
||||||
|
@click="saveSystem"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
to="/systems"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
requiresAdmin: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const valid = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const system = ref({
|
||||||
|
name: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const nameRules = [
|
||||||
|
v => !!v || 'Name is required',
|
||||||
|
v => (v && v.length >= 3) || 'Name must be at least 3 characters'
|
||||||
|
]
|
||||||
|
|
||||||
|
async function saveSystem() {
|
||||||
|
if (!valid.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gui/systems/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: system.value.name
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
throw new Error(errorData.error || 'Failed to create system')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate back to systems list
|
||||||
|
router.push('/systems')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating system:', error)
|
||||||
|
// You could add a notification here
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
174
webapp/src/pages/systems/index.vue
Normal file
174
webapp/src/pages/systems/index.vue
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h4 mb-4">
|
||||||
|
Systems
|
||||||
|
</h1>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
to="/systems/create"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
Create System
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-card>
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="systems"
|
||||||
|
:loading="loading"
|
||||||
|
class="elevation-1"
|
||||||
|
>
|
||||||
|
<template #[`item.actions`]="{ item }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
:to="`/systems/${item.id}`"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-pencil</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
@click="confirmDelete(item)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="deleteDialog"
|
||||||
|
max-width="500px"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Delete System
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Are you sure you want to delete this system? This action cannot be undone.
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="blue-darken-1"
|
||||||
|
variant="text"
|
||||||
|
@click="closeDelete"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="deleteItem"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
requiresAdmin: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Table headers
|
||||||
|
const headers = [
|
||||||
|
{ title: 'ID', key: 'id' },
|
||||||
|
{ title: 'Name', key: 'name' },
|
||||||
|
{ title: 'Created At', key: 'createdAt' },
|
||||||
|
{ title: 'Updated At', key: 'updatedAt' },
|
||||||
|
{ title: 'Actions', key: 'actions', sortable: false }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const systems = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const deleteDialog = ref(false)
|
||||||
|
const systemToDelete = ref(null)
|
||||||
|
|
||||||
|
// Fetch systems on component mount
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchSystems()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch systems from API
|
||||||
|
async function fetchSystems() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gui/systems/')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch systems')
|
||||||
|
}
|
||||||
|
systems.value = await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching systems:', error)
|
||||||
|
// You could add a notification here
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete confirmation
|
||||||
|
function confirmDelete(item) {
|
||||||
|
systemToDelete.value = item
|
||||||
|
deleteDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close delete dialog
|
||||||
|
function closeDelete() {
|
||||||
|
deleteDialog.value = false
|
||||||
|
systemToDelete.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete system
|
||||||
|
async function deleteItem() {
|
||||||
|
if (!systemToDelete.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/gui/systems/${systemToDelete.value.id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete system')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh systems list
|
||||||
|
await fetchSystems()
|
||||||
|
|
||||||
|
// Close dialog
|
||||||
|
closeDelete()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting system:', error)
|
||||||
|
// You could add a notification here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
284
webapp/src/pages/users/[id].vue
Normal file
284
webapp/src/pages/users/[id].vue
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h4 mb-4">
|
||||||
|
User Details
|
||||||
|
</h1>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="loading">
|
||||||
|
<v-col>
|
||||||
|
<v-progress-circular indeterminate />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<v-card class="mb-4">
|
||||||
|
<v-card-title>User Information</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-account" />
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Username</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user.username }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item v-if="user.displayName">
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-card-account-details" />
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Display Name</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user.displayName }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-shield-account" />
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Admin</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-icon
|
||||||
|
v-if="user.admin"
|
||||||
|
color="success"
|
||||||
|
>
|
||||||
|
mdi-check
|
||||||
|
</v-icon>
|
||||||
|
<v-icon
|
||||||
|
v-else
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
mdi-close
|
||||||
|
</v-icon>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-calendar" />
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Created At</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ new Date(user.createdAt).toLocaleString() }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-update" />
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Updated At</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ new Date(user.updatedAt).toLocaleString() }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<span>User Keys</span>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
v-if="isAdmin"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
:to="`/users/${userId}/keys/create`"
|
||||||
|
>
|
||||||
|
Add Key
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-data-table
|
||||||
|
:headers="keyHeaders"
|
||||||
|
:items="keys"
|
||||||
|
:loading="loadingKeys"
|
||||||
|
class="elevation-1"
|
||||||
|
>
|
||||||
|
<template #[`item.actions`]="{ item }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
@click="confirmDeleteKey(item)"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Delete Key Confirmation Dialog -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="showDeleteKeyDialog"
|
||||||
|
max-width="500px"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Delete Key</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Are you sure you want to delete this key? This action cannot be undone.
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="blue-darken-1"
|
||||||
|
variant="text"
|
||||||
|
@click="showDeleteKeyDialog = false"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="deleteKey"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
class="mt-4"
|
||||||
|
to="/users"
|
||||||
|
variant="text"
|
||||||
|
prepend-icon="mdi-arrow-left"
|
||||||
|
>
|
||||||
|
Back to Users
|
||||||
|
</v-btn>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const userId = computed(() => route.params.id)
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
// User data
|
||||||
|
const user = ref({
|
||||||
|
id: null,
|
||||||
|
username: '',
|
||||||
|
displayName: '',
|
||||||
|
admin: false,
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
keys: []
|
||||||
|
})
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// Keys data
|
||||||
|
const keys = ref([])
|
||||||
|
const loadingKeys = ref(true)
|
||||||
|
const keyHeaders = [
|
||||||
|
{ title: 'ID', key: 'id' },
|
||||||
|
{ title: 'Name', key: 'name' },
|
||||||
|
{ title: 'Created At', key: 'createdAt' },
|
||||||
|
{ title: 'Updated At', key: 'updatedAt' },
|
||||||
|
{ title: 'Actions', key: 'actions', sortable: false }
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
// Delete key dialog
|
||||||
|
const showDeleteKeyDialog = ref(false)
|
||||||
|
const keyToDelete = ref(null)
|
||||||
|
|
||||||
|
// Check if current user is admin
|
||||||
|
const isAdmin = computed(() => appStore.user?.admin || false)
|
||||||
|
|
||||||
|
// Fetch user and session data on component mount
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
fetchUser(),
|
||||||
|
fetchKeys()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch user from API
|
||||||
|
async function fetchUser() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/gui/users/${userId.value}`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch user')
|
||||||
|
}
|
||||||
|
user.value = await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch keys for user
|
||||||
|
async function fetchKeys() {
|
||||||
|
loadingKeys.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/gui/users/${userId.value}/keys`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch keys')
|
||||||
|
}
|
||||||
|
keys.value = await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching keys:', error)
|
||||||
|
} finally {
|
||||||
|
loadingKeys.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Confirm delete key
|
||||||
|
function confirmDeleteKey(item) {
|
||||||
|
keyToDelete.value = item
|
||||||
|
showDeleteKeyDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete key
|
||||||
|
async function deleteKey() {
|
||||||
|
if (!keyToDelete.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/gui/keys/${keyToDelete.value.id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete key')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh keys list
|
||||||
|
await fetchKeys()
|
||||||
|
|
||||||
|
// Close dialog
|
||||||
|
showDeleteKeyDialog.value = false
|
||||||
|
keyToDelete.value = null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting key:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
106
webapp/src/pages/users/[id]/keys/create.vue
Normal file
106
webapp/src/pages/users/[id]/keys/create.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h4 mb-4">
|
||||||
|
Add New Key
|
||||||
|
</h1>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="8" lg="6">
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
v-model="valid"
|
||||||
|
@submit.prevent="addKey"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="newKey.name"
|
||||||
|
label="Key Name"
|
||||||
|
required
|
||||||
|
:rules="[v => !!v || 'Key name is required']"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="newKey.content"
|
||||||
|
label="Key Content"
|
||||||
|
required
|
||||||
|
:rules="[v => !!v || 'Key content is required']"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:disabled="!valid"
|
||||||
|
@click="addKey"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
:to="`/users/${userId}`"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
requiresAdmin: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const userId = computed(() => route.params.id)
|
||||||
|
|
||||||
|
// Form data
|
||||||
|
const valid = ref(false)
|
||||||
|
const newKey = ref({
|
||||||
|
name: '',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a new key
|
||||||
|
async function addKey() {
|
||||||
|
if (!valid.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/gui/users/${userId.value}/keys`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: newKey.value.name,
|
||||||
|
content: newKey.value.content
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add key')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate back to user details page
|
||||||
|
router.push(`/users/${userId.value}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding key:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
86
webapp/src/pages/users/index.vue
Normal file
86
webapp/src/pages/users/index.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h4 mb-4">
|
||||||
|
Users
|
||||||
|
</h1>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-card>
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="users"
|
||||||
|
:loading="loading"
|
||||||
|
class="elevation-1"
|
||||||
|
>
|
||||||
|
<template #[`item.admin`]="{ item }">
|
||||||
|
<v-icon v-if="item.admin" color="success">mdi-check</v-icon>
|
||||||
|
<v-icon v-else color="error">mdi-close</v-icon>
|
||||||
|
</template>
|
||||||
|
<template #[`item.actions`]="{ item }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
:to="`/users/${item.id}`"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-eye</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
requiresAdmin: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Table headers
|
||||||
|
const headers = [
|
||||||
|
{ title: 'ID', key: 'id' },
|
||||||
|
{ title: 'Username', key: 'username' },
|
||||||
|
{ title: 'Display Name', key: 'displayName' },
|
||||||
|
{ title: 'Admin', key: 'admin' },
|
||||||
|
{ title: 'Created At', key: 'createdAt' },
|
||||||
|
{ title: 'Actions', key: 'actions', sortable: false }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const users = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// Fetch users on component mount
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch users from API
|
||||||
|
async function fetchUsers() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gui/users/')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch users')
|
||||||
|
}
|
||||||
|
users.value = await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching users:', error)
|
||||||
|
// You could add a notification here
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -38,18 +38,27 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
await appStore.updateUser()
|
await appStore.updateUser()
|
||||||
|
|
||||||
console.log("to", to)
|
// Allow public routes
|
||||||
|
|
||||||
if (to.meta.public) {
|
if (to.meta.public) {
|
||||||
next()
|
next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redirect to signin if not logged in
|
||||||
if (!appStore.user) {
|
if (!appStore.user) {
|
||||||
next('/auth/signin')
|
next('/auth/signin')
|
||||||
} else {
|
return
|
||||||
next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if route requires admin access
|
||||||
|
if (to.meta.requiresAdmin && !appStore.user.admin) {
|
||||||
|
// Redirect to home if not admin
|
||||||
|
next('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow access
|
||||||
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
6
webapp/src/typed-router.d.ts
vendored
6
webapp/src/typed-router.d.ts
vendored
@ -21,5 +21,11 @@ declare module 'vue-router/auto-routes' {
|
|||||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||||
'/auth/signin': RouteRecordInfo<'/auth/signin', '/auth/signin', Record<never, never>, Record<never, never>>,
|
'/auth/signin': RouteRecordInfo<'/auth/signin', '/auth/signin', Record<never, never>, Record<never, never>>,
|
||||||
'/auth/signout': RouteRecordInfo<'/auth/signout', '/auth/signout', Record<never, never>, Record<never, never>>,
|
'/auth/signout': RouteRecordInfo<'/auth/signout', '/auth/signout', Record<never, never>, Record<never, never>>,
|
||||||
|
'/systems/': RouteRecordInfo<'/systems/', '/systems', Record<never, never>, Record<never, never>>,
|
||||||
|
'/systems/[id]': RouteRecordInfo<'/systems/[id]', '/systems/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||||
|
'/systems/create': RouteRecordInfo<'/systems/create', '/systems/create', Record<never, never>, Record<never, never>>,
|
||||||
|
'/users/': RouteRecordInfo<'/users/', '/users', Record<never, never>, Record<never, never>>,
|
||||||
|
'/users/[id]': RouteRecordInfo<'/users/[id]', '/users/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||||
|
'/users/[id]/keys/create': RouteRecordInfo<'/users/[id]/keys/create', '/users/:id/keys/create', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user