Security (requesting-code-review pipeline + independent reviewer): - Fix path traversal on file upload (basename sanitize + resolve-containment) - Fix IDOR: org + owner scoping on all group/chat routes (_authorize_group/_get_owned_group), hide other users' personal groups in listings - Remove XSS via v-html in Chat task (text interpolation) - Add test_security.py (traversal + cross-user denial) — all pass UX/UI (ui-ux-pro-max + frontend-dev-verification): - Global: focus rings, 44px touch targets, hover/press transitions, input focus glow, prefers-reduced-motion, skeleton loaders, empty states, back links, spinner - Login: password toggle, autocomplete, spinner, disabled-when-empty - Cards lift on hover; dashboard skeleton + empty state; analyze button spinner All backend tests pass (m0/m1/routes/security/e2e); frontend builds; served SPA verified via curl.
124 lines
4.1 KiB
Python
124 lines
4.1 KiB
Python
"""Trainee routes: win/lose board, weak-areas, generate own persona."""
|
|
from __future__ import annotations
|
|
|
|
from flask import Blueprint, jsonify, request
|
|
|
|
from ..llm import LLMError
|
|
from ..services.trainee import MyPersonaStore, analyze_weak_areas
|
|
from .helpers import ApiError, current_user, require_auth, require_roles
|
|
|
|
me_bp = Blueprint("me", __name__)
|
|
|
|
|
|
def _stores():
|
|
from flask import current_app
|
|
|
|
return {
|
|
"groups": current_app.extensions["group_store"],
|
|
"sessions": current_app.extensions["session_store"],
|
|
"my_personas": current_app.extensions.get("my_persona_store"),
|
|
"llm": current_app.extensions["llm"],
|
|
}
|
|
|
|
|
|
@me_bp.get("/board")
|
|
@require_auth
|
|
@require_roles("user")
|
|
def win_lose_board():
|
|
"""Per-persona won/lost/not-tried across all groups the user sees."""
|
|
s = _stores()
|
|
uid = current_user()["id"]
|
|
my_sessions = s["sessions"].list_for_user(uid)
|
|
outcome_by = {(x.get("group_id"), x.get("persona_id")): x.get("outcome") for x in my_sessions}
|
|
|
|
groups = s["groups"].list_visible_to(role="user", org_id=current_user().get("org_id"))
|
|
# Only the owner sees their personal groups (IDOR defense).
|
|
groups = [g for g in groups if not g.get("owner_user_id") or g.get("owner_user_id") == uid]
|
|
items = []
|
|
for g in groups:
|
|
for p in g.get("personas", []):
|
|
key = (g["id"], p["id"])
|
|
items.append({
|
|
"group_id": g["id"],
|
|
"group_title": g.get("title"),
|
|
"persona_id": p["id"],
|
|
"persona_name": p.get("name"),
|
|
"tier": p.get("tier"),
|
|
"my_outcome": outcome_by.get(key, "not_tried"),
|
|
})
|
|
return jsonify({"board": items})
|
|
|
|
|
|
@me_bp.get("/weak-areas")
|
|
@require_auth
|
|
@require_roles("user")
|
|
def weak_areas():
|
|
s = _stores()
|
|
uid = current_user()["id"]
|
|
sessions = s["sessions"].list_for_user(uid)
|
|
insight = analyze_weak_areas(sessions)
|
|
return jsonify({"insight": insight})
|
|
|
|
|
|
def _personal_group(s, actor) -> dict:
|
|
"""Return (or create) the user's private group holding their own personas."""
|
|
groups = s["groups"].list_for_org(org_id=actor.get("org_id"))
|
|
for g in groups:
|
|
if g.get("owner_user_id") == actor["id"]:
|
|
return g
|
|
g = s["groups"].create(
|
|
org_id=actor.get("org_id") or "org-default",
|
|
creator_id=actor["id"],
|
|
title=f"{actor.get('name','User')}'s private personas",
|
|
)
|
|
s["groups"].update(
|
|
g["id"],
|
|
status="ready",
|
|
owner_user_id=actor["id"],
|
|
input={"channel": "facebook", "language": "th"},
|
|
sales_kit={"productName": "personal practice", "valueProps": [], "features": []},
|
|
)
|
|
return s["groups"].get(g["id"])
|
|
|
|
|
|
@me_bp.get("/personas")
|
|
@require_auth
|
|
@require_roles("user")
|
|
def my_personas():
|
|
s = _stores()
|
|
uid = current_user()["id"]
|
|
group = _personal_group(s, current_user())
|
|
return jsonify({"group": group, "personas": group.get("personas", [])})
|
|
|
|
|
|
@me_bp.post("/personas/generate")
|
|
@require_auth
|
|
@require_roles("user")
|
|
def generate_persona():
|
|
s = _stores()
|
|
actor = current_user()
|
|
data = request.get_json(silent=True) or {}
|
|
mode = data.get("mode", "manual") # "weak-area" | "manual"
|
|
spec = data.get("spec") or {}
|
|
llm = s["llm"]
|
|
if not llm:
|
|
raise ApiError("LLM not configured", 500)
|
|
if mode == "weak-area" and not spec:
|
|
# auto-detect weak areas from this user's losses if no spec given
|
|
sessions = s["sessions"].list_for_user(actor["id"])
|
|
spec = analyze_weak_areas(sessions)
|
|
from ..services.own_persona import generate_own_persona
|
|
|
|
try:
|
|
persona = generate_own_persona(llm, mode=mode, spec=spec)
|
|
except (LLMError, ValueError) as exc:
|
|
raise ApiError(f"generation failed: {exc}", 500)
|
|
|
|
group = _personal_group(s, actor)
|
|
group = s["groups"].get(group["id"])
|
|
existing = group.get("personas", [])
|
|
persona["id"] = f"myp-{len(existing)+1:02d}"
|
|
existing.append(persona)
|
|
s["groups"].set_personas(group["id"], existing)
|
|
return jsonify({"persona": persona, "group": s["groups"].get(group["id"])}), 201
|