From ebf42e073a37031d0dc8ee952a4901e72484e2eb Mon Sep 17 00:00:00 2001 From: Ari Yeger Date: Fri, 18 Jul 2025 18:19:47 -0400 Subject: [PATCH] feat: Implement session-based authorization for API requests and update upload folder path --- client/package.json | 2 +- client/src/api.ts | 15 +++-- client/src/components/SearchTable.vue | 94 +++++++++++++++------------ client/src/main.ts | 10 +++ client/src/session.ts | 2 + client/src/views/LoginPage.vue | 4 +- client/src/views/SearchMusic.vue | 17 +---- server/app.js | 7 +- server/package.json | 2 +- 9 files changed, 85 insertions(+), 68 deletions(-) diff --git a/client/package.json b/client/package.json index 7b9ff2f..a22c82a 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "0.0.0", + "version": "0.2.0", "private": true, "type": "module", "scripts": { diff --git a/client/src/api.ts b/client/src/api.ts index 9aa7ba4..fe167b1 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -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 = () => { diff --git a/client/src/components/SearchTable.vue b/client/src/components/SearchTable.vue index 2b13c33..cda9986 100644 --- a/client/src/components/SearchTable.vue +++ b/client/src/components/SearchTable.vue @@ -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(0) const results = defineModel('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) => { diff --git a/client/src/main.ts b/client/src/main.ts index 4a2bc38..524ed4a 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -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) diff --git a/client/src/session.ts b/client/src/session.ts index 4b61936..1a0a4e2 100644 --- a/client/src/session.ts +++ b/client/src/session.ts @@ -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: "); diff --git a/client/src/views/LoginPage.vue b/client/src/views/LoginPage.vue index 3d702c9..a9b05ad 100644 --- a/client/src/views/LoginPage.vue +++ b/client/src/views/LoginPage.vue @@ -29,6 +29,7 @@ const showPassword = ref(false) diff --git a/client/src/views/SearchMusic.vue b/client/src/views/SearchMusic.vue index 14e0404..9ca8efb 100644 --- a/client/src/views/SearchMusic.vue +++ b/client/src/views/SearchMusic.vue @@ -86,7 +86,7 @@ watch(filters.value, (value) => { - + @@ -94,20 +94,5 @@ watch(filters.value, (value) => { diff --git a/server/app.js b/server/app.js index 0fb8d5c..ff27ddc 100644 --- a/server/app.js +++ b/server/app.js @@ -37,9 +37,8 @@ app.use("/edit/:id", express.static('../client/dist')); app.use("/login", express.static('../client/dist')); app.use("/register", express.static('../client/dist')); app.use("/assets", express.static("../client/dist/assets")); - - -const UPLOAD_FOLDER = path.join(process.cwd(), 'Scans'); +const __dirname = process.env.NODE_ENV === 'development' ? process.cwd() : "/app/storage"; +const UPLOAD_FOLDER = path.join(__dirname, 'Scans'); const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, UPLOAD_FOLDER); @@ -51,7 +50,7 @@ const storage = multer.diskStorage({ const upload = multer({ storage }); // 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 { 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'; diff --git a/server/package.json b/server/package.json index 5681611..e359b14 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "0.1.0", + "version": "0.2.0", "private": false, "type": "module", "scripts": {