mirror of
https://github.com/computerim/impactflow-discovery.git
synced 2026-08-27 05:00:37 +00:00
b8f176bb31
Discovery service (pre-existing): FastAPI + async SQLAlchemy + Alembic +
SQLite + Anthropic, with a five-prompt static UI that produces an Enneagram
+ Ikigai profile.
Auth implementation (this change set) follows
Impact_Flow_Auth_Plan_OAuth.html, adapted to the discovery_conversation /
discovery_profile schema:
- app/auth.py: Google OAuth registration, JWT issue/decode, dual-auth
dependency (Bearer JWT or X-API-Key), refresh-token hashing, domain
allow-list, synthetic api-key-admin user
- app/tracking.py: ActivityTrackingMiddleware + log_activity helper;
tags machine-to-machine calls source=mcp
- app/routers/auth.py: /api/auth/{login,callback,refresh,logout},
/api/me, /api/me/{stats,sessions,sessions/{id}}
- app/routers/activity.py: /api/activity, /api/activity/summary,
/api/admin/activity, plus prune_old_activity (90-day retention)
- app/routers/discovery.py: every route now user-scoped via the auth
dependency; /discovery/profile/{user_id} -> /discovery/profile/me
- alembic/versions/002_add_auth.py: users, refresh_tokens, activity_log
- tests/test_auth.py: 8 tests covering 401 paths, X-API-Key admin
resolution, JWT round-trip, admin gating, domain allow-list
- README.md: Authentication section, expanded env-var table, updated
data-model and API-reference tables
- .env.example: new GOOGLE_*, JWT_*, IMPACTFLOW_API_KEY, CORS_*,
ALLOWED_EMAIL_DOMAINS placeholders
- .gitignore: also exclude data/*.log
Tests: 19/19 pass (11 pre-existing + 8 new). smoke_test.py exercises the
full discovery flow under X-API-Key plus 401 paths, OAuth login redirect,
activity logging, and /api/me/stats.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
158 lines
5.3 KiB
HTML
158 lines
5.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>ImpactFlow — Your Profile</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Playfair+Display:wght@500;600&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
<link rel="stylesheet" href="/static/style.css" />
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<div class="brand">ImpactFlow · Your Direction</div>
|
|
<div id="content">
|
|
<p>Loading your profile…</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Plain-language description of each enneagram triad. We never show the
|
|
// type number on this page — only the pattern.
|
|
const TRIAD_INFO = {
|
|
gut: {
|
|
label: "You lead with instinct and will",
|
|
text:
|
|
"You move through the world from your gut. You sense what's right before you can fully explain it, and you're driven to act, to protect what matters, and to set things right. Autonomy and integrity are non-negotiable for you, and you'd rather meet a problem head-on than wait for permission.",
|
|
},
|
|
heart: {
|
|
label: "You lead with feeling and connection",
|
|
text:
|
|
"You navigate through emotion and relationship. You're finely attuned to how things land — for you and for the people around you — and you care deeply about being genuinely seen for who you are. Your energy comes alive in connection, recognition, and making others feel that they matter.",
|
|
},
|
|
head: {
|
|
label: "You lead with thought and perception",
|
|
text:
|
|
"You meet the world through your mind. You like to understand before you commit — gathering information, mapping possibilities, and anticipating what's ahead so you can feel prepared and secure. Your gift is seeing patterns and thinking your way toward clarity others miss.",
|
|
},
|
|
};
|
|
|
|
const IKIGAI = [
|
|
{ key: "love_summary", title: "What you love", icon: "♥" },
|
|
{ key: "strength_summary", title: "What you're good at", icon: "★" },
|
|
{ key: "mission_summary", title: "What the world needs", icon: "◆" },
|
|
{ key: "vocation_summary", title: "What you can be paid for", icon: "$" },
|
|
];
|
|
|
|
function getParam(name) {
|
|
return new URLSearchParams(window.location.search).get(name);
|
|
}
|
|
|
|
function confClass(level) {
|
|
if (level === "high") return "high";
|
|
if (level === "medium") return "medium";
|
|
return "low";
|
|
}
|
|
|
|
function dot(level) {
|
|
const c = confClass(level);
|
|
const title = `Confidence: ${level || "low"}`;
|
|
return `<span class="dot ${c}" title="${title}"></span>`;
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
if (s == null) return "";
|
|
return String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}
|
|
|
|
async function load() {
|
|
const userId = getParam("user_id");
|
|
const content = document.getElementById("content");
|
|
|
|
if (!userId) {
|
|
content.innerHTML =
|
|
'<div class="error-box">No user id provided.</div>';
|
|
return;
|
|
}
|
|
|
|
let profile;
|
|
try {
|
|
const res = await fetch(
|
|
`/discovery/profile/${encodeURIComponent(userId)}`
|
|
);
|
|
if (!res.ok) throw new Error("Profile not found");
|
|
profile = await res.json();
|
|
} catch (err) {
|
|
content.innerHTML = `<div class="error-box">Could not load your profile: ${escapeHtml(
|
|
err.message
|
|
)}</div>`;
|
|
return;
|
|
}
|
|
|
|
const conf = profile.confidence || {};
|
|
const triad = TRIAD_INFO[profile.triad] || TRIAD_INFO.gut;
|
|
|
|
const ikigaiCards = IKIGAI.map((item) => {
|
|
return `
|
|
<div class="card">
|
|
<h3>${dot(conf.ikigai)} ${item.icon} ${item.title}</h3>
|
|
<p>${escapeHtml(profile[item.key]) || "—"}</p>
|
|
</div>`;
|
|
}).join("");
|
|
|
|
const locked = profile.locked;
|
|
|
|
content.innerHTML = `
|
|
<div class="profile-narrative">${escapeHtml(
|
|
profile.overlap_narrative
|
|
)}</div>
|
|
|
|
<p class="section-label">Where your four circles meet</p>
|
|
<div class="ikigai-grid">${ikigaiCards}</div>
|
|
|
|
<div class="triad-block">
|
|
<h2>${dot(conf.triad)} ${triad.label}</h2>
|
|
<p>${triad.text}</p>
|
|
</div>
|
|
|
|
<div class="confirm-row">
|
|
<button class="btn-primary ${locked ? "confirmed" : ""}" id="confirmBtn" ${
|
|
locked ? "disabled" : ""
|
|
}>
|
|
${locked ? "Profile confirmed ✓" : "This is me"}
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
const btn = document.getElementById("confirmBtn");
|
|
if (btn && !locked) {
|
|
btn.addEventListener("click", async () => {
|
|
btn.disabled = true;
|
|
try {
|
|
const res = await fetch(
|
|
`/discovery/profile/${encodeURIComponent(userId)}/confirm`,
|
|
{ method: "PUT" }
|
|
);
|
|
if (!res.ok) throw new Error("confirm failed");
|
|
btn.textContent = "Profile confirmed ✓";
|
|
btn.classList.add("confirmed");
|
|
} catch (err) {
|
|
btn.disabled = false;
|
|
btn.textContent = "Try again";
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
load();
|
|
</script>
|
|
</body>
|
|
</html>
|