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
25 changes: 18 additions & 7 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")


Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand Down
78 changes: 75 additions & 3 deletions frontend/src/main.js
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 },
Expand All @@ -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);
Expand Down
Loading