Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e29743d5fa | |||
| d8d7bc9570 | |||
| ff8604eac1 | |||
| d5281e7d57 | |||
| ed99ee772a | |||
| 2f5fc3d0a3 | |||
| b48c42c40c | |||
| 08a273b500 | |||
| 1ae76ed525 | |||
| 7d867af654 | |||
| 11a23d174e | |||
| e746e67e89 | |||
| b47b09eb85 |
@@ -28,6 +28,8 @@ Or with a volume, for example if you created a volume named `bibliomane_data`:
|
|||||||
|
|
||||||
`--add-user` or `-a` can be used to create an account on startup. It requires a string following htpasswd format `[username]:[bcrypt hashed password]`.
|
`--add-user` or `-a` can be used to create an account on startup. It requires a string following htpasswd format `[username]:[bcrypt hashed password]`.
|
||||||
|
|
||||||
|
Use `--add-admin-user` or `-A` instead to add an user as an admin.
|
||||||
|
|
||||||
The password can be generated using `htpasswd -nBC10 [username]`.
|
The password can be generated using `htpasswd -nBC10 [username]`.
|
||||||
|
|
||||||
For example, to create an user account `demo`:
|
For example, to create an user account `demo`:
|
||||||
|
|||||||
15
demodata.sql
15
demodata.sql
@@ -1,6 +1,7 @@
|
|||||||
--users
|
--users
|
||||||
INSERT INTO users(created_at, name, password) VALUES ('NOW', 'demo','$2a$10$7mfCBxBwIzXDU6r9az26o.zPX/r6IlNZVfU9zxSoLVtc0kRPimzba');
|
INSERT INTO users(created_at, name, password) VALUES ('NOW', 'demo','$2a$10$7mfCBxBwIzXDU6r9az26o.zPX/r6IlNZVfU9zxSoLVtc0kRPimzba');
|
||||||
INSERT INTO users(created_at, name, password) VALUES ('NOW', 'demo2','$2a$10$7mfCBxBwIzXDU6r9az26o.zPX/r6IlNZVfU9zxSoLVtc0kRPimzba');
|
INSERT INTO users(created_at, name, password) VALUES ('NOW', 'demo2','$2a$10$7mfCBxBwIzXDU6r9az26o.zPX/r6IlNZVfU9zxSoLVtc0kRPimzba');
|
||||||
|
INSERT INTO users(created_at, name, admin, password) VALUES ('NOW', 'admin', 'true', '$2a$10$7mfCBxBwIzXDU6r9az26o.zPX/r6IlNZVfU9zxSoLVtc0kRPimzba');
|
||||||
|
|
||||||
-- cover
|
-- cover
|
||||||
INSERT INTO static_files(name, path) VALUES ('odingosochateaux.jpg', 'odingosochateaux.jpg');
|
INSERT INTO static_files(name, path) VALUES ('odingosochateaux.jpg', 'odingosochateaux.jpg');
|
||||||
@@ -132,6 +133,9 @@ INSERT INTO collections(name, user_id) VALUES ('Nouvelles',(SELECT id FROM users
|
|||||||
INSERT INTO collections(name, user_id) VALUES ('Non fiction',(SELECT id FROM users WHERE name = 'demo2'));
|
INSERT INTO collections(name, user_id) VALUES ('Non fiction',(SELECT id FROM users WHERE name = 'demo2'));
|
||||||
INSERT INTO collections(name, user_id) VALUES ('Empty',(SELECT id FROM users WHERE name = 'demo'));
|
INSERT INTO collections(name, user_id) VALUES ('Empty',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
INSERT INTO collections(name, user_id) VALUES ('Lu récemment',(SELECT id FROM users WHERE name = 'demo'));
|
INSERT INTO collections(name, user_id) VALUES ('Lu récemment',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
|
INSERT INTO collections(name, user_id) VALUES ('Brouillon',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
|
INSERT INTO collections(name, user_id) VALUES ('Traduit de l''anglais',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
|
INSERT INTO collections(name, user_id) VALUES ('À supprimer',(SELECT id FROM users WHERE name = 'demo'));
|
||||||
|
|
||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Nord'), 1);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Nord'), 1);
|
||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Gargantua'), 2);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Littérature française'), (SELECT id FROM books WHERE title = 'Gargantua'), 2);
|
||||||
@@ -157,3 +161,14 @@ INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT i
|
|||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Recherches philosophiques'), 6);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Recherches philosophiques'), 6);
|
||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Dojoji et autres nouvelles'), 7);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Dojoji et autres nouvelles'), 7);
|
||||||
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Le château'), 8);
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Lu récemment'), (SELECT id FROM books WHERE title = 'Le château'), 8);
|
||||||
|
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Nord'), 1);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Gargantua'), 2);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Duo'), 3);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Un barrage contre le Pacifique'), 4);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Rigodon'), 5);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Brouillon'), (SELECT id FROM books WHERE title = 'Les dieux ont soif'), 6);
|
||||||
|
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Traduit de l''anglais'), (SELECT id FROM books WHERE title = 'Sa majesté des mouches'), 1);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Traduit de l''anglais'), (SELECT id FROM books WHERE title = 'Le complot contre l''Amérique'), 2);
|
||||||
|
INSERT INTO collection_items(collection_id, book_id, position) VALUES ((SELECT id FROM collections WHERE name = 'Traduit de l''anglais'), (SELECT id FROM books WHERE title = 'De sang-froid'), 3);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bibliomane",
|
"name": "bibliomane",
|
||||||
"version": "0.7.1",
|
"version": "0.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -100,6 +100,14 @@ onMounted(() => {
|
|||||||
<RouterLink v-if="authStore.user" to="/add" class="navbar-item" activeClass="is-active">
|
<RouterLink v-if="authStore.user" to="/add" class="navbar-item" activeClass="is-active">
|
||||||
{{ $t('navbar.addbook') }}
|
{{ $t('navbar.addbook') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink
|
||||||
|
v-if="authStore.user && authStore.user.admin"
|
||||||
|
to="/admin/users"
|
||||||
|
class="navbar-item"
|
||||||
|
activeClass="is-active"
|
||||||
|
>
|
||||||
|
{{ $t('navbar.usersmgt') }}
|
||||||
|
</RouterLink>
|
||||||
<div
|
<div
|
||||||
v-if="authStore.user && appInfo && !appInfo.demoMode"
|
v-if="authStore.user && appInfo && !appInfo.demoMode"
|
||||||
class="navbar-item is-hidden-desktop"
|
class="navbar-item is-hidden-desktop"
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ async function importInventaireEdition(inventaireid) {
|
|||||||
<p>{{ error }}</p>
|
<p>{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns no-padding box container has-background-dark">
|
<div class="columns no-padding box container has-background-dark">
|
||||||
|
<slot name="left"></slot>
|
||||||
<div class="media column no-margin clickable" @click="openBook">
|
<div class="media column no-margin clickable" @click="openBook">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image mb-3">
|
<figure class="image mb-3">
|
||||||
@@ -164,7 +165,7 @@ async function importInventaireEdition(inventaireid) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot name="right"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -208,5 +209,12 @@ img {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 100px;
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
import { getCollection, postCollectionChangePosition } from './api.js'
|
import { getCollection, postCollectionChangePosition, deleteCollectionItem } from './api.js'
|
||||||
import CollectionFormElement from './CollectionFormElement.vue'
|
import CollectionFormElement from './CollectionFormElement.vue'
|
||||||
import AddBookToCollection from './AddBookToCollection.vue'
|
import AddBookToCollection from './AddBookToCollection.vue'
|
||||||
import Pagination from './Pagination.vue'
|
import Pagination from './Pagination.vue'
|
||||||
@@ -17,6 +17,8 @@ const offset = computed(() => (pageNumber.value - 1) * limit)
|
|||||||
const data = ref(null)
|
const data = ref(null)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|
||||||
|
const itemRefs = useTemplateRef('items')
|
||||||
|
|
||||||
const itemIdBeingGrabbed = ref(null)
|
const itemIdBeingGrabbed = ref(null)
|
||||||
|
|
||||||
const itemIdBeingOvered = ref(null)
|
const itemIdBeingOvered = ref(null)
|
||||||
@@ -40,25 +42,42 @@ function fetchCollection() {
|
|||||||
pageChange(1)
|
pageChange(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragStart(event, id) {
|
function checkGrabbedPosition(itemId, y) {
|
||||||
event.dataTransfer.effectAllowed = 'move'
|
const itemBeingMoved = itemRefs.value.find((it) => it.id == itemId)
|
||||||
// Custom type to identify a collectionitem drag
|
const itemMovedY = itemBeingMoved.$el.offsetTop + y + itemBeingMoved.$el.offsetHeight / 2
|
||||||
event.dataTransfer.setData('collectionitem', '')
|
itemRefs.value.forEach((it) => {
|
||||||
itemIdBeingGrabbed.value = id
|
if (it.$props.id == itemIdBeingGrabbed.value) {
|
||||||
}
|
|
||||||
|
|
||||||
function onDragover(event) {
|
|
||||||
if (event.dataTransfer.types.includes('collectionitem')) {
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDrop(id, position) {
|
|
||||||
if (id == itemIdBeingGrabbed.value) {
|
|
||||||
//nothing to do
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let lowerDetectionY = it.$el.offsetTop + it.$el.offsetHeight / 2
|
||||||
|
let upperDetectionY = it.$el.offsetTop + (3 * it.$el.offsetHeight) / 2
|
||||||
|
if (it.$props.position < itemBeingMoved.$props.position) {
|
||||||
|
lowerDetectionY = it.$el.offsetTop
|
||||||
|
upperDetectionY = it.$el.offsetTop + it.$el.offsetHeight / 2
|
||||||
|
}
|
||||||
|
if (lowerDetectionY < itemMovedY && itemMovedY < upperDetectionY) {
|
||||||
|
itemIdBeingOvered.value = it.$props.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStopGrab() {
|
||||||
|
if (itemIdBeingOvered.value != null) {
|
||||||
|
const position = data.value.items.find((it) => it.id == itemIdBeingOvered.value).position
|
||||||
changePosition(itemIdBeingGrabbed.value, position)
|
changePosition(itemIdBeingGrabbed.value, position)
|
||||||
|
}
|
||||||
|
itemIdBeingGrabbed.value = null
|
||||||
|
itemIdBeingOvered.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDragoverFromAbove(position) {
|
||||||
|
if (itemIdBeingGrabbed.value == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const grabbedItemPosition = data.value.items.find(
|
||||||
|
(it) => it.id == itemIdBeingGrabbed.value,
|
||||||
|
).position
|
||||||
|
return position > grabbedItemPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePosition(id, position) {
|
function changePosition(id, position) {
|
||||||
@@ -73,9 +92,16 @@ function changePosition(id, position) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragend() {
|
function deleteItem(id) {
|
||||||
itemIdBeingGrabbed.value = null
|
deleteCollectionItem(id).then((res) => {
|
||||||
itemIdBeingOvered.value = null
|
if (res.ok) {
|
||||||
|
getCollection(data, error, props.id, limit, offset.value)
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => {
|
||||||
|
error.value = json
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -84,20 +110,21 @@ function onDragend() {
|
|||||||
<div v-if="data">
|
<div v-if="data">
|
||||||
<h2 class="title">{{ data.name }}</h2>
|
<h2 class="title">{{ data.name }}</h2>
|
||||||
<AddBookToCollection :collection-id="props.id" @created="fetchCollection" />
|
<AddBookToCollection :collection-id="props.id" @created="fetchCollection" />
|
||||||
<div>
|
<TransitionGroup name="list" tag="div">
|
||||||
<CollectionFormElement
|
<CollectionFormElement
|
||||||
@drop="onDrop(item.id, item.position)"
|
|
||||||
@dragstart="(e) => onDragStart(e, item.id)"
|
|
||||||
@dragend="onDragend"
|
|
||||||
@dragover="onDragover"
|
|
||||||
@dragenter="itemIdBeingOvered = item.id"
|
|
||||||
@positionchange="(pos) => changePosition(item.id, pos)"
|
@positionchange="(pos) => changePosition(item.id, pos)"
|
||||||
|
@startgrab="itemIdBeingGrabbed = item.id"
|
||||||
|
@grabbing="(y) => checkGrabbedPosition(item.id, y)"
|
||||||
|
@stopgrab="onStopGrab"
|
||||||
|
@delete="deleteItem(item.id)"
|
||||||
v-for="item in data.items"
|
v-for="item in data.items"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:is-dragover="itemIdBeingOvered === item.id"
|
|
||||||
v-bind="item"
|
v-bind="item"
|
||||||
|
ref="items"
|
||||||
|
:is-dragover="item.id == itemIdBeingOvered"
|
||||||
|
:is-dragover-from-above="isDragoverFromAbove(item.position)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</TransitionGroup>
|
||||||
<Pagination
|
<Pagination
|
||||||
class="mt-5"
|
class="mt-5"
|
||||||
:pageNumber="pageNumber"
|
:pageNumber="pageNumber"
|
||||||
@@ -108,4 +135,16 @@ function onDragend() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style></style>
|
<style scoped>
|
||||||
|
.list-move, /* apply transition to moving elements */
|
||||||
|
.list-enter-active,
|
||||||
|
.list-leave-active {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-enter-from,
|
||||||
|
.list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { ref } from 'vue'
|
|||||||
import BookListElement from './BookListElement.vue'
|
import BookListElement from './BookListElement.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isDragover: Boolean,
|
|
||||||
id: Number,
|
id: Number,
|
||||||
position: Number,
|
position: Number,
|
||||||
book: Array,
|
book: Array,
|
||||||
|
isDragover: Boolean,
|
||||||
|
isDragoverFromAbove: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits('positionchange')
|
const emit = defineEmits(['positionchange', 'startgrab', 'stopgrab', 'grabbing', 'delete'])
|
||||||
|
|
||||||
const vFocus = {
|
const vFocus = {
|
||||||
mounted: (el) => el.focus(),
|
mounted: (el) => el.focus(),
|
||||||
@@ -18,6 +19,10 @@ const vFocus = {
|
|||||||
const isInputtingPosition = ref(false)
|
const isInputtingPosition = ref(false)
|
||||||
const inputtedPosition = ref('')
|
const inputtedPosition = ref('')
|
||||||
|
|
||||||
|
const initialGrabPosition = ref(null)
|
||||||
|
|
||||||
|
const draggedPosition = ref(null)
|
||||||
|
|
||||||
function onPositionInput() {
|
function onPositionInput() {
|
||||||
if (inputtedPosition.value != '' && !isNaN(inputtedPosition.value)) {
|
if (inputtedPosition.value != '' && !isNaN(inputtedPosition.value)) {
|
||||||
const parsedPosition = parseInt(inputtedPosition.value)
|
const parsedPosition = parseInt(inputtedPosition.value)
|
||||||
@@ -32,12 +37,55 @@ function clearPositionInput() {
|
|||||||
isInputtingPosition.value = false
|
isInputtingPosition.value = false
|
||||||
inputtedPosition.value = ''
|
inputtedPosition.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearGrabVariables() {
|
||||||
|
initialGrabPosition.value = null
|
||||||
|
draggedPosition.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerUp() {
|
||||||
|
clearGrabVariables()
|
||||||
|
emit('stopgrab')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerDown(e) {
|
||||||
|
initialGrabPosition.value = e.pageY
|
||||||
|
e.preventDefault()
|
||||||
|
emit('startgrab')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerMove(e) {
|
||||||
|
if (initialGrabPosition.value == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
draggedPosition.value = e.pageY - initialGrabPosition.value
|
||||||
|
emit('grabbing', draggedPosition.value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="isDragover ? 'dragover' : ''" draggable="true" class="collectionitembox">
|
<div>
|
||||||
|
<div v-if="isDragover && !isDragoverFromAbove" class="dragover" />
|
||||||
|
<div
|
||||||
|
:style="
|
||||||
|
draggedPosition
|
||||||
|
? 'transform: translateY(' + draggedPosition + 'px);position:relative;z-index:3'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
ref="collectionitembox"
|
||||||
|
class="collectionitembox"
|
||||||
|
@pointermove.prevent="onPointerMove"
|
||||||
|
@pointerup="onPointerUp"
|
||||||
|
@pointerleave="clearGrabVariables"
|
||||||
|
>
|
||||||
<BookListElement v-bind="props.book">
|
<BookListElement v-bind="props.book">
|
||||||
<div class="separator" />
|
<template v-slot:left>
|
||||||
<div class="centered">
|
<div class="is-hidden-desktop mobile-delete">
|
||||||
|
<div @click="$emit('delete')" class="centered closebtn clickable">
|
||||||
|
<b-icon-x />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inputpositionwidget centered">
|
||||||
<div
|
<div
|
||||||
v-if="!isInputtingPosition"
|
v-if="!isInputtingPosition"
|
||||||
@click="isInputtingPosition = true"
|
@click="isInputtingPosition = true"
|
||||||
@@ -59,17 +107,27 @@ function clearPositionInput() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
<div class="positionwidget centered is-narrow">
|
</template>
|
||||||
|
<template v-slot:right>
|
||||||
|
<div class="separator" />
|
||||||
|
<div class="positionwidget centered is-narrow" @pointerdown="onPointerDown">
|
||||||
<b-icon-list />
|
<b-icon-list />
|
||||||
</div>
|
</div>
|
||||||
|
<div @click="$emit('delete')" class="is-hidden-touch centered closebtn clickable">
|
||||||
|
<b-icon-x />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</BookListElement>
|
</BookListElement>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isDragover && isDragoverFromAbove" class="dragover" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.collectionitembox {
|
.collectionitembox {
|
||||||
transition: ease-in-out 0.04s;
|
transition: ease-in-out 0.04s;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collectionitembox:hover {
|
.collectionitembox:hover {
|
||||||
@@ -102,10 +160,10 @@ function clearPositionInput() {
|
|||||||
color: var(--bulma-scheme-main);
|
color: var(--bulma-scheme-main);
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
margin-left: 30px;
|
margin-left: 30px;
|
||||||
margin-right: 30px;
|
|
||||||
border-top-right-radius: var(--bulma-box-radius);
|
border-top-right-radius: var(--bulma-box-radius);
|
||||||
border-bottom-right-radius: var(--bulma-box-radius);
|
border-bottom-right-radius: var(--bulma-box-radius);
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.positionwidget:active {
|
.positionwidget:active {
|
||||||
@@ -116,4 +174,23 @@ function clearPositionInput() {
|
|||||||
border: 3px solid var(--bulma-primary);
|
border: 3px solid var(--bulma-primary);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.closebtn {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-delete {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.positionwidget {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { getImagePathOrDefault } from './api.js'
|
import { getImagePathOrDefault } from './api.js'
|
||||||
|
import { useTemplateRef } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const emit = defineEmits(['delete'])
|
||||||
|
|
||||||
|
const closeButtonDesktop = useTemplateRef('closeDesktop')
|
||||||
|
const closeButtonMobile = useTemplateRef('closeMobile')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: Number,
|
id: Number,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -11,36 +17,62 @@ const props = defineProps({
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
function goToCollection() {
|
function onClick(e) {
|
||||||
const collectionId = props.id
|
if (
|
||||||
|
(closeButtonDesktop.value && closeButtonDesktop.value.contains(e.target)) ||
|
||||||
|
(closeButtonMobile.value && closeButtonMobile.value.contains(e.target))
|
||||||
|
) {
|
||||||
|
emit('delete')
|
||||||
|
} else {
|
||||||
router.push(`/collection/${props.id}`)
|
router.push(`/collection/${props.id}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBookOpacity(index) {
|
function setBookOpacityClass(index) {
|
||||||
if (props.books.length > 7 && index > 3) {
|
const length = props.books.length
|
||||||
return 'opacity: ' + (100 - index * 13) + '%;'
|
if (length < 5) {
|
||||||
|
return ''
|
||||||
|
} else if (length < 8) {
|
||||||
|
if (index < 4) {
|
||||||
|
return 'opacity-' + index
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return 'opacity-' + index
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="collectioncontainer has-background-dark p-2" @click="goToCollection">
|
<div class="collectioncontainer has-background-dark p-2" @click="onClick">
|
||||||
<div class="collectionheader">
|
<div class="collectionheader">
|
||||||
<h2 class="subtitle">
|
<h2 class="namecontainer subtitle">
|
||||||
{{ props.name }}
|
{{ props.name }}
|
||||||
</h2>
|
</h2>
|
||||||
|
<div class="is-hidden-desktop align-right">
|
||||||
|
<div ref="closeMobile" @click="$emit('delete')" class="centered closebtn clickable">
|
||||||
|
<b-icon-x />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="collectionpreviewbooks" v-if="props.books && props.books.length > 0">
|
<div class="collectionpreviewbooks" v-if="props.books && props.books.length > 0">
|
||||||
<div class="bookpreview mx-1" v-for="(book, index) in props.books" :key="book.id">
|
<div
|
||||||
|
:class="index > 3 ? 'is-hidden-touch' : ''"
|
||||||
|
class="bookpreview mx-1"
|
||||||
|
v-for="(book, index) in props.books"
|
||||||
|
:key="book.id"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
:style="setBookOpacity(index)"
|
:class="setBookOpacityClass(index)"
|
||||||
v-bind:src="getImagePathOrDefault(book.coverPath)"
|
v-bind:src="getImagePathOrDefault(book.coverPath)"
|
||||||
v-bind:alt="book.title"
|
v-bind:alt="book.title"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div ref="closeDesktop" class="is-hidden-touch centered closebtn clickable">
|
||||||
|
<b-icon-x />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -63,25 +95,88 @@ img {
|
|||||||
transition: ease-in-out 0.02s;
|
transition: ease-in-out 0.02s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.namecontainer {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.collectionheader {
|
.collectionheader {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collectionpreviewbooks {
|
.collectionpreviewbooks {
|
||||||
flex: 6;
|
flex: 6;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-1 {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-2 {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-3 {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-4 {
|
||||||
|
opacity: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-5 {
|
||||||
|
opacity: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-6 {
|
||||||
|
opacity: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-7 {
|
||||||
|
opacity: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closebtn {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
img {
|
img {
|
||||||
max-height: 50px;
|
max-height: 75px;
|
||||||
max-width: 50px;
|
max-width: 75px;
|
||||||
}
|
}
|
||||||
.collectionpreviewbooks {
|
.collectionpreviewbooks {
|
||||||
flex: 2;
|
flex: 1;
|
||||||
|
margin-top: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.collectioncontainer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-1 {
|
||||||
|
opacity: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-2 {
|
||||||
|
opacity: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-3 {
|
||||||
|
opacity: 10%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { getCollections } from './api.js'
|
import { getCollections, deleteCollection } from './api.js'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import CollectionListElement from './CollectionListElement.vue'
|
import CollectionListElement from './CollectionListElement.vue'
|
||||||
import Pagination from './Pagination.vue'
|
import Pagination from './Pagination.vue'
|
||||||
@@ -35,6 +35,18 @@ function pageChange(newPageNumber) {
|
|||||||
function goToCollection(id) {
|
function goToCollection(id) {
|
||||||
router.push(`/collection/${id}`)
|
router.push(`/collection/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeList(id) {
|
||||||
|
deleteCollection(id).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
fetchData()
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => {
|
||||||
|
error.value = json
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -44,7 +56,7 @@ function goToCollection(id) {
|
|||||||
<AddCollection @created="goToCollection" />
|
<AddCollection @created="goToCollection" />
|
||||||
<div class="collectionslist">
|
<div class="collectionslist">
|
||||||
<div class="my-2" v-for="collection in data.collections" :key="collection.id">
|
<div class="my-2" v-for="collection in data.collections" :key="collection.id">
|
||||||
<CollectionListElement v-bind="collection" />
|
<CollectionListElement @delete="removeList(collection.id)" v-bind="collection" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const passwordError = computed(() => {
|
|||||||
return extractFormErrorFromField('Password', errors.value)
|
return extractFormErrorFromField('Password', errors.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function onSubmit(e) {
|
async function onSubmit() {
|
||||||
const res = await postLogin(user)
|
const res = await postLogin(user)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let json = await res.json()
|
let json = await res.json()
|
||||||
@@ -36,7 +36,7 @@ async function onSubmit(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login(username, json) {
|
async function login(username, json) {
|
||||||
useAuthStore().login({ username: username, token: json['token'] })
|
useAuthStore().login({ username: username, admin: json['admin'], token: json['token'] })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
76
front/src/UsersManagement.vue
Normal file
76
front/src/UsersManagement.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { getUsers } from './api.js'
|
||||||
|
|
||||||
|
const limit = 50
|
||||||
|
const pageNumber = ref(1)
|
||||||
|
|
||||||
|
const offset = computed(() => (pageNumber.value - 1) * limit)
|
||||||
|
|
||||||
|
const data = ref(null)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
let totalUsersNumber = computed(() =>
|
||||||
|
typeof data != 'undefined' && data.value != null ? data.value['count'] : 0,
|
||||||
|
)
|
||||||
|
let pageTotal = computed(() => Math.ceil(totalUsersNumber.value / limit))
|
||||||
|
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
function fetchData() {
|
||||||
|
getUsers(data, error, limit, offset.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageChange(newPageNumber) {
|
||||||
|
pageNumber.value = newPageNumber
|
||||||
|
data.value = null
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="error">{{ $t('usersmanagement.error', { error: error.message }) }}</div>
|
||||||
|
<div v-else-if="data">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<th>#</th>
|
||||||
|
<th>{{ $t('usersmanagement.name') }}</th>
|
||||||
|
<th>{{ $t('usersmanagement.admin') }}</th>
|
||||||
|
<th>
|
||||||
|
<abbr :title="$t('usersmanagement.addedbookshelp')">{{
|
||||||
|
$t('usersmanagement.addedbooks')
|
||||||
|
}}</abbr>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<abbr :title="$t('usersmanagement.bookshelp')">{{ $t('usersmanagement.books') }}</abbr>
|
||||||
|
</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="user in data.users" :key="user.id">
|
||||||
|
<th class="numbercell">{{ user.id }}</th>
|
||||||
|
<td>{{ user.name }}</td>
|
||||||
|
<td class="boolcell"><input type="checkbox" disabled :checked="user.admin" /></td>
|
||||||
|
<td class="numbercell">{{ user.addedbookscount }}</td>
|
||||||
|
<td class="numbercell">{{ user.userbookscount }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<Pagination
|
||||||
|
:pageNumber="pageNumber"
|
||||||
|
:pageTotal="pageTotal"
|
||||||
|
maxItemDisplayed="11"
|
||||||
|
@pageChange="pageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>{{ $t('usersmanagement.loading') }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.boolcell {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.numbercell {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -118,6 +118,11 @@ export function getBookCall(id) {
|
|||||||
return userFetch('/ws/book/' + 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) {
|
export function postBook(book) {
|
||||||
return genericPayloadCall('/ws/book', book.value, 'POST')
|
return genericPayloadCall('/ws/book', book.value, 'POST')
|
||||||
}
|
}
|
||||||
@@ -146,6 +151,14 @@ export function postCollectionChangePosition(collectionId, itemId, position) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteCollectionItem(itemId) {
|
||||||
|
return deleteApiCall('/ws/collection/item/' + itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCollection(id) {
|
||||||
|
return deleteApiCall('/ws/collection/' + id)
|
||||||
|
}
|
||||||
|
|
||||||
export function putBook(id, book) {
|
export function putBook(id, book) {
|
||||||
return genericPayloadCall('/ws/book/edit/' + id, book.value, 'PUT')
|
return genericPayloadCall('/ws/book/edit/' + id, book.value, 'PUT')
|
||||||
}
|
}
|
||||||
@@ -234,6 +247,22 @@ export function genericPayloadCall(apiRoute, object, method) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteApiCall(apiRoute) {
|
||||||
|
const { user } = useAuthStore()
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
return fetch(apiRoute, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + user.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function extractFormErrorFromField(fieldName, errors) {
|
export function extractFormErrorFromField(fieldName, errors) {
|
||||||
if (errors == null || typeof errors == 'undefined' || !Array.isArray(errors)) {
|
if (errors == null || typeof errors == 'undefined' || !Array.isArray(errors)) {
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"logout": "Log out",
|
"logout": "Log out",
|
||||||
"signup": "Sign up",
|
"signup": "Sign up",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
|
"usersmgt": "Users Management",
|
||||||
"login": "Log In"
|
"login": "Log In"
|
||||||
},
|
},
|
||||||
"barcode": {
|
"barcode": {
|
||||||
@@ -100,5 +101,15 @@
|
|||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"error": "Error when loading collection: {error}"
|
"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..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"logout": "Se déconnecter",
|
"logout": "Se déconnecter",
|
||||||
"signup": "S'inscrire",
|
"signup": "S'inscrire",
|
||||||
"search": "Rechercher",
|
"search": "Rechercher",
|
||||||
|
"usersmgt": "Gestion Des Utilisateurs",
|
||||||
"login": "Se connecter"
|
"login": "Se connecter"
|
||||||
},
|
},
|
||||||
"barcode": {
|
"barcode": {
|
||||||
@@ -100,5 +101,15 @@
|
|||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"error": "Erreur pendant le chargement de la liste : {error}"
|
"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..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import ScanBook from './ScanBook.vue'
|
|||||||
import SearchBook from './SearchBook.vue'
|
import SearchBook from './SearchBook.vue'
|
||||||
import ImportInventaire from './ImportInventaire.vue'
|
import ImportInventaire from './ImportInventaire.vue'
|
||||||
import InstanceBrowser from './InstanceBrowser.vue'
|
import InstanceBrowser from './InstanceBrowser.vue'
|
||||||
|
import UsersManagement from './UsersManagement.vue'
|
||||||
import { useAuthStore } from './auth.store'
|
import { useAuthStore } from './auth.store'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -30,6 +31,7 @@ const routes = [
|
|||||||
{ path: '/add', component: BookFormEdit },
|
{ path: '/add', component: BookFormEdit },
|
||||||
{ path: '/signup', component: SignUp },
|
{ path: '/signup', component: SignUp },
|
||||||
{ path: '/login', component: LogIn },
|
{ path: '/login', component: LogIn },
|
||||||
|
{ path: '/admin/users', component: UsersManagement },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
|||||||
45
internal/apitest/delete_collection_element_test.go
Normal file
45
internal/apitest/delete_collection_element_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeleteCollectionBookHandler_DelOk(t *testing.T) {
|
||||||
|
status := testDeleteCollectionBookHandler("23")
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
_, collection := testGetCollection(t, "6", "10", "0")
|
||||||
|
assert.Equal(t, int64(5), collection.Count)
|
||||||
|
assert.Equal(t, uint(1), collection.Items[0].Position)
|
||||||
|
assert.Equal(t, uint(2), collection.Items[1].Position)
|
||||||
|
assert.Equal(t, uint(3), collection.Items[2].Position)
|
||||||
|
assert.Equal(t, uint(4), collection.Items[3].Position)
|
||||||
|
assert.Equal(t, uint(5), collection.Items[4].Position)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCollectionBookHandler_NotFound(t *testing.T) {
|
||||||
|
status := testDeleteCollectionBookHandler("425")
|
||||||
|
assert.Equal(t, http.StatusNotFound, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCollectionBookHandler_Unauthorized(t *testing.T) {
|
||||||
|
status := testDeleteCollectionBookHandler("11")
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeleteCollectionBookHandler(itemId string) int {
|
||||||
|
router := testutils.TestSetup()
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
token := testutils.ConnectDemoUser(router)
|
||||||
|
req, _ := http.NewRequest("DELETE", "/ws/collection/item/"+itemId, nil)
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
return w.Code
|
||||||
|
}
|
||||||
51
internal/apitest/delete_collection_test.go
Normal file
51
internal/apitest/delete_collection_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeleteCollectionHandler_EmptyCollection(t *testing.T) {
|
||||||
|
collectionId := "8"
|
||||||
|
status := testDeleteCollectionHandler(collectionId)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
getStatus, _ := testGetCollection(t, collectionId, "10", "0")
|
||||||
|
assert.Equal(t, http.StatusNotFound, getStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCollectionHandler_NotEmptyCollection(t *testing.T) {
|
||||||
|
collectionId := "7"
|
||||||
|
status := testDeleteCollectionHandler(collectionId)
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
getStatus, _ := testGetCollection(t, collectionId, "10", "0")
|
||||||
|
assert.Equal(t, http.StatusNotFound, getStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCollectionHandler_NonExistingCollection(t *testing.T) {
|
||||||
|
collectionId := "425"
|
||||||
|
status := testDeleteCollectionHandler(collectionId)
|
||||||
|
assert.Equal(t, http.StatusNotFound, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCollectionHandler_ForbiddenCollection(t *testing.T) {
|
||||||
|
collectionId := "3"
|
||||||
|
status := testDeleteCollectionHandler(collectionId)
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeleteCollectionHandler(id string) int {
|
||||||
|
router := testutils.TestSetup()
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
token := testutils.ConnectDemoUser(router)
|
||||||
|
req, _ := http.NewRequest("DELETE", "/ws/collection/"+id, nil)
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
return w.Code
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
func TestFetchAllCollections_OK(t *testing.T) {
|
func TestFetchAllCollections_OK(t *testing.T) {
|
||||||
status, res := testFetchCollections(t, "10", "0")
|
status, res := testFetchCollections(t, "10", "0")
|
||||||
assert.Equal(t, http.StatusOK, status)
|
assert.Equal(t, http.StatusOK, status)
|
||||||
assert.Equal(t, int64(4), res.Count)
|
assert.Equal(t, int64(5), res.Count)
|
||||||
assert.Equal(t, 4, len(res.Collections))
|
assert.Equal(t, 5, len(res.Collections))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFetchCollections(t *testing.T, limit string, offset string) (int, dto.CollectionItemsGet) {
|
func testFetchCollections(t *testing.T, limit string, offset string) (int, dto.CollectionItemsGet) {
|
||||||
|
|||||||
@@ -28,4 +28,5 @@ func TestGetAppInfo_Ok(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, false, appInfo.RegistrationDisabled)
|
assert.Equal(t, false, appInfo.RegistrationDisabled)
|
||||||
assert.Equal(t, false, appInfo.DemoMode)
|
assert.Equal(t, false, appInfo.DemoMode)
|
||||||
|
assert.Equal(t, false, appInfo.Admin)
|
||||||
}
|
}
|
||||||
|
|||||||
41
internal/apitest/get_users_test.go
Normal file
41
internal/apitest/get_users_test.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ func TestPostImportBookHandler_Ok(t *testing.T) {
|
|||||||
book := testGetBook(t, strconv.FormatUint(uint64(id), 10), 200)
|
book := testGetBook(t, strconv.FormatUint(uint64(id), 10), 200)
|
||||||
assert.Equal(t, "les Hauts de Hurle-Vent", book.Title)
|
assert.Equal(t, "les Hauts de Hurle-Vent", book.Title)
|
||||||
assert.Equal(t, "Emily Brontë", book.Author)
|
assert.Equal(t, "Emily Brontë", book.Author)
|
||||||
assert.Equal(t, "isbn:9782253004752", book.InventaireId)
|
assert.Equal(t, "inv:31cee958a93ba50697a3fec2a360f437", book.InventaireId)
|
||||||
assert.Equal(t, "/static/bookcover/44abbcbdc1092212c2bae66f5165019dac1e2a7b.webp", book.CoverPath)
|
assert.Equal(t, "/static/bookcover/44abbcbdc1092212c2bae66f5165019dac1e2a7b.webp", book.CoverPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ func TestPostImportBookHandler_OkAuthorKey(t *testing.T) {
|
|||||||
book := testGetBook(t, strconv.FormatUint(uint64(id), 10), 200)
|
book := testGetBook(t, strconv.FormatUint(uint64(id), 10), 200)
|
||||||
assert.Equal(t, "Dr Bloodmoney", book.Title)
|
assert.Equal(t, "Dr Bloodmoney", book.Title)
|
||||||
assert.Equal(t, "Philip K. Dick", book.Author)
|
assert.Equal(t, "Philip K. Dick", book.Author)
|
||||||
assert.Equal(t, "isbn:9782290033630", book.InventaireId)
|
assert.Equal(t, "inv:ba7d376567114bd69a6c8f0a135a89d0", book.InventaireId)
|
||||||
assert.Equal(t, "/static/bookcover/1d1493159d031224a42b37c4417fcbb8c76b00bd.webp", book.CoverPath)
|
assert.Equal(t, "/static/bookcover/1d1493159d031224a42b37c4417fcbb8c76b00bd.webp", book.CoverPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func TestSearchBook_ISBNInventaire(t *testing.T) {
|
|||||||
Title: "Les premières enquêtes de Maigret",
|
Title: "Les premières enquêtes de Maigret",
|
||||||
Author: "Georges Simenon",
|
Author: "Georges Simenon",
|
||||||
Description: "roman de Georges Simenon",
|
Description: "roman de Georges Simenon",
|
||||||
InventaireID: "isbn:9782253158400",
|
InventaireID: "inv:cd8af0fc32d2f721d2853d02682dfd44",
|
||||||
IsInventaireEdition: true,
|
IsInventaireEdition: true,
|
||||||
Rating: 0,
|
Rating: 0,
|
||||||
Read: false,
|
Read: false,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type Config struct {
|
|||||||
DemoMode bool `toml:"demo-mode" short:"D" default:"false" help:"Activate demo mode." comment:"Activate demo mode: anyone connecting to the instance will be logged in as a single user."`
|
DemoMode bool `toml:"demo-mode" short:"D" default:"false" help:"Activate demo mode." comment:"Activate demo mode: anyone connecting to the instance will be logged in as a single user."`
|
||||||
DemoUsername string `toml:"demo-username" default:"demo" help:"Name of the single user used for the demo." comment:"Name of the single user used for the demo."`
|
DemoUsername string `toml:"demo-username" default:"demo" help:"Name of the single user used for the demo." comment:"Name of the single user used for the demo."`
|
||||||
AddUser UserListAsStrings `toml:"add-user" short:"a" help:"Add users on startup following htpasswd bcrypt format." comment:"Add users on startup following htpasswd bcrypt format, example: [\"demo:$2y$10$UHR2646SZo2W.Rhna7bn5eWNLXWJZ/Sa3oLd9RlxlXs57Bwp6isOS\",\"user:$2y$10$3WYUp.VDpzJRywtrxO1s/uWfUIKpTE4yh5B1d2RCef3hvczYbEWTC\"]"`
|
AddUser UserListAsStrings `toml:"add-user" short:"a" help:"Add users on startup following htpasswd bcrypt format." comment:"Add users on startup following htpasswd bcrypt format, example: [\"demo:$2y$10$UHR2646SZo2W.Rhna7bn5eWNLXWJZ/Sa3oLd9RlxlXs57Bwp6isOS\",\"user:$2y$10$3WYUp.VDpzJRywtrxO1s/uWfUIKpTE4yh5B1d2RCef3hvczYbEWTC\"]"`
|
||||||
|
AddAdminUser UserListAsStrings `toml:"add-admin-user" short:"A" help:"Same as add-user but the added user has admin privilege." comment:"Same as add-user but the added user has admin privilege."`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserListAsStrings []string
|
type UserListAsStrings []string
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
@@ -21,13 +20,19 @@ func CreateUser(ac appcontext.AppContext, username string, password string) erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return CreateUserWithHashedPassword(ac, username, string(hashedPassword))
|
|
||||||
|
user := model.User{
|
||||||
|
Name: username,
|
||||||
|
Password: string(hashedPassword),
|
||||||
|
Admin: false,
|
||||||
|
}
|
||||||
|
return CreateUserWithHashedPassword(ac, &user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// only call this method with hashed password
|
// only call this method with hashed password
|
||||||
func CreateUserWithHashedPassword(ac appcontext.AppContext, username string, hashedPassword string) error {
|
func CreateUserWithHashedPassword(ac appcontext.AppContext, userToCreate *model.User) error {
|
||||||
var existingUser model.User
|
var existingUser model.User
|
||||||
err := ac.Db.Where("name = ?", username).First(&existingUser).Error
|
err := ac.Db.Where("name = ?", userToCreate.Name).First(&existingUser).Error
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -37,41 +42,72 @@ func CreateUserWithHashedPassword(ac appcontext.AppContext, username string, has
|
|||||||
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "UserAlreadyExists")),
|
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "UserAlreadyExists")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user := model.User{
|
return ac.Db.Model(&model.User{}).Save(&userToCreate).Error
|
||||||
Name: username,
|
|
||||||
Password: hashedPassword,
|
|
||||||
}
|
|
||||||
return ac.Db.Model(&model.User{}).Save(&user).Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateDefaultUsers(ac appcontext.AppContext) error {
|
func CreateDefaultUsers(ac appcontext.AppContext) error {
|
||||||
|
usersPasswordMap, err := createNormalUsersMap(ac)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = createDefaultUsersFromMap(ac, usersPasswordMap, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
adminUsersPasswordMap, err := createDefaultUsersMap(ac, ac.Config.AddAdminUser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return createDefaultUsersFromMap(ac, adminUsersPasswordMap, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNormalUsersMap(ac appcontext.AppContext) (map[string]string, error) {
|
||||||
|
usersPasswordMap, err := createDefaultUsersMap(ac, ac.Config.AddUser)
|
||||||
|
if err != nil {
|
||||||
|
return usersPasswordMap, err
|
||||||
|
}
|
||||||
|
_, ok := usersPasswordMap[ac.Config.DemoUsername]
|
||||||
|
if !ok {
|
||||||
|
usersPasswordMap[ac.Config.DemoUsername] = ""
|
||||||
|
}
|
||||||
|
return usersPasswordMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDefaultUsersMap(ac appcontext.AppContext, adduser []string) (map[string]string, error) {
|
||||||
usersPasswordMap := make(map[string]string)
|
usersPasswordMap := make(map[string]string)
|
||||||
var usernames []string
|
for _, s := range adduser {
|
||||||
for _, s := range ac.Config.AddUser {
|
|
||||||
splittedString := strings.Split(s, ":")
|
splittedString := strings.Split(s, ":")
|
||||||
if len(splittedString) < 2 {
|
if len(splittedString) < 2 {
|
||||||
return fmt.Errorf(i18nresource.GetTranslatedMessage(&ac, "ErrorWhenCreatingUserFromStr"), s)
|
return usersPasswordMap,
|
||||||
|
fmt.Errorf(i18nresource.GetTranslatedMessage(&ac, "ErrorWhenCreatingUserFromStr"), s)
|
||||||
}
|
}
|
||||||
usernames = append(usernames, splittedString[0])
|
|
||||||
usersPasswordMap[splittedString[0]] = splittedString[1]
|
usersPasswordMap[splittedString[0]] = splittedString[1]
|
||||||
}
|
}
|
||||||
if !slices.Contains(usernames, ac.Config.DemoUsername) {
|
return usersPasswordMap, nil
|
||||||
usernames = append(usernames, ac.Config.DemoUsername)
|
}
|
||||||
usersPasswordMap[ac.Config.DemoUsername] = ""
|
|
||||||
|
|
||||||
}
|
func createDefaultUsersFromMap(
|
||||||
|
ac appcontext.AppContext,
|
||||||
|
usersPasswordMap map[string]string,
|
||||||
|
isAdmin bool) error {
|
||||||
|
|
||||||
var existingUsers []model.User
|
var existingUsers []model.User
|
||||||
err := ac.Db.Where("name IN ?", usernames).Find(&existingUsers).Error
|
err := ac.Db.Where("name IN ?", mapToArrayKey(usersPasswordMap)).Find(&existingUsers).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, username := range usernames {
|
for username, password := range usersPasswordMap {
|
||||||
if isInExistingUsers(username, existingUsers) {
|
if isInExistingUsers(username, existingUsers) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = CreateUserWithHashedPassword(ac, username, usersPasswordMap[username])
|
|
||||||
|
user := model.User{
|
||||||
|
Name: username,
|
||||||
|
Password: password,
|
||||||
|
Admin: isAdmin,
|
||||||
|
}
|
||||||
|
err = CreateUserWithHashedPassword(ac, &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -87,3 +123,11 @@ func isInExistingUsers(username string, existingUsers []model.User) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapToArrayKey(m map[string]string) []string {
|
||||||
|
var a []string
|
||||||
|
for k := range m {
|
||||||
|
a = append(a, k)
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ type AppInfo struct {
|
|||||||
RegistrationDisabled bool `json:"registrationDisabled"`
|
RegistrationDisabled bool `json:"registrationDisabled"`
|
||||||
DemoMode bool `json:"demoMode"`
|
DemoMode bool `json:"demoMode"`
|
||||||
DemoUsername string `json:"demoUsername"`
|
DemoUsername string `json:"demoUsername"`
|
||||||
|
Admin bool `json:"admin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FullBookGet struct {
|
type FullBookGet struct {
|
||||||
@@ -72,3 +73,16 @@ type CollectionListBookItemGet struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
CoverPath string `json:"coverPath"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package jwtauth
|
package jwtauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateJwtToken(username string) (string, error) {
|
func GenerateJwtToken(username string, admin bool) (string, error) {
|
||||||
var s string
|
var s string
|
||||||
key, err := GetJwtKey()
|
key, err := GetJwtKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -14,6 +16,7 @@ func GenerateJwtToken(username string) (string, error) {
|
|||||||
jwt.MapClaims{
|
jwt.MapClaims{
|
||||||
"iss": "bibliomane",
|
"iss": "bibliomane",
|
||||||
"sub": username,
|
"sub": username,
|
||||||
|
"admin": strconv.FormatBool(admin),
|
||||||
})
|
})
|
||||||
return t.SignedString(key)
|
return t.SignedString(key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/jwtauth"
|
"git.artlef.fr/bibliomane/internal/jwtauth"
|
||||||
@@ -27,29 +28,33 @@ func Auth() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username, err := parseUserFromJwt(c)
|
jwtokenStr := jwtFromBearerToken(c.GetHeader("Authorization"))
|
||||||
|
jwtoken, err := jwt.Parse(jwtokenStr,
|
||||||
|
func(token *jwt.Token) (any, error) {
|
||||||
|
return jwtauth.GetJwtKey()
|
||||||
|
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized,
|
abortError(c)
|
||||||
gin.H{"error": "You must be logged in to access this resource."})
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//check admin rights
|
||||||
|
if strings.HasPrefix(c.FullPath(), "/ws/admin/") && !hasAdminRights(jwtoken) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusForbidden,
|
||||||
|
gin.H{"error": "You do not have the right to access this resource."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username, err := jwtoken.Claims.GetSubject()
|
||||||
|
if err != nil {
|
||||||
|
abortError(c)
|
||||||
} else {
|
} else {
|
||||||
c.Set("user", username)
|
c.Set("user", username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUserFromJwt(c *gin.Context) (string, error) {
|
|
||||||
|
|
||||||
jwtokenStr := jwtFromBearerToken(c.GetHeader("Authorization"))
|
|
||||||
jwtoken, parseErr := jwt.Parse(jwtokenStr,
|
|
||||||
func(token *jwt.Token) (any, error) {
|
|
||||||
return jwtauth.GetJwtKey()
|
|
||||||
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
|
||||||
if parseErr != nil {
|
|
||||||
return "", parseErr
|
|
||||||
}
|
|
||||||
return jwtoken.Claims.GetSubject()
|
|
||||||
}
|
|
||||||
|
|
||||||
func jwtFromBearerToken(bearerToken string) string {
|
func jwtFromBearerToken(bearerToken string) string {
|
||||||
splitToken := strings.Split(bearerToken, " ")
|
splitToken := strings.Split(bearerToken, " ")
|
||||||
if len(splitToken) == 2 {
|
if len(splitToken) == 2 {
|
||||||
@@ -58,3 +63,28 @@ func jwtFromBearerToken(bearerToken string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasAdminRights(token *jwt.Token) bool {
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
raw, ok := claims["admin"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
adminStr, ok := raw.(string)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
isAdmin, err := strconv.ParseBool(adminStr)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
func abortError(c *gin.Context) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized,
|
||||||
|
gin.H{"error": "You must be logged in to access this resource."})
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ type Collection struct {
|
|||||||
Name string
|
Name string
|
||||||
User User
|
User User
|
||||||
UserID uint
|
UserID uint
|
||||||
Items []CollectionItem
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ type User struct {
|
|||||||
gorm.Model
|
gorm.Model
|
||||||
Name string `gorm:"index;uniqueIndex"`
|
Name string `gorm:"index;uniqueIndex"`
|
||||||
Password string
|
Password string
|
||||||
UserBooks []UserBook
|
Admin bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ func FetchCollectionHeader(db *gorm.DB, collectionId uint) (CollectionHeader, er
|
|||||||
return collection, res.Error
|
return collection, res.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FetchCollectionHeaderFromItem(db *gorm.DB, itemId uint) (CollectionHeader, error) {
|
||||||
|
var collection CollectionHeader
|
||||||
|
query := db.Model(&model.Collection{})
|
||||||
|
query = query.Select("collections.name, collections.user_id")
|
||||||
|
query = query.Joins("left join collection_items on (collection_items.collection_id = collections.id)")
|
||||||
|
query = query.Where("collection_items.id = ?", itemId)
|
||||||
|
res := query.Find(&collection)
|
||||||
|
return collection, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
type CollectionsQueryResult struct {
|
type CollectionsQueryResult struct {
|
||||||
ID uint
|
ID uint
|
||||||
UserID uint
|
UserID uint
|
||||||
|
|||||||
33
internal/query/queryusers.go
Normal file
33
internal/query/queryusers.go
Normal 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
|
||||||
|
}
|
||||||
@@ -5,12 +5,24 @@ import (
|
|||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
"git.artlef.fr/bibliomane/internal/dto"
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
|
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAppInfo(ac appcontext.AppContext) {
|
func GetAppInfo(ac appcontext.AppContext) {
|
||||||
|
admin := false
|
||||||
|
_, userIsInContext := ac.C.Get("user")
|
||||||
|
if userIsInContext {
|
||||||
|
user, err := ac.GetAuthenticatedUser()
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
admin = user.Admin
|
||||||
|
}
|
||||||
ac.C.JSON(http.StatusOK, dto.AppInfo{
|
ac.C.JSON(http.StatusOK, dto.AppInfo{
|
||||||
RegistrationDisabled: ac.Config.DisableRegistration,
|
RegistrationDisabled: ac.Config.DisableRegistration,
|
||||||
DemoMode: ac.Config.DemoMode,
|
DemoMode: ac.Config.DemoMode,
|
||||||
DemoUsername: ac.Config.DemoUsername,
|
DemoUsername: ac.Config.DemoUsername,
|
||||||
|
Admin: admin,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
66
internal/routes/collectiondelete.go
Normal file
66
internal/routes/collectiondelete.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
|
"git.artlef.fr/bibliomane/internal/i18nresource"
|
||||||
|
"git.artlef.fr/bibliomane/internal/model"
|
||||||
|
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||||
|
"git.artlef.fr/bibliomane/internal/query"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteCollectionHandler(ac appcontext.AppContext) {
|
||||||
|
collectionId, err := strconv.ParseUint(ac.C.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
ac.C.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := ac.GetAuthenticatedUser()
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = myvalidator.ValidateId(ac.Db, uint(collectionId), &model.Collection{})
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
collection, err := query.FetchCollectionHeader(ac.Db, uint(collectionId))
|
||||||
|
|
||||||
|
if collection.UserID != user.ID {
|
||||||
|
err := myvalidator.HttpError{
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "Unauthorized")),
|
||||||
|
}
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionDb := model.Collection{}
|
||||||
|
collectionDb.ID = uint(collectionId)
|
||||||
|
|
||||||
|
err = ac.Db.Where("collection_id = ?", collectionId).Delete(&model.CollectionItem{}).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ac.Db.Delete(&collectionDb).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ac.Db.Delete(&collectionDb).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ac.C.JSON(http.StatusOK, "Success")
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
"git.artlef.fr/bibliomane/internal/dto"
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
"git.artlef.fr/bibliomane/internal/i18nresource"
|
"git.artlef.fr/bibliomane/internal/i18nresource"
|
||||||
|
"git.artlef.fr/bibliomane/internal/model"
|
||||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||||
"git.artlef.fr/bibliomane/internal/query"
|
"git.artlef.fr/bibliomane/internal/query"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -21,6 +22,12 @@ func GetCollectionHandler(ac appcontext.AppContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = myvalidator.ValidateId(ac.Db, uint(collectionId), &model.Collection{})
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
user, err := ac.GetAuthenticatedUser()
|
user, err := ac.GetAuthenticatedUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
|||||||
62
internal/routes/collectionremovebookdelete.go
Normal file
62
internal/routes/collectionremovebookdelete.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
|
"git.artlef.fr/bibliomane/internal/i18nresource"
|
||||||
|
"git.artlef.fr/bibliomane/internal/model"
|
||||||
|
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||||
|
"git.artlef.fr/bibliomane/internal/query"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteCollectionBookHandler(ac appcontext.AppContext) {
|
||||||
|
collectionItemId, err := strconv.ParseUint(ac.C.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
ac.C.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := ac.GetAuthenticatedUser()
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var collectionItem model.CollectionItem
|
||||||
|
err = ac.Db.First(&collectionItem, collectionItemId).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collection, err := query.FetchCollectionHeaderFromItem(ac.Db, collectionItem.ID)
|
||||||
|
|
||||||
|
if collection.UserID != user.ID {
|
||||||
|
err := myvalidator.HttpError{
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
Err: errors.New(i18nresource.GetTranslatedMessage(&ac, "Unauthorized")),
|
||||||
|
}
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ac.Db.Delete(&collectionItem).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//update position on remaining items
|
||||||
|
q := ac.Db.Model(&model.CollectionItem{})
|
||||||
|
q = q.Where("collection_id = ? AND position > ?", collectionItem.CollectionID, collectionItem.Position)
|
||||||
|
err = q.UpdateColumn("position", gorm.Expr("position - 1")).Error
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ac.C.JSON(http.StatusOK, "Success")
|
||||||
|
}
|
||||||
@@ -12,12 +12,12 @@ import (
|
|||||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func PostLoginHandler(ac appcontext.AppContext) {
|
func PostLoginHandler(ac appcontext.AppContext) {
|
||||||
|
|
||||||
var username string
|
var username string
|
||||||
|
admin := false
|
||||||
|
|
||||||
if !ac.Config.DemoMode {
|
if !ac.Config.DemoMode {
|
||||||
var user dto.UserLogin
|
var user dto.UserLogin
|
||||||
@@ -26,30 +26,27 @@ func PostLoginHandler(ac appcontext.AppContext) {
|
|||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var userDb model.User
|
||||||
|
ac.Db.Where("name = ?", user.Username).First(&userDb)
|
||||||
|
|
||||||
if !ac.Config.DemoMode && !isUserAndPasswordOk(ac.Db, user.Username, user.Password) {
|
if !ac.Config.DemoMode &&
|
||||||
|
bcrypt.CompareHashAndPassword([]byte(userDb.Password), []byte(user.Password)) != nil {
|
||||||
ac.C.JSON(http.StatusUnauthorized,
|
ac.C.JSON(http.StatusUnauthorized,
|
||||||
gin.H{"error": i18nresource.GetTranslatedMessage(&ac, "InvalidCredentials")})
|
gin.H{"error": i18nresource.GetTranslatedMessage(&ac, "InvalidCredentials")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username = user.Username
|
username = user.Username
|
||||||
|
admin = userDb.Admin
|
||||||
} else {
|
} else {
|
||||||
username = ac.Config.DemoUsername
|
username = ac.Config.DemoUsername
|
||||||
}
|
}
|
||||||
|
|
||||||
var jwtToken string
|
var jwtToken string
|
||||||
jwtToken, err := jwtauth.GenerateJwtToken(username)
|
jwtToken, err := jwtauth.GenerateJwtToken(username, admin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ac.C.JSON(http.StatusUnauthorized,
|
ac.C.JSON(http.StatusUnauthorized,
|
||||||
gin.H{"error": fmt.Errorf("Error when generating JWT token: %w", err)})
|
gin.H{"error": fmt.Errorf("Error when generating JWT token: %w", err)})
|
||||||
return
|
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})
|
||||||
}
|
|
||||||
|
|
||||||
func isUserAndPasswordOk(db *gorm.DB, username string, password string) bool {
|
|
||||||
var user model.User
|
|
||||||
db.Where("name = ?", username).First(&user)
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
|
||||||
return err == nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
34
internal/routes/usersget.go
Normal file
34
internal/routes/usersget.go
Normal 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})
|
||||||
|
}
|
||||||
@@ -102,6 +102,15 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
ws.POST("/upload/cover", func(c *gin.Context) {
|
ws.POST("/upload/cover", func(c *gin.Context) {
|
||||||
routes.PostUploadBookCoverHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.PostUploadBookCoverHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
|
ws.DELETE("/collection/:id", func(c *gin.Context) {
|
||||||
|
routes.DeleteCollectionHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
|
})
|
||||||
|
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)
|
r.Static("/static/bookcover", config.ImageFolderPath)
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ func ConnectDemo2User(router *gin.Engine) string {
|
|||||||
return connectUser(router, loginJson)
|
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 {
|
func connectUser(router *gin.Engine, loginJson string) string {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
req, _ := http.NewRequest("POST", "/ws/auth/login", strings.NewReader(loginJson))
|
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) {
|
func TestFetchModel[T any](t *testing.T, urlpath string, limit string, offset string) (int, T) {
|
||||||
router := TestSetup()
|
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)
|
u, err := url.Parse(urlpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -107,7 +128,6 @@ func TestFetchModel[T any](t *testing.T, urlpath string, limit string, offset st
|
|||||||
q.Set("lang", "fr")
|
q.Set("lang", "fr")
|
||||||
u.RawQuery = q.Encode()
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
token := ConnectDemoUser(router)
|
|
||||||
req, _ := http.NewRequest("GET", u.String(), nil)
|
req, _ := http.NewRequest("GET", u.String(), nil)
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
applicationVersion := "0.7.1"
|
applicationVersion := "0.8.0"
|
||||||
c := config.LoadConfig(applicationVersion)
|
c := config.LoadConfig(applicationVersion)
|
||||||
r := setup.Setup(&c)
|
r := setup.Setup(&c)
|
||||||
r.Run(":" + c.Port)
|
r.Run(":" + c.Port)
|
||||||
|
|||||||
Reference in New Issue
Block a user