diff --git a/front/src/AppNavBar.vue b/front/src/AppNavBar.vue
index 9eee178..d4ae3d7 100644
--- a/front/src/AppNavBar.vue
+++ b/front/src/AppNavBar.vue
@@ -100,6 +100,14 @@ onMounted(() => {
{{ $t('navbar.addbook') }}
+
+ {{ $t('navbar.usersmgt') }}
+
{
return extractFormErrorFromField('Password', errors.value)
})
-async function onSubmit(e) {
+async function onSubmit() {
const res = await postLogin(user)
if (res.ok) {
let json = await res.json()
@@ -36,7 +36,7 @@ async function onSubmit(e) {
}
async function login(username, json) {
- useAuthStore().login({ username: username, token: json['token'] })
+ useAuthStore().login({ username: username, admin: json['admin'], token: json['token'] })
}
diff --git a/front/src/UsersManagement.vue b/front/src/UsersManagement.vue
new file mode 100644
index 0000000..7454cf2
--- /dev/null
+++ b/front/src/UsersManagement.vue
@@ -0,0 +1,76 @@
+
+
+
+ {{ $t('usersmanagement.error', { error: error.message }) }}
+
+ {{ $t('usersmanagement.loading') }}
+
+
+
diff --git a/front/src/api.js b/front/src/api.js
index 66c62c8..a3b7cdd 100644
--- a/front/src/api.js
+++ b/front/src/api.js
@@ -118,6 +118,11 @@ export function getBookCall(id) {
return userFetch('/ws/book/' + id)
}
+export function getUsers(data, error, limit, offset) {
+ const queryParams = new URLSearchParams({ limit: limit, offset: offset })
+ return useFetch(data, error, '/ws/admin/users' + '?' + queryParams.toString())
+}
+
export function postBook(book) {
return genericPayloadCall('/ws/book', book.value, 'POST')
}
diff --git a/front/src/locales/en.json b/front/src/locales/en.json
index 4299b33..d37a80b 100644
--- a/front/src/locales/en.json
+++ b/front/src/locales/en.json
@@ -11,6 +11,7 @@
"logout": "Log out",
"signup": "Sign up",
"search": "Search",
+ "usersmgt": "Users Management",
"login": "Log In"
},
"barcode": {
@@ -100,5 +101,15 @@
},
"collection": {
"error": "Error when loading collection: {error}"
+ },
+ "usersmanagement": {
+ "error": "Error when loading users: {error}",
+ "name": "Name",
+ "admin": "Is Admin ?",
+ "addedbooks": "Added Books",
+ "addedbookshelp": "Number of books the user created or imported.",
+ "books": "Books Number",
+ "bookshelp": "Total number of books of the user.",
+ "loading": "Loading..."
}
}
diff --git a/front/src/locales/fr.json b/front/src/locales/fr.json
index 97a73d9..5630e0c 100644
--- a/front/src/locales/fr.json
+++ b/front/src/locales/fr.json
@@ -11,6 +11,7 @@
"logout": "Se déconnecter",
"signup": "S'inscrire",
"search": "Rechercher",
+ "usersmgt": "Gestion Des Utilisateurs",
"login": "Se connecter"
},
"barcode": {
@@ -100,5 +101,15 @@
},
"collection": {
"error": "Erreur pendant le chargement de la liste : {error}"
+ },
+ "usersmanagement": {
+ "error": "Erreur pendant le chargement des utilisateurs: {error}",
+ "name": "Nom",
+ "admin": "Administrateur ?",
+ "addedbooks": "Livres Ajoutés",
+ "addedbookshelp": "Nombre de livres créés ou importés par l'utilisateur.",
+ "books": "Nombre De Livres",
+ "bookshelp": "Le nombre total de livres de cet utilisateur.",
+ "loading": "Chargement..."
}
}
diff --git a/front/src/router.js b/front/src/router.js
index 2232966..414af32 100644
--- a/front/src/router.js
+++ b/front/src/router.js
@@ -13,6 +13,7 @@ import ScanBook from './ScanBook.vue'
import SearchBook from './SearchBook.vue'
import ImportInventaire from './ImportInventaire.vue'
import InstanceBrowser from './InstanceBrowser.vue'
+import UsersManagement from './UsersManagement.vue'
import { useAuthStore } from './auth.store'
const routes = [
@@ -30,6 +31,7 @@ const routes = [
{ path: '/add', component: BookFormEdit },
{ path: '/signup', component: SignUp },
{ path: '/login', component: LogIn },
+ { path: '/admin/users', component: UsersManagement },
]
export const router = createRouter({
diff --git a/internal/apitest/get_users_test.go b/internal/apitest/get_users_test.go
new file mode 100644
index 0000000..e62cce0
--- /dev/null
+++ b/internal/apitest/get_users_test.go
@@ -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)
+}
diff --git a/internal/dto/out.go b/internal/dto/out.go
index 3531139..e7c8317 100644
--- a/internal/dto/out.go
+++ b/internal/dto/out.go
@@ -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"`
+}
diff --git a/internal/query/queryusers.go b/internal/query/queryusers.go
new file mode 100644
index 0000000..56b8389
--- /dev/null
+++ b/internal/query/queryusers.go
@@ -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
+}
diff --git a/internal/routes/userlogin.go b/internal/routes/userlogin.go
index 3b2674e..8d76ecd 100644
--- a/internal/routes/userlogin.go
+++ b/internal/routes/userlogin.go
@@ -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})
}
diff --git a/internal/routes/usersget.go b/internal/routes/usersget.go
new file mode 100644
index 0000000..fd89bc2
--- /dev/null
+++ b/internal/routes/usersget.go
@@ -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})
+}
diff --git a/internal/setup/setup.go b/internal/setup/setup.go
index 0ce880f..3b325bb 100644
--- a/internal/setup/setup.go
+++ b/internal/setup/setup.go
@@ -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"}
diff --git a/internal/testutils/testutils.go b/internal/testutils/testutils.go
index 850da81..0919af0 100644
--- a/internal/testutils/testutils.go
+++ b/internal/testutils/testutils.go
@@ -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()