lambda-erp 0.1.34__tar.gz → 0.1.35__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.34 → lambda_erp-0.1.35}/PKG-INFO +1 -1
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/auth.py +44 -3
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/frontend/src/api/client.ts +5 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/pyproject.toml +1 -1
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/.gitignore +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/LICENSE +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/README.md +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/__init__.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/attachments.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/bootstrap.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/chat.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/deps.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/errors.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/main.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/oauth.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/pdf.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/providers.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/remarks_md.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/routers/admin.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/routers/documents.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/routers/masters.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/routers/proposals.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/routers/reports.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/routers/setup.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/services.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/templates/document.html +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/api/templates/proposal.html +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/frontend/README.md +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/database.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/model.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.34 → lambda_erp-0.1.35}/terraform/README.md +0 -0
|
@@ -96,6 +96,13 @@ def verify_password(plain: str, hashed: str) -> bool:
|
|
|
96
96
|
return False
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
def has_usable_password(hashed: str | None) -> bool:
|
|
100
|
+
"""True if the stored hash is a real bcrypt password. Social-login-only
|
|
101
|
+
users carry a non-matchable sentinel that fails this check — they have no
|
|
102
|
+
password to change (they can set a first one via /auth/set-password)."""
|
|
103
|
+
return bool(hashed) and hashed.startswith(PASSWORD_HASH_PREFIX)
|
|
104
|
+
|
|
105
|
+
|
|
99
106
|
# ---------------------------------------------------------------------------
|
|
100
107
|
# JWT helpers
|
|
101
108
|
# ---------------------------------------------------------------------------
|
|
@@ -178,6 +185,10 @@ class ChangePasswordRequest(BaseModel):
|
|
|
178
185
|
new_password: str
|
|
179
186
|
|
|
180
187
|
|
|
188
|
+
class SetPasswordRequest(BaseModel):
|
|
189
|
+
new_password: str
|
|
190
|
+
|
|
191
|
+
|
|
181
192
|
# ---------------------------------------------------------------------------
|
|
182
193
|
# Dependencies
|
|
183
194
|
# ---------------------------------------------------------------------------
|
|
@@ -321,7 +332,7 @@ def register(data: RegisterRequest, request: Request, response: Response):
|
|
|
321
332
|
token = create_access_token(user_name)
|
|
322
333
|
set_auth_cookie(request, response, token)
|
|
323
334
|
|
|
324
|
-
return {"name": user_name, "email": data.email.lower(), "full_name": data.full_name, "role": role}
|
|
335
|
+
return {"name": user_name, "email": data.email.lower(), "full_name": data.full_name, "role": role, "has_password": True}
|
|
325
336
|
|
|
326
337
|
|
|
327
338
|
@router.post("/login")
|
|
@@ -345,7 +356,8 @@ def login(data: LoginRequest, request: Request, response: Response):
|
|
|
345
356
|
token = create_access_token(user["name"])
|
|
346
357
|
set_auth_cookie(request, response, token)
|
|
347
358
|
|
|
348
|
-
return {"name": user["name"], "email": user["email"], "full_name": user["full_name"], "role": user["role"]
|
|
359
|
+
return {"name": user["name"], "email": user["email"], "full_name": user["full_name"], "role": user["role"],
|
|
360
|
+
"has_password": has_usable_password(user["hashed_password"])}
|
|
349
361
|
|
|
350
362
|
|
|
351
363
|
@router.post("/logout")
|
|
@@ -356,7 +368,11 @@ def logout(response: Response):
|
|
|
356
368
|
|
|
357
369
|
@router.get("/me")
|
|
358
370
|
def me(user: dict = Depends(get_current_user)):
|
|
359
|
-
|
|
371
|
+
db = get_db()
|
|
372
|
+
row = db.get_value("User", user["name"], ["hashed_password"])
|
|
373
|
+
has_password = has_usable_password(row["hashed_password"]) if row else False
|
|
374
|
+
return {"name": user["name"], "email": user["email"], "full_name": user["full_name"],
|
|
375
|
+
"role": user["role"], "has_password": has_password}
|
|
360
376
|
|
|
361
377
|
|
|
362
378
|
@router.post("/change-password")
|
|
@@ -381,6 +397,31 @@ def change_password(data: ChangePasswordRequest, user: dict = Depends(get_curren
|
|
|
381
397
|
return {"ok": True}
|
|
382
398
|
|
|
383
399
|
|
|
400
|
+
@router.post("/set-password")
|
|
401
|
+
def set_password(data: SetPasswordRequest, user: dict = Depends(get_current_user)):
|
|
402
|
+
"""Set a first password for an account that has none — e.g. a user created
|
|
403
|
+
via social login who wants an email+password fallback. Requires an
|
|
404
|
+
authenticated session; refuses if a usable password already exists (that
|
|
405
|
+
path is change-password, which verifies the current one) or for the shared
|
|
406
|
+
demo account."""
|
|
407
|
+
if user["role"] == "public_manager":
|
|
408
|
+
raise HTTPException(status_code=403, detail="Demo accounts cannot set a password")
|
|
409
|
+
|
|
410
|
+
if len(data.new_password) < 6:
|
|
411
|
+
raise HTTPException(status_code=422, detail="Password must be at least 6 characters")
|
|
412
|
+
|
|
413
|
+
db = get_db()
|
|
414
|
+
row = db.get_value("User", user["name"], ["hashed_password"])
|
|
415
|
+
if not row:
|
|
416
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
417
|
+
if has_usable_password(row["hashed_password"]):
|
|
418
|
+
raise HTTPException(status_code=409, detail="A password is already set; use change password instead")
|
|
419
|
+
|
|
420
|
+
db.set_value("User", user["name"],
|
|
421
|
+
{"hashed_password": hash_password(data.new_password), "modified": now()})
|
|
422
|
+
return {"ok": True}
|
|
423
|
+
|
|
424
|
+
|
|
384
425
|
@router.post("/invite")
|
|
385
426
|
def create_invite(data: InviteRequest, user: dict = Depends(require_admin)):
|
|
386
427
|
db = get_db()
|
|
@@ -424,6 +424,11 @@ export const api = {
|
|
|
424
424
|
method: "POST",
|
|
425
425
|
body: JSON.stringify({ current_password, new_password }),
|
|
426
426
|
}),
|
|
427
|
+
authSetPassword: (new_password: string) =>
|
|
428
|
+
request<any>("/auth/set-password", {
|
|
429
|
+
method: "POST",
|
|
430
|
+
body: JSON.stringify({ new_password }),
|
|
431
|
+
}),
|
|
427
432
|
authInvite: (email: string, role: string) =>
|
|
428
433
|
request<any>("/auth/invite", { method: "POST", body: JSON.stringify({ email, role }) }),
|
|
429
434
|
authListUsers: () => request<any[]>("/auth/users"),
|
|
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
|