From e5fd6b7e7e69883a2bd3048f5b39eab81bcb180d Mon Sep 17 00:00:00 2001 From: jakevanhalder <124746901+jakevanhalder@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:26:07 -0800 Subject: [PATCH 1/4] Authenticate routes. --- backend/musicRecommendationService/urls.py | 5 ++- backend/musicRecommendationService/views.py | 9 ++++- .gitignore => frontend/.gitignore | 0 components.json => frontend/components.json | 0 .../components}/ui/button.tsx | 0 .../components}/ui/input.tsx | 0 {lib => frontend/lib}/utils.ts | 0 next.config.ts => frontend/next.config.ts | 0 .../package-lock.json | 0 package.json => frontend/package.json | 0 .../postcss.config.mjs | 0 {public => frontend/public}/file.svg | 0 {public => frontend/public}/genre_coords.csv | 0 {public => frontend/public}/globe.svg | 0 {public => frontend/public}/next.svg | 0 {public => frontend/public}/vercel.svg | 0 {public => frontend/public}/window.svg | 0 .../src}/app/components/Navbar.tsx | 0 {src => frontend/src}/app/favicon.ico | Bin {src => frontend/src}/app/globals.css | 0 {src => frontend/src}/app/layout.tsx | 0 .../src}/app/login/lastfm-callback/page.tsx | 23 +++++++---- {src => frontend/src}/app/login/page.tsx | 3 +- .../src}/app/music-map/genre-graph.tsx | 0 .../src}/app/music-map/genre-search.tsx | 0 {src => frontend/src}/app/music-map/page.tsx | 0 {src => frontend/src}/app/music-map/types.ts | 0 {src => frontend/src}/app/page.tsx | 11 +++++- .../src}/app/services/lastfm_user.ts | 4 ++ {src => frontend/src}/lib/api/csrfApi.ts | 0 {src => frontend/src}/lib/api/djangoApi.ts | 0 .../src}/lib/components/Navbar.tsx | 16 ++++++-- .../lib/providers/auth-store-provider.tsx | 0 .../src}/lib/stores/auth-store.ts | 0 frontend/src/middleware.ts | 37 ++++++++++++++++++ tsconfig.json => frontend/tsconfig.json | 0 36 files changed, 90 insertions(+), 18 deletions(-) rename .gitignore => frontend/.gitignore (100%) rename components.json => frontend/components.json (100%) rename {components => frontend/components}/ui/button.tsx (100%) rename {components => frontend/components}/ui/input.tsx (100%) rename {lib => frontend/lib}/utils.ts (100%) rename next.config.ts => frontend/next.config.ts (100%) rename package-lock.json => frontend/package-lock.json (100%) rename package.json => frontend/package.json (100%) rename postcss.config.mjs => frontend/postcss.config.mjs (100%) rename {public => frontend/public}/file.svg (100%) rename {public => frontend/public}/genre_coords.csv (100%) rename {public => frontend/public}/globe.svg (100%) rename {public => frontend/public}/next.svg (100%) rename {public => frontend/public}/vercel.svg (100%) rename {public => frontend/public}/window.svg (100%) rename {src => frontend/src}/app/components/Navbar.tsx (100%) rename {src => frontend/src}/app/favicon.ico (100%) rename {src => frontend/src}/app/globals.css (100%) rename {src => frontend/src}/app/layout.tsx (100%) rename {src => frontend/src}/app/login/lastfm-callback/page.tsx (76%) rename {src => frontend/src}/app/login/page.tsx (93%) rename {src => frontend/src}/app/music-map/genre-graph.tsx (100%) rename {src => frontend/src}/app/music-map/genre-search.tsx (100%) rename {src => frontend/src}/app/music-map/page.tsx (100%) rename {src => frontend/src}/app/music-map/types.ts (100%) rename {src => frontend/src}/app/page.tsx (79%) rename {src => frontend/src}/app/services/lastfm_user.ts (84%) rename {src => frontend/src}/lib/api/csrfApi.ts (100%) rename {src => frontend/src}/lib/api/djangoApi.ts (100%) rename {src => frontend/src}/lib/components/Navbar.tsx (93%) rename {src => frontend/src}/lib/providers/auth-store-provider.tsx (100%) rename {src => frontend/src}/lib/stores/auth-store.ts (100%) create mode 100644 frontend/src/middleware.ts rename tsconfig.json => frontend/tsconfig.json (100%) diff --git a/backend/musicRecommendationService/urls.py b/backend/musicRecommendationService/urls.py index 2d9810a..1aa95d3 100644 --- a/backend/musicRecommendationService/urls.py +++ b/backend/musicRecommendationService/urls.py @@ -1,9 +1,10 @@ from django.urls import path -from .views import lastfm_start, lastfm_callback, itsMe, csrfTokenView +from .views import lastfm_start, lastfm_callback, itsMe, csrfTokenView, logout_view urlpatterns = [ + path('csrf/', csrfTokenView, name='csrf_token'), path('lastfm/start/', lastfm_start, name='lastfm_start'), path('lastfm/callback/', lastfm_callback, name='lastfm_callback'), - path('csrf/', csrfTokenView, name='csrf_token'), path('itsme/', itsMe, name='its_me'), + path('logout/', logout_view, name='logout'), ] \ No newline at end of file diff --git a/backend/musicRecommendationService/views.py b/backend/musicRecommendationService/views.py index fe0043e..5f845d0 100644 --- a/backend/musicRecommendationService/views.py +++ b/backend/musicRecommendationService/views.py @@ -8,7 +8,7 @@ from django.shortcuts import render from django.http import JsonResponse from django.contrib.auth.decorators import login_required -from django.contrib.auth import get_user_model, login +from django.contrib.auth import get_user_model, login, logout from .lastfm_stuff import get_session from .models import LastfmLinking @@ -71,4 +71,9 @@ def itsMe(request): def csrfTokenView(request): print("CSRF TOKEN!!!!") token = get_token(request) - return JsonResponse({'csrfToken': token}) \ No newline at end of file + return JsonResponse({'csrfToken': token}) + +# Logout endpoint to properly clear Django session +def logout_view(request): + logout(request) + return JsonResponse({'success': True}) \ No newline at end of file diff --git a/.gitignore b/frontend/.gitignore similarity index 100% rename from .gitignore rename to frontend/.gitignore diff --git a/components.json b/frontend/components.json similarity index 100% rename from components.json rename to frontend/components.json diff --git a/components/ui/button.tsx b/frontend/components/ui/button.tsx similarity index 100% rename from components/ui/button.tsx rename to frontend/components/ui/button.tsx diff --git a/components/ui/input.tsx b/frontend/components/ui/input.tsx similarity index 100% rename from components/ui/input.tsx rename to frontend/components/ui/input.tsx diff --git a/lib/utils.ts b/frontend/lib/utils.ts similarity index 100% rename from lib/utils.ts rename to frontend/lib/utils.ts diff --git a/next.config.ts b/frontend/next.config.ts similarity index 100% rename from next.config.ts rename to frontend/next.config.ts diff --git a/package-lock.json b/frontend/package-lock.json similarity index 100% rename from package-lock.json rename to frontend/package-lock.json diff --git a/package.json b/frontend/package.json similarity index 100% rename from package.json rename to frontend/package.json diff --git a/postcss.config.mjs b/frontend/postcss.config.mjs similarity index 100% rename from postcss.config.mjs rename to frontend/postcss.config.mjs diff --git a/public/file.svg b/frontend/public/file.svg similarity index 100% rename from public/file.svg rename to frontend/public/file.svg diff --git a/public/genre_coords.csv b/frontend/public/genre_coords.csv similarity index 100% rename from public/genre_coords.csv rename to frontend/public/genre_coords.csv diff --git a/public/globe.svg b/frontend/public/globe.svg similarity index 100% rename from public/globe.svg rename to frontend/public/globe.svg diff --git a/public/next.svg b/frontend/public/next.svg similarity index 100% rename from public/next.svg rename to frontend/public/next.svg diff --git a/public/vercel.svg b/frontend/public/vercel.svg similarity index 100% rename from public/vercel.svg rename to frontend/public/vercel.svg diff --git a/public/window.svg b/frontend/public/window.svg similarity index 100% rename from public/window.svg rename to frontend/public/window.svg diff --git a/src/app/components/Navbar.tsx b/frontend/src/app/components/Navbar.tsx similarity index 100% rename from src/app/components/Navbar.tsx rename to frontend/src/app/components/Navbar.tsx diff --git a/src/app/favicon.ico b/frontend/src/app/favicon.ico similarity index 100% rename from src/app/favicon.ico rename to frontend/src/app/favicon.ico diff --git a/src/app/globals.css b/frontend/src/app/globals.css similarity index 100% rename from src/app/globals.css rename to frontend/src/app/globals.css diff --git a/src/app/layout.tsx b/frontend/src/app/layout.tsx similarity index 100% rename from src/app/layout.tsx rename to frontend/src/app/layout.tsx diff --git a/src/app/login/lastfm-callback/page.tsx b/frontend/src/app/login/lastfm-callback/page.tsx similarity index 76% rename from src/app/login/lastfm-callback/page.tsx rename to frontend/src/app/login/lastfm-callback/page.tsx index da3a45c..9a485f2 100644 --- a/src/app/login/lastfm-callback/page.tsx +++ b/frontend/src/app/login/lastfm-callback/page.tsx @@ -14,21 +14,28 @@ export default function LastFmCallback() { useEffect(() => { let mounted = true; - fetchUserInfo() - .then((data: Username) => { + const handleCallback = async () => { + try { + const data = await fetchUserInfo(); + if (!mounted) return; if (data?.username) { setUser(data.username); // TODO: optionally fetch recent scrobbles from backend and call setScrobbles(scrobbles) + router.push("/music-map"); + } else { + router.replace("/login"); } - - router.replace("/music-map"); - }) - .catch((err: unknown) => { + } catch (err: unknown) { console.error("Last.fm callback: failed to fetch user info", err); - router.replace("/login"); - }); + if (mounted) { + router.replace("/login"); + } + } + }; + + handleCallback(); return () => { mounted = false; diff --git a/src/app/login/page.tsx b/frontend/src/app/login/page.tsx similarity index 93% rename from src/app/login/page.tsx rename to frontend/src/app/login/page.tsx index 9aaede4..4998320 100644 --- a/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -1,4 +1,5 @@ 'use client'; + import { logIntoLastFM } from "../services/lastfm_user"; export default function LoginPage() { @@ -11,7 +12,7 @@ export default function LoginPage() {
diff --git a/src/app/music-map/genre-graph.tsx b/frontend/src/app/music-map/genre-graph.tsx similarity index 100% rename from src/app/music-map/genre-graph.tsx rename to frontend/src/app/music-map/genre-graph.tsx diff --git a/src/app/music-map/genre-search.tsx b/frontend/src/app/music-map/genre-search.tsx similarity index 100% rename from src/app/music-map/genre-search.tsx rename to frontend/src/app/music-map/genre-search.tsx diff --git a/src/app/music-map/page.tsx b/frontend/src/app/music-map/page.tsx similarity index 100% rename from src/app/music-map/page.tsx rename to frontend/src/app/music-map/page.tsx diff --git a/src/app/music-map/types.ts b/frontend/src/app/music-map/types.ts similarity index 100% rename from src/app/music-map/types.ts rename to frontend/src/app/music-map/types.ts diff --git a/src/app/page.tsx b/frontend/src/app/page.tsx similarity index 79% rename from src/app/page.tsx rename to frontend/src/app/page.tsx index 3e1fb72..46acfc6 100644 --- a/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,3 +1,7 @@ +'use client' + +import Link from 'next/link' + export default function Home() { return (
@@ -10,9 +14,12 @@ export default function Home() { Unlock your music map by signing in with your Last.fm account.

- +

diff --git a/src/app/services/lastfm_user.ts b/frontend/src/app/services/lastfm_user.ts similarity index 84% rename from src/app/services/lastfm_user.ts rename to frontend/src/app/services/lastfm_user.ts index 97ebad5..e6a0cad 100644 --- a/src/app/services/lastfm_user.ts +++ b/frontend/src/app/services/lastfm_user.ts @@ -15,3 +15,7 @@ export async function logIntoLastFM() { const { data } = await csrfRoute.get('lastfm/start/'); window.location.assign(data.auth_url); } + +export async function logoutUser() { + await djangoRoute.get('logout/'); +} diff --git a/src/lib/api/csrfApi.ts b/frontend/src/lib/api/csrfApi.ts similarity index 100% rename from src/lib/api/csrfApi.ts rename to frontend/src/lib/api/csrfApi.ts diff --git a/src/lib/api/djangoApi.ts b/frontend/src/lib/api/djangoApi.ts similarity index 100% rename from src/lib/api/djangoApi.ts rename to frontend/src/lib/api/djangoApi.ts diff --git a/src/lib/components/Navbar.tsx b/frontend/src/lib/components/Navbar.tsx similarity index 93% rename from src/lib/components/Navbar.tsx rename to frontend/src/lib/components/Navbar.tsx index a5dbf63..8049d06 100644 --- a/src/lib/components/Navbar.tsx +++ b/frontend/src/lib/components/Navbar.tsx @@ -5,6 +5,7 @@ import { useState } from "react"; import { Menu, X } from "lucide-react"; import { useRouter } from "next/navigation"; import { useAuthStore } from "../providers/auth-store-provider"; +import { logoutUser } from "../../app/services/lastfm_user"; export default function Navbar() { const [open, setOpen] = useState(false); @@ -17,17 +18,26 @@ export default function Navbar() { // Make Music Map the primary "home" landing page const navItems = [ { href: "/music-map", label: "Home" }, - { href: "/library", label: "Library" }, ]; - function signOut() { + async function signOut() { + try { + await logoutUser(); + } catch (error) { + console.error('Logout failed:', error); + } + + // Clear client-side state clearUser(); + + // Clear localStorage try { localStorage.removeItem("lastfm-store"); } catch (e) { // ignore (server-side render or restricted storage) } - router.push("/music-map"); + + router.replace("/"); } if (!isAuthenticated) { diff --git a/src/lib/providers/auth-store-provider.tsx b/frontend/src/lib/providers/auth-store-provider.tsx similarity index 100% rename from src/lib/providers/auth-store-provider.tsx rename to frontend/src/lib/providers/auth-store-provider.tsx diff --git a/src/lib/stores/auth-store.ts b/frontend/src/lib/stores/auth-store.ts similarity index 100% rename from src/lib/stores/auth-store.ts rename to frontend/src/lib/stores/auth-store.ts diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts new file mode 100644 index 0000000..a43a896 --- /dev/null +++ b/frontend/src/middleware.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from 'next/server' + +const protectedRoutes = ['/music-map'] + +export default async function middleware(req: NextRequest) { + const path = req.nextUrl.pathname + const isProtectedRoute = protectedRoutes.some(route => path.startsWith(route)) + const sessionCookie = req.cookies.get('sessionid')?.value + const hasSession = !!sessionCookie + + if (path.includes('/lastfm-callback')) { + return NextResponse.next() + } + + if (isProtectedRoute && !hasSession) { + return NextResponse.redirect(new URL('/login', req.nextUrl)) + } + + if (path === '/login' && hasSession) { + return NextResponse.redirect(new URL('/music-map', req.nextUrl)) + } + + if (path === '/') { + if (hasSession) { + return NextResponse.redirect(new URL('/music-map', req.nextUrl)) + } + } + + return NextResponse.next() +} + +// Routes middleware should run on +export const config = { + matcher: [ + '/((?!api|_next/static|_next/image|favicon.ico|.*\\.png$|.*\\.jpg$|.*\\.jpeg$|.*\\.gif$|.*\\.svg$|.*\\.css$|.*\\.js$).*)', + ], +} \ No newline at end of file diff --git a/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from tsconfig.json rename to frontend/tsconfig.json From 3a6216512894e6a6b1948e72b5e038a618654a61 Mon Sep 17 00:00:00 2001 From: jakevanhalder <124746901+jakevanhalder@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:51:36 -0800 Subject: [PATCH 2/4] Fix failing vercel builds. --- .github/workflows/build-check.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml index 9e268dc..540be9d 100644 --- a/.github/workflows/build-check.yml +++ b/.github/workflows/build-check.yml @@ -12,8 +12,10 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 'lts/*' + node-version: "lts/*" - name: Install dependencies run: npm install + working-directory: ./frontend - name: Run build run: npm run build + working-directory: ./frontend From 531d8579d70d00ab67d9c5602d5a39d297b4dd6a Mon Sep 17 00:00:00 2001 From: jakevanhalder <124746901+jakevanhalder@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:59:49 -0800 Subject: [PATCH 3/4] Add missing require http method to logout view. --- backend/musicRecommendationService/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/musicRecommendationService/views.py b/backend/musicRecommendationService/views.py index da79efa..0e48270 100644 --- a/backend/musicRecommendationService/views.py +++ b/backend/musicRecommendationService/views.py @@ -1,7 +1,7 @@ import secrets from urllib.parse import urlencode from django_ratelimit.decorators import ratelimit -from django.views.decorators.http import require_GET +from django.views.decorators.http import require_GET, require_http_methods from django.shortcuts import redirect from rest_framework.views import APIView from django.middleware.csrf import get_token @@ -116,6 +116,7 @@ def csrfTokenView(request): return JsonResponse({'csrfToken': token}) # Logout endpoint to properly clear Django session +@require_http_methods(['POST']) def logout_view(request): logout(request) return JsonResponse({'success': True}) \ No newline at end of file From a6c79d0eee0c18fac5d2c4c2fcdb34ff62963c8e Mon Sep 17 00:00:00 2001 From: jakevanhalder <124746901+jakevanhalder@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:00:49 -0800 Subject: [PATCH 4/4] Remove unused imports. --- backend/musicRecommendationService/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/musicRecommendationService/views.py b/backend/musicRecommendationService/views.py index 0e48270..2c4f903 100644 --- a/backend/musicRecommendationService/views.py +++ b/backend/musicRecommendationService/views.py @@ -7,12 +7,9 @@ from django.middleware.csrf import get_token from django.conf import settings from django.utils.decorators import method_decorator -from django.shortcuts import render -from django.core.cache import cache from django.http import JsonResponse from django.contrib.auth.decorators import login_required from django.contrib.auth import get_user_model, login, logout -from .lastfm_stuff import get_session from .models import LastfmLinking from .songRecModel import musicRecommendationSystem