supython 0.5.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.
Files changed (188) hide show
  1. supython/__init__.py +8 -0
  2. supython/admin/__init__.py +3 -0
  3. supython/admin/api/__init__.py +24 -0
  4. supython/admin/api/auth.py +118 -0
  5. supython/admin/api/auth_templates.py +67 -0
  6. supython/admin/api/auth_users.py +225 -0
  7. supython/admin/api/db.py +174 -0
  8. supython/admin/api/functions.py +92 -0
  9. supython/admin/api/jobs.py +192 -0
  10. supython/admin/api/ops.py +224 -0
  11. supython/admin/api/realtime.py +281 -0
  12. supython/admin/api/service_auth.py +49 -0
  13. supython/admin/api/service_auth_templates.py +83 -0
  14. supython/admin/api/service_auth_users.py +346 -0
  15. supython/admin/api/service_db.py +214 -0
  16. supython/admin/api/service_functions.py +287 -0
  17. supython/admin/api/service_jobs.py +282 -0
  18. supython/admin/api/service_ops.py +213 -0
  19. supython/admin/api/service_realtime.py +30 -0
  20. supython/admin/api/service_storage.py +220 -0
  21. supython/admin/api/storage.py +117 -0
  22. supython/admin/api/system.py +37 -0
  23. supython/admin/audit.py +29 -0
  24. supython/admin/deps.py +22 -0
  25. supython/admin/errors.py +16 -0
  26. supython/admin/schemas.py +310 -0
  27. supython/admin/session.py +52 -0
  28. supython/admin/spa.py +38 -0
  29. supython/admin/static/assets/Alert-dluGVkos.js +49 -0
  30. supython/admin/static/assets/Audit-Njung3HI.js +2 -0
  31. supython/admin/static/assets/Backups-DzPlFgrm.js +2 -0
  32. supython/admin/static/assets/Buckets-ByacGkU1.js +2 -0
  33. supython/admin/static/assets/Channels-BoIuTtam.js +353 -0
  34. supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +2 -0
  35. supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +2 -0
  36. supython/admin/static/assets/Crons-B67vc39F.js +2 -0
  37. supython/admin/static/assets/DashboardView-CUTFVL6k.js +2 -0
  38. supython/admin/static/assets/DataTable-COAAWEft.js +747 -0
  39. supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +75 -0
  40. supython/admin/static/assets/DrawerContent-TpYTFgF1.js +139 -0
  41. supython/admin/static/assets/Empty-cr2r7e2u.js +25 -0
  42. supython/admin/static/assets/EmptyState-DeDck-OL.js +2 -0
  43. supython/admin/static/assets/Grid-hFkp9F4P.js +2 -0
  44. supython/admin/static/assets/Input-DppYTq9C.js +259 -0
  45. supython/admin/static/assets/Invoke-DW3Nveeh.js +2 -0
  46. supython/admin/static/assets/JsonField-DibyJgun.js +2 -0
  47. supython/admin/static/assets/LoginView-BjLyE3Ds.css +1 -0
  48. supython/admin/static/assets/LoginView-CoOjECT_.js +111 -0
  49. supython/admin/static/assets/Logs-D9WYrnIT.js +2 -0
  50. supython/admin/static/assets/Logs-DS1XPa0h.css +1 -0
  51. supython/admin/static/assets/Migrations-DOSC2ddQ.js +2 -0
  52. supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +2 -0
  53. supython/admin/static/assets/Queue-CywZs6vI.js +2 -0
  54. supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +2 -0
  55. supython/admin/static/assets/RlsEditor-BSlH9vSc.js +2 -0
  56. supython/admin/static/assets/Routes-BiLXE49D.js +2 -0
  57. supython/admin/static/assets/Routes-C-ianIGD.css +1 -0
  58. supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +1 -0
  59. supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +2 -0
  60. supython/admin/static/assets/Select-DIzZyRZb.js +434 -0
  61. supython/admin/static/assets/Space-n5-XcguU.js +400 -0
  62. supython/admin/static/assets/SqlEditor-b8pTsILY.js +3 -0
  63. supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +104 -0
  64. supython/admin/static/assets/TableData-CQIagLKn.js +2 -0
  65. supython/admin/static/assets/Tag-D1fOKpTH.js +72 -0
  66. supython/admin/static/assets/Templates-BS-ugkdq.js +2 -0
  67. supython/admin/static/assets/Thing-CEAniuMg.js +107 -0
  68. supython/admin/static/assets/Users-wzwajhlh.js +2 -0
  69. supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +1 -0
  70. supython/admin/static/assets/dist-VXIJLCYq.js +13 -0
  71. supython/admin/static/assets/format-length-CGCY1rMh.js +2 -0
  72. supython/admin/static/assets/get-Ca6unauB.js +2 -0
  73. supython/admin/static/assets/index-CeE6v959.js +951 -0
  74. supython/admin/static/assets/pinia-COXwfrOX.js +2 -0
  75. supython/admin/static/assets/resources-Bt6thQCD.js +44 -0
  76. supython/admin/static/assets/use-locale-mtgM0a3a.js +2 -0
  77. supython/admin/static/assets/use-merged-state-BvhkaHNX.js +2 -0
  78. supython/admin/static/assets/useConfirm-tMjvBFXR.js +2 -0
  79. supython/admin/static/assets/useResource-C_rJCY8C.js +2 -0
  80. supython/admin/static/assets/useTable-CnZc5zhi.js +363 -0
  81. supython/admin/static/assets/useTable-Dg0XlRlq.css +1 -0
  82. supython/admin/static/assets/useToast-DsZKx0IX.js +2 -0
  83. supython/admin/static/assets/utils-sbXoq7Ir.js +2 -0
  84. supython/admin/static/favicon.svg +1 -0
  85. supython/admin/static/icons.svg +24 -0
  86. supython/admin/static/index.html +24 -0
  87. supython/app.py +149 -0
  88. supython/auth/__init__.py +3 -0
  89. supython/auth/_email_job.py +11 -0
  90. supython/auth/providers/__init__.py +34 -0
  91. supython/auth/providers/github.py +22 -0
  92. supython/auth/providers/google.py +19 -0
  93. supython/auth/providers/oauth.py +56 -0
  94. supython/auth/providers/registry.py +16 -0
  95. supython/auth/ratelimit.py +39 -0
  96. supython/auth/router.py +282 -0
  97. supython/auth/schemas.py +79 -0
  98. supython/auth/service.py +587 -0
  99. supython/body_size.py +184 -0
  100. supython/cli.py +1653 -0
  101. supython/client/__init__.py +67 -0
  102. supython/client/_auth.py +249 -0
  103. supython/client/_client.py +145 -0
  104. supython/client/_config.py +92 -0
  105. supython/client/_functions.py +69 -0
  106. supython/client/_storage.py +255 -0
  107. supython/client/py.typed +0 -0
  108. supython/db.py +151 -0
  109. supython/db_admin.py +8 -0
  110. supython/functions/__init__.py +19 -0
  111. supython/functions/context.py +262 -0
  112. supython/functions/loader.py +307 -0
  113. supython/functions/router.py +228 -0
  114. supython/functions/schemas.py +50 -0
  115. supython/gen/__init__.py +5 -0
  116. supython/gen/_introspect.py +137 -0
  117. supython/gen/types_py.py +270 -0
  118. supython/gen/types_ts.py +365 -0
  119. supython/health.py +229 -0
  120. supython/hooks.py +117 -0
  121. supython/jobs/__init__.py +31 -0
  122. supython/jobs/backends.py +97 -0
  123. supython/jobs/context.py +58 -0
  124. supython/jobs/cron.py +152 -0
  125. supython/jobs/cron_inproc.py +118 -0
  126. supython/jobs/decorators.py +76 -0
  127. supython/jobs/registry.py +79 -0
  128. supython/jobs/router.py +136 -0
  129. supython/jobs/schemas.py +92 -0
  130. supython/jobs/service.py +311 -0
  131. supython/jobs/worker.py +219 -0
  132. supython/jwks.py +257 -0
  133. supython/keyset.py +279 -0
  134. supython/logging_config.py +291 -0
  135. supython/mail.py +33 -0
  136. supython/mailer.py +65 -0
  137. supython/migrate.py +81 -0
  138. supython/migrations/0001_extensions_and_roles.sql +46 -0
  139. supython/migrations/0002_auth_schema.sql +66 -0
  140. supython/migrations/0003_demo_todos.sql +42 -0
  141. supython/migrations/0004_auth_v0_2.sql +47 -0
  142. supython/migrations/0005_storage_schema.sql +117 -0
  143. supython/migrations/0006_realtime_schema.sql +206 -0
  144. supython/migrations/0007_jobs_schema.sql +254 -0
  145. supython/migrations/0008_jobs_last_error.sql +56 -0
  146. supython/migrations/0009_auth_rate_limits.sql +33 -0
  147. supython/migrations/0010_worker_heartbeat.sql +14 -0
  148. supython/migrations/0011_admin_schema.sql +45 -0
  149. supython/migrations/0012_auth_banned_until.sql +10 -0
  150. supython/migrations/0013_email_templates.sql +19 -0
  151. supython/migrations/0014_realtime_payload_warning.sql +96 -0
  152. supython/migrations/0015_backups_schema.sql +14 -0
  153. supython/passwords.py +15 -0
  154. supython/realtime/__init__.py +6 -0
  155. supython/realtime/broker.py +814 -0
  156. supython/realtime/protocol.py +234 -0
  157. supython/realtime/router.py +184 -0
  158. supython/realtime/schemas.py +207 -0
  159. supython/realtime/service.py +261 -0
  160. supython/realtime/topics.py +175 -0
  161. supython/realtime/websocket.py +586 -0
  162. supython/scaffold/__init__.py +5 -0
  163. supython/scaffold/init_project.py +133 -0
  164. supython/scaffold/templates/Caddyfile.tmpl +4 -0
  165. supython/scaffold/templates/README.md.tmpl +22 -0
  166. supython/scaffold/templates/docker-compose.prod.yml.tmpl +84 -0
  167. supython/scaffold/templates/docker-compose.yml.tmpl +41 -0
  168. supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +9 -0
  169. supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +3 -0
  170. supython/scaffold/templates/env.example.tmpl +149 -0
  171. supython/scaffold/templates/functions_README.md.tmpl +21 -0
  172. supython/scaffold/templates/gitignore.tmpl +14 -0
  173. supython/scaffold/templates/migrations/.gitkeep +0 -0
  174. supython/secretset.py +347 -0
  175. supython/security_headers.py +78 -0
  176. supython/settings.py +198 -0
  177. supython/storage/__init__.py +5 -0
  178. supython/storage/backends.py +392 -0
  179. supython/storage/router.py +341 -0
  180. supython/storage/schemas.py +50 -0
  181. supython/storage/service.py +445 -0
  182. supython/storage/signing.py +119 -0
  183. supython/tokens.py +85 -0
  184. supython-0.5.0.dist-info/METADATA +714 -0
  185. supython-0.5.0.dist-info/RECORD +188 -0
  186. supython-0.5.0.dist-info/WHEEL +4 -0
  187. supython-0.5.0.dist-info/entry_points.txt +2 -0
  188. supython-0.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,282 @@
