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
141 changes: 113 additions & 28 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import io
import json
import logging
import random
import requests
import os
import sys
Expand Down Expand Up @@ -319,30 +320,103 @@ def convert_github_blob(url: str) -> str:

@app.route("/api/repo")
@app.route("/api/repo/<category>")
@login_required
def get_repo(category: str = ""):
# Get recommended repo
repo_id = None
for _ in range(2):
try:
repo_id = gorse_client.get_recommend(current_user.login, category)[0]
break
except UnknownObjectException:
logging.warn("repo %s not found" % repo_id)
gorse_client.delete_item(repo_id)
"""Get a recommended repository. Uses Gorse for logged-in users,
or random trending repo for anonymous users."""

if current_user.is_authenticated:
# Get recommended repo for logged-in users
repo_id = None
for _ in range(2):
try:
repo_id = gorse_client.get_recommend(current_user.login, category)[0]
break
except UnknownObjectException:
logging.warn("repo %s not found" % repo_id)
gorse_client.delete_item(repo_id)
Comment thread
zhenghaoz marked this conversation as resolved.

if repo_id is None:
return Response("No repository found", status=404)

# Check cache first
cache_key = f"repo:{repo_id}"
cached = get_cached(cache_key)
if cached is not None:
return cached

full_name = repo_id.replace(":", "/")
github_client = Github(current_user.token["access_token"])
else:
# For anonymous users, get a random trending repo
language = category.lower() if category else "all"
# First try to get from trending cache
trending_cache_key = f"api:trending:{language}:daily"
trending_data = get_cached(trending_cache_key)

if trending_data is None:
# Fetch trending data if not cached
url = (
"https://raw.githubusercontent.com/isboyjc/github-trending-api/main/"
f"data/daily/{language}.json"
)
Comment thread
zhenghaoz marked this conversation as resolved.
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
payload = resp.json()
trending_data = payload.get("items", [])
if trending_data:
save_cache(trending_cache_key, trending_data, expiry_hours=1)
except Exception as e:
app.logger.error(f"Error fetching trending for anonymous user: {e}")
return Response("Failed to fetch trending repositories", status=500)

if not trending_data:
return Response("No trending repositories available", status=404)

# Filter out already read repos from session
read_repos = session.get("read_repos", [])
available_repos = [
repo
for repo in trending_data
if isinstance(repo.get("full_name"), str)
and repo.get("full_name", "").strip()
and "/" in repo.get("full_name", "").strip()
and repo.get("full_name", "").strip().replace("/", ":").lower() not in read_repos
]

# If all repos have been read, reset session and use all valid trending repos
if not available_repos:
available_repos = [
repo
for repo in trending_data
if isinstance(repo.get("full_name"), str)
and repo.get("full_name", "").strip()
and "/" in repo.get("full_name", "").strip()
]
session["read_repos"] = []

Comment thread
zhenghaoz marked this conversation as resolved.
if not available_repos:
return Response("No valid trending repositories available", status=404)

# Randomly select a trending repo
random_repo = random.choice(available_repos)
full_name = random_repo.get("full_name", "").strip()

if not full_name or "/" not in full_name:
return Response("Invalid trending repository", status=500)

repo_id = full_name.replace("/", ":").lower()

# Check cache first
cache_key = f"repo:{repo_id}"
cached = get_cached(cache_key)
if cached is not None:
return cached

# Use global github client for anonymous users
github_client = global_github_client

if repo_id is None:
return Response("No repository found", status=404)

# Check cache first
cache_key = f"repo:{repo_id}"
cached = get_cached(cache_key)
if cached is not None:
return cached

# Fetch from GitHub API
full_name = repo_id.replace(":", "/")
github_client = Github(current_user.token["access_token"])
repo = github_client.get_repo(full_name)
readme = repo.get_readme()
download_url = readme.download_url.lower()
Expand Down Expand Up @@ -439,17 +513,28 @@ def like_repo(repo_name: str):


@app.route("/api/read/<repo_name>", methods=["POST"])
@login_required
def read_repo(repo_name: str):
"""
Insert a "read" feedback.
For logged-in users: insert to Gorse.
For anonymous users: save to session.
"""
try:
return gorse_client.insert_feedback(
"read", current_user.login, repo_name.lower(), datetime.now().isoformat(), 1
)
except gorse.GorseException as e:
return Response(e.message, status=e.status_code)
repo_name = repo_name.lower()

if current_user.is_authenticated:
try:
return gorse_client.insert_feedback(
"read", current_user.login, repo_name, datetime.now().isoformat(), 1
)
except gorse.GorseException as e:
return Response(e.message, status=e.status_code)
else:
# For anonymous users, save read repos in session
read_repos = session.get("read_repos", [])
if repo_name not in read_repos:
read_repos.append(repo_name)
session["read_repos"] = read_repos
return Response(json.dumps({"success": True}), mimetype="application/json")
Comment thread
zhenghaoz marked this conversation as resolved.


