Add an user management page for admins

This commit is contained in:
2026-04-30 23:51:11 +02:00
parent d8d7bc9570
commit e29743d5fa
14 changed files with 262 additions and 4 deletions

View File

@@ -0,0 +1,41 @@
package apitest
import (
"net/http"
"testing"
"git.artlef.fr/bibliomane/internal/dto"
"git.artlef.fr/bibliomane/internal/testutils"
"github.com/stretchr/testify/assert"
)
func TestGetUsersHandler_OK(t *testing.T) {
status, result := testFetchUsers(t, "15", "0")
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, int64(3), result.Count)
assert.Equal(t, 3, len(result.Users))
}
func TestGetUsersHandler_Limit(t *testing.T) {
status, result := testFetchUsers(t, "2", "0")
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, int64(3), result.Count)
assert.Equal(t, 2, len(result.Users))
}
func TestGetUsersHandler_Forbidden(t *testing.T) {
status, _ := testutils.TestFetchModel[dto.UsersGet](t, "/ws/admin/users", "10", "0")
assert.Equal(t, http.StatusForbidden, status)
}
func TestGetUsersHandler_BookCountOK(t *testing.T) {
status, result := testFetchUsers(t, "1", "1")
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, "demo2", result.Users[0].Name)
assert.Equal(t, int64(1), result.Users[0].AddedBooksCount)
assert.Equal(t, int64(3), result.Users[0].UserBooksCount)
}
func testFetchUsers(t *testing.T, limit string, offset string) (int, dto.UsersGet) {
return testutils.TestFetchAdminModel[dto.UsersGet](t, "/ws/admin/users", limit, offset)
}

View File

@@ -73,3 +73,16 @@ type CollectionListBookItemGet struct {
Title string `json:"title"`
CoverPath string `json:"coverPath"`
}
type UsersGet struct {
Count int64 `json:"count"`
Users []UserGet `json:"users"`
}
type UserGet struct {
ID uint `json:"id"`
Name string `json:"name"`
Admin bool `json:"admin"`
AddedBooksCount int64 `json:"addedbookscount"`
UserBooksCount int64 `json:"userbookscount"`
}

View File

@@ -0,0 +1,33 @@
package query
import (
"git.artlef.fr/bibliomane/internal/dto"
"git.artlef.fr/bibliomane/internal/model"
"gorm.io/gorm"
)
func FetchAllUsers(db *gorm.DB, limit int, offset int) ([]dto.UserGet, error) {
var books []dto.UserGet
query := fetchAllUserQuery(db)
query = query.Limit(limit)
query = query.Offset(offset)
query = query.Order("users.id DESC")
res := query.Find(&books)
return books, res.Error
}
func FetchAllUsersCount(db *gorm.DB) (int64, error) {
var count int64
query := fetchAllUserQuery(db)
res := query.Count(&count)
return count, res.Error
}
func fetchAllUserQuery(db *gorm.DB) *gorm.DB {
query := db.Model(&model.User{})
query = query.Select("users.id, users.name, users.admin, count(distinct books.id) as added_books_count, count(distinct user_books.id) as user_books_count")
query = query.Joins("left join books on (books.added_by_id = users.id)")
query = query.Joins("left join user_books on user_books.user_id = users.id")
query = query.Group("users.id, users.name")
return query
}

View File

@@ -48,5 +48,5 @@ func PostLoginHandler(ac appcontext.AppContext) {
gin.H{"error": fmt.Errorf("Error when generating JWT token: %w", err)})
return
}
ac.C.JSON(http.StatusOK, gin.H{"message": i18nresource.GetTranslatedMessage(&ac, "AuthenticationSuccess"), "token": jwtToken})
ac.C.JSON(http.StatusOK, gin.H{"message": i18nresource.GetTranslatedMessage(&ac, "AuthenticationSuccess"), "admin": admin, "token": jwtToken})
}

View File

@@ -0,0 +1,34 @@
package routes
import (
"net/http"
"git.artlef.fr/bibliomane/internal/appcontext"
"git.artlef.fr/bibliomane/internal/dto"
"git.artlef.fr/bibliomane/internal/myvalidator"
"git.artlef.fr/bibliomane/internal/query"
)
func GetUsersHandler(ac appcontext.AppContext) {
limit, err := ac.GetQueryLimit()
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
offset, err := ac.GetQueryOffset()
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
users, err := query.FetchAllUsers(ac.Db, limit, offset)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
count, err := query.FetchAllUsersCount(ac.Db)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
ac.C.JSON(http.StatusOK, dto.UsersGet{Count: count, Users: users})
}

View File

@@ -108,6 +108,10 @@ func Setup(config *config.Config) *gin.Engine {
ws.DELETE("/collection/item/:id", func(c *gin.Context) {
routes.DeleteCollectionBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
ws.GET("/admin/users", func(c *gin.Context) {
routes.GetUsersHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
r.Static("/static/bookcover", config.ImageFolderPath)
folders := []string{"assets", "css", "image"}

View File

@@ -44,6 +44,15 @@ func ConnectDemo2User(router *gin.Engine) string {
return connectUser(router, loginJson)
}
func ConnectAdminUser(router *gin.Engine) string {
loginJson :=
`{
"username": "admin",
"password":"demopw"
}`
return connectUser(router, loginJson)
}
func connectUser(router *gin.Engine, loginJson string) string {
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/ws/auth/login", strings.NewReader(loginJson))
@@ -87,6 +96,18 @@ func TestFetchOneModel[T any](t *testing.T, urlpath string, id string) (int, T)
func TestFetchModel[T any](t *testing.T, urlpath string, limit string, offset string) (int, T) {
router := TestSetup()
token := ConnectDemoUser(router)
return testFetchModelWithUser[T](t, urlpath, limit, offset, token)
}
func TestFetchAdminModel[T any](t *testing.T, urlpath string, limit string, offset string) (int, T) {
router := TestSetup()
token := ConnectAdminUser(router)
return testFetchModelWithUser[T](t, urlpath, limit, offset, token)
}
func testFetchModelWithUser[T any](t *testing.T, urlpath string, limit string, offset string, token string) (int, T) {
router := TestSetup()
u, err := url.Parse(urlpath)
if err != nil {
@@ -107,7 +128,6 @@ func TestFetchModel[T any](t *testing.T, urlpath string, limit string, offset st
q.Set("lang", "fr")
u.RawQuery = q.Encode()
token := ConnectDemoUser(router)
req, _ := http.NewRequest("GET", u.String(), nil)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
w := httptest.NewRecorder()