1
+ from typing import Annotated
2
+ from uuid import UUID
3
+
4
+ import jwt
5
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
6
+ from fastapi.responses import RedirectResponse
7
+
8
+ from .. import db, tokens
9
+ from ..settings import get_settings
10
+ from . import ratelimit, service
11
+ from .schemas import (
12
+ MagicLinkRequest,
13
+ OtpRequest,
14
+ OtpVerifyRequest,
15
+ RecoverRequest,
16
+ RecoverVerifyRequest,
17
+ RefreshRequest,
18
+ SignUpRequest,
19
+ TokenRequest,
20
+ TokenResponse,
21
+ UserResponse,
22
+ )
23
+
24
+ router = APIRouter(prefix="/auth/v1", tags=["auth"])
25
+
26
+
27
+ def _to_token_response(
28
+ user: UserResponse, access: str, refresh: str, ttl: int
29
+ ) -> TokenResponse:
30
+ return TokenResponse(
31
+ access_token=access,
32
+ expires_in=ttl,
33
+ refresh_token=refresh,
34
+ user=user,
35
+ )
36
+
37
+
38
+ def _auth_error(exc: service.AuthError) -> HTTPException:
39
+ return HTTPException(
40
+ status_code=exc.status,
41
+ detail={"code": exc.code, "message": exc.message},
42
+ )
43
+
44
+
45
+ async def _current_user_id(
46
+ authorization: Annotated[str | None, Header()] = None,
47
+ ) -> UUID:
48
+ if not authorization or not authorization.lower().startswith("bearer "):
49
+ raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing bearer token")
50
+ token = authorization.split(" ", 1)[1]
51
+ try:
52
+ claims = tokens.decode_access_token(token)
53
+ except jwt.PyJWTError as exc:
54
+ raise HTTPException(
55
+ status.HTTP_401_UNAUTHORIZED, f"Invalid token: {exc}"
56
+ ) from exc
57
+ try:
58
+ return UUID(claims["sub"])
59
+ except (KeyError, ValueError) as exc:
60
+ raise HTTPException(
61
+ status.HTTP_401_UNAUTHORIZED, "Token missing valid sub claim"
62
+ ) from exc
63
+
64
+
65
+ def _client_ip(request: Request) -> str:
66
+ if request.client is None:
67
+ return "unknown"
68
+ return request.client.host
69
+
70
+
71
+ def _client_ip_or_none(request: Request) -> str | None:
72
+ """Return the client IP for audit logging, or None when unavailable.
73
+
74
+ Unlike _client_ip, returns None instead of "unknown" so callers can safely
75
+ pass the result to a Postgres inet column without a cast error.
76
+ """
77
+ if request.client is None:
78
+ return None
79
+ return request.client.host
80
+
81
+
82
+ def _rate_limit_dep(prefix: str, rule_attr: str):
83
+ async def dep(request: Request) -> None:
84
+ settings = get_settings()
85
+ if not settings.auth_rate_limit_enabled:
86
+ return
87
+ rule = ratelimit.RateLimit(
88
+ limit=getattr(settings, rule_attr),
89
+ window_seconds=settings.auth_rate_limit_window_seconds,
90
+ )
91
+ bucket = f"{prefix}:{_client_ip(request)}"
92
+ async with db.as_service_role() as conn:
93
+ blocked, _ = await ratelimit.hit(conn, bucket=bucket, rule=rule)
94
+ if blocked:
95
+ raise HTTPException(
96
+ status.HTTP_429_TOO_MANY_REQUESTS,
97
+ detail={"code": "rate_limited", "message": "Too many requests"},
98
+ headers={"Retry-After": str(rule.window_seconds)},
99
+ )
100
+
101
+ return dep
102
+
103
+
104
+ _rl_token = _rate_limit_dep("auth.token", "auth_rate_limit_token_per_window")
105
+ _rl_signup = _rate_limit_dep("auth.signup", "auth_rate_limit_signup_per_window")
106
+ _rl_recover = _rate_limit_dep("auth.recover", "auth_rate_limit_recover_per_window")
107
+ _rl_otp = _rate_limit_dep("auth.otp", "auth_rate_limit_otp_per_window")
108
+ _rl_magiclink = _rate_limit_dep(
109
+ "auth.magiclink", "auth_rate_limit_magiclink_per_window"
110
+ )
111
+
112
+
113
+ @router.post(
114
+ "/signup",
115
+ response_model=TokenResponse,
116
+ status_code=201,
117
+ dependencies=[Depends(_rl_signup)],
118
+ )
119
+ async def signup(payload: SignUpRequest) -> TokenResponse:
120
+ try:
121
+ async with db.acquire() as conn:
122
+ result = await service.signup(conn, payload.email, payload.password)
123
+ except service.AuthError as exc:
124
+ raise _auth_error(exc) from exc
125
+ return _to_token_response(*result)
126
+
127
+
128
+ @router.post("/token", response_model=TokenResponse, dependencies=[Depends(_rl_token)])
129
+ async def token(payload: TokenRequest) -> TokenResponse:
130
+ try:
131
+ async with db.acquire() as conn:
132
+ result = await service.password_grant(
133
+ conn, payload.email, payload.password
134
+ )
135
+ except service.AuthError as exc:
136
+ raise _auth_error(exc) from exc
137
+ return _to_token_response(*result)
138
+
139
+
140
+ @router.post("/refresh", response_model=TokenResponse)
141
+ async def refresh(payload: RefreshRequest, request: Request) -> TokenResponse:
142
+ try:
143
+ async with db.acquire() as conn:
144
+ result = await service.refresh_grant(
145
+ conn,
146
+ payload.refresh_token,
147
+ ip=_client_ip_or_none(request),
148
+ ua=request.headers.get("user-agent"),
149
+ )
150
+ except service.AuthError as exc:
151
+ raise _auth_error(exc) from exc
152
+ return _to_token_response(*result)
153
+
154
+
155
+ @router.post("/logout", status_code=204)
156
+ async def logout(payload: RefreshRequest) -> None:
157
+ async with db.acquire() as conn:
158
+ await service.logout(conn, payload.refresh_token)
159
+
160
+
161
+ @router.get("/user", response_model=UserResponse)
162
+ async def me(
163
+ user_id: Annotated[UUID, Depends(_current_user_id)],
164
+ ) -> UserResponse:
165
+ async with db.acquire() as conn:
166
+ user = await service.get_user(conn, user_id)
167
+ if not user:
168
+ raise HTTPException(404, "User not found")
169
+ return user
170
+
171
+
172
+ @router.post("/recover", status_code=202, dependencies=[Depends(_rl_recover)])
173
+ async def request_recover(payload: RecoverRequest) -> None:
174
+ async with db.acquire() as conn:
175
+ await service.request_recover(conn, payload.email)
176
+
177
+
178
+ @router.post(
179
+ "/recover/verify",
180
+ response_model=TokenResponse,
181
+ dependencies=[Depends(_rl_recover)],
182
+ )
183
+ async def verify_recover(payload: RecoverVerifyRequest, request: Request) -> TokenResponse:
184
+ try:
185
+ async with db.acquire() as conn:
186
+ result = await service.verify_recover(
187
+ conn,
188
+ payload.email,
189
+ payload.token,
190
+ payload.password,
191
+ ip=_client_ip_or_none(request),
192
+ ua=request.headers.get("user-agent"),
193
+ )
194
+ except service.AuthError as exc:
195
+ raise _auth_error(exc) from exc
196
+ return _to_token_response(*result)
197
+
198
+
199
+ @router.post("/magiclink", status_code=202, dependencies=[Depends(_rl_magiclink)])
200
+ async def request_magic_link(payload: MagicLinkRequest) -> None:
201
+ async with db.acquire() as conn:
202
+ await service.request_magic_link(conn, payload.email)
203
+
204
+
205
+ @router.get(
206
+ "/magiclink/verify",
207
+ response_model=TokenResponse,
208
+ dependencies=[Depends(_rl_magiclink)],
209
+ )
210
+ async def verify_magic_link(
211
+ token: Annotated[str, Query(description="Raw magic-link token from the email")],
212
+ ) -> TokenResponse:
213
+ try:
214
+ async with db.acquire() as conn:
215
+ result = await service.verify_magic_link(conn, token)
216
+ except service.AuthError as exc:
217
+ raise _auth_error(exc) from exc
218
+ return _to_token_response(*result)
219
+
220
+
221
+ @router.post("/otp", status_code=202, dependencies=[Depends(_rl_otp)])
222
+ async def request_otp(payload: OtpRequest) -> None:
223
+ async with db.acquire() as conn:
224
+ await service.request_otp(conn, payload.email)
225
+
226
+
227
+ @router.post("/otp/verify", response_model=TokenResponse, dependencies=[Depends(_rl_otp)])
228
+ async def verify_otp(payload: OtpVerifyRequest) -> TokenResponse:
229
+ try:
230
+ async with db.acquire() as conn:
231
+ result = await service.verify_otp(conn, payload.email, payload.token)
232
+ except service.AuthError as exc:
233
+ raise _auth_error(exc) from exc
234
+ return _to_token_response(*result)
235
+
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # OAuth
239
+ # ---------------------------------------------------------------------------
240
+
241
+
242
+ @router.get("/authorize/{provider}", status_code=302)
243
+ async def oauth_authorize(
244
+ provider: str,
245
+ redirect_uri: Annotated[str, Query(description="URL to redirect back to after login")],
246
+ ) -> RedirectResponse:
247
+ """Initiate an OAuth2 Authorization Code flow for the given provider."""
248
+ try:
249
+ url = await service.oauth_start(provider, redirect_uri)
250
+ except service.AuthError as exc:
251
+ raise _auth_error(exc) from exc
252
+ return RedirectResponse(url, status_code=302)
253
+
254
+
255
+ @router.get("/callback/{provider}", status_code=302)
256
+ async def oauth_callback(
257
+ provider: str,
258
+ code: Annotated[str, Query()],
259
+ state: Annotated[str, Query()],
260
+ request: Request,
261
+ ) -> RedirectResponse:
262
+ """Handle the provider callback, exchange the code, and redirect with tokens."""
263
+ try:
264
+ async with db.acquire() as conn:
265
+ result = await service.oauth_finish(
266
+ conn,
267
+ provider,
268
+ code,
269
+ state,
270
+ ip=_client_ip_or_none(request),
271
+ ua=request.headers.get("user-agent"),
272
+ )
273
+ except service.AuthError as exc:
274
+ raise _auth_error(exc) from exc
275
+ _user, access, refresh, ttl, redirect_uri = result
276
+ fragment = (
277
+ f"access_token={access}"
278
+ f"&refresh_token={refresh}"
279
+ f"&expires_in={ttl}"
280
+ f"&token_type=bearer"
281
+ )
282
+ return RedirectResponse(f"{redirect_uri}#{fragment}", status_code=302)
@@ -0,0 +1,79 @@
1
+ from datetime import datetime
2
+ from typing import Annotated
3
+ from uuid import UUID
4
+
5
+ from pydantic import BaseModel, EmailStr, Field
6
+
7
+ # Per-field input caps. The body-size middleware already rejects absurdly
8
+ # large payloads, but per-field caps give the user a precise 422 instead
9
+ # of a generic 413 and stop unbounded values from reaching argon2 / the
10
+ # DB. RFC 5321 caps email at 254; argon2id is happy with arbitrary
11
+ # password lengths but slow on huge ones, so we mirror what the underlying
12
+ # password column / hash assumes.
13
+ MAX_EMAIL_LEN = 254
14
+ MIN_PASSWORD_LEN = 8
15
+ MAX_PASSWORD_LEN = 128
16
+ # Refresh tokens / one-time tokens are short opaque strings (under 100
17
+ # chars in practice). 4096 keeps a generous head-room for any future
18
+ # encoding while still rejecting "1 MB token" abuse.
19
+ MAX_TOKEN_LEN = 4096
20
+
21
+ EmailField = Annotated[EmailStr, Field(max_length=MAX_EMAIL_LEN)]
22
+ PasswordField = Annotated[
23
+ str, Field(min_length=MIN_PASSWORD_LEN, max_length=MAX_PASSWORD_LEN)
24
+ ]
25
+ TokenField = Annotated[str, Field(min_length=1, max_length=MAX_TOKEN_LEN)]
26
+
27
+
28
+ class UserResponse(BaseModel):
29
+ id: UUID
30
+ email: EmailStr
31
+ created_at: datetime
32
+
33
+
34
+ class SignUpRequest(BaseModel):
35
+ email: EmailField
36
+ password: PasswordField
37
+
38
+
39
+ class TokenRequest(BaseModel):
40
+ email: EmailField
41
+ # Login deliberately does not enforce ``min_length``: an old account
42
+ # may predate the current minimum. The max cap still applies so we
43
+ # don't argon2-hash megabyte payloads.
44
+ password: Annotated[str, Field(max_length=MAX_PASSWORD_LEN)]
45
+
46
+
47
+ class RefreshRequest(BaseModel):
48
+ refresh_token: TokenField
49
+
50
+
51
+ class TokenResponse(BaseModel):
52
+ access_token: str
53
+ token_type: str = "bearer"
54
+ expires_in: int
55
+ refresh_token: str
56
+ user: UserResponse
57
+
58
+
59
+ class RecoverRequest(BaseModel):
60
+ email: EmailField
61
+
62
+
63
+ class RecoverVerifyRequest(BaseModel):
64
+ email: EmailField
65
+ token: TokenField
66
+ password: PasswordField
67
+
68
+
69
+ class MagicLinkRequest(BaseModel):
70
+ email: EmailField
71
+
72
+
73
+ class OtpRequest(BaseModel):
74
+ email: EmailField
75
+
76
+
77
+ class OtpVerifyRequest(BaseModel):
78
+ email: EmailField
79
+ token: TokenField