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 diff --git a/backend/musicRecommendationService/urls.py b/backend/musicRecommendationService/urls.py index 1359446..2cf132d 100644 --- a/backend/musicRecommendationService/urls.py +++ b/backend/musicRecommendationService/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import lastfm_start, lastfm_callback, itsMe, csrfTokenView, RecommendationView +from .views import lastfm_start, lastfm_callback, itsMe, csrfTokenView, RecommendationView, logout_view urlpatterns = [ path('csrf/', csrfTokenView, name='csrf_token'), @@ -7,4 +7,5 @@ path('lastfm/callback/', lastfm_callback, name='lastfm_callback'), path('itsme/', itsMe, name='its_me'), path('recommendation/', RecommendationView.as_view(), name='recommendation'), + 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 11475f4..2c4f903 100644 --- a/backend/musicRecommendationService/views.py +++ b/backend/musicRecommendationService/views.py @@ -1,17 +1,15 @@ 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 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 +from django.contrib.auth import get_user_model, login, logout from .models import LastfmLinking from .songRecModel import musicRecommendationSystem @@ -114,5 +112,8 @@ def csrfTokenView(request): token = get_token(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 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/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