fastapi-auth-starter 0.1.3__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 (44) hide show
  1. fastapi_auth_starter/__init__.py +7 -0
  2. fastapi_auth_starter/cli.py +326 -0
  3. fastapi_auth_starter-0.1.3.data/data/README.md +247 -0
  4. fastapi_auth_starter-0.1.3.data/data/alembic/README +1 -0
  5. fastapi_auth_starter-0.1.3.data/data/alembic/env.py +100 -0
  6. fastapi_auth_starter-0.1.3.data/data/alembic/script.py.mako +28 -0
  7. fastapi_auth_starter-0.1.3.data/data/alembic/versions/279c472f4fd8_add_user_table.py +42 -0
  8. fastapi_auth_starter-0.1.3.data/data/alembic/versions/5f062b3648fa_change_user_id_from_uuid_to_string_for_.py +38 -0
  9. fastapi_auth_starter-0.1.3.data/data/alembic/versions/8d275132562b_create_tasks_table.py +44 -0
  10. fastapi_auth_starter-0.1.3.data/data/alembic.ini +150 -0
  11. fastapi_auth_starter-0.1.3.data/data/app/__init__.py +5 -0
  12. fastapi_auth_starter-0.1.3.data/data/app/api/__init__.py +4 -0
  13. fastapi_auth_starter-0.1.3.data/data/app/api/v1/__init__.py +4 -0
  14. fastapi_auth_starter-0.1.3.data/data/app/api/v1/api.py +21 -0
  15. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/__init__.py +4 -0
  16. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/auth.py +513 -0
  17. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/health.py +50 -0
  18. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/task.py +182 -0
  19. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/user.py +144 -0
  20. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/__init__.py +8 -0
  21. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/auth.py +198 -0
  22. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/task.py +61 -0
  23. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/user.py +96 -0
  24. fastapi_auth_starter-0.1.3.data/data/app/core/__init__.py +4 -0
  25. fastapi_auth_starter-0.1.3.data/data/app/core/config.py +107 -0
  26. fastapi_auth_starter-0.1.3.data/data/app/core/database.py +106 -0
  27. fastapi_auth_starter-0.1.3.data/data/app/core/dependencies.py +148 -0
  28. fastapi_auth_starter-0.1.3.data/data/app/core/exceptions.py +7 -0
  29. fastapi_auth_starter-0.1.3.data/data/app/db/__init__.py +4 -0
  30. fastapi_auth_starter-0.1.3.data/data/app/main.py +91 -0
  31. fastapi_auth_starter-0.1.3.data/data/app/models/__init__.py +14 -0
  32. fastapi_auth_starter-0.1.3.data/data/app/models/task.py +56 -0
  33. fastapi_auth_starter-0.1.3.data/data/app/models/user.py +45 -0
  34. fastapi_auth_starter-0.1.3.data/data/app/services/__init__.py +8 -0
  35. fastapi_auth_starter-0.1.3.data/data/app/services/auth.py +405 -0
  36. fastapi_auth_starter-0.1.3.data/data/app/services/task.py +165 -0
  37. fastapi_auth_starter-0.1.3.data/data/app/services/user.py +108 -0
  38. fastapi_auth_starter-0.1.3.data/data/pyproject.toml +77 -0
  39. fastapi_auth_starter-0.1.3.data/data/runtime.txt +2 -0
  40. fastapi_auth_starter-0.1.3.data/data/vercel.json +19 -0
  41. fastapi_auth_starter-0.1.3.dist-info/METADATA +283 -0
  42. fastapi_auth_starter-0.1.3.dist-info/RECORD +44 -0
  43. fastapi_auth_starter-0.1.3.dist-info/WHEEL +4 -0
  44. fastapi_auth_starter-0.1.3.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,513 @@
