Coverage for modules\authentication\schemas.py: 90%
100 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-10-17 16:50 +0800
« prev ^ index » next coverage.py v7.16.0, created at 2026-10-17 16:50 +0800
1import re
2from typing import Optional, List
3from pydantic import BaseModel, ConfigDict, field_validator
4from uuid import UUID
5from datetime import datetime
8def validate_email_field(value: str) -> str:
9 if value is None:
10 raise ValueError("Email is required.")
12 email = str(value).strip()
13 if not email:
14 raise ValueError("Email is required.")
16 if "@" not in email:
17 raise ValueError("Enter a valid email address.")
19 local_part, domain = email.rsplit("@", 1)
20 if not local_part or not domain:
21 raise ValueError("Enter a valid email address.")
23 if domain.startswith(".") or domain.endswith("."):
24 raise ValueError("Enter a valid email address.")
26 if not re.fullmatch(r"^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9.-]+\.[A-Za-z0-9-]+$", email):
27 raise ValueError("Enter a valid email address.")
29 return email.lower()
32class RegisterIn(BaseModel):
33 email: str
34 password: str
35 first_name: str
36 last_name: str
37 organization_name: str
39 @field_validator("email")
40 @classmethod
41 def validate_email(cls, value: str) -> str:
42 return validate_email_field(value)
45class LoginIn(BaseModel):
46 email: str
47 password: str
49 @field_validator("email")
50 @classmethod
51 def validate_email(cls, value: str) -> str:
52 return validate_email_field(value)
54class RefreshIn(BaseModel):
55 refresh_token: str
57class PasswordResetRequestIn(BaseModel):
58 email: str
60 @field_validator("email")
61 @classmethod
62 def validate_email(cls, value: str) -> str:
63 return validate_email_field(value)
65class PasswordResetConfirmIn(BaseModel):
66 token: str
67 password: str
69 @field_validator("password")
70 @classmethod
71 def validate_password(cls, value: str) -> str:
72 if len(value) < 8:
73 raise ValueError("Password must be at least 8 characters.")
74 return value
76class SwitchOrgIn(BaseModel):
77 organization_id: UUID
79class UserUpdateIn(BaseModel):
80 first_name: Optional[str] = None
81 last_name: Optional[str] = None
82 phone_number: Optional[str] = None
83 avatar_url: Optional[str] = None
85class OrganizationOut(BaseModel):
86 id: UUID
87 name: str
88 slug: str
89 logo_url: Optional[str] = None
90 currency: str
91 fiscal_year_start: int
92 tax_id: Optional[str] = None
93 website: Optional[str] = None
94 address: Optional[str] = None
95 created_at: datetime
96 updated_at: datetime
98 model_config = ConfigDict(from_attributes=True)
100class UserOut(BaseModel):
101 id: UUID
102 email: str
103 first_name: str
104 last_name: str
105 full_name: str
106 avatar_url: Optional[str] = None
107 phone_number: Optional[str] = None
108 is_active: bool = True
109 is_staff: bool = False
110 is_superuser: bool = False
111 current_organization_id: Optional[UUID] = None
112 role: Optional[str] = None
113 created_at: datetime
114 updated_at: datetime
116 model_config = ConfigDict(from_attributes=True)
118class TokenOut(BaseModel):
119 access_token: str
120 refresh_token: str
121 token_type: str = "Bearer"
122 expires_in: int
124class AuthResponse(BaseModel):
125 user: UserOut
126 organization: Optional[OrganizationOut] = None
127 tokens: TokenOut