Skip to content
Open
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
43 changes: 18 additions & 25 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1496,7 +1496,7 @@ def _product_price(product):


# ── Feature: Paginated Recommendations ───────────────────────────────
@app.get("/api/recommend")
@app.get("/api/recommend/paginated")
async def recommend_item(
title: str = Query(..., min_length=1, description="Item title to base recommendations on"),
limit: int = Query(default=20, ge=1, le=100, description="Items per page"),
Expand Down Expand Up @@ -2057,15 +2057,15 @@ def get_recommendations(
if rate_limited is not None:
return rate_limited

cache_key = _cache_key("recommend", query_title, top_n, explain, target_catalog, model_version, user_id)
# ----- EDGE CASES SAFE CHECK -----
query_title = title or item_title
if not query_title:
raise HTTPException(422, "Query parameter 'title' is required.")

# ----- EDGE CASES SAFE CHECK -----
# Agar model ready nahi hai ya database bilkul khali hai
if not models or "ready" not in models or not models["ready"]:
raise HTTPException(status_code=400, detail="Models not built or dynamic dataset is empty.")
# ---------------------------------
query_title = title or item_title
if not query_title:
raise HTTPException(422, "Query parameter 'title' is required.")
selected_models = models

if model_version == "staging":
Expand Down Expand Up @@ -2595,28 +2595,21 @@ def get_categories():
except Exception as e:
logger.error("Failed to retrieve categories: %s", e)
return {"categories": []}

@app.post("/api/interactions")
def log_interaction(data: InteractionCreate):
@app.post("/api/interactions")
def log_interaction(data: InteractionCreate):

USER_INTERACTIONS.append({
"user_id": data.user_id,
"item_id": data.item_id,
"interaction_type": data.interaction_type,
"timestamp": datetime.now(timezone.utc).isoformat()
})

return {
"message": "Interaction logged successfully",
"interaction": USER_INTERACTIONS[-1]
}
@app.post("/api/interactions")
def log_interaction(data: InteractionCreate):
USER_INTERACTIONS.append({
"user_id": data.user_id,
"item_id": data.item_id,
"interaction_type": data.interaction_type,
"timestamp": datetime.now(timezone.utc).isoformat()
})

return {
"message": "Interaction logged successfully",
"interaction": USER_INTERACTIONS[-1]
}
return {
"message": "Interaction logged successfully",
"interaction": USER_INTERACTIONS[-1]
}

# ── Purchases ─────────────────────────────────────────────────────────
@app.get("/api/purchases/{user_id}")
Expand Down
2 changes: 1 addition & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ <h2 class="modal__title" id="modal-title">Sign In</h2>
</div>
<div class="form-group">
<label for="auth-password">Password</label>
<input type="password" id="auth-password" placeholder="Min 6 characters" required minlength="6">
<input type="password" id="auth-password" placeholder="Min 8 chars, uppercase, number, symbol" required minlength="8">
</div>
<button type="submit" class="btn btn--primary btn--full" id="auth-submit">Sign In</button>
</form>
Expand Down
23 changes: 22 additions & 1 deletion frontend/js/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ import { showToast, showModal, hideModal, setLoadingState } from './ui.js';

let _supabase = null;

function _validateStrongPassword(password) {
if (typeof password !== 'string' || password.length < 8) {
return 'Use at least 8 characters with an uppercase letter, a number, and a special character.';
}
if (!/[A-Z]/.test(password)) {
return 'Use at least 8 characters with an uppercase letter, a number, and a special character.';
}
if (!/\d/.test(password)) {
return 'Use at least 8 characters with an uppercase letter, a number, and a special character.';
}
if (!/[!@#$%^&*()\-_=+[\]{};:'",.<>/?\\|`~]/.test(password)) {
return 'Use at least 8 characters with an uppercase letter, a number, and a special character.';
}
return null;
}

/** Called once from app.js after Supabase client is created. */
export function initAuth(supabaseClient) {
_supabase = supabaseClient;
Expand Down Expand Up @@ -52,6 +68,11 @@ export async function signInWithEmail(email, password) {

export async function signUpWithEmail(email, password) {
try {
const passwordError = _validateStrongPassword(password);
if (passwordError) {
showToast(passwordError, 'error');
return null;
}
setLoadingState('auth', true);
const { data, error } = await _supabase.auth.signUp({ email, password });
if (error) throw error;
Expand Down Expand Up @@ -138,4 +159,4 @@ export function bindAuthEvents() {
? 'Already have an account? Sign in'
: "Don't have an account? Sign up";
});
}
}
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ redis>=5.0.1
python-dotenv>=1.0.1
pytest-env>=1.1.3
bleach
faiss-cpu>=1.8.0

# Pre-commit hooks
pre-commit>=3.7
Expand Down
15 changes: 14 additions & 1 deletion tests/test_recommend_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def __init__(self):
self.last_target_catalog = None
self.collab_model = FakeCollabModel()

def recommend(self, title, top_n=10, explain=False, target_catalog=None):
def recommend(self, title, top_n=10, explain=False, target_catalog=None, user_id=None):
self.last_title = title
self.last_target_catalog = target_catalog
return [{"title": "Related Item", "hybrid_score": 0.91}]
Expand Down Expand Up @@ -59,6 +59,19 @@ def test_recommend_accepts_reserved_characters_in_query_title():
main._clear_response_cache()


def test_recommend_route_registry_is_unambiguous():
recommend_routes = [
route
for route in app.routes
if getattr(route, "path", None) in {"/api/recommend", "/api/recommend/paginated"}
and "GET" in getattr(route, "methods", set())
]

paths = [route.path for route in recommend_routes]
assert paths.count("/api/recommend") == 1
assert paths.count("/api/recommend/paginated") == 1


def test_user_recommendations_known_user():
hybrid = FakeHybridModel()
original_ready = models["ready"]
Expand Down
Loading