-- Runtime auth/session tables used by the BFF boundary. -- This runs during DB initialization so the API can use a non-owner app role. ALTER TABLE app.app_user ADD COLUMN IF NOT EXISTS affiliation TEXT NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(), ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now(); ALTER TABLE app.app_user ALTER COLUMN affiliation SET DEFAULT ''; CREATE TABLE IF NOT EXISTS app.auth_session ( sid_hash TEXT PRIMARY KEY, user_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE CASCADE, role TEXT NOT NULL, display_name TEXT NOT NULL, cohort_ids JSONB NOT NULL DEFAULT '[]'::jsonb, expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_auth_session_user_active ON app.auth_session(user_id, expires_at) WHERE revoked_at IS NULL; CREATE TABLE IF NOT EXISTS app.user_preferences ( user_id UUID PRIMARY KEY REFERENCES app.app_user(user_id) ON DELETE CASCADE, theme TEXT NOT NULL DEFAULT 'system', voice_preset_id TEXT NOT NULL DEFAULT 'soft-young-fem', voice_rate REAL NOT NULL DEFAULT 1.0, notifications JSONB NOT NULL DEFAULT '{}'::jsonb, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS app.admin_engine_config ( id BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK (id), engine_mode TEXT NOT NULL, engine_url TEXT NOT NULL, model TEXT NOT NULL, updated_by TEXT, updated_at TIMESTAMPTZ ); ALTER TABLE app.sessions ADD COLUMN IF NOT EXISTS runtime_case_id UUID, ADD COLUMN IF NOT EXISTS persona_code TEXT, ADD COLUMN IF NOT EXISTS persona_display_name TEXT, ADD COLUMN IF NOT EXISTS persona_difficulty TEXT, ADD COLUMN IF NOT EXISTS prev_rapport_credit REAL NOT NULL DEFAULT 0.0; ALTER TABLE app.session_state ADD COLUMN IF NOT EXISTS turns_in_stage INT NOT NULL DEFAULT 0; INSERT INTO app.stage_def (stage_code, display_name, seq, base_openness) VALUES ('라포', '라포', 1, 0.15), ('탐색', '탐색', 2, 0.35), ('개입', '개입', 3, 0.55), ('정리', '정리', 4, 0.45) ON CONFLICT (stage_code) DO UPDATE SET display_name = EXCLUDED.display_name, seq = EXCLUDED.seq, base_openness = EXCLUDED.base_openness; DROP POLICY IF EXISTS p_sessions_modify ON app.sessions; DROP POLICY IF EXISTS p_sessions_insert ON app.sessions; DROP POLICY IF EXISTS p_sessions_update ON app.sessions; DROP POLICY IF EXISTS p_sessions_delete ON app.sessions; CREATE POLICY p_sessions_insert ON app.sessions FOR INSERT WITH CHECK ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR learner_id = app.current_uid() ); CREATE POLICY p_sessions_update ON app.sessions FOR UPDATE USING ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR learner_id = app.current_uid() ) WITH CHECK ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR learner_id = app.current_uid() ); CREATE POLICY p_sessions_delete ON app.sessions FOR DELETE USING ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR learner_id = app.current_uid() ); DROP POLICY IF EXISTS p_turns_modify ON app.turns; DROP POLICY IF EXISTS p_turns_insert ON app.turns; DROP POLICY IF EXISTS p_turns_update ON app.turns; DROP POLICY IF EXISTS p_turns_delete ON app.turns; CREATE POLICY p_turns_insert ON app.turns FOR INSERT WITH CHECK ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.turns.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_turns_update ON app.turns FOR UPDATE USING ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.turns.session_id AND s.learner_id = app.current_uid() ) ) WITH CHECK ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.turns.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_turns_delete ON app.turns FOR DELETE USING ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.turns.session_id AND s.learner_id = app.current_uid() ) );