개선관리 요구사항과 Google 로그인을 완료
This commit is contained in:
parent
cc0a15b7c6
commit
2a39636163
112 changed files with 10166 additions and 527 deletions
|
|
@ -39,6 +39,7 @@ class SessionUser:
|
|||
consent_at: float | None
|
||||
profile_completed_at: float | None
|
||||
expires_at: float
|
||||
learner_feedback_enabled: bool = True
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
|
@ -67,6 +68,7 @@ class ManagedUser:
|
|||
privacy_version: str
|
||||
created_at: float
|
||||
last_seen_at: float
|
||||
learner_feedback_enabled: bool = True
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
|
@ -93,6 +95,7 @@ class ManagedUserMemoryInput:
|
|||
privacy_agreed_at: float | None = None
|
||||
terms_version: str | None = None
|
||||
privacy_version: str | None = None
|
||||
learner_feedback_enabled: bool | None = None
|
||||
reactivate: bool = False
|
||||
|
||||
@classmethod
|
||||
|
|
@ -125,6 +128,7 @@ class ManagedUserMemoryInput:
|
|||
privacy_agreed_at=user.privacy_agreed_at,
|
||||
terms_version=user.terms_version,
|
||||
privacy_version=user.privacy_version,
|
||||
learner_feedback_enabled=user.learner_feedback_enabled,
|
||||
reactivate=reactivate,
|
||||
)
|
||||
|
||||
|
|
@ -140,6 +144,7 @@ class ManagedUserUpsertInput:
|
|||
external_id: str | None = None
|
||||
affiliation: str | None = None
|
||||
account_status: AccountStatus | None = None
|
||||
learner_feedback_enabled: bool | None = None
|
||||
reactivate: bool = False
|
||||
|
||||
|
||||
|
|
@ -162,6 +167,7 @@ class ManagedUserPatch:
|
|||
complete_onboarding: bool = False
|
||||
terms_version: str | None = None
|
||||
privacy_version: str | None = None
|
||||
learner_feedback_enabled: bool | None = None
|
||||
|
||||
|
||||
class InactiveUserError(Exception):
|
||||
|
|
@ -203,6 +209,21 @@ def _normalize_email(email: str) -> str:
|
|||
return email.strip().lower()
|
||||
|
||||
|
||||
def _email_domain(email: str) -> str:
|
||||
normalized = _normalize_email(email)
|
||||
if "@" not in normalized:
|
||||
return ""
|
||||
return normalized.rsplit("@", 1)[1]
|
||||
|
||||
|
||||
def _allowed_email_domain_set() -> set[str]:
|
||||
return {
|
||||
normalized
|
||||
for value in settings.auth_allowed_email_domains
|
||||
if (normalized := str(value).strip().lower().lstrip("@"))
|
||||
}
|
||||
|
||||
|
||||
def _normalize_external_id(external_id: str | None, email: str) -> str:
|
||||
value = (external_id or "").strip().lower()
|
||||
return value or f"email:{_normalize_email(email)}"
|
||||
|
|
@ -270,7 +291,13 @@ def _initial_account_status(
|
|||
normalized_email = _normalize_email(email)
|
||||
if reactivate or normalized_email in _auto_approved_email_set():
|
||||
return "approved"
|
||||
if settings.environment == "dev" and _is_dev_login_external_id(external_id):
|
||||
# 로컬 E2E 자동 승인은 허용된 내부 도메인에만 적용한다. 관리자 exact-email로
|
||||
# 사전등록한 외부 연구참여자의 pending 상태를 dev 로그인이 승격하면 안 된다.
|
||||
if (
|
||||
settings.environment == "dev"
|
||||
and _is_dev_login_external_id(external_id)
|
||||
and _email_domain(normalized_email) in _allowed_email_domain_set()
|
||||
):
|
||||
return "approved"
|
||||
return _account_status(settings.auth_new_user_default_status)
|
||||
|
||||
|
|
@ -332,10 +359,11 @@ async def _runtime_tables_ready(conn) -> bool:
|
|||
'last_seen_at',
|
||||
'account_status',
|
||||
'admin_access',
|
||||
'learner_feedback_enabled',
|
||||
'updated_at'
|
||||
)
|
||||
GROUP BY table_schema, table_name
|
||||
HAVING count(*) = 19
|
||||
HAVING count(*) = 20
|
||||
) AS has_user_columns,
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
|
|
@ -362,10 +390,11 @@ async def _runtime_tables_ready(conn) -> bool:
|
|||
'persona_display_name',
|
||||
'persona_difficulty',
|
||||
'prev_rapport_credit',
|
||||
'session_goals'
|
||||
'session_goals',
|
||||
'learner_feedback_enabled'
|
||||
)
|
||||
GROUP BY table_schema, table_name
|
||||
HAVING count(*) = 6
|
||||
HAVING count(*) = 7
|
||||
) AS has_session_columns,
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
|
|
@ -542,10 +571,19 @@ async def ensure_runtime_tables() -> None:
|
|||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('app.persona_card') IS NOT NULL THEN
|
||||
IF to_regclass('app.persona_card') IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'app'
|
||||
AND table_name = 'persona_card'
|
||||
AND column_name = 'triggers'
|
||||
) THEN
|
||||
ALTER TABLE app.persona_card
|
||||
ADD COLUMN IF NOT EXISTS triggers JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
END IF;
|
||||
|
||||
IF to_regclass('app.persona_card') IS NOT NULL
|
||||
AND to_regclass('app.persona_voice_map') IS NULL THEN
|
||||
CREATE TABLE IF NOT EXISTS app.persona_voice_map (
|
||||
persona_id UUID NOT NULL,
|
||||
version INT NOT NULL,
|
||||
|
|
@ -564,6 +602,7 @@ async def ensure_runtime_tables() -> None:
|
|||
WHERE table_schema = 'app'
|
||||
AND table_name = 'app_user'
|
||||
AND column_name = 'affiliation'
|
||||
AND column_default IS DISTINCT FROM ''''::text
|
||||
) THEN
|
||||
ALTER TABLE app.app_user ALTER COLUMN affiliation SET DEFAULT '';
|
||||
END IF;
|
||||
|
|
@ -593,6 +632,7 @@ async def ensure_runtime_tables() -> None:
|
|||
ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
ADD COLUMN IF NOT EXISTS account_status TEXT NOT NULL DEFAULT 'approved',
|
||||
ADD COLUMN IF NOT EXISTS admin_access BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS learner_feedback_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
"""
|
||||
)
|
||||
|
|
@ -926,7 +966,8 @@ async def ensure_runtime_tables() -> None:
|
|||
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,
|
||||
ADD COLUMN IF NOT EXISTS session_goals JSONB NOT NULL DEFAULT '[]'::jsonb
|
||||
ADD COLUMN IF NOT EXISTS session_goals JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS learner_feedback_enabled BOOLEAN NOT NULL DEFAULT true
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
|
|
@ -1108,6 +1149,9 @@ def _managed_user_from_row(row) -> ManagedUser:
|
|||
privacy_version=_row_value(row, "privacy_version", "") or "",
|
||||
created_at=_ts(row["created_at"]),
|
||||
last_seen_at=_ts(row["last_seen_at"]),
|
||||
learner_feedback_enabled=bool(
|
||||
_row_value(row, "learner_feedback_enabled", True)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1133,8 +1177,14 @@ def _memory_upsert_managed_user(data: ManagedUserMemoryInput) -> ManagedUser:
|
|||
admin_access=data.admin_access
|
||||
if data.admin_access is not None
|
||||
else (current.admin_access if current else False),
|
||||
account_status=data.account_status
|
||||
or (current.account_status if current else "approved"),
|
||||
account_status=(
|
||||
"suspended"
|
||||
if current is not None
|
||||
and current.account_status == "suspended"
|
||||
and not data.reactivate
|
||||
else data.account_status
|
||||
or (current.account_status if current else "approved")
|
||||
),
|
||||
cohort_ids=(
|
||||
list(data.cohort_ids)
|
||||
if data.cohort_ids is not None
|
||||
|
|
@ -1209,6 +1259,11 @@ def _memory_upsert_managed_user(data: ManagedUserMemoryInput) -> ManagedUser:
|
|||
),
|
||||
created_at=current.created_at if current else now,
|
||||
last_seen_at=now,
|
||||
learner_feedback_enabled=(
|
||||
data.learner_feedback_enabled
|
||||
if data.learner_feedback_enabled is not None
|
||||
else (current.learner_feedback_enabled if current else True)
|
||||
),
|
||||
)
|
||||
_users[uid] = user
|
||||
_email_index[normalized_email] = uid
|
||||
|
|
@ -1232,6 +1287,7 @@ def _sync_managed_user_sessions(user: ManagedUser) -> None:
|
|||
session.cohort_ids = list(user.cohort_ids)
|
||||
session.consent_at = user.consent_at
|
||||
session.profile_completed_at = user.profile_completed_at
|
||||
session.learner_feedback_enabled = user.learner_feedback_enabled
|
||||
|
||||
|
||||
async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
||||
|
|
@ -1267,6 +1323,11 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
WHEN $7::boolean IS NULL THEN admin_access
|
||||
ELSE $7::boolean
|
||||
END,
|
||||
account_status = CASE
|
||||
WHEN account_status = 'suspended' THEN 'suspended'
|
||||
WHEN $8::text = 'approved' THEN 'approved'
|
||||
ELSE account_status
|
||||
END,
|
||||
last_seen_at = now(),
|
||||
updated_at = now()
|
||||
WHERE user_id = (
|
||||
|
|
@ -1288,6 +1349,7 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
display_name,
|
||||
role,
|
||||
admin_access,
|
||||
learner_feedback_enabled,
|
||||
account_status,
|
||||
cohort,
|
||||
affiliation,
|
||||
|
|
@ -1319,6 +1381,7 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
data.affiliation or DEFAULT_AFFILIATION,
|
||||
manual_external_id,
|
||||
desired_admin_access,
|
||||
desired_account_status,
|
||||
)
|
||||
if row is not None:
|
||||
user = _managed_user_from_row(row)
|
||||
|
|
@ -1334,13 +1397,17 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
display_name,
|
||||
role,
|
||||
admin_access,
|
||||
learner_feedback_enabled,
|
||||
cohort,
|
||||
affiliation,
|
||||
account_status,
|
||||
last_seen_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, COALESCE($9::boolean, false), $5, $6, $8, now(), now())
|
||||
VALUES (
|
||||
$1, $2, $3, $4, COALESCE($9::boolean, false),
|
||||
COALESCE($10::boolean, true), $5, $6, $8, now(), now()
|
||||
)
|
||||
ON CONFLICT (external_id) DO UPDATE SET
|
||||
email = EXCLUDED.email,
|
||||
display_name = COALESCE(NULLIF(EXCLUDED.display_name, ''), app.app_user.display_name),
|
||||
|
|
@ -1349,6 +1416,10 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
WHEN $9::boolean IS NULL THEN app.app_user.admin_access
|
||||
ELSE EXCLUDED.admin_access
|
||||
END,
|
||||
learner_feedback_enabled = CASE
|
||||
WHEN $10::boolean IS NULL THEN app.app_user.learner_feedback_enabled
|
||||
ELSE EXCLUDED.learner_feedback_enabled
|
||||
END,
|
||||
cohort = COALESCE(EXCLUDED.cohort, app.app_user.cohort),
|
||||
affiliation = COALESCE(NULLIF(EXCLUDED.affiliation, ''), app.app_user.affiliation),
|
||||
is_active = CASE WHEN $7 THEN TRUE ELSE app.app_user.is_active END,
|
||||
|
|
@ -1367,6 +1438,7 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
display_name,
|
||||
role,
|
||||
admin_access,
|
||||
learner_feedback_enabled,
|
||||
account_status,
|
||||
cohort,
|
||||
affiliation,
|
||||
|
|
@ -1396,6 +1468,7 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
data.reactivate,
|
||||
desired_account_status,
|
||||
desired_admin_access,
|
||||
data.learner_feedback_enabled,
|
||||
)
|
||||
if row is None:
|
||||
_inactive_emails.add(normalized_email)
|
||||
|
|
@ -1438,6 +1511,7 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
cohort_ids=data.cohort_ids,
|
||||
user_id=fallback_uid,
|
||||
affiliation=data.affiliation,
|
||||
learner_feedback_enabled=data.learner_feedback_enabled,
|
||||
reactivate=data.reactivate,
|
||||
)
|
||||
)
|
||||
|
|
@ -1455,6 +1529,7 @@ async def get_managed_user(user_id: str) -> ManagedUser | None:
|
|||
display_name,
|
||||
role,
|
||||
admin_access,
|
||||
learner_feedback_enabled,
|
||||
account_status,
|
||||
cohort,
|
||||
affiliation,
|
||||
|
|
@ -1504,6 +1579,7 @@ async def get_managed_user_by_email(email: str) -> ManagedUser | None:
|
|||
display_name,
|
||||
role,
|
||||
admin_access,
|
||||
learner_feedback_enabled,
|
||||
account_status,
|
||||
cohort,
|
||||
affiliation,
|
||||
|
|
@ -1555,6 +1631,7 @@ async def list_managed_users() -> tuple[list[ManagedUser], bool]:
|
|||
display_name,
|
||||
role,
|
||||
admin_access,
|
||||
learner_feedback_enabled,
|
||||
account_status,
|
||||
cohort,
|
||||
affiliation,
|
||||
|
|
@ -1599,6 +1676,7 @@ async def update_managed_user(
|
|||
role = COALESCE($3, role),
|
||||
account_status = COALESCE($18, account_status),
|
||||
admin_access = COALESCE($19, admin_access),
|
||||
learner_feedback_enabled = COALESCE($20, learner_feedback_enabled),
|
||||
cohort = CASE WHEN $4 THEN $5 ELSE cohort END,
|
||||
affiliation = COALESCE($6, affiliation),
|
||||
legal_name = COALESCE($7, legal_name),
|
||||
|
|
@ -1623,6 +1701,7 @@ async def update_managed_user(
|
|||
display_name,
|
||||
role,
|
||||
admin_access,
|
||||
learner_feedback_enabled,
|
||||
account_status,
|
||||
cohort,
|
||||
affiliation,
|
||||
|
|
@ -1670,6 +1749,7 @@ async def update_managed_user(
|
|||
else None,
|
||||
patch.account_status,
|
||||
patch.admin_access,
|
||||
patch.learner_feedback_enabled,
|
||||
)
|
||||
if row is not None:
|
||||
next_user = _managed_user_from_row(row)
|
||||
|
|
@ -1698,6 +1778,9 @@ async def update_managed_user(
|
|||
account_status=patch.account_status
|
||||
if patch.account_status is not None
|
||||
else current.account_status,
|
||||
learner_feedback_enabled=patch.learner_feedback_enabled
|
||||
if patch.learner_feedback_enabled is not None
|
||||
else current.learner_feedback_enabled,
|
||||
cohort_ids=list(patch.cohort_ids)
|
||||
if patch.cohort_ids is not None
|
||||
else current.cohort_ids,
|
||||
|
|
@ -1934,6 +2017,7 @@ async def create_session(
|
|||
cohort_ids: list[str] | None = None,
|
||||
user_id: str | None = None,
|
||||
external_id: str | None = None,
|
||||
account_status: AccountStatus | None = None,
|
||||
) -> tuple[str, SessionUser]:
|
||||
raw_sid = secrets.token_urlsafe(32)
|
||||
normalized_email = _normalize_email(email)
|
||||
|
|
@ -1945,6 +2029,7 @@ async def create_session(
|
|||
cohort_ids=cohort_ids,
|
||||
user_id=user_id,
|
||||
external_id=external_id,
|
||||
account_status=account_status,
|
||||
reactivate=False,
|
||||
)
|
||||
)
|
||||
|
|
@ -1973,6 +2058,7 @@ async def create_session(
|
|||
consent_at=managed.consent_at,
|
||||
profile_completed_at=managed.profile_completed_at,
|
||||
expires_at=expires_at,
|
||||
learner_feedback_enabled=managed.learner_feedback_enabled,
|
||||
)
|
||||
sid_hash = _sid_hash(raw_sid)
|
||||
try:
|
||||
|
|
@ -2022,6 +2108,7 @@ async def get_session(raw_sid: str | None) -> SessionUser | None:
|
|||
COALESCE(u.display_name, s.display_name, u.email) AS display_name,
|
||||
u.role,
|
||||
u.admin_access,
|
||||
u.learner_feedback_enabled,
|
||||
u.account_status,
|
||||
u.cohort,
|
||||
u.consent_at,
|
||||
|
|
@ -2078,6 +2165,9 @@ async def get_session(raw_sid: str | None) -> SessionUser | None:
|
|||
consent_at=_optional_ts(row["consent_at"]),
|
||||
profile_completed_at=_optional_ts(row["profile_completed_at"]),
|
||||
expires_at=_ts(row["expires_at"]),
|
||||
learner_feedback_enabled=bool(
|
||||
_row_value(row, "learner_feedback_enabled", True)
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
require_runtime_fallback_allowed("browser session")
|
||||
|
|
@ -2103,6 +2193,7 @@ async def get_session(raw_sid: str | None) -> SessionUser | None:
|
|||
user.cohort_ids = list(managed.cohort_ids)
|
||||
user.consent_at = managed.consent_at
|
||||
user.profile_completed_at = managed.profile_completed_at
|
||||
user.learner_feedback_enabled = managed.learner_feedback_enabled
|
||||
return user
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue