Backend
This commit is contained in:
@@ -21,7 +21,7 @@ from app.core.access import (
|
||||
require_permission,
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.core.security import issue_token
|
||||
from app.core.security import hash_password, issue_token, verify_password
|
||||
from app.db.session import get_db
|
||||
from app.models.access import Permission, Role, User
|
||||
|
||||
@@ -129,6 +129,61 @@ def read_my_permissions(user: User = Depends(get_current_user)):
|
||||
return sorted(get_user_permissions(user))
|
||||
|
||||
|
||||
class UpdateMeRequest(BaseModel):
|
||||
name: str | None = None
|
||||
email: str | None = None
|
||||
current_password: str | None = None
|
||||
new_password: str | None = None
|
||||
|
||||
|
||||
@router.patch("/me", response_model=UserSession)
|
||||
def update_me(
|
||||
payload: UpdateMeRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Allow an internal user to update their own name, email, or password."""
|
||||
if payload.new_password:
|
||||
# Require current password verification before allowing a password change.
|
||||
# Users who have never set a personal password must supply the shared
|
||||
# admin password as the current credential.
|
||||
current_ok = (
|
||||
verify_password(payload.current_password or "", user.password_hash)
|
||||
if user.password_hash
|
||||
else (payload.current_password or "") == settings.admin_password
|
||||
)
|
||||
if not current_ok:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Current password is incorrect",
|
||||
)
|
||||
if len(payload.new_password) < 8:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="New password must be at least 8 characters",
|
||||
)
|
||||
user.password_hash = hash_password(payload.new_password)
|
||||
|
||||
if payload.name is not None:
|
||||
name = payload.name.strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Name cannot be empty")
|
||||
user.name = name
|
||||
|
||||
if payload.email is not None:
|
||||
email = payload.email.strip().lower()
|
||||
if not email or "@" not in email:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid email address")
|
||||
existing = db.scalar(select(User).where(User.email == email, User.id != user.id))
|
||||
if existing:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email is already in use")
|
||||
user.email = email
|
||||
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return _serialize_session(user, include_token=True)
|
||||
|
||||
|
||||
# Permission-enforced administrative endpoints. Route bodies should not check
|
||||
# role names — every gate is the require_permission(...) dependency.
|
||||
|
||||
|
||||
@@ -84,6 +84,28 @@ def ensure_tenant_columns(engine: Engine) -> tuple[str, ...]:
|
||||
return tuple(added_columns)
|
||||
|
||||
|
||||
# Ad-hoc column additions for tables that pre-existed before a column was
|
||||
# introduced on the model. Each entry is (table, column, DDL fragment).
|
||||
_LEGACY_COLUMN_PATCHES: tuple[tuple[str, str, str], ...] = (
|
||||
("users", "password_hash", "VARCHAR(255)"),
|
||||
)
|
||||
|
||||
|
||||
def ensure_legacy_columns(engine: Engine) -> tuple[str, ...]:
|
||||
added: list[str] = []
|
||||
for table_name, column_name, ddl in _LEGACY_COLUMN_PATCHES:
|
||||
if not _table_exists(engine, table_name):
|
||||
continue
|
||||
if _has_column(engine, table_name, column_name):
|
||||
continue
|
||||
with engine.begin() as connection:
|
||||
connection.execute(
|
||||
text(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {ddl}")
|
||||
)
|
||||
added.append(f"{table_name}.{column_name}")
|
||||
return tuple(added)
|
||||
|
||||
|
||||
def sync_tenant_ids(engine: Engine) -> dict[str, int]:
|
||||
existing_tables = set(inspect(engine).get_table_names())
|
||||
|
||||
@@ -339,5 +361,5 @@ def sync_tenant_ids(engine: Engine) -> dict[str, int]:
|
||||
|
||||
def bootstrap_schema(engine: Engine, metadata: MetaData) -> MigrationReport:
|
||||
created_tables = ensure_metadata_tables(engine, metadata)
|
||||
added_columns = ensure_tenant_columns(engine)
|
||||
added_columns = ensure_tenant_columns(engine) + ensure_legacy_columns(engine)
|
||||
return MigrationReport(created_tables=created_tables, added_columns=added_columns)
|
||||
|
||||
Reference in New Issue
Block a user