From abe403b7aad4ed4c253901ee65c64e85baab2bf1 Mon Sep 17 00:00:00 2001 From: zhangzhenghao Date: Sat, 25 Apr 2026 12:14:23 +0800 Subject: [PATCH 1/3] Move auth check to frontend for CDN-friendly caching - Backend: Remove login checks from page routes (/ /login /privacy /404) - Backend: Add /api/me endpoint for frontend auth status check - Backend: Add Cache-Control headers to disable CDN caching on API endpoints - Frontend: Add router guard to check authentication before accessing protected routes This change makes all page routes return static files that can be safely cached by CDN, while authentication logic is handled by the frontend. --- app.py | 26 +++++++++++++++++++++----- frontend/src/main.js | 25 ++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/app.py b/app.py index b579fad..249de62 100644 --- a/app.py +++ b/app.py @@ -120,14 +120,17 @@ def github_logged_in(blueprint, token): @app.after_request def set_headers(response): response.headers["Referrer-Policy"] = "no-referrer" + + # Disable CDN caching for API endpoints + if request.path.startswith("/api/"): + response.headers["Cache-Control"] = "private, no-store, no-cache, must-revalidate" + 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 +272,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 ( diff --git a/frontend/src/main.js b/frontend/src/main.js index 5a99dc2..3f6a81d 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,24 @@ const router = createRouter({ routes }); +// Router guard: check authentication before accessing protected routes +router.beforeEach(async (to, from, next) => { + if (to.meta.requiresAuth) { + try { + const response = await axios.get("/api/me", { withCredentials: true }); + if (response.data.is_authenticated) { + next(); + } else { + next("/login"); + } + } catch (error) { + next("/login"); + } + } else { + next(); + } +}); + const app = createApp(App); app.use(router); app.use(vuetify); From fd26a882073a23d1f92477b6a7b9bf5c2d8d5335 Mon Sep 17 00:00:00 2001 From: zhangzhenghao Date: Sat, 25 Apr 2026 20:35:10 +0800 Subject: [PATCH 2/3] feat: add auth state cache and global 401 handler - Add localStorage cache for auth state with 5-minute expiry - Add axios interceptor to handle 401 errors globally - Clear cache and redirect to login on auth failure - Reduce unnecessary /api/me requests --- frontend/src/main.js | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/frontend/src/main.js b/frontend/src/main.js index 3f6a81d..5624ebb 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -33,17 +33,70 @@ 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 { From debf0b9795167efc6f88388a5a52c60d3429d336 Mon Sep 17 00:00:00 2001 From: Zhenghao Zhang Date: Sun, 26 Apr 2026 10:36:18 +0800 Subject: [PATCH 3/3] refactor: improve response header handling for API caching --- app.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 249de62..99a3f89 100644 --- a/app.py +++ b/app.py @@ -120,11 +120,6 @@ def github_logged_in(blueprint, token): @app.after_request def set_headers(response): response.headers["Referrer-Policy"] = "no-referrer" - - # Disable CDN caching for API endpoints - if request.path.startswith("/api/"): - response.headers["Cache-Control"] = "private, no-store, no-cache, must-revalidate" - return response @@ -528,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. @@ -553,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)