cloudcost-cli 0.1.0__py3-none-any.whl

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.
backend/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """CloudCost AI backend package."""
@@ -0,0 +1 @@
1
+ """Application modules for CloudCost AI."""
backend/app/auth.py ADDED
@@ -0,0 +1,104 @@
1
+ import base64
2
+ import hashlib
3
+ import hmac
4
+ import re
5
+ import secrets
6
+ from datetime import UTC, datetime, timedelta
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ from backend.app.config import Settings
12
+
13
+
14
+ EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
15
+ PASSWORD_ITERATIONS = 260_000
16
+
17
+
18
+ class SignupRequest(BaseModel):
19
+ email: str
20
+ password: str = Field(min_length=8, max_length=256)
21
+ full_name: str | None = Field(default=None, max_length=160)
22
+ company: str | None = Field(default=None, max_length=160)
23
+
24
+
25
+ class VerifySignupRequest(BaseModel):
26
+ email: str
27
+ code: str = Field(min_length=4, max_length=12)
28
+
29
+
30
+ class ResendSignupOtpRequest(BaseModel):
31
+ email: str
32
+
33
+
34
+ class LoginRequest(BaseModel):
35
+ email: str
36
+ password: str = Field(min_length=1, max_length=256)
37
+
38
+
39
+ def normalize_email(email: str) -> str:
40
+ normalized = email.strip().lower()
41
+ if not EMAIL_PATTERN.match(normalized):
42
+ raise ValueError("Enter a valid email address.")
43
+ return normalized
44
+
45
+
46
+ def utc_now() -> datetime:
47
+ return datetime.now(tz=UTC)
48
+
49
+
50
+ def hash_password(password: str) -> str:
51
+ salt = secrets.token_bytes(16)
52
+ digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, PASSWORD_ITERATIONS)
53
+ return "pbkdf2_sha256${}${}${}".format(
54
+ PASSWORD_ITERATIONS,
55
+ base64.b64encode(salt).decode("ascii"),
56
+ base64.b64encode(digest).decode("ascii"),
57
+ )
58
+
59
+
60
+ def verify_password(password: str, password_hash: str | None) -> bool:
61
+ if not password_hash:
62
+ return False
63
+ try:
64
+ scheme, iterations, salt_b64, digest_b64 = password_hash.split("$", 3)
65
+ if scheme != "pbkdf2_sha256":
66
+ return False
67
+ expected = base64.b64decode(digest_b64)
68
+ salt = base64.b64decode(salt_b64)
69
+ actual = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, int(iterations))
70
+ return hmac.compare_digest(actual, expected)
71
+ except (ValueError, TypeError):
72
+ return False
73
+
74
+
75
+ def generate_otp_code() -> str:
76
+ return f"{secrets.randbelow(1_000_000):06d}"
77
+
78
+
79
+ def hash_otp_code(settings: Settings, email: str, code: str, purpose: str) -> str:
80
+ payload = f"{purpose}:{email}:{code}".encode("utf-8")
81
+ return hmac.new(settings.require_auth_secret().encode("utf-8"), payload, hashlib.sha256).hexdigest()
82
+
83
+
84
+ def new_session_token() -> str:
85
+ return secrets.token_urlsafe(48)
86
+
87
+
88
+ def hash_session_token(settings: Settings, token: str) -> str:
89
+ return hmac.new(settings.require_auth_secret().encode("utf-8"), token.encode("utf-8"), hashlib.sha256).hexdigest()
90
+
91
+
92
+ def session_expires_at(settings: Settings) -> datetime:
93
+ return utc_now() + timedelta(days=settings.auth_session_days)
94
+
95
+
96
+ def public_user(user: dict[str, Any]) -> dict[str, Any]:
97
+ return {
98
+ "id": user["id"],
99
+ "email": user["email"],
100
+ "full_name": user.get("full_name"),
101
+ "company": user.get("company"),
102
+ "email_verified": bool(user.get("email_verified_at")),
103
+ "created_at": user.get("created_at").isoformat() if user.get("created_at") else None,
104
+ }