diff --git a/app.py b/app.py index b579fad..99a3f89 100644 --- a/app.py +++ b/app.py @@ -123,11 +123,9 @@ def set_headers(response): return response +# Serve SPA for all page routes (CDN-friendly) @app.route("/") def index(): - if not current_user.is_authenticated: - return redirect("/login") - session.permanent = True return app.send_static_file("index.html") @@ -269,11 +267,24 @@ def get_hackernews(): @app.errorhandler(404) def page_not_found(e): - if not current_user.is_authenticated: - return redirect("/login") return app.send_static_file("index.html") +# API endpoint for frontend to check authentication status +@app.route("/api/me") +def get_me(): + if current_user.is_authenticated: + return Response( + json.dumps({"is_authenticated": True, "login": current_user.login}), + mimetype="application/json" + ) + else: + return Response( + json.dumps({"is_authenticated": False}), + mimetype="application/json" + ) + + def is_github_blob(url: str) -> bool: splits = url.split("/") return ( @@ -512,7 +523,7 @@ def get_neighbors_v2(repo_name: str): mimetype="application/json", ) response.headers["Cache-Control"] = "private, max-age=3600" - response.headers["Vary"] = "Cookie" + response.vary.add("Cookie") return response else: # Upsert the repository if it doesn't exist in Gorse. @@ -537,7 +548,7 @@ def get_neighbors_v2(repo_name: str): mimetype="application/json", ) response.headers["Cache-Control"] = "private, max-age=3600" - response.headers["Vary"] = "Cookie" + response.vary.add("Cookie") return response except gorse.GorseException as e: return Response(e.message, status=e.status_code) diff --git a/frontend/src/main.js b/frontend/src/main.js index 5a99dc2..5624ebb 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,5 +1,6 @@ import { createApp } from "vue"; import { createRouter, createWebHistory } from "vue-router"; +import axios from "axios"; import "github-markdown-css/github-markdown-light.css"; import vuetify from "./plugins/vuetify"; @@ -15,11 +16,11 @@ import NotFound from "./views/NotFound.vue"; const routes = [ { path: '/', component: MainLayout, children: [ - { name: 'Explore', path: '', component: Home }, - { name: 'Favorites', path: 'favorites', component: Favorites }, + { name: 'Explore', path: '', component: Home, meta: { requiresAuth: true } }, + { name: 'Favorites', path: 'favorites', component: Favorites, meta: { requiresAuth: true } }, { name: 'Trending', path: 'trending', component: Trending }, { name: 'Trending Language', path: 'trending/:language', component: Trending }, - { name: 'Explore Topic', path: 'topic/:topic', component: Home }, + { name: 'Explore Topic', path: 'topic/:topic', component: Home, meta: { requiresAuth: true } }, ]}, { name: 'Login', path: '/login', component: Login }, { name: 'Privacy', path: '/privacy', component: Privacy }, @@ -32,6 +33,77 @@ const router = createRouter({ routes }); +// Auth state cache with expiration (5 minutes) +const AUTH_CACHE_KEY = "gitrec_auth_state"; +const AUTH_CACHE_EXPIRY = 5 * 60 * 1000; // 5 minutes in milliseconds + +function getCachedAuthState() { + const cached = localStorage.getItem(AUTH_CACHE_KEY); + if (!cached) return null; + + const { is_authenticated, login, timestamp } = JSON.parse(cached); + if (Date.now() - timestamp > AUTH_CACHE_EXPIRY) { + localStorage.removeItem(AUTH_CACHE_KEY); + return null; + } + return { is_authenticated, login }; +} + +function setCachedAuthState(is_authenticated, login) { + localStorage.setItem(AUTH_CACHE_KEY, JSON.stringify({ + is_authenticated, + login, + timestamp: Date.now() + })); +} + +function clearCachedAuthState() { + localStorage.removeItem(AUTH_CACHE_KEY); +} + +// Global axios interceptor for 401 errors +axios.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + clearCachedAuthState(); + if (window.location.pathname !== "/login") { + router.push("/login"); + } + } + return Promise.reject(error); + } +); + +// Router guard: check authentication before accessing protected routes +router.beforeEach(async (to, from, next) => { + if (to.meta.requiresAuth) { + // First check cache + const cached = getCachedAuthState(); + if (cached && cached.is_authenticated) { + next(); + return; + } + + // Cache expired or missing, check with API + try { + const response = await axios.get("/api/me", { withCredentials: true }); + if (response.data.is_authenticated) { + setCachedAuthState(response.data.is_authenticated, response.data.login); + next(); + } else { + clearCachedAuthState(); + next("/login"); + } + } catch (error) { + clearCachedAuthState(); + next("/login"); + } + } else { + next(); + } +}); + const app = createApp(App); app.use(router); app.use(vuetify);