-
-
Notifications
You must be signed in to change notification settings - Fork 50
Expand file tree
/
Copy pathauth.py
More file actions
251 lines (194 loc) · 8.57 KB
/
Copy pathauth.py
File metadata and controls
251 lines (194 loc) · 8.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
"""
Response DTOs for authentication endpoints.
AuthProviderInfo — auth provider entry in UserProfileResponse
UserPfp — profile picture in UserProfileResponse
UserProfileResponse — shape returned by UserProfileResponse.from_user()
LoginResponse — POST /auth/login (200)
RegisterResponse — POST /auth/register (201)
RefreshResponse — POST /auth/refresh (200)
LogoutResponse — POST /auth/logout (200)
VerifyEmailResponse — POST /auth/verify-email (200)
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Literal
from pydantic import Field
from schemas.dto.base import ResponseBase, UtcDatetime
from schemas.models.user import OAuthProvider, ProviderProfile
if TYPE_CHECKING:
from schemas.models.user import UserDoc
class AuthProviderInfo(ResponseBase):
"""Minimal OAuth provider entry returned inside UserProfileResponse."""
provider: OAuthProvider | None = Field(
default=None, description="OAuth provider name", examples=["google"]
)
email: str | None = Field(
default=None,
description="Email address from the OAuth provider",
examples=["user@gmail.com"],
)
linked_at: UtcDatetime | None = Field(
default=None,
description="When the provider was linked",
examples=["2025-01-15T10:30:00+00:00"],
)
class UserPfp(ResponseBase):
"""Profile picture info returned inside UserProfileResponse."""
url: str | None = Field(
default=None,
description="Profile picture URL",
examples=["https://lh3.googleusercontent.com/a/photo"],
)
source: OAuthProvider | Literal["upload"] | None = Field(
default=None,
description="Source of the profile picture — an OAuth provider or `upload`",
examples=["google"],
)
class UserProfileResponse(ResponseBase):
"""User profile shape — used in login/register/me responses."""
id: str = Field(description="User ID", examples=["507f1f77bcf86cd799439011"])
email: str | None = Field(
default=None, description="User's email address", examples=["user@example.com"]
)
email_verified: bool = Field(
description="Whether the email address has been verified"
)
user_name: str | None = Field(
default=None, description="Display name", examples=["Jane Doe"]
)
plan: str = Field(description="Effective plan", examples=["free"])
password_set: bool = Field(description="Whether the user has set a password")
onboarded_at: UtcDatetime | None = Field(
default=None,
description="When the user completed onboarding (null = never)",
)
pro_onboarded_at: UtcDatetime | None = Field(
default=None,
description="When the user completed the Pro tour (null = not yet)",
)
auth_providers: list[AuthProviderInfo] = Field(description="Linked OAuth providers")
# pfp is absent from the JSON when None (route handlers use exclude_none=True)
pfp: UserPfp | None = Field(
default=None, description="Profile picture (absent when not set)"
)
@classmethod
def from_user(cls, user: UserDoc, *, plan: str) -> UserProfileResponse:
"""Build a UserProfileResponse from a UserDoc and the resolved plan.
This is the single authoritative place for the profile response shape,
replacing the old AuthService.get_user_profile() static helper.
"""
return cls(
id=str(user.id),
email=user.email,
email_verified=user.email_verified,
user_name=user.user_name,
plan=plan,
password_set=user.password_set,
onboarded_at=user.onboarded_at,
pro_onboarded_at=user.pro_onboarded_at,
auth_providers=[
AuthProviderInfo(
provider=p.provider,
email=p.email,
linked_at=p.linked_at,
)
for p in user.auth_providers
],
pfp=UserPfp(url=user.pfp.url, source=user.pfp.source) if user.pfp else None,
)
class LoginResponse(ResponseBase):
"""Response body for POST /auth/login (200)."""
access_token: str = Field(
description="JWT access token", examples=["eyJhbGciOiJIUzI1NiIs..."]
)
user: UserProfileResponse = Field(description="Authenticated user's profile")
class RegisterResponse(ResponseBase):
"""Response body for POST /auth/register (201)."""
access_token: str = Field(
description="JWT access token", examples=["eyJhbGciOiJIUzI1NiIs..."]
)
user: UserProfileResponse = Field(description="Newly created user's profile")
requires_verification: bool = Field(
description="Whether email verification is required before accessing protected resources"
)
verification_sent: bool = Field(
description="Whether the verification email was sent successfully"
)
class RefreshResponse(ResponseBase):
"""Response body for POST /auth/refresh (200)."""
access_token: str = Field(
description="New JWT access token", examples=["eyJhbGciOiJIUzI1NiIs..."]
)
class LogoutResponse(ResponseBase):
"""Response body for POST /auth/logout (200)."""
success: bool = Field(description="Always true on successful logout")
class VerifyEmailResponse(ResponseBase):
"""Response body for POST /auth/verify-email (200)."""
success: bool = Field(description="Whether verification succeeded")
message: str = Field(
description="Human-readable status message",
examples=["email verified successfully"],
)
email_verified: bool = Field(
description="Updated email verification status (always true on success)"
)
class MeResponse(ResponseBase):
"""Response body for GET /auth/me (200)."""
user: UserProfileResponse = Field(
description="Current authenticated user's profile"
)
class SendVerificationResponse(ResponseBase):
"""Response body for POST /auth/send-verification (200)."""
success: bool = Field(description="Whether the verification email was sent")
message: str = Field(description="Human-readable status message")
expires_in: int = Field(
description="OTP expiry duration in seconds", examples=[600]
)
class DeviceTokenResponse(ResponseBase):
"""Response body for POST /auth/device/token (200)."""
access_token: str = Field(description="JWT access token")
refresh_token: str = Field(description="JWT refresh token")
user: UserProfileResponse = Field(description="User profile")
class DeviceRefreshResponse(ResponseBase):
"""Response body for POST /auth/device/refresh (200)."""
access_token: str = Field(description="New JWT access token")
refresh_token: str = Field(description="New JWT refresh token")
class OAuthProviderDetail(ResponseBase):
"""Detailed OAuth provider entry for the providers list endpoint."""
provider: OAuthProvider = Field(description="Provider name", examples=["google"])
email: str | None = Field(default=None, description="Email from provider")
email_verified: bool = Field(
default=False, description="Email verified by provider"
)
linked_at: UtcDatetime | None = Field(
default=None, description="When the provider was linked"
)
profile: ProviderProfile = Field(
default_factory=ProviderProfile, description="Provider profile (name, picture)"
)
class OAuthProvidersResponse(ResponseBase):
"""Response body for GET /oauth/providers (200)."""
providers: list[OAuthProviderDetail] = Field(
description="List of linked OAuth providers with name, email, and linked_at"
)
password_set: bool = Field(
description="Whether the user has a password set (affects unlink eligibility)"
)
class OnboardingStateResponse(ResponseBase):
"""Response body for GET/PUT /auth/onboarding (200).
A resume pointer, nothing more. Empty (step=null) means nothing to
resume: never started, expired, or already completed — completion is
a permanent account fact exposed as ``user.onboarded_at`` on
/auth/me, not part of this cache.
"""
step: str | None = Field(
default=None, description="Stored wizard step", examples=["link"]
)
path: str | None = Field(
default=None, description="Chosen path (links or api)", examples=["links"]
)
class OnboardingCompleteResponse(ResponseBase):
"""Response body for POST /auth/onboarding/complete (200)."""
success: bool = Field(description="Always true on success")
onboarded_at: UtcDatetime = Field(
description="When onboarding was completed (first completion wins)"
)