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",
|
"name": "client",
|
||||||
"version": "0.0.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { type autocompleteRecord, type DBRecord} from '@/DBRecord.ts'
|
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) => {
|
export const addMusicScan = (musicData: FormData) => {
|
||||||
|
if (!session.token) throw new Error('Unauthorized: No session token found')
|
||||||
return axios.post(`${API_URL}/add`, musicData, {
|
return axios.post(`${API_URL}/add`, musicData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
|
authorization: session.token
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -18,7 +21,8 @@ export const searchMusicScans = (filters: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const deleteMusicScan = (id: number) => {
|
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) => {
|
export const getMusicScanById = (id: string) => {
|
||||||
@ -26,11 +30,14 @@ export const getMusicScanById = (id: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const updateMusicScanById = (id: string, data: DBRecord) => {
|
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) => {
|
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 = () => {
|
export const getAllAutocompleteOptions = () => {
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { nextTick, ref, watch } from 'vue'
|
|||||||
import { deleteMusicScan, downloadMusicScan, getMusicScanById, searchMusicScans } from '@/api.ts'
|
import { deleteMusicScan, downloadMusicScan, getMusicScanById, searchMusicScans } from '@/api.ts'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { filters, type ServerResponseRecord } from '@/DBRecord.ts'
|
import { filters, type ServerResponseRecord } from '@/DBRecord.ts'
|
||||||
|
import { toast } from '@/toast.ts'
|
||||||
|
import { session } from '@/session'
|
||||||
|
|
||||||
const totalPages = ref<number>(0)
|
const totalPages = ref<number>(0)
|
||||||
const results = defineModel<ServerResponseRecord[]>('results')
|
const results = defineModel<ServerResponseRecord[]>('results')
|
||||||
@ -63,11 +65,20 @@ const confirmDelete = (id: number | undefined) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const downloadMusic = async (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) {
|
if (!id) {
|
||||||
|
toast.error('No ID provided for download.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const file = await downloadMusicScan(id)
|
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)
|
const filename = await getMusicScanById(id.toString()).then((response) => response.data.name)
|
||||||
saveAs(file, filename)
|
saveAs(file, filename)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -78,41 +89,43 @@ const downloadMusic = async (id: number | undefined) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<table class="table is-fullwidth is-striped is-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="has-text-weight-bold has-text-centered">Name</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">Composer</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">Arranged By</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">Words 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">Year</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">Genre</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">Language</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">Scored For</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">Controls</th>
|
<th class="has-text-weight-bold has-text-centered is-align-content-center">Controls</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="scan in shownResults" :key="scan!.id">
|
<tr v-for="scan in shownResults" :key="scan!.id">
|
||||||
<td>{{ scan.name }}</td>
|
<td class="is-align-content-center">{{ scan.name }}</td>
|
||||||
<td
|
<td class="is-align-content-center"
|
||||||
:class="{ 'is-italic': scan.composer == 'unknown' }">
|
:class="{ 'is-italic': scan.composer === 'unknown' }">
|
||||||
{{ scan.composer }}
|
{{ scan.composer }}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td class="is-align-content-center"
|
||||||
:class="{ 'is-italic': scan.arranger == 'unknown' }">
|
:class="{ 'is-italic': scan.arranger === 'unknown' }">
|
||||||
{{ scan.arranger }}
|
{{ scan.arranger }}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td class="is-align-content-center"
|
||||||
:class="{ 'is-italic': scan.words == 'unknown' }">
|
:class="{ 'is-italic': scan.words === 'unknown' }">
|
||||||
{{ scan.words }}
|
{{ scan.words }}
|
||||||
</td>
|
</td>
|
||||||
<td :class="{ 'is-italic': !scan.year }">{{ scan.year ? scan.year : 'null' }}</td>
|
<td class="is-align-content-center"
|
||||||
<td>{{ scan.genre }}</td>
|
:class="{ 'is-italic': !scan.year }">{{ scan.year ? scan.year : 'null' }}</td>
|
||||||
<td>{{ scan.language }}</td>
|
<td class="is-align-content-center">{{ scan.genre }}</td>
|
||||||
<td>{{ scan.instrument }}</td>
|
<td class="is-align-content-center">{{ scan.language }}</td>
|
||||||
<td class="buttons has-addons" style="display: inline-flex; flex-wrap: nowrap;">
|
<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)">
|
<button class="button is-small is-info" @click="downloadMusic(scan.id)">
|
||||||
<font-awesome-icon :icon="faDownload" />
|
<font-awesome-icon :icon="faDownload" />
|
||||||
</button>
|
</button>
|
||||||
@ -122,6 +135,7 @@ const downloadMusic = async (id: number | undefined) => {
|
|||||||
<button class="button is-small is-danger" @click="confirmDelete(scan.id)">
|
<button class="button is-small is-danger" @click="confirmDelete(scan.id)">
|
||||||
<font-awesome-icon :icon="faTrash" />
|
<font-awesome-icon :icon="faTrash" />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -152,15 +166,13 @@ const downloadMusic = async (id: number | undefined) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
td.buttons {
|
td {
|
||||||
display: flex; /* Make the cell flexible */
|
text-align: center;
|
||||||
align-items: stretch; /* Ensure it stretches to match the row height */
|
|
||||||
}
|
}
|
||||||
|
div.button-group {
|
||||||
td.buttons .button-group {
|
display: inline-flex;
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 0.25rem; /* Small space between buttons */
|
justify-content: center;
|
||||||
align-self: center; /* Center the buttons vertically */
|
align-items: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -7,6 +7,16 @@ import router from './router'
|
|||||||
import './assets/styles.scss'
|
import './assets/styles.scss'
|
||||||
import Oruga from '@oruga-ui/oruga-next'
|
import Oruga from '@oruga-ui/oruga-next'
|
||||||
import ToastPlugin from 'vue-toast-notification';
|
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)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export function useLogin() {
|
|||||||
session.redirectURL = null;
|
session.redirectURL = null;
|
||||||
toast.success("Welcome " + session.user.username + "!\nYou are now logged in.");
|
toast.success("Welcome " + session.user.username + "!\nYou are now logged in.");
|
||||||
localStorage.setItem("username", session.user.username);
|
localStorage.setItem("username", session.user.username);
|
||||||
|
localStorage.setItem("role", session.user.role ?? "user");
|
||||||
localStorage.setItem("token", session.token ?? "");
|
localStorage.setItem("token", session.token ?? "");
|
||||||
return session.user;
|
return session.user;
|
||||||
})
|
})
|
||||||
@ -42,6 +43,7 @@ export function useLogin() {
|
|||||||
session.user = null;
|
session.user = null;
|
||||||
session.token = null;
|
session.token = null;
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("role");
|
||||||
localStorage.removeItem("username");
|
localStorage.removeItem("username");
|
||||||
for(let i = 0; i < session.messages.length; i++) {
|
for(let i = 0; i < session.messages.length; i++) {
|
||||||
console.debug("Messages: ");
|
console.debug("Messages: ");
|
||||||
|
|||||||
@ -29,6 +29,7 @@ const showPassword = ref(false)
|
|||||||
<button type="button" class="password-toggle-btn svg"
|
<button type="button" class="password-toggle-btn svg"
|
||||||
style="cursor:pointer; border-left:0; z-index: 9999"
|
style="cursor:pointer; border-left:0; z-index: 9999"
|
||||||
@click="showPassword = !showPassword">
|
@click="showPassword = !showPassword">
|
||||||
|
<!--suppress HtmlDeprecatedAttribute -->
|
||||||
<svg v-if="showPassword" width="20px" height="20px" fill="currentColor"
|
<svg v-if="showPassword" width="20px" height="20px" fill="currentColor"
|
||||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27 27"> >
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27 27"> >
|
||||||
<path clip-rule="evenodd"
|
<path clip-rule="evenodd"
|
||||||
@ -37,6 +38,7 @@ const showPassword = ref(false)
|
|||||||
<path
|
<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" />
|
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>
|
</svg>
|
||||||
|
<!--suppress HtmlDeprecatedAttribute -->
|
||||||
<svg v-else width="20px" height="20px" fill="currentColor"
|
<svg v-else width="20px" height="20px" fill="currentColor"
|
||||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27 27">
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27 27">
|
||||||
<path clip-rule="evenodd"
|
<path clip-rule="evenodd"
|
||||||
@ -51,7 +53,7 @@ const showPassword = ref(false)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid">
|
<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
|
@click.prevent="login(username, password)">Login
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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.jewish" label="Jewish" :undefined-option="true"/>
|
||||||
<radio-group v-model="filters.choral" label="Choral" :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"/>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -94,20 +94,5 @@ watch(filters.value, (value) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
|
|||||||
@ -37,9 +37,8 @@ app.use("/edit/:id", express.static('../client/dist'));
|
|||||||
app.use("/login", express.static('../client/dist'));
|
app.use("/login", express.static('../client/dist'));
|
||||||
app.use("/register", express.static('../client/dist'));
|
app.use("/register", express.static('../client/dist'));
|
||||||
app.use("/assets", express.static("../client/dist/assets"));
|
app.use("/assets", express.static("../client/dist/assets"));
|
||||||
|
const __dirname = process.env.NODE_ENV === 'development' ? process.cwd() : "/app/storage";
|
||||||
|
const UPLOAD_FOLDER = path.join(__dirname, 'Scans');
|
||||||
const UPLOAD_FOLDER = path.join(process.cwd(), 'Scans');
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
cb(null, UPLOAD_FOLDER);
|
cb(null, UPLOAD_FOLDER);
|
||||||
@ -51,7 +50,7 @@ const storage = multer.diskStorage({
|
|||||||
const upload = multer({ storage });
|
const upload = multer({ storage });
|
||||||
|
|
||||||
// Add a new music scan
|
// Add a new music scan
|
||||||
app.post('/api/music-scans/add', upload.single('file'), async (req, res) => {
|
app.post('/api/music-scans/add', AuthHandler, upload.single('file'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name, composer, arranger, words, year, genre, jewish, choral, liturgical, language, instrument } = req.body;
|
const { name, composer, arranger, words, year, genre, jewish, choral, liturgical, language, instrument } = req.body;
|
||||||
const safeName = name.replace(/[^a-zA-Z0-9_\- ]/g, '').toLowerCase() + '.pdf';
|
const safeName = name.replace(/[^a-zA-Z0-9_\- ]/g, '').toLowerCase() + '.pdf';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user