diff --git a/lib/apis/apis.go b/lib/apis/apis.go index 3b43346..0c69b27 100644 --- a/lib/apis/apis.go +++ b/lib/apis/apis.go @@ -33,4 +33,6 @@ func SetupRouter(r *gin.Engine, authMiddleware gin.HandlerFunc) { InitAuthEndpoints(private) InitUsersEndpoints(private) + InitSystemsEndpoints(private) + InitKeysEndpoints(private) } diff --git a/lib/apis/auth_middleware.go b/lib/apis/auth_middleware.go index 4fc49b1..cd43804 100644 --- a/lib/apis/auth_middleware.go +++ b/lib/apis/auth_middleware.go @@ -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() + } +} diff --git a/lib/apis/auth_middleware_test.go b/lib/apis/auth_middleware_test.go new file mode 100644 index 0000000..9e6da7d --- /dev/null +++ b/lib/apis/auth_middleware_test.go @@ -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") + }) +} \ No newline at end of file diff --git a/lib/apis/keys_endpoints.go b/lib/apis/keys_endpoints.go new file mode 100644 index 0000000..d19f534 --- /dev/null +++ b/lib/apis/keys_endpoints.go @@ -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) +} diff --git a/lib/apis/keys_endpoints_test.go b/lib/apis/keys_endpoints_test.go new file mode 100644 index 0000000..36d81f0 --- /dev/null +++ b/lib/apis/keys_endpoints_test.go @@ -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) +} diff --git a/lib/apis/systems_endpoints.go b/lib/apis/systems_endpoints.go index 3529442..5eac0a6 100644 --- a/lib/apis/systems_endpoints.go +++ b/lib/apis/systems_endpoints.go @@ -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"}) } diff --git a/lib/apis/systems_endpoints_test.go b/lib/apis/systems_endpoints_test.go index 473b613..10414d9 100644 --- a/lib/apis/systems_endpoints_test.go +++ b/lib/apis/systems_endpoints_test.go @@ -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) + // Create a non-admin user + regularUser, err := InitUser("regular_test", "password") + if err != nil { + t.Fatal(err) + } + regularUser.Admin = false + db.Connection.Save(®ularUser) - reqBody := loginRequest{Username: "admin", Password: "password"} - strReqBody, _ := json.Marshal(reqBody) - w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/gui/auth/signin", strings.NewReader(string(strReqBody))) - r.ServeHTTP(w, req) + // Clear existing systems + db.Connection.Exec("DELETE FROM systems") - 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=")) + 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") + }) } diff --git a/lib/apis/users_endpoints.go b/lib/apis/users_endpoints.go index 5b83f75..3bdbddd 100644 --- a/lib/apis/users_endpoints.go +++ b/lib/apis/users_endpoints.go @@ -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"}) } diff --git a/lib/apis/users_endpoints_test.go b/lib/apis/users_endpoints_test.go new file mode 100644 index 0000000..b5e124e --- /dev/null +++ b/lib/apis/users_endpoints_test.go @@ -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"]) +} \ No newline at end of file diff --git a/lib/db/entities.go b/lib/db/entities.go index 71912f6..0d3d43d 100644 --- a/lib/db/entities.go +++ b/lib/db/entities.go @@ -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"` } diff --git a/lib/keys/keys.go b/lib/keys/keys.go new file mode 100644 index 0000000..d39c9c0 --- /dev/null +++ b/lib/keys/keys.go @@ -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 +} diff --git a/lib/keys/keys_test.go b/lib/keys/keys_test.go new file mode 100644 index 0000000..2f8359b --- /dev/null +++ b/lib/keys/keys_test.go @@ -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") + } +} diff --git a/lib/systems/systems.go b/lib/systems/systems.go index 6e592a8..5a6d3af 100644 --- a/lib/systems/systems.go +++ b/lib/systems/systems.go @@ -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 +} diff --git a/lib/systems/systems_test.go b/lib/systems/systems_test.go index a0717a2..9648ac4 100644 --- a/lib/systems/systems_test.go +++ b/lib/systems/systems_test.go @@ -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 +} diff --git a/lib/users/users_functions.go b/lib/users/users_functions.go new file mode 100644 index 0000000..5a362ab --- /dev/null +++ b/lib/users/users_functions.go @@ -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 +} \ No newline at end of file diff --git a/webapp/src/layouts/default.vue b/webapp/src/layouts/default.vue index 3456cad..9f7a336 100644 --- a/webapp/src/layouts/default.vue +++ b/webapp/src/layouts/default.vue @@ -1,6 +1,24 @@