This commit is contained in:
Maxime Duchene-Savard 2025-04-22 09:25:56 -04:00
parent ea2bb235a2
commit e8d01e7f44
24 changed files with 2922 additions and 29 deletions

View File

@ -33,4 +33,6 @@ func SetupRouter(r *gin.Engine, authMiddleware gin.HandlerFunc) {
InitAuthEndpoints(private) InitAuthEndpoints(private)
InitUsersEndpoints(private) InitUsersEndpoints(private)
InitSystemsEndpoints(private)
InitKeysEndpoints(private)
} }

View File

@ -1,6 +1,7 @@
package apis package apis
import ( import (
"clortho/lib/db"
"clortho/lib/users" "clortho/lib/users"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
@ -36,3 +37,25 @@ func LoggedInMiddleware() gin.HandlerFunc {
c.Next() c.Next()
} }
} }
func AdminMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// First ensure user is logged in
sessionInterface, hasSession := c.Get("session")
if !hasSession {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
c.Abort()
return
}
// Check if user is admin
session, ok := sessionInterface.(*db.UserSession)
if !ok || session.User == nil || !session.User.Admin {
c.JSON(http.StatusForbidden, gin.H{"error": "admin access required"})
c.Abort()
return
}
c.Next()
}
}

View 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(&regularUser)
// 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
View 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)
}

View 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)
}

View File

@ -1,18 +1,98 @@
package apis package apis
import ( import (
"clortho/lib/users" "clortho/lib/systems"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http"
"strconv"
) )
func InitSystemsEndpoints(r *gin.RouterGroup) { type systemRequest struct {
group := r.Group("/users") Name string `json:"name" binding:"required"`
group.Use(LoggedInMiddleware())
group.GET("/", getUsers)
group.GET("/me", getMe)
} }
func getServers(c *gin.Context) { func InitSystemsEndpoints(r *gin.RouterGroup) {
userList := users.GetUsers() group := r.Group("/systems")
c.JSON(200, &userList) group.Use(LoggedInMiddleware())
group.Use(AdminMiddleware())
group.GET("/", getSystems)
group.GET("/:id", getSystem)
group.POST("/", createSystem)
group.PUT("/:id", updateSystem)
group.DELETE("/:id", deleteSystem)
}
func getSystems(c *gin.Context) {
systemList := systems.GetSystems()
c.JSON(http.StatusOK, &systemList)
}
func getSystem(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
return
}
system, err := systems.GetSystem(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "System not found"})
return
}
c.JSON(http.StatusOK, system)
}
func createSystem(c *gin.Context) {
var req systemRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
system, err := systems.CreateSystem(req.Name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, system)
}
func updateSystem(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
return
}
var req systemRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
system, err := systems.UpdateSystem(uint(id), req.Name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, system)
}
func deleteSystem(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
return
}
err = systems.DeleteSystem(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "System deleted successfully"})
} }

View File

