feat: protect write endpoints with JWT auth (story #08)
Add bcrypt user file auth, JWT middleware on all write routes, Pinia authStore with localStorage persistence, login view with redirect support, and v-if guards on all CRUD controls and admin nav link. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePublicationStore } from '../stores/publicationStore'
|
||||
import { useAuthStore } from '../stores/authStore'
|
||||
import {
|
||||
uploadCoverImage,
|
||||
deleteCoverImage,
|
||||
@@ -23,6 +24,7 @@ import ImageOverlayDrawer from '../components/ImageOverlayDrawer.vue'
|
||||
const props = defineProps<{ id: string }>()
|
||||
const router = useRouter()
|
||||
const store = usePublicationStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
// Edit state
|
||||
const editing = ref(false)
|
||||
@@ -212,7 +214,7 @@ async function deleteFruitImg(imgId: number) {
|
||||
<p class="text-sm text-gray-500 font-mono">{{ store.current.osdb_pub_id }}</p>
|
||||
<p v-if="store.current.author" class="text-gray-700">{{ store.current.author }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div v-if="auth.isLoggedIn" class="flex gap-2">
|
||||
<button
|
||||
class="rounded border px-3 py-1 text-sm hover:bg-gray-50"
|
||||
@click="editing = !editing"
|
||||
@@ -230,7 +232,7 @@ async function deleteFruitImg(imgId: number) {
|
||||
</div>
|
||||
|
||||
<!-- Edit form -->
|
||||
<div v-if="editing" class="mb-6 rounded border p-4">
|
||||
<div v-if="auth.isLoggedIn && editing" class="mb-6 rounded border p-4">
|
||||
<div v-if="serverError" class="mb-3 text-red-600">{{ serverError }}</div>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
@@ -266,6 +268,7 @@ async function deleteFruitImg(imgId: number) {
|
||||
@click="overlayOpen = true"
|
||||
/>
|
||||
<button
|
||||
v-if="auth.isLoggedIn"
|
||||
class="rounded border px-2 py-1 text-sm text-red-600 hover:bg-red-50"
|
||||
@click="removeCover"
|
||||
>
|
||||
@@ -274,7 +277,7 @@ async function deleteFruitImg(imgId: number) {
|
||||
</div>
|
||||
<div v-else class="mb-3 text-sm text-gray-400">Kein Titelbild vorhanden.</div>
|
||||
<div v-if="coverError" class="mb-2 text-red-600 text-sm">{{ coverError }}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="auth.isLoggedIn" class="flex items-center gap-2">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@@ -297,13 +300,13 @@ async function deleteFruitImg(imgId: number) {
|
||||
<ul v-if="fruits.length" class="mb-3 space-y-1">
|
||||
<li v-for="f in fruits" :key="f.fruit_id" class="flex items-center justify-between rounded border px-3 py-1 text-sm">
|
||||
<span>{{ f.name }} <span class="text-gray-400 font-mono text-xs">({{ f.osdb_number }})</span></span>
|
||||
<button class="text-red-600 hover:underline text-xs" @click="doUnlinkFruit(f.fruit_id)">
|
||||
<button v-if="auth.isLoggedIn" class="text-red-600 hover:underline text-xs" @click="doUnlinkFruit(f.fruit_id)">
|
||||
Entfernen
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="mb-3 text-sm text-gray-400">Keine Früchte verknüpft.</div>
|
||||
<div class="flex gap-2">
|
||||
<div v-if="auth.isLoggedIn" class="flex gap-2">
|
||||
<input
|
||||
v-model="linkFruitId"
|
||||
type="number"
|
||||
@@ -332,14 +335,14 @@ async function deleteFruitImg(imgId: number) {
|
||||
<a :href="d.url" target="_blank" class="text-blue-600 hover:underline">
|
||||
{{ d.fruit_name || `Frucht #${d.fruit_id}` }}
|
||||
</a>
|
||||
<button class="text-red-600 hover:underline text-xs" @click="deleteDesc(d.id)">
|
||||
<button v-if="auth.isLoggedIn" class="text-red-600 hover:underline text-xs" @click="deleteDesc(d.id)">
|
||||
Löschen
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="mb-3 text-sm text-gray-400">Keine Beschreibungen vorhanden.</div>
|
||||
<div v-if="descError" class="mb-2 text-red-600 text-sm">{{ descError }}</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div v-if="auth.isLoggedIn" class="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
v-model="descFruitId"
|
||||
type="number"
|
||||
@@ -377,14 +380,14 @@ async function deleteFruitImg(imgId: number) {
|
||||
class="h-24 w-24 rounded border object-cover"
|
||||
/>
|
||||
<span class="text-xs text-gray-500">Frucht #{{ img.fruit_id }}</span>
|
||||
<button class="text-xs text-red-600 hover:underline" @click="deleteFruitImg(img.id)">
|
||||
<button v-if="auth.isLoggedIn" class="text-xs text-red-600 hover:underline" @click="deleteFruitImg(img.id)">
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mb-3 text-sm text-gray-400">Keine Fruchtbilder vorhanden.</div>
|
||||
<div v-if="fruitImgError" class="mb-2 text-red-600 text-sm">{{ fruitImgError }}</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div v-if="auth.isLoggedIn" class="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
v-model="fruitImgFruitId"
|
||||
type="number"
|
||||
|
||||
Reference in New Issue
Block a user