lambda-erp 0.1.47__tar.gz → 0.1.49__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/PKG-INFO +1 -1
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/auth.py +100 -26
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/frontend/src/api/client.ts +6 -3
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/database.py +36 -1
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/pyproject.toml +1 -1
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/.gitignore +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/LICENSE +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/README.md +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/__init__.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/attachments.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/bootstrap.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/chat.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/deps.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/errors.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/main.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/oauth.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/pdf.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/providers.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/remarks_md.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/admin.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/documents.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/masters.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/proposals.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/reports.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/routers/setup.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/services.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/templates/document.html +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/api/templates/proposal.html +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/frontend/README.md +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/model.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.47 → lambda_erp-0.1.49}/terraform/README.md +0 -0
|
@@ -651,14 +651,22 @@ def generate_api_key() -> tuple[str, str, str]:
|
|
|
651
651
|
return token, token[: len(API_KEY_PREFIX) + 4], hash_api_key(token)
|
|
652
652
|
|
|
653
653
|
|
|
654
|
+
def _role_rank(role: str) -> int:
|
|
655
|
+
return ROLE_HIERARCHY.get(role, 0)
|
|
656
|
+
|
|
657
|
+
|
|
654
658
|
def get_api_caller(request: Request) -> dict:
|
|
655
659
|
"""FastAPI dependency for the programmatic chat API.
|
|
656
660
|
|
|
657
661
|
Gated by the `chat_api_enabled` Settings flag — when off the whole surface
|
|
658
662
|
404s (never advertised on instances that don't use it). Otherwise validates
|
|
659
|
-
the `Authorization: Bearer <token>` header against a non-revoked Api Key
|
|
660
|
-
|
|
661
|
-
|
|
663
|
+
the `Authorization: Bearer <token>` header against a non-revoked Api Key.
|
|
664
|
+
|
|
665
|
+
Keys are per-user credentials: the effective role is resolved LIVE as
|
|
666
|
+
min(key's role cap, owner's current role), and a disabled or deleted owner
|
|
667
|
+
kills every one of their keys immediately. `name` is the key's
|
|
668
|
+
session_owner (``api:<username>``) — all of one user's keys share a session
|
|
669
|
+
space, separate from their interactive web chats.
|
|
662
670
|
"""
|
|
663
671
|
db = get_db()
|
|
664
672
|
if not _setting_enabled(db, "chat_api_enabled"):
|
|
@@ -672,74 +680,123 @@ def get_api_caller(request: Request) -> dict:
|
|
|
672
680
|
raise HTTPException(status_code=401, detail="Empty bearer token")
|
|
673
681
|
|
|
674
682
|
rows = db.sql(
|
|
675
|
-
'SELECT id, name, role, session_owner FROM "Api Key" WHERE key_hash = ? AND revoked = 0',
|
|
683
|
+
'SELECT id, name, owner, role, session_owner FROM "Api Key" WHERE key_hash = ? AND revoked = 0',
|
|
676
684
|
[hash_api_key(token)],
|
|
677
685
|
)
|
|
678
686
|
if not rows:
|
|
679
687
|
raise HTTPException(status_code=401, detail="Invalid API key")
|
|
680
688
|
key = dict(rows[0])
|
|
681
689
|
|
|
690
|
+
owner = db.get_value("User", key["owner"], ["name", "full_name", "role", "enabled"])
|
|
691
|
+
if not owner or not owner.get("enabled"):
|
|
692
|
+
raise HTTPException(status_code=401, detail="Invalid API key")
|
|
693
|
+
effective_role = (
|
|
694
|
+
key["role"] if _role_rank(key["role"]) <= _role_rank(owner["role"]) else owner["role"]
|
|
695
|
+
)
|
|
696
|
+
|
|
682
697
|
db.sql('UPDATE "Api Key" SET last_used_at = ? WHERE id = ?', [now(), key["id"]])
|
|
683
698
|
db.conn.commit()
|
|
684
699
|
|
|
685
700
|
return {
|
|
686
701
|
"name": key["session_owner"],
|
|
687
|
-
"full_name":
|
|
688
|
-
"role":
|
|
702
|
+
"full_name": f"{owner.get('full_name') or key['owner']} (API)",
|
|
703
|
+
"role": effective_role,
|
|
689
704
|
"api_key_id": key["id"],
|
|
705
|
+
"api_key_user": key["owner"],
|
|
690
706
|
}
|
|
691
707
|
|
|
692
708
|
|
|
693
709
|
class ApiKeyCreate(BaseModel):
|
|
694
710
|
name: str
|
|
695
|
-
role
|
|
711
|
+
# Optional role CAP. Defaults to the creator's own role; may only be equal
|
|
712
|
+
# or lower — a key can never out-rank its owner.
|
|
713
|
+
role: str | None = None
|
|
696
714
|
|
|
697
715
|
|
|
698
716
|
def _serialize_api_key(row: dict) -> dict:
|
|
699
717
|
return {
|
|
700
718
|
"id": row["id"],
|
|
701
719
|
"name": row["name"],
|
|
720
|
+
"user": row.get("owner"),
|
|
702
721
|
"role": row["role"],
|
|
703
722
|
"key_prefix": row["key_prefix"],
|
|
704
|
-
"created_by": row.get("created_by"),
|
|
705
723
|
"created_at": row.get("created_at"),
|
|
706
724
|
"last_used_at": row.get("last_used_at"),
|
|
707
725
|
"revoked": bool(row.get("revoked")),
|
|
708
726
|
}
|
|
709
727
|
|
|
710
728
|
|
|
729
|
+
def _get_key_or_404(db, key_id: str) -> dict:
|
|
730
|
+
rows = db.sql('SELECT id, owner, revoked FROM "Api Key" WHERE id = ?', [key_id])
|
|
731
|
+
if not rows:
|
|
732
|
+
raise HTTPException(status_code=404, detail="API key not found")
|
|
733
|
+
return dict(rows[0])
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _require_key_access(key: dict, user: dict) -> None:
|
|
737
|
+
"""A key is managed by its owner; admins can manage every key."""
|
|
738
|
+
if key["owner"] != user["name"] and user["role"] != "admin":
|
|
739
|
+
raise HTTPException(status_code=403, detail="Not your API key")
|
|
740
|
+
|
|
741
|
+
|
|
711
742
|
@router.get("/api-keys")
|
|
712
|
-
def list_api_keys(user: dict = Depends(
|
|
713
|
-
"""List API keys (metadata only — the token is never returned).
|
|
743
|
+
def list_api_keys(user: dict = Depends(require_viewer)):
|
|
744
|
+
"""List API keys (metadata only — the token is never returned).
|
|
745
|
+
|
|
746
|
+
Users see their own keys; admins see everyone's.
|
|
747
|
+
"""
|
|
714
748
|
db = get_db()
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
749
|
+
if user["role"] == "admin":
|
|
750
|
+
rows = db.sql(
|
|
751
|
+
'SELECT id, name, owner, role, key_prefix, created_at, last_used_at, revoked '
|
|
752
|
+
'FROM "Api Key" ORDER BY created_at DESC'
|
|
753
|
+
)
|
|
754
|
+
else:
|
|
755
|
+
rows = db.sql(
|
|
756
|
+
'SELECT id, name, owner, role, key_prefix, created_at, last_used_at, revoked '
|
|
757
|
+
'FROM "Api Key" WHERE owner = ? ORDER BY created_at DESC',
|
|
758
|
+
[user["name"]],
|
|
759
|
+
)
|
|
719
760
|
return [_serialize_api_key(dict(r)) for r in rows]
|
|
720
761
|
|
|
721
762
|
|
|
722
763
|
@router.post("/api-keys")
|
|
723
|
-
def create_api_key(data: ApiKeyCreate, user: dict = Depends(
|
|
724
|
-
"""Create an API key. Returns the full token exactly once.
|
|
764
|
+
def create_api_key(data: ApiKeyCreate, user: dict = Depends(require_viewer)):
|
|
765
|
+
"""Create an API key for YOURSELF. Returns the full token exactly once.
|
|
766
|
+
|
|
767
|
+
Self-service: any logged-in (non-demo) user can mint keys, because a key
|
|
768
|
+
can never exceed its owner's live role — the optional `role` is a cap that
|
|
769
|
+
must be equal or lower.
|
|
770
|
+
"""
|
|
771
|
+
if user["role"] == "public_manager":
|
|
772
|
+
raise HTTPException(status_code=403, detail="Demo mode cannot create API keys.")
|
|
725
773
|
name = (data.name or "").strip()
|
|
726
774
|
if not name:
|
|
727
775
|
raise HTTPException(status_code=422, detail="A name is required.")
|
|
728
|
-
|
|
776
|
+
|
|
777
|
+
role = data.role or user["role"]
|
|
778
|
+
if role not in API_KEY_ROLES:
|
|
779
|
+
raise HTTPException(status_code=422, detail=f"Invalid role: {role}")
|
|
780
|
+
if _role_rank(role) > _role_rank(user["role"]):
|
|
781
|
+
raise HTTPException(
|
|
782
|
+
status_code=403,
|
|
783
|
+
detail=f"A key cannot out-rank its owner (your role: {user['role']}).",
|
|
784
|
+
)
|
|
729
785
|
|
|
730
786
|
db = get_db()
|
|
731
787
|
key_id = str(uuid.uuid4())
|
|
732
788
|
token, key_prefix, key_hash = generate_api_key()
|
|
789
|
+
created_at = now()
|
|
733
790
|
db.sql(
|
|
734
791
|
'INSERT INTO "Api Key" '
|
|
735
|
-
'(id, name, key_hash, key_prefix, role, session_owner,
|
|
792
|
+
'(id, name, owner, key_hash, key_prefix, role, session_owner, created_at, last_used_at, revoked) '
|
|
736
793
|
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)',
|
|
737
|
-
[key_id, name, key_hash, key_prefix, role, f"
|
|
794
|
+
[key_id, name, user["name"], key_hash, key_prefix, role, f"api:{user['name']}", created_at, None],
|
|
738
795
|
)
|
|
739
796
|
db.conn.commit()
|
|
740
797
|
row = {
|
|
741
|
-
"id": key_id, "name": name, "role": role, "key_prefix": key_prefix,
|
|
742
|
-
"
|
|
798
|
+
"id": key_id, "name": name, "owner": user["name"], "role": role, "key_prefix": key_prefix,
|
|
799
|
+
"created_at": created_at, "last_used_at": None, "revoked": 0,
|
|
743
800
|
}
|
|
744
801
|
result = _serialize_api_key(row)
|
|
745
802
|
result["token"] = token # shown once
|
|
@@ -747,12 +804,29 @@ def create_api_key(data: ApiKeyCreate, user: dict = Depends(require_admin)):
|
|
|
747
804
|
|
|
748
805
|
|
|
749
806
|
@router.post("/api-keys/{key_id}/revoke")
|
|
750
|
-
def revoke_api_key(key_id: str, user: dict = Depends(
|
|
751
|
-
"""Revoke (soft-disable) an API key
|
|
807
|
+
def revoke_api_key(key_id: str, user: dict = Depends(require_viewer)):
|
|
808
|
+
"""Revoke (soft-disable) an API key. Owner or admin."""
|
|
752
809
|
db = get_db()
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
raise HTTPException(status_code=404, detail="API key not found")
|
|
810
|
+
key = _get_key_or_404(db, key_id)
|
|
811
|
+
_require_key_access(key, user)
|
|
756
812
|
db.sql('UPDATE "Api Key" SET revoked = 1 WHERE id = ?', [key_id])
|
|
757
813
|
db.conn.commit()
|
|
758
814
|
return {"id": key_id, "revoked": True}
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
@router.delete("/api-keys/{key_id}")
|
|
818
|
+
def delete_api_key(key_id: str, user: dict = Depends(require_viewer)):
|
|
819
|
+
"""Hard-delete a REVOKED API key. Owner or admin.
|
|
820
|
+
|
|
821
|
+
Two-step by design: a live key must be revoked first (409 otherwise), so a
|
|
822
|
+
key can't vanish while still usable. Past sessions keep their session_owner
|
|
823
|
+
string, so historical attribution survives the row's deletion.
|
|
824
|
+
"""
|
|
825
|
+
db = get_db()
|
|
826
|
+
key = _get_key_or_404(db, key_id)
|
|
827
|
+
_require_key_access(key, user)
|
|
828
|
+
if not key.get("revoked"):
|
|
829
|
+
raise HTTPException(status_code=409, detail="Revoke the key first, then delete it.")
|
|
830
|
+
db.sql('DELETE FROM "Api Key" WHERE id = ?', [key_id])
|
|
831
|
+
db.conn.commit()
|
|
832
|
+
return {"id": key_id, "deleted": True}
|
|
@@ -452,15 +452,18 @@ export const api = {
|
|
|
452
452
|
request<Record<string, string>>("/auth/settings", { method: "PUT", body: JSON.stringify(data) }),
|
|
453
453
|
getApiKeys: () =>
|
|
454
454
|
request<Array<{
|
|
455
|
-
id: string; name: string; role: string; key_prefix: string;
|
|
456
|
-
|
|
455
|
+
id: string; name: string; user?: string; role: string; key_prefix: string;
|
|
456
|
+
created_at?: string; last_used_at?: string | null; revoked: boolean;
|
|
457
457
|
}>>("/auth/api-keys"),
|
|
458
458
|
createApiKey: (name: string, role: string) =>
|
|
459
|
-
request<{ id: string; name: string; role: string; key_prefix: string; token: string }>(
|
|
459
|
+
request<{ id: string; name: string; user?: string; role: string; key_prefix: string; token: string }>(
|
|
460
460
|
"/auth/api-keys", { method: "POST", body: JSON.stringify({ name, role }) }),
|
|
461
461
|
revokeApiKey: (id: string) =>
|
|
462
462
|
request<{ id: string; revoked: boolean }>(
|
|
463
463
|
`/auth/api-keys/${encodeURIComponent(id)}/revoke`, { method: "POST" }),
|
|
464
|
+
deleteApiKey: (id: string) =>
|
|
465
|
+
request<{ id: string; deleted: boolean }>(
|
|
466
|
+
`/auth/api-keys/${encodeURIComponent(id)}`, { method: "DELETE" }),
|
|
464
467
|
|
|
465
468
|
getDemoSpend: () =>
|
|
466
469
|
request<{
|
|
@@ -1314,14 +1314,18 @@ class Database:
|
|
|
1314
1314
|
# agent's tool access; `session_owner` is the user_id the key's chat
|
|
1315
1315
|
# sessions are owned under (isolated from human users). See
|
|
1316
1316
|
# docs/chat-api-plan.md.
|
|
1317
|
+
# Per-user API keys (v2): a key BELONGS to a User and can never act
|
|
1318
|
+
# above its owner — `role` is only a CAP; the effective role is
|
|
1319
|
+
# min(cap, owner's current role) resolved live at auth time, so a
|
|
1320
|
+
# demotion or disable applies to existing keys immediately.
|
|
1317
1321
|
"""CREATE TABLE IF NOT EXISTS "Api Key" (
|
|
1318
1322
|
id TEXT PRIMARY KEY,
|
|
1319
1323
|
name TEXT,
|
|
1324
|
+
owner TEXT NOT NULL,
|
|
1320
1325
|
key_hash TEXT UNIQUE,
|
|
1321
1326
|
key_prefix TEXT,
|
|
1322
1327
|
role TEXT DEFAULT 'manager',
|
|
1323
1328
|
session_owner TEXT,
|
|
1324
|
-
created_by TEXT,
|
|
1325
1329
|
created_at TEXT,
|
|
1326
1330
|
last_used_at TEXT,
|
|
1327
1331
|
revoked INTEGER DEFAULT 0
|
|
@@ -1894,6 +1898,36 @@ def _m018_quotation_item_frequency(db: "Database") -> None:
|
|
|
1894
1898
|
db._add_column_if_missing("Quotation Item", "frequency", "TEXT")
|
|
1895
1899
|
|
|
1896
1900
|
|
|
1901
|
+
def _m019_api_keys_per_user(db: "Database") -> None:
|
|
1902
|
+
"""API keys v2 — per-user credentials (clean break, deliberate).
|
|
1903
|
+
|
|
1904
|
+
v1 keys were org-global objects minted by admins with a free-standing role
|
|
1905
|
+
and a synthetic identity; anyone holding the token WAS the key. v2 binds
|
|
1906
|
+
every key to a User (self-service creation, effective role capped by the
|
|
1907
|
+
owner's live role). Existing v1 rows are DROPPED, not migrated — decided
|
|
1908
|
+
2026-07-14: no legacy rules; holders simply re-issue their keys.
|
|
1909
|
+
"""
|
|
1910
|
+
if "owner" in db._get_table_columns("Api Key"):
|
|
1911
|
+
return # fresh install — table already has the v2 shape
|
|
1912
|
+
db.conn.execute('DROP TABLE "Api Key"')
|
|
1913
|
+
db.conn.execute(
|
|
1914
|
+
"""CREATE TABLE "Api Key" (
|
|
1915
|
+
id TEXT PRIMARY KEY,
|
|
1916
|
+
name TEXT,
|
|
1917
|
+
owner TEXT NOT NULL,
|
|
1918
|
+
key_hash TEXT UNIQUE,
|
|
1919
|
+
key_prefix TEXT,
|
|
1920
|
+
role TEXT DEFAULT 'manager',
|
|
1921
|
+
session_owner TEXT,
|
|
1922
|
+
created_at TEXT,
|
|
1923
|
+
last_used_at TEXT,
|
|
1924
|
+
revoked INTEGER DEFAULT 0
|
|
1925
|
+
)"""
|
|
1926
|
+
)
|
|
1927
|
+
db._col_cache.pop("Api Key", None)
|
|
1928
|
+
db._text_col_cache.pop("Api Key", None)
|
|
1929
|
+
|
|
1930
|
+
|
|
1897
1931
|
Database.MIGRATIONS = [
|
|
1898
1932
|
(1, "chat_message_session_id", _m001_chat_message_session_id),
|
|
1899
1933
|
(2, "chat_session_user_id", _m002_chat_session_user_id),
|
|
@@ -1913,6 +1947,7 @@ Database.MIGRATIONS = [
|
|
|
1913
1947
|
(16, "customer_contact_person", _m016_customer_contact_person),
|
|
1914
1948
|
(17, "proposal_cover_template", _m017_proposal_cover_template),
|
|
1915
1949
|
(18, "quotation_item_frequency", _m018_quotation_item_frequency),
|
|
1950
|
+
(19, "api_keys_per_user", _m019_api_keys_per_user),
|
|
1916
1951
|
]
|
|
1917
1952
|
|
|
1918
1953
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|