lambda-erp 0.1.33__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.
Files changed (70) hide show
  1. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/PKG-INFO +1 -1
  2. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/auth.py +44 -3
  3. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/frontend/src/api/client.ts +5 -0
  4. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/pyproject.toml +1 -1
  5. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/.gitignore +0 -0
  6. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/LICENSE +0 -0
  7. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/README.md +0 -0
  8. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/__init__.py +0 -0
  9. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/attachments.py +0 -0
  10. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/bootstrap.py +0 -0
  11. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/chat.py +0 -0
  12. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/demo_limits.py +0 -0
  13. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/deps.py +0 -0
  14. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/errors.py +0 -0
  15. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/main.py +0 -0
  16. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/oauth.py +0 -0
  17. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/pdf.py +0 -0
  18. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/providers.py +0 -0
  19. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/remarks_md.py +0 -0
  20. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/routers/__init__.py +0 -0
  21. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/routers/accounting.py +0 -0
  22. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/routers/admin.py +0 -0
  23. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/routers/analytics.py +0 -0
  24. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/routers/bank_reconciliation.py +0 -0
  25. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/routers/documents.py +0 -0
  26. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/routers/masters.py +0 -0
  27. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/routers/proposals.py +0 -0
  28. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/routers/reports.py +0 -0
  29. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/routers/setup.py +0 -0
  30. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/services.py +0 -0
  31. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/templates/document.html +0 -0
  32. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/api/templates/proposal.html +0 -0
  33. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/docs/agents/README.md +0 -0
  34. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/frontend/README.md +0 -0
  35. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/__init__.py +0 -0
  36. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/__init__.py +0 -0
  37. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/bank_transaction.py +0 -0
  38. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/budget.py +0 -0
  39. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  40. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/general_ledger.py +0 -0
  41. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/journal_entry.py +0 -0
  42. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/payment_entry.py +0 -0
  43. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/pos_invoice.py +0 -0
  44. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/purchase_invoice.py +0 -0
  45. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/revaluation.py +0 -0
  46. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/sales_invoice.py +0 -0
  47. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/accounting/subscription.py +0 -0
  48. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/buying/__init__.py +0 -0
  49. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/buying/purchase_order.py +0 -0
  50. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/controllers/__init__.py +0 -0
  51. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/controllers/currency.py +0 -0
  52. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/controllers/defaults.py +0 -0
  53. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/controllers/pricing_rule.py +0 -0
  54. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  55. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/database.py +0 -0
  56. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/exceptions.py +0 -0
  57. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/hooks.py +0 -0
  58. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/model.py +0 -0
  59. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/selling/__init__.py +0 -0
  60. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/selling/proposal.py +0 -0
  61. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/selling/quotation.py +0 -0
  62. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/selling/sales_order.py +0 -0
  63. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/simulation.py +0 -0
  64. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/stock/__init__.py +0 -0
  65. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/stock/delivery_note.py +0 -0
  66. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/stock/purchase_receipt.py +0 -0
  67. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/stock/stock_entry.py +0 -0
  68. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/stock/stock_ledger.py +0 -0
  69. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/lambda_erp/utils.py +0 -0
  70. {lambda_erp-0.1.33 → lambda_erp-0.1.35}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.1.33
3
+ Version: 0.1.35
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: MIT
@@ -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
- return {"name": user["name"], "email": user["email"], "full_name": user["full_name"], "role": user["role"]}
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"),
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.1.33"
3
+ version = "0.1.35"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "MIT"
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