mirror of
https://github.com/computerim/impactflow-discovery.git
synced 2026-08-27 06:20:36 +00:00
33674f92f4
Close the remaining Phase 1 DoD gaps and reconcile the browser flow with the auth layer. Goals (5 -> 7 prompts): - Add near-term (6-12mo) and long-term (3-5yr) goal prompts; collect raw text on the conversation and store AI-articulated goal summaries on the profile. Extractor articulates the person's own stated goals (mirror, not compass) and never fabricates. Alembic 003 adds the four columns. Cookie-based browser sessions (fixes frontend<->auth desync): - OAuth callback now sets httpOnly session cookies and redirects into the app instead of returning JSON. get_current_user gains a cookie fallback (X-API-Key -> Bearer -> cookie). refresh/logout read the refresh cookie and set/clear cookies. New shared auth.js (authedFetch) sends cookies and silently refreshes on 401. Static pages drop the bogus user_id and call the correct /me endpoints. Profile editing (read/edit/affirm): - PATCH /discovery/profile/me edits the prose (Ikigai summaries, overlap narrative, goals); owner-scoped, partial update, 409 when locked. Edit mode in profile.html with Save/Cancel. Also: bump default model to claude-sonnet-4-6, align ports to 8011 (OAuth redirect, CORS), add COOKIE_SECURE/POST_LOGIN_REDIRECT config, and refresh the README to match the shipped behavior. Tests: 33 passing (added cookie-auth, profile-edit, goal-extraction cases; factored a shared app_client fixture into conftest.py). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
149 lines
5.9 KiB
Python
149 lines
5.9 KiB
Python
"""SQLAlchemy ORM models for the self-discovery module."""
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class User(Base):
|
|
"""A signed-in human (Google OAuth) or the synthetic admin record used
|
|
by the X-API-Key dual-auth path for the MCP server."""
|
|
|
|
__tablename__ = "users"
|
|
|
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
email: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
|
display_name: Mapped[str] = mapped_column(String, nullable=False)
|
|
avatar_url: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
# Google's "sub" claim. Null only for the synthetic API-key admin user.
|
|
google_id: Mapped[Optional[str]] = mapped_column(
|
|
String, unique=True, nullable=True
|
|
)
|
|
role: Mapped[str] = mapped_column(String, nullable=False, default="user")
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
last_login_at: Mapped[Optional[datetime]] = mapped_column(
|
|
DateTime, nullable=True
|
|
)
|
|
|
|
|
|
class RefreshToken(Base):
|
|
"""One row per issued refresh token. token_hash stores a SHA-256 of the
|
|
raw token so a DB leak can't be replayed back at the auth endpoint."""
|
|
|
|
__tablename__ = "refresh_tokens"
|
|
|
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
user_id: Mapped[str] = mapped_column(
|
|
String, ForeignKey("users.id"), nullable=False, index=True
|
|
)
|
|
token_hash: Mapped[str] = mapped_column(
|
|
String, unique=True, nullable=False
|
|
)
|
|
device: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
revoked_at: Mapped[Optional[datetime]] = mapped_column(
|
|
DateTime, nullable=True
|
|
)
|
|
|
|
|
|
class ActivityLog(Base):
|
|
"""Append-only audit trail. Pruned to 90 days on app startup."""
|
|
|
|
__tablename__ = "activity_log"
|
|
|
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
user_id: Mapped[str] = mapped_column(
|
|
String, ForeignKey("users.id"), nullable=False, index=True
|
|
)
|
|
action: Mapped[str] = mapped_column(String, nullable=False)
|
|
resource: Mapped[str] = mapped_column(String, nullable=False)
|
|
resource_id: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
metadata_json: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
source: Mapped[str] = mapped_column(String, nullable=False, default="web")
|
|
ip_address: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
user_agent: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime, nullable=False, index=True
|
|
)
|
|
|
|
|
|
class DiscoveryConversation(Base):
|
|
"""A single self-discovery conversation: the five narrative responses."""
|
|
|
|
__tablename__ = "discovery_conversation"
|
|
|
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
user_id: Mapped[str] = mapped_column(
|
|
String, ForeignKey("users.id"), nullable=False, index=True
|
|
)
|
|
started_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
completed_at: Mapped[Optional[datetime]] = mapped_column(
|
|
DateTime, nullable=True
|
|
)
|
|
|
|
prompt_alive: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
prompt_friction: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
prompt_pull: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
prompt_recognition: Mapped[Optional[str]] = mapped_column(
|
|
Text, nullable=True
|
|
)
|
|
prompt_future: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
|
|
# Phase 1 goal-articulation prompts: the person's own near- and long-term
|
|
# goals, in their own words.
|
|
prompt_goals_short: Mapped[Optional[str]] = mapped_column(
|
|
Text, nullable=True
|
|
)
|
|
prompt_goals_long: Mapped[Optional[str]] = mapped_column(
|
|
Text, nullable=True
|
|
)
|
|
|
|
|
|
class DiscoveryProfile(Base):
|
|
"""The extracted enneagram + Ikigai profile for a conversation."""
|
|
|
|
__tablename__ = "discovery_profile"
|
|
|
|
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
user_id: Mapped[str] = mapped_column(
|
|
String, ForeignKey("users.id"), nullable=False, index=True
|
|
)
|
|
conversation_id: Mapped[str] = mapped_column(
|
|
String, ForeignKey("discovery_conversation.id"), nullable=False
|
|
)
|
|
generated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
|
|
triad: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
probable_type: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
wing: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
instinctual_variant: Mapped[Optional[str]] = mapped_column(
|
|
String, nullable=True
|
|
)
|
|
instinctual_stack: Mapped[Optional[str]] = mapped_column(
|
|
String, nullable=True
|
|
)
|
|
|
|
love_summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
strength_summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
mission_summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
vocation_summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
overlap_narrative: Mapped[Optional[str]] = mapped_column(
|
|
Text, nullable=True
|
|
)
|
|
|
|
# AI-articulated goals: the person's own stated goals, clarified and
|
|
# connected to their Ikigai/enneagram pattern (mirror, never prescription).
|
|
short_term_goals: Mapped[Optional[str]] = mapped_column(
|
|
Text, nullable=True
|
|
)
|
|
long_term_goals: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
|
|
confidence_json: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
locked: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=False
|
|
)
|