Stabilize runtime auth and E2E coverage

This commit is contained in:
Yun Chan 2026-06-26 14:47:00 +09:00
parent 6a3e3b541c
commit 188e899394
133 changed files with 55987 additions and 6775 deletions

View file

@ -171,13 +171,24 @@ CREATE POLICY p_sessions_select ON app.sessions FOR SELECT USING (
AND u.cohort = current_setting('app.current_cohort', true)))
);
DROP POLICY IF EXISTS p_sessions_modify ON app.sessions;
CREATE POLICY p_sessions_modify ON app.sessions FOR ALL USING (
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()
);
-- ── turns: 세션 소유 + visible_to(레이어1) 동시 ──
ALTER TABLE app.turns ENABLE ROW LEVEL SECURITY;

View file

@ -0,0 +1,125 @@
-- 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()
)
);

View file

@ -0,0 +1,72 @@
-- Persist session-level evaluator output for learner reviews and teacher/admin views.
CREATE TABLE IF NOT EXISTS app.session_evaluation (
session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE,
status TEXT NOT NULL CHECK (status IN ('ready','degraded','error')),
source TEXT NOT NULL,
scope TEXT NOT NULL,
stage TEXT NOT NULL,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE app.session_evaluation ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS p_session_evaluation_select ON app.session_evaluation;
DROP POLICY IF EXISTS p_session_evaluation_insert ON app.session_evaluation;
DROP POLICY IF EXISTS p_session_evaluation_update ON app.session_evaluation;
DROP POLICY IF EXISTS p_session_evaluation_delete ON app.session_evaluation;
CREATE POLICY p_session_evaluation_select
ON app.session_evaluation FOR SELECT 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.session_evaluation.session_id
AND s.learner_id = app.current_uid()
)
);
CREATE POLICY p_session_evaluation_insert
ON app.session_evaluation 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.session_evaluation.session_id
AND s.learner_id = app.current_uid()
)
);
CREATE POLICY p_session_evaluation_update
ON app.session_evaluation 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.session_evaluation.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.session_evaluation.session_id
AND s.learner_id = app.current_uid()
)
);
CREATE POLICY p_session_evaluation_delete
ON app.session_evaluation 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.session_evaluation.session_id
AND s.learner_id = app.current_uid()
)
);

View file

@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -e
APP_DB_USER="${APP_DB_USER:-vignette_app}"
APP_DB_PASSWORD="${APP_DB_PASSWORD:-vignette_app}"
case "$APP_DB_USER" in
*[!a-zA-Z0-9_]*)
echo "APP_DB_USER must contain only letters, numbers, and underscores" >&2
exit 1
;;
esac
if psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -tAc "SELECT 1 FROM pg_roles WHERE rolname = '$APP_DB_USER'" | grep -q 1; then
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
-v app_user="$APP_DB_USER" \
-v app_password="$APP_DB_PASSWORD" <<'EOSQL'
ALTER ROLE :"app_user" WITH LOGIN PASSWORD :'app_password' NOBYPASSRLS;
EOSQL
else
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
-v app_user="$APP_DB_USER" \
-v app_password="$APP_DB_PASSWORD" <<'EOSQL'
CREATE ROLE :"app_user" WITH LOGIN PASSWORD :'app_password' NOBYPASSRLS;
EOSQL
fi
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
-v app_user="$APP_DB_USER" \
-v db_name="$POSTGRES_DB" <<'EOSQL'
GRANT CONNECT ON DATABASE :"db_name" TO :"app_user";
GRANT USAGE ON SCHEMA app, kb, audit, ds TO :"app_user";
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app, kb, audit, ds TO :"app_user";
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app, kb, audit, ds TO :"app_user";
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA app TO :"app_user";
ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO :"app_user";
ALTER DEFAULT PRIVILEGES IN SCHEMA kb GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO :"app_user";
ALTER DEFAULT PRIVILEGES IN SCHEMA audit GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO :"app_user";
ALTER DEFAULT PRIVILEGES IN SCHEMA ds GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO :"app_user";
ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT USAGE, SELECT ON SEQUENCES TO :"app_user";
ALTER DEFAULT PRIVILEGES IN SCHEMA kb GRANT USAGE, SELECT ON SEQUENCES TO :"app_user";
ALTER DEFAULT PRIVILEGES IN SCHEMA audit GRANT USAGE, SELECT ON SEQUENCES TO :"app_user";
ALTER DEFAULT PRIVILEGES IN SCHEMA ds GRANT USAGE, SELECT ON SEQUENCES TO :"app_user";
ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT EXECUTE ON FUNCTIONS TO :"app_user";
EOSQL