전 저장소 리팩터링과 SSOT 정비
This commit is contained in:
parent
14ecbd4e7d
commit
3dfddcac6f
173 changed files with 19679 additions and 6952 deletions
|
|
@ -20,6 +20,7 @@ from .auth_types import AccountStatus, RoleName
|
|||
from .config import settings
|
||||
from .db import get_pool
|
||||
from .runtime_policy import require_runtime_fallback_allowed, runtime_fallback_allowed
|
||||
from .runtime_schema import runtime_schema_bootstrap_required
|
||||
from .services import notifications
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -191,7 +192,11 @@ def user_id_from_email(email: str) -> str:
|
|||
|
||||
|
||||
def user_id_from_external_id(external_id: str) -> str:
|
||||
return str(uuid.uuid5(uuid.NAMESPACE_URL, f"vignette:user-external:{external_id.strip().lower()}"))
|
||||
return str(
|
||||
uuid.uuid5(
|
||||
uuid.NAMESPACE_URL, f"vignette:user-external:{external_id.strip().lower()}"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _normalize_email(email: str) -> str:
|
||||
|
|
@ -344,10 +349,11 @@ async def _runtime_tables_ready(conn) -> bool:
|
|||
'persona_code',
|
||||
'persona_display_name',
|
||||
'persona_difficulty',
|
||||
'prev_rapport_credit'
|
||||
'prev_rapport_credit',
|
||||
'session_goals'
|
||||
)
|
||||
GROUP BY table_schema, table_name
|
||||
HAVING count(*) = 5
|
||||
HAVING count(*) = 6
|
||||
) AS has_session_columns,
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
|
|
@ -517,13 +523,9 @@ async def ensure_runtime_tables() -> None:
|
|||
"""Ensure DB-backed auth/user runtime tables exist when a pool is available."""
|
||||
pool = get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
if await _runtime_tables_ready(conn):
|
||||
ready = await _runtime_tables_ready(conn)
|
||||
if not runtime_schema_bootstrap_required("auth/user", ready=ready):
|
||||
return
|
||||
if settings.environment != "dev":
|
||||
raise RuntimeError(
|
||||
"runtime DB schema is incomplete; run owner migration/init and "
|
||||
"scripts/check-deploy-preflight.py before starting the API"
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
DO $$
|
||||
|
|
@ -904,7 +906,8 @@ async def ensure_runtime_tables() -> None:
|
|||
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
|
||||
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
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
|
|
@ -1054,6 +1057,10 @@ async def ensure_runtime_tables() -> None:
|
|||
)
|
||||
"""
|
||||
)
|
||||
if not await _runtime_tables_ready(conn):
|
||||
raise RuntimeError(
|
||||
"auth/user development schema bootstrap did not satisfy readiness"
|
||||
)
|
||||
|
||||
|
||||
def _managed_user_from_row(row) -> ManagedUser:
|
||||
|
|
@ -1092,15 +1099,23 @@ def _memory_upsert_managed_user(data: ManagedUserMemoryInput) -> ManagedUser:
|
|||
raise InactiveUserError("user is inactive")
|
||||
if data.reactivate:
|
||||
_inactive_emails.discard(normalized_email)
|
||||
uid = data.user_id or _email_index.get(normalized_email) or user_id_from_email(normalized_email)
|
||||
uid = (
|
||||
data.user_id
|
||||
or _email_index.get(normalized_email)
|
||||
or user_id_from_email(normalized_email)
|
||||
)
|
||||
current = _users.get(uid)
|
||||
user = ManagedUser(
|
||||
user_id=uid,
|
||||
email=normalized_email,
|
||||
display_name=(data.display_name.strip() if data.display_name else "") or normalized_email,
|
||||
display_name=(data.display_name.strip() if data.display_name else "")
|
||||
or normalized_email,
|
||||
role=data.role,
|
||||
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"),
|
||||
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"),
|
||||
cohort_ids=(
|
||||
list(data.cohort_ids)
|
||||
if data.cohort_ids is not None
|
||||
|
|
@ -1126,20 +1141,28 @@ def _memory_upsert_managed_user(data: ManagedUserMemoryInput) -> ManagedUser:
|
|||
if data.grade_level is not None
|
||||
else (current.grade_level if current else "")
|
||||
),
|
||||
phone=data.phone.strip() if data.phone is not None else (current.phone if current else ""),
|
||||
phone=data.phone.strip()
|
||||
if data.phone is not None
|
||||
else (current.phone if current else ""),
|
||||
contact_address=(
|
||||
data.contact_address.strip()
|
||||
if data.contact_address is not None
|
||||
else (current.contact_address if current else "")
|
||||
),
|
||||
nickname=data.nickname.strip() if data.nickname is not None else (current.nickname if current else ""),
|
||||
nickname=data.nickname.strip()
|
||||
if data.nickname is not None
|
||||
else (current.nickname if current else ""),
|
||||
self_introduction=(
|
||||
data.self_introduction.strip()
|
||||
if data.self_introduction is not None
|
||||
else (current.self_introduction if current else "")
|
||||
),
|
||||
avatar_url=data.avatar_url.strip() if data.avatar_url is not None else (current.avatar_url if current else ""),
|
||||
consent_at=data.consent_at if data.consent_at is not None else (current.consent_at if current else None),
|
||||
avatar_url=data.avatar_url.strip()
|
||||
if data.avatar_url is not None
|
||||
else (current.avatar_url if current else ""),
|
||||
consent_at=data.consent_at
|
||||
if data.consent_at is not None
|
||||
else (current.consent_at if current else None),
|
||||
profile_completed_at=(
|
||||
data.profile_completed_at
|
||||
if data.profile_completed_at is not None
|
||||
|
|
@ -1173,6 +1196,25 @@ def _memory_upsert_managed_user(data: ManagedUserMemoryInput) -> ManagedUser:
|
|||
return user
|
||||
|
||||
|
||||
def _sync_managed_user_sessions(user: ManagedUser) -> None:
|
||||
"""관리 사용자 변경을 현재 로그인 세션의 권한 스냅샷에 반영한다."""
|
||||
for session in _sessions.values():
|
||||
if session.user_id != user.user_id:
|
||||
continue
|
||||
session.display_name = user.display_name
|
||||
session.role = user.role
|
||||
session.admin_access = has_admin_access(
|
||||
user.email,
|
||||
user.role,
|
||||
user.admin_access,
|
||||
)
|
||||
session.super_admin = is_super_admin_email(user.email)
|
||||
session.account_status = user.account_status
|
||||
session.cohort_ids = list(user.cohort_ids)
|
||||
session.consent_at = user.consent_at
|
||||
session.profile_completed_at = user.profile_completed_at
|
||||
|
||||
|
||||
async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
||||
normalized_email = _normalize_email(data.email)
|
||||
normalized_external_id = _normalize_external_id(data.external_id, normalized_email)
|
||||
|
|
@ -1183,11 +1225,11 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
reactivate=data.reactivate,
|
||||
)
|
||||
try:
|
||||
pool = get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
if data.user_id is None and normalized_external_id != manual_external_id:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
pool = get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
if data.user_id is None and normalized_external_id != manual_external_id:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
UPDATE app.app_user SET
|
||||
external_id = $1,
|
||||
display_name = COALESCE(NULLIF(display_name, ''), $3),
|
||||
|
|
@ -1234,19 +1276,25 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
created_at,
|
||||
last_seen_at
|
||||
""",
|
||||
normalized_external_id,
|
||||
normalized_email,
|
||||
(data.display_name.strip() if data.display_name else normalized_email),
|
||||
_cohort_value(data.cohort_ids),
|
||||
data.affiliation or DEFAULT_AFFILIATION,
|
||||
manual_external_id,
|
||||
)
|
||||
if row is not None:
|
||||
user = _managed_user_from_row(row)
|
||||
_memory_upsert_managed_user(ManagedUserMemoryInput.from_user(user, reactivate=True))
|
||||
return user
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
normalized_external_id,
|
||||
normalized_email,
|
||||
(
|
||||
data.display_name.strip()
|
||||
if data.display_name
|
||||
else normalized_email
|
||||
),
|
||||
_cohort_value(data.cohort_ids),
|
||||
data.affiliation or DEFAULT_AFFILIATION,
|
||||
manual_external_id,
|
||||
)
|
||||
if row is not None:
|
||||
user = _managed_user_from_row(row)
|
||||
_memory_upsert_managed_user(
|
||||
ManagedUserMemoryInput.from_user(user, reactivate=True)
|
||||
)
|
||||
return user
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO app.app_user (
|
||||
external_id,
|
||||
email,
|
||||
|
|
@ -1306,35 +1354,44 @@ async def upsert_managed_user(data: ManagedUserUpsertInput) -> ManagedUser:
|
|||
created_at,
|
||||
last_seen_at
|
||||
""",
|
||||
normalized_external_id,
|
||||
normalized_email,
|
||||
(data.display_name.strip() if data.display_name else normalized_email),
|
||||
_db_role(data.role),
|
||||
_cohort_value(data.cohort_ids),
|
||||
data.affiliation or DEFAULT_AFFILIATION,
|
||||
data.reactivate,
|
||||
desired_account_status,
|
||||
data.admin_access,
|
||||
)
|
||||
if row is None:
|
||||
_inactive_emails.add(normalized_email)
|
||||
raise InactiveUserError("user is inactive")
|
||||
user = _managed_user_from_row(row)
|
||||
_memory_upsert_managed_user(ManagedUserMemoryInput.from_user(user, reactivate=True))
|
||||
return user
|
||||
normalized_external_id,
|
||||
normalized_email,
|
||||
(data.display_name.strip() if data.display_name else normalized_email),
|
||||
_db_role(data.role),
|
||||
_cohort_value(data.cohort_ids),
|
||||
data.affiliation or DEFAULT_AFFILIATION,
|
||||
data.reactivate,
|
||||
desired_account_status,
|
||||
data.admin_access,
|
||||
)
|
||||
if row is None:
|
||||
_inactive_emails.add(normalized_email)
|
||||
raise InactiveUserError("user is inactive")
|
||||
user = _managed_user_from_row(row)
|
||||
_memory_upsert_managed_user(
|
||||
ManagedUserMemoryInput.from_user(user, reactivate=True)
|
||||
)
|
||||
return user
|
||||
except InactiveUserError:
|
||||
raise
|
||||
except Exception:
|
||||
require_runtime_fallback_allowed("managed user")
|
||||
current = _users.get(data.user_id or "") or _users.get(_email_index.get(normalized_email, ""))
|
||||
current = _users.get(data.user_id or "") or _users.get(
|
||||
_email_index.get(normalized_email, "")
|
||||
)
|
||||
fallback_uid = data.user_id or (
|
||||
current.user_id if current is not None else user_id_from_external_id(normalized_external_id)
|
||||
current.user_id
|
||||
if current is not None
|
||||
else user_id_from_external_id(normalized_external_id)
|
||||
)
|
||||
fallback_account_status = desired_account_status
|
||||
if current is not None:
|
||||
if current.account_status == "suspended":
|
||||
fallback_account_status = "suspended"
|
||||
elif current.account_status == "approved" and desired_account_status == "pending":
|
||||
elif (
|
||||
current.account_status == "approved"
|
||||
and desired_account_status == "pending"
|
||||
):
|
||||
fallback_account_status = "approved"
|
||||
return _memory_upsert_managed_user(
|
||||
ManagedUserMemoryInput(
|
||||
|
|
@ -1561,33 +1618,28 @@ async def update_managed_user(
|
|||
patch.department.strip() if patch.department is not None else None,
|
||||
patch.grade_level.strip() if patch.grade_level is not None else None,
|
||||
patch.phone.strip() if patch.phone is not None else None,
|
||||
patch.contact_address.strip() if patch.contact_address is not None else None,
|
||||
patch.contact_address.strip()
|
||||
if patch.contact_address is not None
|
||||
else None,
|
||||
patch.nickname.strip() if patch.nickname is not None else None,
|
||||
patch.self_introduction.strip() if patch.self_introduction is not None else None,
|
||||
patch.self_introduction.strip()
|
||||
if patch.self_introduction is not None
|
||||
else None,
|
||||
patch.avatar_url.strip() if patch.avatar_url is not None else None,
|
||||
patch.complete_onboarding,
|
||||
patch.terms_version.strip() if patch.terms_version is not None else None,
|
||||
patch.privacy_version.strip() if patch.privacy_version is not None else None,
|
||||
patch.terms_version.strip()
|
||||
if patch.terms_version is not None
|
||||
else None,
|
||||
patch.privacy_version.strip()
|
||||
if patch.privacy_version is not None
|
||||
else None,
|
||||
patch.account_status,
|
||||
patch.admin_access,
|
||||
)
|
||||
if row is not None:
|
||||
next_user = _managed_user_from_row(row)
|
||||
_memory_upsert_managed_user(ManagedUserMemoryInput.from_user(next_user))
|
||||
for session in _sessions.values():
|
||||
if session.user_id == user_id:
|
||||
session.display_name = next_user.display_name
|
||||
session.role = next_user.role
|
||||
session.admin_access = has_admin_access(
|
||||
next_user.email,
|
||||
next_user.role,
|
||||
next_user.admin_access,
|
||||
)
|
||||
session.super_admin = is_super_admin_email(next_user.email)
|
||||
session.account_status = next_user.account_status
|
||||
session.cohort_ids = list(next_user.cohort_ids)
|
||||
session.consent_at = next_user.consent_at
|
||||
session.profile_completed_at = next_user.profile_completed_at
|
||||
_sync_managed_user_sessions(next_user)
|
||||
return next_user
|
||||
except Exception:
|
||||
require_runtime_fallback_allowed("managed user update")
|
||||
|
|
@ -1601,32 +1653,58 @@ async def update_managed_user(
|
|||
next_user = ManagedUser(
|
||||
user_id=current.user_id,
|
||||
email=current.email,
|
||||
display_name=patch.display_name.strip() if patch.display_name is not None else current.display_name,
|
||||
display_name=patch.display_name.strip()
|
||||
if patch.display_name is not None
|
||||
else current.display_name,
|
||||
role=patch.role if patch.role is not None else current.role,
|
||||
admin_access=patch.admin_access if patch.admin_access is not None else current.admin_access,
|
||||
account_status=patch.account_status if patch.account_status is not None else current.account_status,
|
||||
cohort_ids=list(patch.cohort_ids) if patch.cohort_ids is not None else current.cohort_ids,
|
||||
affiliation=patch.affiliation.strip() if patch.affiliation is not None else current.affiliation,
|
||||
legal_name=patch.legal_name.strip() if patch.legal_name is not None else current.legal_name,
|
||||
department=patch.department.strip() if patch.department is not None else current.department,
|
||||
grade_level=patch.grade_level.strip() if patch.grade_level is not None else current.grade_level,
|
||||
admin_access=patch.admin_access
|
||||
if patch.admin_access is not None
|
||||
else current.admin_access,
|
||||
account_status=patch.account_status
|
||||
if patch.account_status is not None
|
||||
else current.account_status,
|
||||
cohort_ids=list(patch.cohort_ids)
|
||||
if patch.cohort_ids is not None
|
||||
else current.cohort_ids,
|
||||
affiliation=patch.affiliation.strip()
|
||||
if patch.affiliation is not None
|
||||
else current.affiliation,
|
||||
legal_name=patch.legal_name.strip()
|
||||
if patch.legal_name is not None
|
||||
else current.legal_name,
|
||||
department=patch.department.strip()
|
||||
if patch.department is not None
|
||||
else current.department,
|
||||
grade_level=patch.grade_level.strip()
|
||||
if patch.grade_level is not None
|
||||
else current.grade_level,
|
||||
phone=patch.phone.strip() if patch.phone is not None else current.phone,
|
||||
contact_address=(
|
||||
patch.contact_address.strip()
|
||||
if patch.contact_address is not None
|
||||
else current.contact_address
|
||||
),
|
||||
nickname=patch.nickname.strip() if patch.nickname is not None else current.nickname,
|
||||
nickname=patch.nickname.strip()
|
||||
if patch.nickname is not None
|
||||
else current.nickname,
|
||||
self_introduction=(
|
||||
patch.self_introduction.strip()
|
||||
if patch.self_introduction is not None
|
||||
else current.self_introduction
|
||||
),
|
||||
avatar_url=patch.avatar_url.strip() if patch.avatar_url is not None else current.avatar_url,
|
||||
avatar_url=patch.avatar_url.strip()
|
||||
if patch.avatar_url is not None
|
||||
else current.avatar_url,
|
||||
consent_at=current.consent_at,
|
||||
profile_completed_at=time.time() if patch.complete_onboarding else current.profile_completed_at,
|
||||
terms_agreed_at=time.time() if patch.complete_onboarding else current.terms_agreed_at,
|
||||
privacy_agreed_at=time.time() if patch.complete_onboarding else current.privacy_agreed_at,
|
||||
profile_completed_at=time.time()
|
||||
if patch.complete_onboarding
|
||||
else current.profile_completed_at,
|
||||
terms_agreed_at=time.time()
|
||||
if patch.complete_onboarding
|
||||
else current.terms_agreed_at,
|
||||
privacy_agreed_at=time.time()
|
||||
if patch.complete_onboarding
|
||||
else current.privacy_agreed_at,
|
||||
terms_version=(
|
||||
patch.terms_version.strip()
|
||||
if patch.complete_onboarding and patch.terms_version is not None
|
||||
|
|
@ -1641,21 +1719,7 @@ async def update_managed_user(
|
|||
last_seen_at=time.time(),
|
||||
)
|
||||
_users[user_id] = next_user
|
||||
|
||||
for session in _sessions.values():
|
||||
if session.user_id == user_id:
|
||||
session.display_name = next_user.display_name
|
||||
session.role = next_user.role
|
||||
session.admin_access = has_admin_access(
|
||||
next_user.email,
|
||||
next_user.role,
|
||||
next_user.admin_access,
|
||||
)
|
||||
session.super_admin = is_super_admin_email(next_user.email)
|
||||
session.account_status = next_user.account_status
|
||||
session.cohort_ids = list(next_user.cohort_ids)
|
||||
session.consent_at = next_user.consent_at
|
||||
session.profile_completed_at = next_user.profile_completed_at
|
||||
_sync_managed_user_sessions(next_user)
|
||||
return next_user
|
||||
|
||||
|
||||
|
|
@ -1865,7 +1929,9 @@ async def create_session(
|
|||
email=normalized_email,
|
||||
display_name=managed.display_name,
|
||||
role=managed.role,
|
||||
admin_access=has_admin_access(managed.email, managed.role, managed.admin_access),
|
||||
admin_access=has_admin_access(
|
||||
managed.email, managed.role, managed.admin_access
|
||||
),
|
||||
super_admin=is_super_admin_email(managed.email),
|
||||
account_status=managed.account_status,
|
||||
cohort_ids=list(managed.cohort_ids),
|
||||
|
|
@ -1976,7 +2042,9 @@ async def get_session(raw_sid: str | None) -> SessionUser | None:
|
|||
managed.last_seen_at = time.time()
|
||||
user.display_name = managed.display_name
|
||||
user.role = managed.role
|
||||
user.admin_access = has_admin_access(managed.email, managed.role, managed.admin_access)
|
||||
user.admin_access = has_admin_access(
|
||||
managed.email, managed.role, managed.admin_access
|
||||
)
|
||||
user.super_admin = is_super_admin_email(managed.email)
|
||||
user.account_status = managed.account_status
|
||||
user.cohort_ids = list(managed.cohort_ids)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue