Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion backend/musicRecommendationService/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
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'),
path('lastfm/start/', lastfm_start, name='lastfm_start'),
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'),
]
13 changes: 7 additions & 6 deletions backend/musicRecommendationService/views.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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})
Comment thread
jakevanhalder marked this conversation as resolved.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/app/login/page.tsx → frontend/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client';

import { logIntoLastFM } from "../services/lastfm_user";

export default function LoginPage() {
Expand All @@ -11,7 +12,7 @@ export default function LoginPage() {
<div className="flex justify-end">
<button
onClick={() => logIntoLastFM().catch(console.error)}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
Sign in with LastFM
</button>
Expand Down
File renamed without changes.
File renamed without changes.
11 changes: 9 additions & 2 deletions src/app/page.tsx → frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
'use client'

import Link from 'next/link'

export default function Home() {
return (
<div className="font-sans flex flex-col flex-1 justify-between min-h-full mt-8 sm:mt-10">
Expand All @@ -10,9 +14,12 @@ export default function Home() {
Unlock your music map by signing in with your Last.fm account.
</p>
</div>
<button className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-md">
<Link
href="/login"
className="inline-block mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
Start Your Personalized Experience
</button>
</Link>
</div>
<div className="text-center pb-4 sm:text-right sm:flex sm:justify-end sm:pr-4 md:pr-8">
<p className="text-sm text-muted-foreground sm:w-100 md:w-100">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/');
}
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
File renamed without changes.
37 changes: 37 additions & 0 deletions frontend/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -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$).*)',
],
}
File renamed without changes.