1
+ from typing import Union
2
+ from fastapi import APIRouter, Depends, HTTPException, Request, status
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from sqlalchemy.exc import IntegrityError
5
+ from workos.exceptions import BadRequestException, EmailVerificationRequiredException, NotFoundException
6
+
7
+ from app.api.v1.schemas.auth import AuthorizationRequest, AuthorizationUrlResponse, EmailVerificationRequiredResponse, ForgotPasswordRequest, ForgotPasswordResponse, LoginRequest, LoginResponse, OAuthCallbackRequest, RefreshTokenRequest, RefreshTokenResponse, ResetPasswordRequest, SignupRequest, SignupResponse, VerifyEmailRequest, VerifyEmailResponse, WorkOSAuthorizationRequest, WorkOSLoginRequest, WorkOSRefreshTokenRequest, WorkOSResetPasswordRequest, WorkOsVerifyEmailRequest
8
+ from app.api.v1.schemas.user import WorkOSUserResponse
9
+ from app.core.config import settings
10
+ from app.core.database import get_db
11
+ from app.core.dependencies import get_auth_service
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ router = APIRouter(
17
+ prefix="/auth",
18
+ tags=["auth"],
19
+ )
20
+
21
+
22
+ @router.post(
23
+ "/signup",
24
+ response_model=SignupResponse,
25
+ summary="Sign up a new user",
26
+ description="Create a new user account. Email verification required before login.",
27
+ status_code=status.HTTP_201_CREATED
28
+ )
29
+ async def signup(
30
+ signup_request: SignupRequest,
31
+ db: AsyncSession = Depends(get_db)
32
+ ) -> SignupResponse:
33
+ """
34
+ Sign up a new user.
35
+
36
+ Creates a new user account in WorkOS and your database.
37
+ User must verify their email before they can login.
38
+
39
+ Args:
40
+ signup_request: User signup data (email, password, name, etc.)
41
+ db: Database session
42
+
43
+ Returns:
44
+ SignupResponse: User information (no tokens - email verification required)
45
+
46
+ Raises:
47
+ HTTPException: 409 if email already exists, 400 for validation errors
48
+ """
49
+ auth_service = get_auth_service()
50
+
51
+ try:
52
+ return await auth_service.signup(
53
+ db=db,
54
+ email=signup_request.email,
55
+ password=signup_request.password,
56
+ first_name=signup_request.first_name,
57
+ last_name=signup_request.last_name
58
+ )
59
+ except BadRequestException as e:
60
+ # Handle WorkOS validation errors
61
+ if hasattr(e, 'errors') and e.errors:
62
+ for error in e.errors:
63
+ error_code = error.get('code', '')
64
+
65
+ if error_code == 'email_not_available':
66
+ raise HTTPException(
67
+ status_code=status.HTTP_409_CONFLICT,
68
+ detail="Email address is already registered. Please use a different email or try logging in."
69
+ ) from e
70
+
71
+ if error_code == 'invalid_email':
72
+ raise HTTPException(
73
+ status_code=status.HTTP_400_BAD_REQUEST,
74
+ detail="Invalid email address format"
75
+ ) from e
76
+
77
+ # Generic WorkOS error
78
+ raise HTTPException(
79
+ status_code=status.HTTP_400_BAD_REQUEST,
80
+ detail=f"Failed to create account: {e.message if hasattr(e, 'message') else str(e)}"
81
+ )
82
+ except IntegrityError as e:
83
+ # Handle database integrity errors (e.g., duplicate email)
84
+ error_str = str(e.orig) if hasattr(e, 'orig') else str(e)
85
+
86
+ # Check if it's a duplicate email constraint violation
87
+ if "ix_users_email" in error_str or "duplicate key" in error_str.lower() or "unique constraint" in error_str.lower():
88
+ logger.warning(f"Duplicate email during signup: {signup_request.email}")
89
+ raise HTTPException(
90
+ status_code=status.HTTP_409_CONFLICT,
91
+ detail="An account with this email address already exists. Please try logging in or resetting your password."
92
+ ) from e
93
+
94
+ # Other integrity errors (shouldn't happen, but handle gracefully)
95
+ logger.error(f"Database integrity error during signup: {e}", exc_info=True)
96
+ raise HTTPException(
97
+ status_code=status.HTTP_400_BAD_REQUEST,
98
+ detail="Failed to create account due to a data conflict"
99
+ ) from e
100
+ except Exception as e:
101
+ logger.error(f"Unexpected error during signup: {type(e).__name__}: {e}", exc_info=True)
102
+ raise HTTPException(
103
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
104
+ detail="An unexpected error occurred while creating your account"
105
+ ) from e
106
+
107
+
108
+
109
+ @router.post(
110
+ "/signin",
111
+ response_model=Union[LoginResponse, EmailVerificationRequiredResponse],
112
+ summary="Sign in a user with email and password",
113
+ status_code=status.HTTP_200_OK
114
+ )
115
+ async def login(login_request: LoginRequest, request: Request) -> Union[LoginResponse, EmailVerificationRequiredResponse]:
116
+ """
117
+ Sign in a user with email and password.
118
+
119
+ If the user is not verified, returns an `EmailVerificationRequiredResponse`
120
+ containing a `pending_authentication_token` and `email_verification_id`.
121
+ These are used with the code sent to the user’s email to complete verification
122
+ through the `verify-email` endpoint.
123
+
124
+ Args:
125
+ login_request (LoginRequest): User credentials.
126
+ request (Request): Current HTTP request context.
127
+
128
+ Returns:
129
+ Union[LoginResponse, EmailVerificationRequiredResponse]
130
+ """
131
+
132
+
133
+
134
+ auth_service = get_auth_service()
135
+
136
+ try:
137
+ workos_login_request = WorkOSLoginRequest(
138
+ email=login_request.email,
139
+ password=login_request.password,
140
+ ip_address=request.client.host if request.client else "",
141
+ user_agent=request.headers.get("user-agent") or ""
142
+ )
143
+
144
+ return await auth_service.login(login_request=workos_login_request)
145
+
146
+ except EmailVerificationRequiredException as e:
147
+ response_data = e.response_json
148
+
149
+ return EmailVerificationRequiredResponse(
150
+ message="Email verification required",
151
+ pending_authentication_token=response_data.get('pending_authentication_token'),
152
+ email_verification_id=response_data.get('email_verification_id'),
153
+ email=response_data.get('email', login_request.email),
154
+ requires_verification=True
155
+ )
156
+
157
+ except BadRequestException as e:
158
+ # Handle WorkOS validation errors
159
+ error_code = getattr(e, 'code', None)
160
+
161
+ # Invalid credentials
162
+ if error_code == 'invalid_credentials':
163
+ logger.warning(f"Invalid login attempt for email: {login_request.email}")
164
+ raise HTTPException(
165
+ status_code=status.HTTP_401_UNAUTHORIZED,
166
+ detail="Invalid email or password. Please check your credentials and try again."
167
+ ) from e
168
+
169
+ # Check errors array if present
170
+ if hasattr(e, 'errors') and e.errors:
171
+ for error in e.errors:
172
+ error_code = error.get('code', '')
173
+
174
+ if error_code == 'invalid_email':
175
+ raise HTTPException(
176
+ status_code=status.HTTP_400_BAD_REQUEST,
177
+ detail="Invalid email address format"
178
+ )
179
+
180
+ # Generic BadRequest
181
+ logger.warning(f"BadRequest during login: {e}")
182
+ raise HTTPException(
183
+ status_code=status.HTTP_400_BAD_REQUEST,
184
+ detail=f"Invalid request: {e.message if hasattr(e, 'message') else str(e)}"
185
+ ) from e
186
+
187
+ except NotFoundException:
188
+ # User not found
189
+ raise HTTPException(
190
+ status_code=status.HTTP_404_NOT_FOUND,
191
+ detail="No account found with this email address. Please sign up first."
192
+ ) from None
193
+
194
+ except Exception as e:
195
+ # Log but don't expose internal errors
196
+ logger.error(f"Unexpected error during login: {e}", exc_info=True)
197
+ raise HTTPException(
198
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
199
+ detail="An unexpected error occurred. Please try again later."
200
+ ) from e
201
+
202
+
203
+ @router.post(
204
+ "/verify-email",
205
+ response_model=VerifyEmailResponse,
206
+ summary="Verify an email address",
207
+ status_code=status.HTTP_200_OK
208
+ )
209
+ async def verify_email(verify_email_request: VerifyEmailRequest, request: Request):
210
+ """This endpoint is used to verify an email address.
211
+
212
+ Args:
213
+ verify_email_request: WorkOsVerifyEmailRequest
214
+ request: Request
215
+
216
+ Returns:
217
+ VerifyEmailResponse
218
+ """
219
+ auth_service = get_auth_service()
220
+ try:
221
+ workos_verify_email_request = WorkOsVerifyEmailRequest(
222
+ pending_authentication_token=verify_email_request.pending_authentication_token,
223
+ code=verify_email_request.code,
224
+ ip_address=request.client.host if request.client else "",
225
+ user_agent=request.headers.get("user-agent") or ""
226
+ )
227
+ return await auth_service.verify_email(verify_email_request=workos_verify_email_request)
228
+ except BadRequestException as e:
229
+ error_code = getattr(e, "code", None)
230
+ error_description = getattr(e, "error_description", "") or ""
231
+ if error_code in {"invalid_code", "invalid_token"} or "invalid" in error_description:
232
+ raise HTTPException(
233
+ status_code=status.HTTP_400_BAD_REQUEST,
234
+ detail="Invalid or expired verification code."
235
+ ) from e
236
+ logger.error(f"Failed to verify email: {e}", exc_info=True)
237
+ raise HTTPException(
238
+ status_code=status.HTTP_400_BAD_REQUEST,
239
+ detail="Unable to verify email with the provided details."
240
+ ) from e
241
+ except Exception as e:
242
+ logger.error(f"Unexpected error during email verification: {e}", exc_info=True)
243
+ raise HTTPException(
244
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
245
+ detail="An unexpected error occurred while verifying email."
246
+ ) from e
247
+
248
+ @router.post(
249
+ "/forgot-password",
250
+ response_model=ForgotPasswordResponse,
251
+ summary="Forgot password",
252
+ status_code=status.HTTP_200_OK
253
+ )
254
+ async def forgot_password(forgot_password_request: ForgotPasswordRequest) -> ForgotPasswordResponse:
255
+ """
256
+ Request a password reset for a user account.
257
+
258
+ Sends a password reset email to the provided email address if an account exists.
259
+ The email contains a link to reset the password. The reset URL is configured
260
+ in the WorkOS Dashboard under Developer → Redirects → Password reset URL.
261
+
262
+ For security, the response message does not reveal whether the email address
263
+ is registered in the system.
264
+
265
+ Args:
266
+ forgot_password_request (ForgotPasswordRequest): Request containing the user's email address.
267
+
268
+ Returns:
269
+ ForgotPasswordResponse: Success message indicating a reset link was sent (if account exists).
270
+
271
+ Raises:
272
+ HTTPException: 400 if email format is invalid, 500 for other errors.
273
+ """
274
+ auth_service = get_auth_service()
275
+ try:
276
+ return await auth_service.forgot_password(forgot_password_request=forgot_password_request)
277
+ except BadRequestException as e:
278
+ # Handle validation errors (invalid email format)
279
+ error_code = getattr(e, 'code', None)
280
+ errors = getattr(e, 'errors', [])
281
+
282
+ # Check for email validation errors
283
+ email_error_codes = ['email_required', 'invalid_email']
284
+ has_email_error = (
285
+ error_code in email_error_codes or
286
+ any(err.get('code') in email_error_codes for err in errors if isinstance(err, dict))
287
+ )
288
+
289
+ if has_email_error:
290
+ logger.warning(f"Invalid email format in forgot password request: {forgot_password_request.email}")
291
+ raise HTTPException(
292
+ status_code=status.HTTP_400_BAD_REQUEST,
293
+ detail="Invalid email address format"
294
+ ) from e
295
+
296
+ # Other BadRequestException errors
297
+ logger.error(f"Error during forgot password: {e}", exc_info=True)
298
+ raise HTTPException(
299
+ status_code=status.HTTP_400_BAD_REQUEST,
300
+ detail="Invalid request"
301
+ ) from e
302
+ except NotFoundException:
303
+ # User not found - return generic success message to prevent email enumeration
304
+ # This is a security best practice: don't reveal if an email exists
305
+ logger.debug(f"Password reset requested for non-existent email: {forgot_password_request.email}")
306
+ return ForgotPasswordResponse(
307
+ message="If an account exists with this email address, a password reset link has been sent."
308
+ )
309
+ except Exception as e:
310
+ # Unexpected errors
311
+ logger.error(f"Unexpected error during forgot password: {type(e).__name__}: {e}", exc_info=True)
312
+ raise HTTPException(
313
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
314
+ detail="Failed to send password reset email"
315
+ ) from e
316
+
317
+ @router.post(
318
+ "/reset-password",
319
+ response_model=WorkOSUserResponse,
320
+ summary="Reset password",
321
+ status_code=status.HTTP_200_OK
322
+ )
323
+ async def reset_password(reset_password_request: ResetPasswordRequest) -> WorkOSUserResponse:
324
+ """
325
+ Reset a user's password.
326
+
327
+ Args:
328
+ reset_password_request (ResetPasswordRequest): Reset password request.
329
+
330
+ Returns:
331
+ WorkOSUserResponse: User information
332
+ """
333
+
334
+ auth_service = get_auth_service()
335
+ try:
336
+ workos_reset_password_request = WorkOSResetPasswordRequest(
337
+ token=reset_password_request.token,
338
+ new_password=reset_password_request.new_password
339
+ )
340
+ return await auth_service.reset_password(reset_password_request=workos_reset_password_request)
341
+ except BadRequestException as e:
342
+ error_code = getattr(e, 'code', None)
343
+ error_description = getattr(e, 'error_description', '')
344
+ if 'invalid_token' in error_description or error_code == 'invalid_token':
345
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired reset password token") from e
346
+ logger.error(f"Error during reset password: {e}", exc_info=True)
347
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to reset password") from e
348
+ except Exception as e:
349
+ logger.error(f"Error during reset password: {e}", exc_info=True)
350
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to reset password") from e
351
+
352
+
353
+ @router.post(
354
+ "/authorize",
355
+ summary="Generate OAuth2 authorization URL",
356
+ response_model=AuthorizationUrlResponse,
357
+ status_code=status.HTTP_200_OK
358
+ )
359
+ async def authorize(authorization_request: AuthorizationRequest) -> AuthorizationUrlResponse:
360
+ """
361
+ Generate an OAuth2 authorization URL.
362
+ The supported provider values are `GoogleOAuth`, `MicrosoftOAuth`, `GitHubOAuth`, and `AppleOAuth`.
363
+
364
+ Frontend can choose:
365
+ - `provider="authkit"`: For unified interface with multiple auth methods
366
+ - `connection_id="conn_xxx"`: For direct provider connection (better UX for specific buttons)
367
+
368
+ Args:
369
+ authorization_request (AuthorizationRequest): Authorization request.
370
+
371
+ Returns:
372
+ AuthorizationUrlResponse: Authorization URL.
373
+ """
374
+ # Validate redirect_uri against whitelist (security requirement)
375
+ if authorization_request.redirect_uri not in settings.allowed_redirect_uris_list:
376
+ raise HTTPException(
377
+ status_code=status.HTTP_400_BAD_REQUEST,
378
+ detail=f"Invalid redirect_uri. Must be one of: {settings.allowed_redirect_uris_list}"
379
+ )
380
+
381
+ # For SSO: Use default connection_id if not provided
382
+ if authorization_request.connection_id and not authorization_request.provider:
383
+ # SSO pattern - connection_id provided
384
+ workos_request = WorkOSAuthorizationRequest(
385
+ connection_id=authorization_request.connection_id,
386
+ redirect_uri=authorization_request.redirect_uri,
387
+ state=authorization_request.state
388
+ )
389
+ elif authorization_request.provider:
390
+ # AuthKit pattern
391
+ workos_request = WorkOSAuthorizationRequest(
392
+ provider=authorization_request.provider,
393
+ redirect_uri=authorization_request.redirect_uri,
394
+ state=authorization_request.state
395
+ )
396
+ else:
397
+ # Fallback: Try default connection_id if available
398
+ if settings.WORKOS_DEFAULT_CONNECTION_ID:
399
+ workos_request = WorkOSAuthorizationRequest(
400
+ connection_id=settings.WORKOS_DEFAULT_CONNECTION_ID,
401
+ redirect_uri=authorization_request.redirect_uri,
402
+ state=authorization_request.state
403
+ )
404
+ else:
405
+ raise HTTPException(
406
+ status_code=status.HTTP_400_BAD_REQUEST,
407
+ detail="Either 'provider' or 'connection_id' must be provided"
408
+ )
409
+
410
+ auth_service = get_auth_service()
411
+ try:
412
+ authorization_url = await auth_service.generate_oauth2_authorization_url(workos_request)
413
+ return {"authorization_url": authorization_url}
414
+ except Exception as e:
415
+ logger.error(f"Error generating authorization URL: {e}", exc_info=True)
416
+ raise HTTPException(
417
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
418
+ detail="Failed to generate authorization URL"
419
+ ) from e
420
+
421
+ @router.post(
422
+ "/callback",
423
+ response_model=LoginResponse,
424
+ summary="Exchange OAuth2 code for access token and refresh token",
425
+ status_code=status.HTTP_200_OK
426
+ )
427
+ async def callback(callback_request: OAuthCallbackRequest) -> LoginResponse:
428
+ """
429
+ Exchange an OAuth2 code for access and refresh token.
430
+
431
+ Args:
432
+ callback_request (OAuthCallbackRequest): Callback request.
433
+
434
+ Returns:
435
+ LoginResponse: Access token and refresh token
436
+ """
437
+ auth_service = get_auth_service()
438
+ try:
439
+ return await auth_service.oauth2_callback(code=callback_request.code)
440
+ except BadRequestException as e:
441
+ error_code = getattr(e, 'code', None)
442
+ error_description = getattr(e, 'error_description', '')
443
+ if 'invalid_grant' in error_description or error_code == 'invalid_grant':
444
+ raise HTTPException(
445
+ status_code=status.HTTP_400_BAD_REQUEST,
446
+ detail="Invalid or expired authorization code. Please request a new authorization code."
447
+ ) from e
448
+ if error_code == 'invalid_credentials':
449
+ raise HTTPException(
450
+ status_code=status.HTTP_401_UNAUTHORIZED,
451
+ detail="Invalid credentials"
452
+ ) from e
453
+ if error_code == 'invalid_code':
454
+ raise HTTPException(
455
+ status_code=status.HTTP_400_BAD_REQUEST,
456
+ detail="Invalid code"
457
+ ) from e
458
+ logger.error(f"Error exchanging OAuth2 code: {e}", exc_info=True)
459
+ raise HTTPException(
460
+ status_code=status.HTTP_400_BAD_REQUEST,
461
+ detail="Failed to exchange OAuth2 code"
462
+ ) from e
463
+ except Exception as e:
464
+ logger.error(f"Error exchanging OAuth2 code: {e}", exc_info=True)
465
+ raise HTTPException(
466
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
467
+ detail="Failed to exchange OAuth2 code"
468
+ ) from e
469
+
470
+ @router.post(
471
+ "/refresh-token",
472
+ response_model=RefreshTokenResponse,
473
+ summary="Refresh a token",
474
+ status_code=status.HTTP_200_OK
475
+ )
476
+ async def refresh_token(refresh_token_request: RefreshTokenRequest, request: Request) -> RefreshTokenResponse:
477
+ """
478
+ Refresh a token.
479
+
480
+ Args:
481
+ refresh_token_request (RefreshTokenRequest): Refresh token request.
482
+
483
+ Returns:
484
+ RefreshTokenResponse: Access token and refresh token
485
+ """
486
+ auth_service = get_auth_service()
487
+ try:
488
+ workos_refresh_token_request = WorkOSRefreshTokenRequest(
489
+ refresh_token=refresh_token_request.refresh_token,
490
+ ip_address=request.client.host if request.client else "",
491
+ user_agent=request.headers.get("user-agent") or ""
492
+ )
493
+ return await auth_service.refresh_token(refresh_token_request=workos_refresh_token_request)
494
+ except BadRequestException as e:
495
+ error_code = getattr(e, 'code', None)
496
+ error_description = getattr(e, 'error_description', '')
497
+ if 'invalid_grant' in error_description or error_code == 'invalid_grant':
498
+ raise HTTPException(
499
+ status_code=status.HTTP_400_BAD_REQUEST,
500
+ detail="Invalid or expired refresh token. Please request a new refresh token."
501
+ ) from e
502
+ # Handle other BadRequestException cases
503
+ logger.error(f"Error refreshing token: {e}", exc_info=True)
504
+ raise HTTPException(
505
+ status_code=status.HTTP_400_BAD_REQUEST,
506
+ detail="Failed to refresh token"
507
+ ) from e
508
+ except Exception as e:
509
+ logger.error(f"Error refreshing token: {e}", exc_info=True)
510
+ raise HTTPException(
511
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
512
+ detail="Failed to refresh token"
513
+ ) from e
@@ -0,0 +1,50 @@
1
+ """
2
+ Health check endpoint
3
+ Provides basic health status for the application
4
+ Reference: https://fastapi.tiangolo.com/tutorial/bigger-applications/
5
+ """
6
+ from fastapi import APIRouter
7
+ from pydantic import BaseModel
8
+
9
+
10
+ # Create router for health-related endpoints
11
+ # Reference: https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter
12
+ router = APIRouter(
13
+ prefix="/health",
14
+ tags=["health"], # Groups endpoints in API documentation
15
+ )
16
+
17
+
18
+ class HealthResponse(BaseModel):
19
+ """
20
+ Response model for health check endpoint
21
+ Reference: https://fastapi.tiangolo.com/tutorial/response-model/
22
+ """
23
+ status: str
24
+ message: str
25
+
26
+
27
+ @router.get(
28
+ "",
29
+ response_model=HealthResponse,
30
+ summary="Health check",
31
+ description="Returns the health status of the application",
32
+ )
33
+ async def health_check() -> HealthResponse:
34
+ """
35
+ Health check endpoint
36
+
37
+ Returns a simple status message indicating the service is running.
38
+ This is useful for:
39
+ - Load balancer health checks
40
+ - Monitoring systems
41
+ - Container orchestration (Kubernetes liveness/readiness probes)
42
+
43
+ Returns:
44
+ HealthResponse: Status and message indicating service health
45
+ """
46
+ return HealthResponse(
47
+ status="healthy",
48
+ message="Service is running"
49
+ )
50
+