@app.route("/api/delete/<repo_name>", methods=["POST"])
Expand Down
67 changes: 61 additions & 6 deletions frontend/src/layouts/MainLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@
<v-list nav density="comfortable">
<v-list-item title="Explore" :active="isExploreRoute" @click="goTo('/')" />
<v-list-item title="Trending" :active="isTrendingRoute" @click="goTo('/trending')" />
<v-list-item title="Favorites" :active="$route.path === '/favorites'" @click="goTo('/favorites')" />
<v-list-item prepend-icon="mdi-github" href="https://github.com/gorse-io/gitrec" target="_blank" />
<v-list-item v-if="isAuthenticated" title="Favorites" :active="$route.path === '/favorites'" @click="goTo('/favorites')" />
<v-list-item v-if="isAuthenticated">
<div class="d-flex align-center w-100">
<v-btn variant="text" icon="mdi-github" href="https://github.com/gorse-io/gitrec" target="_blank" />
<v-btn variant="text" icon="mdi-logout" @click="logout" />
</div>
</v-list-item>
<v-list-item v-if="!isAuthenticated">
<div class="d-flex align-center w-100">
<v-btn variant="text" icon="mdi-github" href="https://github.com/gorse-io/gitrec" target="_blank" />
<v-btn variant="text" icon="mdi-login" @click="goTo('/login')" />
</div>
</v-list-item>
</v-list>
</v-navigation-drawer>

Expand All @@ -20,7 +31,7 @@
<div class="d-none d-md-flex align-center ga-1">
<v-btn variant="text" :to="'/'" :active="isExploreRoute" color="white">Explore</v-btn>
<v-btn variant="text" :to="'/trending'" :active="isTrendingRoute" color="white">Trending</v-btn>
<v-btn variant="text" :to="'/favorites'" :active="$route.path === '/favorites'" color="white">Favorites</v-btn>
<v-btn v-if="isAuthenticated" variant="text" :to="'/favorites'" :active="$route.path === '/favorites'" color="white">Favorites</v-btn>

<v-menu location="bottom end">
<template #activator="{ props }">
Expand Down Expand Up @@ -48,7 +59,8 @@
</v-menu>

<v-btn variant="text" href="https://github.com/gorse-io/gitrec" target="_blank" color="white" icon="mdi-github" />
<v-btn variant="text" color="white" icon="mdi-logout" @click="logout" />
<v-btn v-if="isAuthenticated" variant="text" color="white" icon="mdi-logout" @click="logout" />
<v-btn v-if="!isAuthenticated" variant="text" color="white" icon="mdi-login" to="/login" />
</div>
</v-container>

Expand All @@ -63,7 +75,7 @@
class="topic-tabs"
>
<v-tab
v-for="topic in topics"
v-for="topic in visibleTopics"
:key="topic"
:value="topicToPath(topic)"
:to="topicToPath(topic)"
Expand Down Expand Up @@ -111,6 +123,7 @@ export default {
data() {
return {
drawer: false,
isAuthenticated: false,
topics: [
"all",
"ai",
Expand Down Expand Up @@ -159,8 +172,49 @@ export default {
activeLanguage() {
return this.$route.params.language || "all";
},
// Filter topics: hide 'ai' for anonymous users
visibleTopics() {
if (this.isAuthenticated) {
return this.topics;
}
return this.topics.filter(topic => topic !== "ai");
},
},
async mounted() {
await this.checkAuth();
},
methods: {
async checkAuth() {
const cached = localStorage.getItem("gitrec_auth_state");
if (cached) {
try {
const { is_authenticated, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < 5 * 60 * 1000) {
this.isAuthenticated = is_authenticated;
return;
}
} catch (error) {
localStorage.removeItem("gitrec_auth_state");
}
}

try {
const response = await axios.get("/api/me", { withCredentials: true });
this.isAuthenticated = response.data.is_authenticated;
if (this.isAuthenticated) {
localStorage.setItem("gitrec_auth_state", JSON.stringify({
is_authenticated: true,
login: response.data.login,
timestamp: Date.now()
}));
} else {
localStorage.removeItem("gitrec_auth_state");
}
} catch (error) {
Comment thread
zhenghaoz marked this conversation as resolved.
localStorage.removeItem("gitrec_auth_state");
this.isAuthenticated = false;
}
},
goTo(path) {
this.drawer = false;
if (this.$route.path !== path) {
Expand All @@ -180,7 +234,8 @@ export default {
try {
await axios.get("/api/logout");
localStorage.removeItem("gitrec_auth_state");
Comment thread
zhenghaoz marked this conversation as resolved.
this.$router.push("/login");
this.isAuthenticated = false;
this.$router.push("/");
} catch (error) {
console.error("Logout failed:", error);
}
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import NotFound from "./views/NotFound.vue";

const routes = [
{ path: '/', component: MainLayout, children: [
{ name: 'Explore', path: '', component: Home, meta: { requiresAuth: true } },
{ name: 'Explore', path: '', component: Home },
{ 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, meta: { requiresAuth: true } },
{ name: 'Explore Topic', path: 'topic/:topic', component: Home },
]},
{ name: 'Login', path: '/login', component: Login },
{ name: 'Privacy', path: '/privacy', component: Privacy },
Expand Down Expand Up @@ -62,12 +62,14 @@ function clearCachedAuthState() {
}

// Global axios interceptor for 401 errors
// Only redirect to login for Favorites route (requires auth)
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
clearCachedAuthState();
if (window.location.pathname !== "/login") {
// Only redirect to login if on Favorites page
if (window.location.pathname === "/favorites") {
router.push("/login");
}
}
Expand Down
Loading
Loading