feat(auth): add local login, audit, API keys, and system admin UI

Gate netx Web/API/WebCRT with JWT and per-user API tokens, bootstrap an admin with forced password change, and expose users/audit/API-key management under a System section. MCP can reuse data/auth/mcp_token without extra env for local labs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
hansjone 2026-07-30 02:34:25 +00:00
parent 14f14d34bd
commit 6d4cd741ef
35 changed files with 2699 additions and 21 deletions

View file

@ -3,11 +3,16 @@ from __future__ import annotations
from datetime import datetime
from uuid import uuid4
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.types import JSON
from .db import Base
# JSONB on Postgres; plain JSON elsewhere (unit tests / sqlite).
_JsonType = JSON().with_variant(JSONB(), "postgresql")
class AlarmBatch(Base):
__tablename__ = "alarm_batches"
@ -430,3 +435,52 @@ class TopologyEdge(Base):
discovered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class AppUser(Base):
"""Local netx application user (login account)."""
__tablename__ = "app_user"
id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: uuid4().hex)
username: Mapped[str] = mapped_column(String(128), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(String(255), default="")
role: Mapped[str] = mapped_column(String(32), default="user", index=True) # admin | user
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
must_change_password: Mapped[bool] = mapped_column(Boolean, default=False)
created_by: Mapped[str] = mapped_column(String(64), default="")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class AuditLog(Base):
"""Application audit trail for authenticated (and auth) actions."""
__tablename__ = "audit_log"
id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: uuid4().hex)
ts: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
actor_user_id: Mapped[str] = mapped_column(String(64), default="", index=True)
actor_username: Mapped[str] = mapped_column(String(128), default="", index=True)
action: Mapped[str] = mapped_column(String(128), default="", index=True)
method: Mapped[str] = mapped_column(String(16), default="")
path: Mapped[str] = mapped_column(String(512), default="", index=True)
status_code: Mapped[int] = mapped_column(Integer, default=0)
client_ip: Mapped[str] = mapped_column(String(128), default="")
user_agent: Mapped[str] = mapped_column(String(512), default="")
detail: Mapped[dict] = mapped_column(_JsonType, default=dict)
class ApiToken(Base):
"""Long-lived API token (MCP/scripts); hashed at rest."""
__tablename__ = "api_token"
id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: uuid4().hex)
name: Mapped[str] = mapped_column(String(128), default="")
token_hash: Mapped[str] = mapped_column(String(128), unique=True, index=True)
user_id: Mapped[str] = mapped_column(String(64), index=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
last_used_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)