feat: Implement session-based authorization for API requests and update upload folder path
All checks were successful
deploy / deploy (push) Successful in 3m55s
All checks were successful
deploy / deploy (push) Successful in 3m55s
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "0.0.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import axios from 'axios'
|
||||
import { type autocompleteRecord, type DBRecord} from '@/DBRecord.ts'
|
||||
import { session } from '@/session.ts'
|
||||
|
||||
const API_URL = `${window.location.origin}/api/music-scans`
|
||||
const API_URL = (import.meta.env.VITE_API_ROOT ?? window.location.origin) + "/api/music-scans"
|
||||
|
||||
export const addMusicScan = (musicData: FormData) => {
|
||||
if (!session.token) throw new Error('Unauthorized: No session token found')
|
||||
return axios.post(`${API_URL}/add`, musicData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
authorization: session.token
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -18,7 +21,8 @@ export const searchMusicScans = (filters: any) => {
|
||||
}
|
||||
|
||||
export const deleteMusicScan = (id: number) => {
|
||||
return axios.delete(`${API_URL}/delete/${id}`)
|
||||
if (!session.token) throw new Error('Unauthorized: No session token found')
|
||||
return axios.delete(`${API_URL}/delete/${id}`, {headers: {authorization: session.token}})
|
||||
}
|
||||
|
||||
export const getMusicScanById = (id: string) => {
|
||||
@ -26,11 +30,14 @@ export const getMusicScanById = (id: string) => {
|
||||
}
|
||||
|
||||
export const updateMusicScanById = (id: string, data: DBRecord) => {
|
||||
return axios.patch(`${API_URL}/scan/${id}`, data)
|
||||
if (!session.token) throw new Error('Unauthorized: No session token found')
|
||||
return axios.patch(`${API_URL}/scan/${id}`, data, {headers: {authorization: session.token}})
|
||||
}
|
||||
|
||||
export const downloadMusicScan = async (id: number) => {
|
||||
return axios.get(`${API_URL}/download/${id}`, { responseType: 'blob' }).then((response) => response.data)
|
||||
if (!session.token) throw new Error('Unauthorized: No session token found')
|
||||
return axios.get(`${API_URL}/download/${id}`, { responseType: 'blob', headers: {authorization: session.token} })
|
||||
.then((response) => response.data)
|
||||
}
|
||||
|
||||
export const getAllAutocompleteOptions = () => {
|
||||
|
||||
@ -5,6 +5,8 @@ import { nextTick, ref, watch } from 'vue'
|
||||
import { deleteMusicScan, downloadMusicScan, getMusicScanById, searchMusicScans } from '@/api.ts'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { filters, type ServerResponseRecord } from '@/DBRecord.ts'
|
||||
import { toast } from '@/toast.ts'
|
||||
import { session } from '@/session'
|
||||
|
||||
const totalPages = ref<number>(0)
|
||||
const results = defineModel<ServerResponseRecord[]>('results')
|
||||
@ -28,8 +30,8 @@ const fetchResults = async () => {
|
||||
shownResults.value = results.value?.slice(0, pageSize.value) ?? []
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching results:', error)
|
||||
})
|
||||
console.error('Error fetching results:', error)
|
||||
})
|
||||
}
|
||||
|
||||
const changePage = (newPage: number) => {
|
||||
@ -63,11 +65,20 @@ const confirmDelete = (id: number | undefined) => {
|
||||
}
|
||||
}
|
||||
const downloadMusic = async (id: number | undefined) => {
|
||||
if (session.user?.role !== 'admin') {
|
||||
toast.error('You do not have permission to download this file.')
|
||||
return
|
||||
}
|
||||
if (!id) {
|
||||
toast.error('No ID provided for download.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const file = await downloadMusicScan(id)
|
||||
if (!file) {
|
||||
toast.error('An error occurred while downloading the file. Please contact the site administrator.')
|
||||
return
|
||||
}
|
||||
const filename = await getMusicScanById(id.toString()).then((response) => response.data.name)
|
||||
saveAs(file, filename)
|
||||
} catch (error) {
|
||||
@ -78,50 +89,53 @@ const downloadMusic = async (id: number | undefined) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container is-fluid" v-if="shownResults.length != 0">
|
||||
<div class="container is-fluid" v-if="shownResults.length != 0">
|
||||
<table class="table is-fullwidth is-striped is-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-text-weight-bold has-text-centered">Name</th>
|
||||
<th class="has-text-weight-bold has-text-centered">Composer</th>
|
||||
<th class="has-text-weight-bold has-text-centered">Arranged By</th>
|
||||
<th class="has-text-weight-bold has-text-centered">Words By</th>
|
||||
<th class="has-text-weight-bold has-text-centered">Year</th>
|
||||
<th class="has-text-weight-bold has-text-centered">Genre</th>
|
||||
<th class="has-text-weight-bold has-text-centered">Language</th>
|
||||
<th class="has-text-weight-bold has-text-centered">Scored For</th>
|
||||
<th class="has-text-weight-bold has-text-centered">Controls</th>
|
||||
<th class="has-text-weight-bold has-text-centered is-align-content-center">Name</th>
|
||||
<th class="has-text-weight-bold has-text-centered is-align-content-center">Composer</th>
|
||||
<th class="has-text-weight-bold has-text-centered is-align-content-center">Arranged By</th>
|
||||
<th class="has-text-weight-bold has-text-centered is-align-content-center">Words By</th>
|
||||
<th class="has-text-weight-bold has-text-centered is-align-content-center">Year</th>
|
||||
<th class="has-text-weight-bold has-text-centered is-align-content-center">Genre</th>
|
||||
<th class="has-text-weight-bold has-text-centered is-align-content-center">Language</th>
|
||||
<th class="has-text-weight-bold has-text-centered is-align-content-center">Scored For</th>
|
||||
<th class="has-text-weight-bold has-text-centered is-align-content-center">Controls</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="scan in shownResults" :key="scan!.id">
|
||||
<td>{{ scan.name }}</td>
|
||||
<td
|
||||
:class="{ 'is-italic': scan.composer == 'unknown' }">
|
||||
<td class="is-align-content-center">{{ scan.name }}</td>
|
||||
<td class="is-align-content-center"
|
||||
:class="{ 'is-italic': scan.composer === 'unknown' }">
|
||||
{{ scan.composer }}
|
||||
</td>
|
||||
<td
|
||||
:class="{ 'is-italic': scan.arranger == 'unknown' }">
|
||||
<td class="is-align-content-center"
|
||||
:class="{ 'is-italic': scan.arranger === 'unknown' }">
|
||||
{{ scan.arranger }}
|
||||
</td>
|
||||
<td
|
||||
:class="{ 'is-italic': scan.words == 'unknown' }">
|
||||
<td class="is-align-content-center"
|
||||
:class="{ 'is-italic': scan.words === 'unknown' }">
|
||||
{{ scan.words }}
|
||||
</td>
|
||||
<td :class="{ 'is-italic': !scan.year }">{{ scan.year ? scan.year : 'null' }}</td>
|
||||
<td>{{ scan.genre }}</td>
|
||||
<td>{{ scan.language }}</td>
|
||||
<td>{{ scan.instrument }}</td>
|
||||
<td class="buttons has-addons" style="display: inline-flex; flex-wrap: nowrap;">
|
||||
<button class="button is-small is-info" @click="downloadMusic(scan.id)">
|
||||
<font-awesome-icon :icon="faDownload" />
|
||||
</button>
|
||||
<RouterLink class="button is-small is-primary" :to="generateEditLink(scan.id)">
|
||||
<font-awesome-icon :icon="faPenToSquare" />
|
||||
</RouterLink>
|
||||
<button class="button is-small is-danger" @click="confirmDelete(scan.id)">
|
||||
<font-awesome-icon :icon="faTrash" />
|
||||
</button>
|
||||
<td class="is-align-content-center"
|
||||
:class="{ 'is-italic': !scan.year }">{{ scan.year ? scan.year : 'null' }}</td>
|
||||
<td class="is-align-content-center">{{ scan.genre }}</td>
|
||||
<td class="is-align-content-center">{{ scan.language }}</td>
|
||||
<td class="is-align-content-center">{{ scan.instrument }}</td>
|
||||
<td class="is-align-content-center">
|
||||
<div class="buttons has-addons button-group">
|
||||
<button class="button is-small is-info" @click="downloadMusic(scan.id)">
|
||||
<font-awesome-icon :icon="faDownload" />
|
||||
</button>
|
||||
<RouterLink class="button is-small is-primary" :to="generateEditLink(scan.id)">
|
||||
<font-awesome-icon :icon="faPenToSquare" />
|
||||
</RouterLink>
|
||||
<button class="button is-small is-danger" @click="confirmDelete(scan.id)">
|
||||
<font-awesome-icon :icon="faTrash" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -152,15 +166,13 @@ const downloadMusic = async (id: number | undefined) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
td.buttons {
|
||||
display: flex; /* Make the cell flexible */
|
||||
align-items: stretch; /* Ensure it stretches to match the row height */
|
||||
td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.buttons .button-group {
|
||||
display: flex;
|
||||
div.button-group {
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.25rem; /* Small space between buttons */
|
||||
align-self: center; /* Center the buttons vertically */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,6 +7,16 @@ import router from './router'
|
||||
import './assets/styles.scss'
|
||||
import Oruga from '@oruga-ui/oruga-next'
|
||||
import ToastPlugin from 'vue-toast-notification';
|
||||
import { session } from './session';
|
||||
|
||||
if (localStorage.getItem('token') && localStorage.getItem('username') && localStorage.getItem('role')) {
|
||||
session.user = {
|
||||
username: localStorage.getItem('username') || '',
|
||||
token: localStorage.getItem('token') || '',
|
||||
role: localStorage.getItem('role') || 'user',
|
||||
};
|
||||
session.token = localStorage.getItem('token');
|
||||
}
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ export function useLogin() {
|
||||
session.redirectURL = null;
|
||||
toast.success("Welcome " + session.user.username + "!\nYou are now logged in.");
|
||||
localStorage.setItem("username", session.user.username);
|
||||
localStorage.setItem("role", session.user.role ?? "user");
|
||||
localStorage.setItem("token", session.token ?? "");
|
||||
return session.user;
|
||||
})
|
||||
@ -42,6 +43,7 @@ export function useLogin() {
|
||||
session.user = null;
|
||||
session.token = null;
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("role");
|
||||
localStorage.removeItem("username");
|
||||
for(let i = 0; i < session.messages.length; i++) {
|
||||
console.debug("Messages: ");
|
||||
|
||||
@ -29,6 +29,7 @@ const showPassword = ref(false)
|
||||
<button type="button" class="password-toggle-btn svg"
|
||||
style="cursor:pointer; border-left:0; z-index: 9999"
|
||||
@click="showPassword = !showPassword">
|
||||
<!--suppress HtmlDeprecatedAttribute -->
|
||||
<svg v-if="showPassword" width="20px" height="20px" fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27 27"> >
|
||||
<path clip-rule="evenodd"
|
||||
@ -37,6 +38,7 @@ const showPassword = ref(false)
|
||||
<path
|
||||
d="M14.6873 22.1761C19.1311 21.9148 23.4056 19.0687 26.8864 13.931C26.9593 13.8234 27 13.7121 27 13.5797C27 13.4535 26.965 13.3481 26.8956 13.2455C25.5579 11.2677 24.1025 9.62885 22.5652 8.34557L21.506 10.2052C22.3887 10.9653 23.2531 11.87 24.0894 12.9239C24.3904 13.3032 24.3904 13.8561 24.0894 14.2354C21.5676 17.4135 18.7903 19.2357 16.0254 19.827L14.6873 22.1761Z" />
|
||||
</svg>
|
||||
<!--suppress HtmlDeprecatedAttribute -->
|
||||
<svg v-else width="20px" height="20px" fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27 27">
|
||||
<path clip-rule="evenodd"
|
||||
@ -51,7 +53,7 @@ const showPassword = ref(false)
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-submit mt-4" type="submit"
|
||||
<button class="button is-primary is-fullwidth" type="submit"
|
||||
@click.prevent="login(username, password)">Login
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -86,7 +86,7 @@ watch(filters.value, (value) => {
|
||||
<radio-group v-model="filters.jewish" label="Jewish" :undefined-option="true"/>
|
||||
<radio-group v-model="filters.choral" label="Choral" :undefined-option="true"/>
|
||||
<radio-group v-model="filters.liturgical" label="Liturgical" :undefined-option="true"/>
|
||||
<button class="button is-primary" type="submit">Search</button>
|
||||
<button class="button is-primary is-fullwidth" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,20 +94,5 @@ watch(filters.value, (value) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div.column.is-half {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/*input {
|
||||
overflow-y: visible !important;
|
||||
}*/
|
||||
|
||||
/*.field {
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
}*/
|
||||
|
||||
/*form {
|
||||
overflow: visible !important;
|
||||
}*/
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user