@ -1,7 +1,10 @@
package apis package apis
import ( import (
"clortho/lib/db"
"clortho/lib/systems"
"encoding/json" "encoding/json"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net/http" "net/http"
@ -10,23 +13,401 @@ import (
"testing" "testing"
) )
func TestInitSystemsEndpoints_getSystems(t *testing.T) { func setupSystemsTest(t *testing.T) (*db.User, *db.User) {
_, err := InitUser("admin", "password") // Create an admin user
adminUser, err := InitUser("admin_test", "password")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
adminUser.Admin = true
db.Connection.Save(&adminUser)
r := gin.Default() // Create a non-admin user
SetupRouter(r, nil) regularUser, err := InitUser("regular_test", "password")
if err != nil {
t.Fatal(err)
}
regularUser.Admin = false
db.Connection.Save(&regularUser)
reqBody := loginRequest{Username: "admin", Password: "password"} // Clear existing systems
strReqBody, _ := json.Marshal(reqBody) db.Connection.Exec("DELETE FROM systems")
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/gui/auth/signin", strings.NewReader(string(strReqBody)))
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) return adminUser, regularUser
assert.JSONEq(t, `{"valid": true}`, w.Body.String()) }
setCookie := w.Header().Get("Set-Cookie")
assert.True(t, strings.Contains(setCookie, "CLORTHO_AUTH=")) func TestGetSystems(t *testing.T) {
adminUser, regularUser := setupSystemsTest(t)
// Create test systems
system1, _ := systems.CreateSystem("System 1")
system2, _ := systems.CreateSystem("System 2")
// Test with admin user
t.Run("Admin user can get systems", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/gui/systems/", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var systems []db.System
err := json.Unmarshal(w.Body.Bytes(), &systems)
assert.NoError(t, err)
assert.Len(t, systems, 2)
// Verify systems are returned correctly
foundSystem1 := false
foundSystem2 := false
for _, s := range systems {
if s.ID == system1.ID {
assert.Equal(t, "System 1", s.Name)
foundSystem1 = true
}
if s.ID == system2.ID {
assert.Equal(t, "System 2", s.Name)
foundSystem2 = true
}
}
assert.True(t, foundSystem1)
assert.True(t, foundSystem2)
})
// Test with non-admin user
t.Run("Non-admin user cannot get systems", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*regularUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/gui/systems/", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), "admin access required")
})
// Test with no session
t.Run("Unauthenticated user cannot get systems", func(t *testing.T) {
router := gin.New()
group := router.Group("/gui")
InitSystemsEndpoints(group)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/gui/systems/", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Contains(t, w.Body.String(), "unauthorized")
})
}
func TestGetSystem(t *testing.T) {
adminUser, regularUser := setupSystemsTest(t)
// Create a test system
system, _ := systems.CreateSystem("Test System")
// Test with admin user
t.Run("Admin user can get a system", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", fmt.Sprintf("/gui/systems/%d", system.ID), nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var returnedSystem db.System
err := json.Unmarshal(w.Body.Bytes(), &returnedSystem)
assert.NoError(t, err)
assert.Equal(t, system.ID, returnedSystem.ID)
assert.Equal(t, "Test System", returnedSystem.Name)
})
// Test with non-admin user
t.Run("Non-admin user cannot get a system", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*regularUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", fmt.Sprintf("/gui/systems/%d", system.ID), nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), "admin access required")
})
// Test with invalid ID
t.Run("Invalid ID returns bad request", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/gui/systems/invalid", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "Invalid ID format")
})
// Test with non-existent ID
t.Run("Non-existent ID returns not found", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/gui/systems/9999", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
assert.Contains(t, w.Body.String(), "System not found")
})
}
func TestCreateSystem(t *testing.T) {
adminUser, regularUser := setupSystemsTest(t)
// Test with admin user
t.Run("Admin user can create a system", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
reqBody := systemRequest{Name: "New System"}
strReqBody, _ := json.Marshal(reqBody)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/gui/systems/", strings.NewReader(string(strReqBody)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var createdSystem db.System
err := json.Unmarshal(w.Body.Bytes(), &createdSystem)
assert.NoError(t, err)
assert.NotZero(t, createdSystem.ID)
assert.Equal(t, "New System", createdSystem.Name)
})
// Test with non-admin user
t.Run("Non-admin user cannot create a system", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*regularUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
reqBody := systemRequest{Name: "New System"}
strReqBody, _ := json.Marshal(reqBody)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/gui/systems/", strings.NewReader(string(strReqBody)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), "admin access required")
})
// Test with invalid request body
t.Run("Invalid request body returns bad request", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
reqBody := `{"invalid": "json"`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/gui/systems/", strings.NewReader(reqBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
// Test with missing required field
t.Run("Missing required field returns bad request", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
reqBody := `{}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/gui/systems/", strings.NewReader(reqBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
}
func TestUpdateSystem(t *testing.T) {
adminUser, regularUser := setupSystemsTest(t)
// Create a test system
system, _ := systems.CreateSystem("Test System")
// Test with admin user
t.Run("Admin user can update a system", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
reqBody := systemRequest{Name: "Updated System"}
strReqBody, _ := json.Marshal(reqBody)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", fmt.Sprintf("/gui/systems/%d", system.ID), strings.NewReader(string(strReqBody)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var updatedSystem db.System
err := json.Unmarshal(w.Body.Bytes(), &updatedSystem)
assert.NoError(t, err)
assert.Equal(t, system.ID, updatedSystem.ID)
assert.Equal(t, "Updated System", updatedSystem.Name)
})
// Test with non-admin user
t.Run("Non-admin user cannot update a system", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*regularUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
reqBody := systemRequest{Name: "Updated System"}
strReqBody, _ := json.Marshal(reqBody)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", fmt.Sprintf("/gui/systems/%d", system.ID), strings.NewReader(string(strReqBody)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), "admin access required")
})
// Test with invalid ID
t.Run("Invalid ID returns bad request", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
reqBody := systemRequest{Name: "Updated System"}
strReqBody, _ := json.Marshal(reqBody)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/gui/systems/invalid", strings.NewReader(string(strReqBody)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "Invalid ID format")
})
// Test with invalid request body
t.Run("Invalid request body returns bad request", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
reqBody := `{"invalid": "json"`
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", fmt.Sprintf("/gui/systems/%d", system.ID), strings.NewReader(reqBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
}
func TestDeleteSystem(t *testing.T) {
adminUser, regularUser := setupSystemsTest(t)
// Test with admin user
t.Run("Admin user can delete a system", func(t *testing.T) {
// Create a system to delete
system, _ := systems.CreateSystem("System to Delete")
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/gui/systems/%d", system.ID), nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "System deleted successfully")
// Verify the system is deleted
deletedSystem, err := systems.GetSystem(system.ID)
assert.Error(t, err)
assert.Nil(t, deletedSystem)
})
// Test with non-admin user
t.Run("Non-admin user cannot delete a system", func(t *testing.T) {
// Create a system to delete
system, _ := systems.CreateSystem("System to Not Delete")
router := gin.New()
router.Use(MockAuthMiddleware(*regularUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/gui/systems/%d", system.ID), nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), "admin access required")
// Verify the system is not deleted
existingSystem, err := systems.GetSystem(system.ID)
assert.NoError(t, err)
assert.NotNil(t, existingSystem)
})
// Test with invalid ID
t.Run("Invalid ID returns bad request", func(t *testing.T) {
router := gin.New()
router.Use(MockAuthMiddleware(*adminUser))
group := router.Group("/gui")
InitSystemsEndpoints(group)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/gui/systems/invalid", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "Invalid ID format")
})
} }

View File

@ -1,18 +1,149 @@
package apis package apis
import ( import (
"clortho/lib/db"
"clortho/lib/users" "clortho/lib/users"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http"
"strconv"
) )
type userRequest struct {
Username string `json:"username" binding:"required"`
DisplayName string `json:"displayName"`
Admin bool `json:"admin"`
}
type userUpdateRequest struct {
DisplayName string `json:"displayName"`
Admin bool `json:"admin"`
}
func InitUsersEndpoints(r *gin.RouterGroup) { func InitUsersEndpoints(r *gin.RouterGroup) {
group := r.Group("/users") group := r.Group("/users")
group.Use(LoggedInMiddleware()) group.Use(LoggedInMiddleware())
group.GET("/", getUsers) group.GET("/", getUsers)
group.GET("/me", getMe) group.GET("/me", getMe)
group.GET("/:userId", getUser)
group.POST("/", AdminMiddleware(), createUser)
group.PUT("/:userId", AdminMiddleware(), updateUser)
group.DELETE("/:userId", AdminMiddleware(), deleteUser)
} }
func getUsers(c *gin.Context) { func getUsers(c *gin.Context) {
userList := users.GetUsers() userList := users.GetUsers()
c.JSON(200, &userList) c.JSON(http.StatusOK, &userList)
}
func getUser(c *gin.Context) {
// Get the user ID from the URL parameter
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
// Get the user
user, err := users.GetUserByID(uint(userID))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, user)
}
func createUser(c *gin.Context) {
var req userRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Create the user
user, err := users.CreateUser(req.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Update the user's display name and admin status
if req.DisplayName != "" || req.Admin {
displayName := req.DisplayName
if displayName == "" && user.DisplayName != nil {
displayName = *user.DisplayName
}
user, err = users.UpdateUser(user.ID, displayName, req.Admin)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusCreated, user)
}
func updateUser(c *gin.Context) {
// Get the user ID from the URL parameter
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
var req userUpdateRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Get the current user to preserve existing values if not provided
currentUser, err := users.GetUserByID(uint(userID))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
// Use existing display name if not provided
displayName := req.DisplayName
if displayName == "" && currentUser.DisplayName != nil {
displayName = *currentUser.DisplayName
}
// Update the user
user, err := users.UpdateUser(uint(userID), displayName, req.Admin)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
func deleteUser(c *gin.Context) {
// Get the user ID from the URL parameter
userID, err := strconv.ParseUint(c.Param("userId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
// Get the current session
sessionInterface, _ := c.Get("session")
session := sessionInterface.(*db.UserSession)
// Prevent users from deleting themselves
if session.User.ID == uint(userID) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete your own account"})
return
}
// Delete the user
err = users.DeleteUser(uint(userID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
} }

View 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"])
}

View File

@ -35,6 +35,7 @@ type Key struct {
UserID uint `gorm:"not null" json:"-"` UserID uint `gorm:"not null" json:"-"`
User *User `gorm:"foreignKey:UserID" json:"user"` User *User `gorm:"foreignKey:UserID" json:"user"`
Name string `json:"name"`
Content string `gorm:"not null" json:"-" json:"content"` Content string `gorm:"not null" json:"-" json:"content"`
} }

94
lib/keys/keys.go Normal file
View 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
View 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")
}
}

View File

@ -16,3 +16,31 @@ func GetSystems() []db.System {
db.Connection.Find(&systems) db.Connection.Find(&systems)
return systems return systems
} }
func GetSystem(id uint) (*db.System, error) {
var system db.System
result := db.Connection.First(&system, id)
if result.Error != nil {
return nil, result.Error
}
return &system, nil
}
func UpdateSystem(id uint, name string) (*db.System, error) {
system, err := GetSystem(id)
if err != nil {
return nil, err
}
system.Name = name
result := db.Connection.Save(&system)
if result.Error != nil {
return nil, result.Error
}
return system, nil
}
func DeleteSystem(id uint) error {
result := db.Connection.Delete(&db.System{}, id)
return result.Error
}

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -22,3 +23,102 @@ func TestMain(m *testing.M) {
os.Exit(exitCode) os.Exit(exitCode)
} }
func TestCreateSystem(t *testing.T) {
// Test creating a new system
system, err := CreateSystem("Test System")
assert.NoError(t, err)
assert.NotNil(t, system)
assert.Equal(t, "Test System", system.Name)
assert.NotZero(t, system.ID)
// Test creating a system with duplicate name (should fail)
duplicateSystem, err := CreateSystem("Test System")
assert.Error(t, err)
assert.Nil(t, duplicateSystem)
}
func TestGetSystems(t *testing.T) {
// Clear existing systems
db.Connection.Exec("DELETE FROM systems")
// Create test systems
system1, _ := CreateSystem("System 1")
system2, _ := CreateSystem("System 2")
// Get all systems
systems := GetSystems()
assert.Len(t, systems, 2)
// Verify systems are returned correctly
foundSystem1 := false
foundSystem2 := false
for _, s := range systems {
if s.ID == system1.ID {
assert.Equal(t, "System 1", s.Name)
foundSystem1 = true
}
if s.ID == system2.ID {
assert.Equal(t, "System 2", s.Name)
foundSystem2 = true
}
}
assert.True(t, foundSystem1)
assert.True(t, foundSystem2)
}
func TestGetSystem(t *testing.T) {
// Create a test system
createdSystem, _ := CreateSystem("Get System Test")
// Get the system by ID
system, err := GetSystem(createdSystem.ID)
assert.NoError(t, err)
assert.NotNil(t, system)
assert.Equal(t, createdSystem.ID, system.ID)
assert.Equal(t, "Get System Test", system.Name)
// Test getting a non-existent system
nonExistentSystem, err := GetSystem(9999)
assert.Error(t, err)
assert.Nil(t, nonExistentSystem)
}
func TestUpdateSystem(t *testing.T) {
// Create a test system
createdSystem, _ := CreateSystem("Update System Test")
// Update the system
updatedSystem, err := UpdateSystem(createdSystem.ID, "Updated System")
assert.NoError(t, err)
assert.NotNil(t, updatedSystem)
assert.Equal(t, createdSystem.ID, updatedSystem.ID)
assert.Equal(t, "Updated System", updatedSystem.Name)
// Verify the update in the database
system, _ := GetSystem(createdSystem.ID)
assert.Equal(t, "Updated System", system.Name)
// Test updating a non-existent system
nonExistentSystem, err := UpdateSystem(9999, "Non-existent")
assert.Error(t, err)
assert.Nil(t, nonExistentSystem)
}
func TestDeleteSystem(t *testing.T) {
// Create a test system
createdSystem, _ := CreateSystem("Delete System Test")
// Delete the system
err := DeleteSystem(createdSystem.ID)
assert.NoError(t, err)
// Verify the system is deleted
system, err := GetSystem(createdSystem.ID)
assert.Error(t, err)
assert.Nil(t, system)
// Test deleting a non-existent system
err = DeleteSystem(9999)
assert.NoError(t, err) // GORM doesn't return an error when deleting non-existent records
}

View 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
}

View File

@ -1,6 +1,24 @@
<template> <template>
<v-navigation-drawer v-model="drawer"> <v-navigation-drawer v-model="drawer">
<!-- --> <v-list>
<v-list-item
to="/"
title="Home"
prepend-icon="mdi-home"
/>
<v-list-item
v-if="appStore.user?.admin"
to="/systems"
title="Systems"
prepend-icon="mdi-server"
/>
<v-list-item
v-if="appStore.user?.admin"
to="/users"
title="Users"
prepend-icon="mdi-account-group"
/>
</v-list>
</v-navigation-drawer> </v-navigation-drawer>
<v-app-bar> <v-app-bar>
@ -58,7 +76,15 @@ watch(() => appStore.user, initMenu)
function initMenu() { function initMenu() {
if (appStore.user) { if (appStore.user) {
items.value = [{title: "Log out", link: "/auth/signout"}] items.value = [
{title: "Log out", link: "/auth/signout"}
]
// Add admin links for admin users
if (appStore.user.admin) {
items.value.unshift({title: "Users", link: "/users"})
items.value.unshift({title: "Systems", link: "/systems"})
}
} else { } else {
items.value = [{title: "Sign In", link: "/auth/signin"}] items.value = [{title: "Sign In", link: "/auth/signin"}]
} }

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -38,18 +38,27 @@ router.beforeEach(async (to, from, next) => {
const appStore = useAppStore(); const appStore = useAppStore();
await appStore.updateUser() await appStore.updateUser()
console.log("to", to) // Allow public routes
if (to.meta.public) { if (to.meta.public) {
next() next()
return return
} }
// Redirect to signin if not logged in
if (!appStore.user) { if (!appStore.user) {
next('/auth/signin') next('/auth/signin')
} else { return
next()
} }
// Check if route requires admin access
if (to.meta.requiresAdmin && !appStore.user.admin) {
// Redirect to home if not admin
next('/')
return
}
// Allow access
next()
}) })
export default router export default router

View File

@ -21,5 +21,11 @@ declare module 'vue-router/auto-routes' {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>, '/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/auth/signin': RouteRecordInfo<'/auth/signin', '/auth/signin', Record<never, never>, Record<never, never>>, '/auth/signin': RouteRecordInfo<'/auth/signin', '/auth/signin', Record<never, never>, Record<never, never>>,
'/auth/signout': RouteRecordInfo<'/auth/signout', '/auth/signout', Record<never, never>, Record<never, never>>, '/auth/signout': RouteRecordInfo<'/auth/signout', '/auth/signout', Record<never, never>, Record<never, never>>,
'/systems/': RouteRecordInfo<'/systems/', '/systems', Record<never, never>, Record<never, never>>,
'/systems/[id]': RouteRecordInfo<'/systems/[id]', '/systems/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
'/systems/create': RouteRecordInfo<'/systems/create', '/systems/create', Record<never, never>, Record<never, never>>,
'/users/': RouteRecordInfo<'/users/', '/users', Record<never, never>, Record<never, never>>,
'/users/[id]': RouteRecordInfo<'/users/[id]', '/users/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
'/users/[id]/keys/create': RouteRecordInfo<'/users/[id]/keys/create', '/users/:id/keys/create', { id: ParamValue<true> }, { id: ParamValue<false> }>,
} }
} }