work
This commit is contained in:
parent
ea2bb235a2
commit
e8d01e7f44
@ -33,4 +33,6 @@ func SetupRouter(r *gin.Engine, authMiddleware gin.HandlerFunc) {
|
||||
|
||||
InitAuthEndpoints(private)
|
||||
InitUsersEndpoints(private)
|
||||
InitSystemsEndpoints(private)
|
||||
InitKeysEndpoints(private)
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"clortho/lib/db"
|
||||
"clortho/lib/users"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
@ -36,3 +37,25 @@ func LoggedInMiddleware() gin.HandlerFunc {
|
||||
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
|
||||
|
||||
import (
|
||||
"clortho/lib/users"
|
||||
"clortho/lib/systems"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func InitSystemsEndpoints(r *gin.RouterGroup) {
|
||||
group := r.Group("/users")
|
||||
group.Use(LoggedInMiddleware())
|
||||
group.GET("/", getUsers)
|
||||
group.GET("/me", getMe)
|
||||
type systemRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
|
||||
func getServers(c *gin.Context) {
|
||||
userList := users.GetUsers()
|
||||
c.JSON(200, &userList)
|
||||
func InitSystemsEndpoints(r *gin.RouterGroup) {
|
||||
group := r.Group("/systems")
|
||||
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
|
||||
|
||||
import (
|
||||
"clortho/lib/db"
|
||||
"clortho/lib/systems"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
@ -10,23 +13,401 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInitSystemsEndpoints_getSystems(t *testing.T) {
|
||||
_, err := InitUser("admin", "password")
|
||||
func setupSystemsTest(t *testing.T) (*db.User, *db.User) {
|
||||
// Create an admin user
|
||||
adminUser, err := InitUser("admin_test", "password")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
adminUser.Admin = true
|
||||
db.Connection.Save(&adminUser)
|
||||
|
||||
r := gin.Default()
|
||||
SetupRouter(r, nil)
|
||||
|
||||
reqBody := loginRequest{Username: "admin", Password: "password"}
|
||||
strReqBody, _ := json.Marshal(reqBody)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/gui/auth/signin", strings.NewReader(string(strReqBody)))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.JSONEq(t, `{"valid": true}`, w.Body.String())
|
||||
setCookie := w.Header().Get("Set-Cookie")
|
||||
assert.True(t, strings.Contains(setCookie, "CLORTHO_AUTH="))
|
||||
// Create a non-admin user
|
||||
regularUser, err := InitUser("regular_test", "password")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
regularUser.Admin = false
|
||||
db.Connection.Save(®ularUser)
|
||||
|
||||
// Clear existing systems
|
||||
db.Connection.Exec("DELETE FROM systems")
|
||||
|
||||
return adminUser, regularUser
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"clortho/lib/db"
|
||||
"clortho/lib/users"
|
||||
"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) {
|
||||
group := r.Group("/users")
|
||||
group.Use(LoggedInMiddleware())
|
||||
group.GET("/", getUsers)
|
||||
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) {
|
||||
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:"-"`
|
||||
User *User `gorm:"foreignKey:UserID" json:"user"`
|
||||
Name string `json:"name"`
|
||||
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)
|
||||
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"
|
||||
"os"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@ -22,3 +23,102 @@ func TestMain(m *testing.M) {
|
||||
|
||||
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>
|
||||
<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-app-bar>
|
||||
@ -58,7 +76,15 @@ watch(() => appStore.user, initMenu)
|
||||
|
||||
function initMenu() {
|
||||
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 {
|
||||
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();
|
||||
await appStore.updateUser()
|
||||
|
||||
console.log("to", to)
|
||||
|
||||
// Allow public routes
|
||||
if (to.meta.public) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to signin if not logged in
|
||||
if (!appStore.user) {
|
||||
next('/auth/signin')
|
||||
} else {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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>>,
|
||||
'/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>>,
|
||||
'/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