fastapi-fullauth 0.2.0__tar.gz → 0.4.0__tar.gz
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.
- fastapi_fullauth-0.4.0/CHANGELOG.md +109 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/PKG-INFO +95 -30
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/README.md +92 -7
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/auth.py +2 -2
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/__init__.py +3 -3
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/base.py +23 -1
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/memory.py +33 -1
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlalchemy/adapter.py +85 -1
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlalchemy/models.py +19 -1
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlmodel/adapter.py +90 -2
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlmodel/models.py +19 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/config.py +5 -1
- fastapi_fullauth-0.4.0/fastapi_fullauth/core/crypto.py +47 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/core/tokens.py +20 -9
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/dependencies/__init__.py +6 -8
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/dependencies/current_user.py +24 -15
- fastapi_fullauth-0.4.0/fastapi_fullauth/dependencies/require_role.py +33 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/exceptions.py +17 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/login.py +13 -9
- fastapi_fullauth-0.4.0/fastapi_fullauth/flows/oauth.py +123 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/password_reset.py +3 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/register.py +8 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/fullauth.py +107 -30
- fastapi_fullauth-0.4.0/fastapi_fullauth/hooks.py +50 -0
- fastapi_fullauth-0.4.0/fastapi_fullauth/oauth/__init__.py +9 -0
- fastapi_fullauth-0.4.0/fastapi_fullauth/oauth/base.py +55 -0
- fastapi_fullauth-0.4.0/fastapi_fullauth/oauth/github.py +79 -0
- fastapi_fullauth-0.4.0/fastapi_fullauth/oauth/google.py +65 -0
- fastapi_fullauth-0.4.0/fastapi_fullauth/protection/ratelimit.py +166 -0
- fastapi_fullauth-0.4.0/fastapi_fullauth/router/auth.py +505 -0
- fastapi_fullauth-0.4.0/fastapi_fullauth/router/oauth.py +173 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/types.py +42 -15
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/pyproject.toml +3 -17
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_customization.py +2 -5
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_dx_improvements.py +9 -14
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_email_verify.py +2 -5
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_middleware.py +83 -0
- fastapi_fullauth-0.4.0/tests/test_oauth.py +270 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_tokens.py +7 -5
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/uv.lock +6 -377
- fastapi_fullauth-0.2.0/CHANGELOG.md +0 -47
- fastapi_fullauth-0.2.0/fastapi_fullauth/core/crypto.py +0 -19
- fastapi_fullauth-0.2.0/fastapi_fullauth/dependencies/require_role.py +0 -78
- fastapi_fullauth-0.2.0/fastapi_fullauth/hooks.py +0 -16
- fastapi_fullauth-0.2.0/fastapi_fullauth/protection/ratelimit.py +0 -86
- fastapi_fullauth-0.2.0/fastapi_fullauth/router/auth.py +0 -486
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/.github/workflows/ci.yml +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/.github/workflows/publish.yml +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/.gitignore +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/.python-version +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/LICENSE +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/memory_app/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/memory_app/auth.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/memory_app/main.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/memory_app/routes.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/config.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/main.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/models.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/routes.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlalchemy/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlmodel/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/backends/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/backends/base.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/backends/bearer.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/backends/cookie.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/core/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/core/redis_blacklist.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/email_verify.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/logout.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/middleware/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/middleware/csrf.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/middleware/ratelimit.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/middleware/security_headers.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/migrations/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/migrations/helpers.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/protection/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/protection/lockout.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/rbac/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/router/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/utils.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/validators.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/__init__.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/conftest.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_auth_flows.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_crypto.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_lockout.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_new_endpoints.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_refresh_tokens.py +0 -0
- {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_roles.py +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **OAuth2 social login** — Google and GitHub out of the box, extensible for custom providers
|
|
8
|
+
- `GET /oauth/{provider}/authorize` — get authorization URL
|
|
9
|
+
- `POST /oauth/{provider}/callback` — exchange code for JWT tokens
|
|
10
|
+
- `GET /oauth/providers` — list configured providers
|
|
11
|
+
- `GET /oauth/accounts` — list linked OAuth accounts
|
|
12
|
+
- `DELETE /oauth/accounts/{provider}` — unlink a provider (with lockout prevention)
|
|
13
|
+
- `OAuthProvider` abstract base class for implementing custom providers
|
|
14
|
+
- `OAuthAccount` and `OAuthUserInfo` types
|
|
15
|
+
- `OAuthAccountRecord` / `OAuthAccountModel` for SQLModel and SQLAlchemy adapters
|
|
16
|
+
- OAuth adapter methods on all adapters (memory, SQLModel, SQLAlchemy)
|
|
17
|
+
- `OAUTH_PROVIDERS`, `OAUTH_STATE_EXPIRE_SECONDS`, `OAUTH_AUTO_LINK_BY_EMAIL` config fields
|
|
18
|
+
- `after_oauth_login` hook event
|
|
19
|
+
- `oauth` optional dependency group (`pip install fastapi-fullauth[oauth]`)
|
|
20
|
+
- Auto-link OAuth to existing user by email (configurable)
|
|
21
|
+
- Auto-verify email when provider confirms it
|
|
22
|
+
- Lockout prevention — can't unlink last login method
|
|
23
|
+
- Multiple `redirect_uris` per OAuth provider — supports web, mobile, and production frontends from one config. Client passes `?redirect_uri=` on authorize, validated against allowed list.
|
|
24
|
+
|
|
25
|
+
## 0.3.0
|
|
26
|
+
|
|
27
|
+
### Breaking changes
|
|
28
|
+
|
|
29
|
+
- **`create_refresh_token` returns `RefreshTokenMeta`** — previously returned a plain `str`. Now returns a `NamedTuple` with `.token`, `.expires_at`, `.family_id`. Callers that used the raw string must access `.token`.
|
|
30
|
+
- **`create_token_pair` returns `tuple[str, RefreshTokenMeta]`** — second element is now `RefreshTokenMeta` instead of `str`.
|
|
31
|
+
- **`revoke_all_user_refresh_tokens` is now required** on custom adapters — new abstract method on `AbstractUserAdapter`.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- `current_superuser` dependency and `SuperUser` annotated type
|
|
36
|
+
- `CurrentUser`, `VerifiedUser`, `SuperUser` annotated types in `dependencies.current_user` for cleaner route signatures
|
|
37
|
+
- `RefreshTokenMeta` named tuple — avoids decoding freshly created tokens just to read `expires_at` and `family_id`
|
|
38
|
+
- `FullAuth.get_custom_claims(user)` — moved custom claims logic from router into the class, with validation against reserved JWT keys (`sub`, `exp`, `type`, etc.)
|
|
39
|
+
- `revoke_all_user_refresh_tokens(user_id)` on all adapters — bulk session revocation
|
|
40
|
+
- Session revocation on password reset, password change, and account deletion
|
|
41
|
+
- `configure_hasher()` — wires `PASSWORD_HASH_ALGORITHM` config to the actual hasher; supports `argon2id` and `bcrypt`
|
|
42
|
+
- Automatic password rehash on login when hash algorithm or params have changed
|
|
43
|
+
- Register now checks uniqueness on `login_field` (not just email) when `login_field != "email"`
|
|
44
|
+
- `InMemoryBlacklist` now respects `ttl_seconds` — expired entries are evicted on lookup
|
|
45
|
+
- `RateLimiter` evicts keys with empty timestamp lists to prevent unbounded dict growth
|
|
46
|
+
- `description` parameter on all route decorators for Swagger docs
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- `current_active_verified_user` was missing `payload.type != "access"` check — refresh tokens could pass through
|
|
51
|
+
- Purpose tokens (password reset, email verify) could be used as regular access tokens — `current_user` now rejects tokens with `extra.purpose`
|
|
52
|
+
- Duplicate token decode + user lookup across dependencies, router endpoints, and admin routes — consolidated into reusable `current_user` dependency chain
|
|
53
|
+
- Duplicate `roles` + `extra_claims` fetch in refresh route — pulled above the if/else branch
|
|
54
|
+
- Login flow fetched the user from DB twice (once in router, once in `login()`) — now accepts pre-fetched user
|
|
55
|
+
- Unused `request: Request` parameters in dependencies and routes
|
|
56
|
+
- Removed duplicate docstrings on routes (kept `description=` on decorators)
|
|
57
|
+
- `require_permission` was a full copy of `require_role` — now delegates to it
|
|
58
|
+
|
|
59
|
+
### Internal
|
|
60
|
+
|
|
61
|
+
- Route order follows auth lifecycle: register → login → refresh → logout → user → email/password → admin
|
|
62
|
+
- `require_role` / `require_permission` use `Depends(current_user)` instead of duplicating token logic
|
|
63
|
+
- Removed `_get_custom_claims` module-level function from router
|
|
64
|
+
|
|
65
|
+
## 0.2.0
|
|
66
|
+
|
|
67
|
+
### Breaking changes
|
|
68
|
+
|
|
69
|
+
- **JSON login** — `POST /login` now accepts `{"email": "...", "password": "..."}` instead of form data. Swagger auth uses bearer token input instead of username/password form.
|
|
70
|
+
- **No default User model** — SQLModel and SQLAlchemy adapters no longer ship a concrete `User`/`UserModel` table class. Users must define their own model from `UserBase`. This eliminates relationship conflicts when subclassing.
|
|
71
|
+
- **`user_model` is required** — `SQLModelAdapter(session_maker, user_model=MyUser)` — no default.
|
|
72
|
+
- **Removed `min_length=8`** from `CreateUserSchema` — password length is now fully controlled by `PasswordValidator` and `PASSWORD_MIN_LENGTH` config.
|
|
73
|
+
- **`SQLAlchemyAdapter` renamed `UserModel` to `UserBase`** — import `UserBase` instead.
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
- `POST /auth/change-password` — verifies current password, validates new
|
|
78
|
+
- `PATCH /auth/me` — update profile with protected field filtering
|
|
79
|
+
- `DELETE /auth/me` — self-deletion
|
|
80
|
+
- `expires_in` in login/refresh responses
|
|
81
|
+
- Per-IP auth rate limiting on login, register, password-reset (`AUTH_RATE_LIMIT_*` config)
|
|
82
|
+
- `LOGIN_FIELD` config — login by email, username, phone, or any model field
|
|
83
|
+
- `get_user_by_field()` on all adapters for generic field lookups
|
|
84
|
+
- Structured example apps (`examples/memory_app/`, `examples/sqlmodel_app/`)
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- `InMemoryAdapter.update_user` returning base `UserSchema` instead of custom schema
|
|
89
|
+
- Stale `User.id` / `UserModel` references in adapter queries after model removal
|
|
90
|
+
- Parameter ordering in adapter constructors (required params before optional)
|
|
91
|
+
|
|
92
|
+
## 0.1.0
|
|
93
|
+
|
|
94
|
+
Initial release.
|
|
95
|
+
|
|
96
|
+
- JWT access/refresh tokens with rotation and blacklisting
|
|
97
|
+
- Argon2id password hashing
|
|
98
|
+
- Auth flows: register, login, logout, password reset, email verification
|
|
99
|
+
- Brute-force lockout, per-IP rate limiting, CSRF, security headers
|
|
100
|
+
- Bearer and cookie backends
|
|
101
|
+
- SQLAlchemy, SQLModel, and InMemory adapters
|
|
102
|
+
- Redis blacklist backend
|
|
103
|
+
- Refresh token persistence with family tracking and reuse detection
|
|
104
|
+
- Flat config (`secret_key=...`) or full `FullAuthConfig` object
|
|
105
|
+
- Auto-derive schemas from ORM model fields
|
|
106
|
+
- Auto-wire middleware from config flags
|
|
107
|
+
- Route enum, event hooks, email hooks
|
|
108
|
+
- `current_user`, `current_active_verified_user`, `require_role` dependencies
|
|
109
|
+
- 97 tests
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-fullauth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Production-grade, async-native authentication and authorization library for FastAPI
|
|
5
5
|
Project-URL: Homepage, https://github.com/mdfarhankc/fastapi-fullauth
|
|
6
6
|
Project-URL: Documentation, https://github.com/mdfarhankc/fastapi-fullauth
|
|
@@ -25,29 +25,15 @@ Requires-Dist: fastapi>=0.110
|
|
|
25
25
|
Requires-Dist: pydantic-settings>=2.0
|
|
26
26
|
Requires-Dist: pydantic[email]>=2.0
|
|
27
27
|
Requires-Dist: pyjwt>=2.8
|
|
28
|
-
Requires-Dist: python-multipart>=0.0.22
|
|
29
28
|
Requires-Dist: uuid-utils>=0.14.1
|
|
30
29
|
Provides-Extra: all
|
|
31
30
|
Requires-Dist: alembic>=1.13; extra == 'all'
|
|
32
|
-
Requires-Dist:
|
|
33
|
-
Requires-Dist: httpx-oauth>=0.13; extra == 'all'
|
|
34
|
-
Requires-Dist: itsdangerous>=2.1; extra == 'all'
|
|
35
|
-
Requires-Dist: pyotp>=2.9; extra == 'all'
|
|
36
|
-
Requires-Dist: qrcode>=7.4; extra == 'all'
|
|
31
|
+
Requires-Dist: httpx>=0.25; extra == 'all'
|
|
37
32
|
Requires-Dist: redis>=5.0; extra == 'all'
|
|
38
33
|
Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'all'
|
|
39
34
|
Requires-Dist: sqlmodel>=0.0.16; extra == 'all'
|
|
40
|
-
Requires-Dist: tortoise-orm>=0.21; extra == 'all'
|
|
41
|
-
Requires-Dist: webauthn>=2.0; extra == 'all'
|
|
42
|
-
Provides-Extra: audit
|
|
43
|
-
Provides-Extra: beanie
|
|
44
|
-
Requires-Dist: beanie>=1.25; extra == 'beanie'
|
|
45
|
-
Provides-Extra: magic-link
|
|
46
|
-
Requires-Dist: itsdangerous>=2.1; extra == 'magic-link'
|
|
47
35
|
Provides-Extra: oauth
|
|
48
|
-
Requires-Dist: httpx
|
|
49
|
-
Provides-Extra: passkeys
|
|
50
|
-
Requires-Dist: webauthn>=2.0; extra == 'passkeys'
|
|
36
|
+
Requires-Dist: httpx>=0.25; extra == 'oauth'
|
|
51
37
|
Provides-Extra: redis
|
|
52
38
|
Requires-Dist: redis>=5.0; extra == 'redis'
|
|
53
39
|
Provides-Extra: sqlalchemy
|
|
@@ -56,12 +42,6 @@ Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'sqlalchemy'
|
|
|
56
42
|
Provides-Extra: sqlmodel
|
|
57
43
|
Requires-Dist: alembic>=1.13; extra == 'sqlmodel'
|
|
58
44
|
Requires-Dist: sqlmodel>=0.0.16; extra == 'sqlmodel'
|
|
59
|
-
Provides-Extra: tenants
|
|
60
|
-
Provides-Extra: tortoise
|
|
61
|
-
Requires-Dist: tortoise-orm>=0.21; extra == 'tortoise'
|
|
62
|
-
Provides-Extra: totp
|
|
63
|
-
Requires-Dist: pyotp>=2.9; extra == 'totp'
|
|
64
|
-
Requires-Dist: qrcode>=7.4; extra == 'totp'
|
|
65
45
|
Description-Content-Type: text/markdown
|
|
66
46
|
|
|
67
47
|
# fastapi-fullauth
|
|
@@ -82,6 +62,8 @@ pip install fastapi-fullauth[sqlmodel]
|
|
|
82
62
|
pip install fastapi-fullauth[sqlalchemy]
|
|
83
63
|
# with redis for token blacklisting:
|
|
84
64
|
pip install fastapi-fullauth[sqlmodel,redis]
|
|
65
|
+
# with OAuth2 social login:
|
|
66
|
+
pip install fastapi-fullauth[sqlmodel,oauth]
|
|
85
67
|
```
|
|
86
68
|
|
|
87
69
|
## Quick start
|
|
@@ -131,16 +113,31 @@ No need to create separate schema classes or subclass the adapter. Registration
|
|
|
131
113
|
|
|
132
114
|
## Protected routes
|
|
133
115
|
|
|
116
|
+
Use the `Annotated` types for clean route signatures:
|
|
117
|
+
|
|
134
118
|
```python
|
|
135
|
-
from
|
|
136
|
-
from fastapi_fullauth.dependencies import current_user, require_role
|
|
119
|
+
from fastapi_fullauth.dependencies import CurrentUser, VerifiedUser, SuperUser, require_role
|
|
137
120
|
|
|
138
121
|
@app.get("/profile")
|
|
139
|
-
async def profile(user
|
|
122
|
+
async def profile(user: CurrentUser):
|
|
140
123
|
return user
|
|
141
124
|
|
|
125
|
+
@app.get("/dashboard")
|
|
126
|
+
async def dashboard(user: VerifiedUser):
|
|
127
|
+
# only email-verified users
|
|
128
|
+
return {"email": user.email}
|
|
129
|
+
|
|
142
130
|
@app.delete("/admin/users/{id}")
|
|
143
|
-
async def delete_user(user
|
|
131
|
+
async def delete_user(user: SuperUser):
|
|
132
|
+
# only superusers
|
|
133
|
+
...
|
|
134
|
+
|
|
135
|
+
# or use require_role for custom roles
|
|
136
|
+
from fastapi import Depends
|
|
137
|
+
from fastapi_fullauth.dependencies import require_role
|
|
138
|
+
|
|
139
|
+
@app.get("/editor")
|
|
140
|
+
async def editor_panel(user=Depends(require_role("editor"))):
|
|
144
141
|
...
|
|
145
142
|
```
|
|
146
143
|
|
|
@@ -190,15 +187,83 @@ fullauth.hooks.on("after_register", welcome)
|
|
|
190
187
|
|
|
191
188
|
Events: `after_register`, `after_login`, `after_logout`, `after_password_change`, `after_password_reset`, `after_email_verify`, `send_verification_email`, `send_password_reset_email`
|
|
192
189
|
|
|
193
|
-
##
|
|
190
|
+
## Custom token claims
|
|
191
|
+
|
|
192
|
+
Embed app-specific data into JWTs (available in `payload.extra`):
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
async def add_claims(user):
|
|
196
|
+
return {"tenant_id": "acme", "plan": "pro"}
|
|
197
|
+
|
|
198
|
+
fullauth = FullAuth(
|
|
199
|
+
secret_key="...",
|
|
200
|
+
adapter=adapter,
|
|
201
|
+
on_create_token_claims=add_claims,
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Reserved keys (`sub`, `exp`, `iat`, `jti`, `type`, `roles`, `extra`, `family_id`) are rejected to prevent accidental overwrites.
|
|
206
|
+
|
|
207
|
+
## Password hashing
|
|
208
|
+
|
|
209
|
+
Argon2id by default. Switch to bcrypt via config:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
fullauth = FullAuth(
|
|
213
|
+
secret_key="...",
|
|
214
|
+
adapter=adapter,
|
|
215
|
+
password_hash_algorithm="bcrypt", # requires: pip install bcrypt
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
When switching algorithms, existing users are transparently rehashed on their next login.
|
|
220
|
+
|
|
221
|
+
## OAuth2 social login
|
|
222
|
+
|
|
223
|
+
Add Google and/or GitHub login with a few config lines:
|
|
194
224
|
|
|
195
225
|
```python
|
|
196
|
-
|
|
226
|
+
fullauth = FullAuth(
|
|
227
|
+
secret_key="...",
|
|
228
|
+
adapter=adapter,
|
|
229
|
+
oauth_providers={
|
|
230
|
+
"google": {
|
|
231
|
+
"client_id": "your-google-client-id",
|
|
232
|
+
"client_secret": "your-google-secret",
|
|
233
|
+
"redirect_uris": [
|
|
234
|
+
"http://localhost:3000/auth/callback",
|
|
235
|
+
"https://myapp.com/auth/callback",
|
|
236
|
+
"myapp://auth/callback", # Flutter deep link
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
"github": {
|
|
240
|
+
"client_id": "your-github-client-id",
|
|
241
|
+
"client_secret": "your-github-secret",
|
|
242
|
+
"redirect_uri": "http://localhost:3000/auth/callback", # single URI also works
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This registers these routes automatically:
|
|
249
|
+
|
|
250
|
+
- `GET /auth/oauth/providers` — list configured providers
|
|
251
|
+
- `GET /auth/oauth/{provider}/authorize?redirect_uri=...` — get the authorization URL (optional `redirect_uri` param, validated against allowed list, defaults to first)
|
|
252
|
+
- `POST /auth/oauth/{provider}/callback` — exchange code for JWT tokens
|
|
253
|
+
- `GET /auth/oauth/accounts` — list linked OAuth accounts
|
|
254
|
+
- `DELETE /auth/oauth/accounts/{provider}` — unlink a provider
|
|
197
255
|
|
|
256
|
+
Users can link multiple providers and keep email/password login alongside OAuth. New users are auto-created on first OAuth login, and existing users are auto-linked by email.
|
|
257
|
+
|
|
258
|
+
Requires `httpx`: `pip install fastapi-fullauth[oauth]`
|
|
259
|
+
|
|
260
|
+
## Route control
|
|
261
|
+
|
|
262
|
+
```python
|
|
198
263
|
fullauth = FullAuth(
|
|
199
264
|
secret_key="...",
|
|
200
265
|
adapter=adapter,
|
|
201
|
-
enabled_routes=[
|
|
266
|
+
enabled_routes=["login", "logout", "refresh"],
|
|
202
267
|
)
|
|
203
268
|
```
|
|
204
269
|
|
|
@@ -16,6 +16,8 @@ pip install fastapi-fullauth[sqlmodel]
|
|
|
16
16
|
pip install fastapi-fullauth[sqlalchemy]
|
|
17
17
|
# with redis for token blacklisting:
|
|
18
18
|
pip install fastapi-fullauth[sqlmodel,redis]
|
|
19
|
+
# with OAuth2 social login:
|
|
20
|
+
pip install fastapi-fullauth[sqlmodel,oauth]
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
## Quick start
|
|
@@ -65,16 +67,31 @@ No need to create separate schema classes or subclass the adapter. Registration
|
|
|
65
67
|
|
|
66
68
|
## Protected routes
|
|
67
69
|
|
|
70
|
+
Use the `Annotated` types for clean route signatures:
|
|
71
|
+
|
|
68
72
|
```python
|
|
69
|
-
from
|
|
70
|
-
from fastapi_fullauth.dependencies import current_user, require_role
|
|
73
|
+
from fastapi_fullauth.dependencies import CurrentUser, VerifiedUser, SuperUser, require_role
|
|
71
74
|
|
|
72
75
|
@app.get("/profile")
|
|
73
|
-
async def profile(user
|
|
76
|
+
async def profile(user: CurrentUser):
|
|
74
77
|
return user
|
|
75
78
|
|
|
79
|
+
@app.get("/dashboard")
|
|
80
|
+
async def dashboard(user: VerifiedUser):
|
|
81
|
+
# only email-verified users
|
|
82
|
+
return {"email": user.email}
|
|
83
|
+
|
|
76
84
|
@app.delete("/admin/users/{id}")
|
|
77
|
-
async def delete_user(user
|
|
85
|
+
async def delete_user(user: SuperUser):
|
|
86
|
+
# only superusers
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
# or use require_role for custom roles
|
|
90
|
+
from fastapi import Depends
|
|
91
|
+
from fastapi_fullauth.dependencies import require_role
|
|
92
|
+
|
|
93
|
+
@app.get("/editor")
|
|
94
|
+
async def editor_panel(user=Depends(require_role("editor"))):
|
|
78
95
|
...
|
|
79
96
|
```
|
|
80
97
|
|
|
@@ -124,15 +141,83 @@ fullauth.hooks.on("after_register", welcome)
|
|
|
124
141
|
|
|
125
142
|
Events: `after_register`, `after_login`, `after_logout`, `after_password_change`, `after_password_reset`, `after_email_verify`, `send_verification_email`, `send_password_reset_email`
|
|
126
143
|
|
|
127
|
-
##
|
|
144
|
+
## Custom token claims
|
|
145
|
+
|
|
146
|
+
Embed app-specific data into JWTs (available in `payload.extra`):
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
async def add_claims(user):
|
|
150
|
+
return {"tenant_id": "acme", "plan": "pro"}
|
|
151
|
+
|
|
152
|
+
fullauth = FullAuth(
|
|
153
|
+
secret_key="...",
|
|
154
|
+
adapter=adapter,
|
|
155
|
+
on_create_token_claims=add_claims,
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Reserved keys (`sub`, `exp`, `iat`, `jti`, `type`, `roles`, `extra`, `family_id`) are rejected to prevent accidental overwrites.
|
|
160
|
+
|
|
161
|
+
## Password hashing
|
|
162
|
+
|
|
163
|
+
Argon2id by default. Switch to bcrypt via config:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
fullauth = FullAuth(
|
|
167
|
+
secret_key="...",
|
|
168
|
+
adapter=adapter,
|
|
169
|
+
password_hash_algorithm="bcrypt", # requires: pip install bcrypt
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
When switching algorithms, existing users are transparently rehashed on their next login.
|
|
174
|
+
|
|
175
|
+
## OAuth2 social login
|
|
176
|
+
|
|
177
|
+
Add Google and/or GitHub login with a few config lines:
|
|
128
178
|
|
|
129
179
|
```python
|
|
130
|
-
|
|
180
|
+
fullauth = FullAuth(
|
|
181
|
+
secret_key="...",
|
|
182
|
+
adapter=adapter,
|
|
183
|
+
oauth_providers={
|
|
184
|
+
"google": {
|
|
185
|
+
"client_id": "your-google-client-id",
|
|
186
|
+
"client_secret": "your-google-secret",
|
|
187
|
+
"redirect_uris": [
|
|
188
|
+
"http://localhost:3000/auth/callback",
|
|
189
|
+
"https://myapp.com/auth/callback",
|
|
190
|
+
"myapp://auth/callback", # Flutter deep link
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
"github": {
|
|
194
|
+
"client_id": "your-github-client-id",
|
|
195
|
+
"client_secret": "your-github-secret",
|
|
196
|
+
"redirect_uri": "http://localhost:3000/auth/callback", # single URI also works
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
This registers these routes automatically:
|
|
203
|
+
|
|
204
|
+
- `GET /auth/oauth/providers` — list configured providers
|
|
205
|
+
- `GET /auth/oauth/{provider}/authorize?redirect_uri=...` — get the authorization URL (optional `redirect_uri` param, validated against allowed list, defaults to first)
|
|
206
|
+
- `POST /auth/oauth/{provider}/callback` — exchange code for JWT tokens
|
|
207
|
+
- `GET /auth/oauth/accounts` — list linked OAuth accounts
|
|
208
|
+
- `DELETE /auth/oauth/accounts/{provider}` — unlink a provider
|
|
131
209
|
|
|
210
|
+
Users can link multiple providers and keep email/password login alongside OAuth. New users are auto-created on first OAuth login, and existing users are auto-linked by email.
|
|
211
|
+
|
|
212
|
+
Requires `httpx`: `pip install fastapi-fullauth[oauth]`
|
|
213
|
+
|
|
214
|
+
## Route control
|
|
215
|
+
|
|
216
|
+
```python
|
|
132
217
|
fullauth = FullAuth(
|
|
133
218
|
secret_key="...",
|
|
134
219
|
adapter=adapter,
|
|
135
|
-
enabled_routes=[
|
|
220
|
+
enabled_routes=["login", "logout", "refresh"],
|
|
136
221
|
)
|
|
137
222
|
```
|
|
138
223
|
|
|
@@ -21,8 +21,8 @@ async def add_custom_claims(user: UserSchema) -> dict:
|
|
|
21
21
|
fullauth = FullAuth(
|
|
22
22
|
secret_key="change-me-use-a-32-byte-key-here",
|
|
23
23
|
adapter=SQLModelAdapter(session_maker=session_maker, user_model=User),
|
|
24
|
-
on_send_verification_email=send_verification_email,
|
|
25
|
-
on_send_password_reset_email=send_password_reset_email,
|
|
26
24
|
on_create_token_claims=add_custom_claims,
|
|
27
25
|
include_user_in_login=True,
|
|
28
26
|
)
|
|
27
|
+
fullauth.hooks.on("send_verification_email", send_verification_email)
|
|
28
|
+
fullauth.hooks.on("send_password_reset_email", send_password_reset_email)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.4.0"
|
|
2
2
|
|
|
3
3
|
from fastapi_fullauth.config import FullAuthConfig
|
|
4
4
|
from fastapi_fullauth.fullauth import FullAuth
|
|
5
|
-
from fastapi_fullauth.types import
|
|
5
|
+
from fastapi_fullauth.types import RouteName
|
|
6
6
|
from fastapi_fullauth.utils import create_superuser, generate_secret_key
|
|
7
7
|
from fastapi_fullauth.validators import PasswordValidator
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ __all__ = [
|
|
|
10
10
|
"FullAuth",
|
|
11
11
|
"FullAuthConfig",
|
|
12
12
|
"PasswordValidator",
|
|
13
|
-
"
|
|
13
|
+
"RouteName",
|
|
14
14
|
"create_superuser",
|
|
15
15
|
"generate_secret_key",
|
|
16
16
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
-
from fastapi_fullauth.types import CreateUserSchema, RefreshToken, UserSchema
|
|
4
|
+
from fastapi_fullauth.types import CreateUserSchema, OAuthAccount, RefreshToken, UserSchema
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class AbstractUserAdapter(ABC):
|
|
@@ -55,6 +55,9 @@ class AbstractUserAdapter(ABC):
|
|
|
55
55
|
@abstractmethod
|
|
56
56
|
async def revoke_refresh_token_family(self, family_id: str) -> None: ...
|
|
57
57
|
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def revoke_all_user_refresh_tokens(self, user_id: str) -> None: ...
|
|
60
|
+
|
|
58
61
|
@abstractmethod
|
|
59
62
|
async def set_user_verified(self, user_id: str) -> None: ...
|
|
60
63
|
|
|
@@ -63,3 +66,22 @@ class AbstractUserAdapter(ABC):
|
|
|
63
66
|
|
|
64
67
|
@abstractmethod
|
|
65
68
|
async def remove_role(self, user_id: str, role_name: str) -> None: ...
|
|
69
|
+
|
|
70
|
+
# ── OAuth (optional — override when using OAuth) ─────────────────
|
|
71
|
+
|
|
72
|
+
async def get_oauth_account(self, provider: str, provider_user_id: str) -> OAuthAccount | None:
|
|
73
|
+
raise NotImplementedError("Implement OAuth adapter methods to use OAuth")
|
|
74
|
+
|
|
75
|
+
async def get_user_oauth_accounts(self, user_id: str) -> list[OAuthAccount]:
|
|
76
|
+
raise NotImplementedError("Implement OAuth adapter methods to use OAuth")
|
|
77
|
+
|
|
78
|
+
async def create_oauth_account(self, data: OAuthAccount) -> OAuthAccount:
|
|
79
|
+
raise NotImplementedError("Implement OAuth adapter methods to use OAuth")
|
|
80
|
+
|
|
81
|
+
async def update_oauth_account(
|
|
82
|
+
self, provider: str, provider_user_id: str, data: dict[str, Any]
|
|
83
|
+
) -> OAuthAccount | None:
|
|
84
|
+
raise NotImplementedError("Implement OAuth adapter methods to use OAuth")
|
|
85
|
+
|
|
86
|
+
async def delete_oauth_account(self, provider: str, provider_user_id: str) -> None:
|
|
87
|
+
raise NotImplementedError("Implement OAuth adapter methods to use OAuth")
|
|
@@ -3,7 +3,7 @@ from typing import Any
|
|
|
3
3
|
from uuid_utils import uuid7
|
|
4
4
|
|
|
5
5
|
from fastapi_fullauth.adapters.base import AbstractUserAdapter
|
|
6
|
-
from fastapi_fullauth.types import CreateUserSchema, RefreshToken, UserSchema
|
|
6
|
+
from fastapi_fullauth.types import CreateUserSchema, OAuthAccount, RefreshToken, UserSchema
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class InMemoryAdapter(AbstractUserAdapter):
|
|
@@ -13,6 +13,7 @@ class InMemoryAdapter(AbstractUserAdapter):
|
|
|
13
13
|
self._passwords: dict[str, str] = {}
|
|
14
14
|
self._refresh_tokens: dict[str, RefreshToken] = {}
|
|
15
15
|
self._roles: dict[str, list[str]] = {}
|
|
16
|
+
self._oauth_accounts: dict[tuple[str, str], OAuthAccount] = {}
|
|
16
17
|
|
|
17
18
|
async def get_user_by_id(self, user_id: str) -> UserSchema | None:
|
|
18
19
|
data = self._users.get(user_id)
|
|
@@ -82,6 +83,11 @@ class InMemoryAdapter(AbstractUserAdapter):
|
|
|
82
83
|
if tok.family_id == family_id:
|
|
83
84
|
tok.revoked = True
|
|
84
85
|
|
|
86
|
+
async def revoke_all_user_refresh_tokens(self, user_id: str) -> None:
|
|
87
|
+
for tok in self._refresh_tokens.values():
|
|
88
|
+
if tok.user_id == user_id:
|
|
89
|
+
tok.revoked = True
|
|
90
|
+
|
|
85
91
|
async def set_user_verified(self, user_id: str) -> None:
|
|
86
92
|
if user_id in self._users:
|
|
87
93
|
self._users[user_id]["is_verified"] = True
|
|
@@ -99,3 +105,29 @@ class InMemoryAdapter(AbstractUserAdapter):
|
|
|
99
105
|
roles.remove(role_name)
|
|
100
106
|
if user_id in self._users:
|
|
101
107
|
self._users[user_id]["roles"] = roles
|
|
108
|
+
|
|
109
|
+
# ── OAuth ────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
async def get_oauth_account(self, provider: str, provider_user_id: str) -> OAuthAccount | None:
|
|
112
|
+
return self._oauth_accounts.get((provider, provider_user_id))
|
|
113
|
+
|
|
114
|
+
async def get_user_oauth_accounts(self, user_id: str) -> list[OAuthAccount]:
|
|
115
|
+
return [a for a in self._oauth_accounts.values() if a.user_id == user_id]
|
|
116
|
+
|
|
117
|
+
async def create_oauth_account(self, data: OAuthAccount) -> OAuthAccount:
|
|
118
|
+
self._oauth_accounts[(data.provider, data.provider_user_id)] = data
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
async def update_oauth_account(
|
|
122
|
+
self, provider: str, provider_user_id: str, data: dict[str, Any]
|
|
123
|
+
) -> OAuthAccount | None:
|
|
124
|
+
key = (provider, provider_user_id)
|
|
125
|
+
account = self._oauth_accounts.get(key)
|
|
126
|
+
if account is None:
|
|
127
|
+
return None
|
|
128
|
+
updated = account.model_copy(update=data)
|
|
129
|
+
self._oauth_accounts[key] = updated
|
|
130
|
+
return updated
|
|
131
|
+
|
|
132
|
+
async def delete_oauth_account(self, provider: str, provider_user_id: str) -> None:
|
|
133
|
+
self._oauth_accounts.pop((provider, provider_user_id), None)
|
{fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlalchemy/adapter.py
RENAMED
|
@@ -6,11 +6,12 @@ from sqlalchemy.orm import selectinload
|
|
|
6
6
|
|
|
7
7
|
from fastapi_fullauth.adapters.base import AbstractUserAdapter
|
|
8
8
|
from fastapi_fullauth.adapters.sqlalchemy.models import (
|
|
9
|
+
OAuthAccountModel,
|
|
9
10
|
RefreshTokenModel,
|
|
10
11
|
RoleModel,
|
|
11
12
|
UserBase,
|
|
12
13
|
)
|
|
13
|
-
from fastapi_fullauth.types import CreateUserSchema, RefreshToken, UserSchema
|
|
14
|
+
from fastapi_fullauth.types import CreateUserSchema, OAuthAccount, RefreshToken, UserSchema
|
|
14
15
|
|
|
15
16
|
# Map SQLAlchemy column types to Python types for schema auto-derivation
|
|
16
17
|
_SA_TYPE_MAP: dict[type, type] = {}
|
|
@@ -203,6 +204,15 @@ class SQLAlchemyAdapter(AbstractUserAdapter):
|
|
|
203
204
|
)
|
|
204
205
|
await session.commit()
|
|
205
206
|
|
|
207
|
+
async def revoke_all_user_refresh_tokens(self, user_id: str) -> None:
|
|
208
|
+
async with self._session_maker() as session:
|
|
209
|
+
await session.execute(
|
|
210
|
+
update(RefreshTokenModel)
|
|
211
|
+
.where(RefreshTokenModel.user_id == user_id)
|
|
212
|
+
.values(revoked=True)
|
|
213
|
+
)
|
|
214
|
+
await session.commit()
|
|
215
|
+
|
|
206
216
|
async def set_user_verified(self, user_id: str) -> None:
|
|
207
217
|
async with self._session_maker() as session:
|
|
208
218
|
await session.execute(
|
|
@@ -244,3 +254,77 @@ class SQLAlchemyAdapter(AbstractUserAdapter):
|
|
|
244
254
|
if user:
|
|
245
255
|
user.roles = [r for r in user.roles if r.name != role_name]
|
|
246
256
|
await session.commit()
|
|
257
|
+
|
|
258
|
+
# ── OAuth ────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
def _to_oauth_account(self, row: OAuthAccountModel) -> OAuthAccount:
|
|
261
|
+
return OAuthAccount(
|
|
262
|
+
provider=row.provider,
|
|
263
|
+
provider_user_id=row.provider_user_id,
|
|
264
|
+
user_id=str(row.user_id),
|
|
265
|
+
provider_email=row.provider_email,
|
|
266
|
+
access_token=row.access_token,
|
|
267
|
+
refresh_token=row.refresh_token,
|
|
268
|
+
expires_at=row.expires_at,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
async def get_oauth_account(self, provider: str, provider_user_id: str) -> OAuthAccount | None:
|
|
272
|
+
async with self._session_maker() as session:
|
|
273
|
+
result = await session.execute(
|
|
274
|
+
select(OAuthAccountModel).where(
|
|
275
|
+
OAuthAccountModel.provider == provider,
|
|
276
|
+
OAuthAccountModel.provider_user_id == provider_user_id,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
row = result.scalars().first()
|
|
280
|
+
return self._to_oauth_account(row) if row else None
|
|
281
|
+
|
|
282
|
+
async def get_user_oauth_accounts(self, user_id: str) -> list[OAuthAccount]:
|
|
283
|
+
async with self._session_maker() as session:
|
|
284
|
+
result = await session.execute(
|
|
285
|
+
select(OAuthAccountModel).where(OAuthAccountModel.user_id == user_id)
|
|
286
|
+
)
|
|
287
|
+
return [self._to_oauth_account(row) for row in result.scalars().all()]
|
|
288
|
+
|
|
289
|
+
async def create_oauth_account(self, data: OAuthAccount) -> OAuthAccount:
|
|
290
|
+
async with self._session_maker() as session:
|
|
291
|
+
record = OAuthAccountModel(
|
|
292
|
+
provider=data.provider,
|
|
293
|
+
provider_user_id=data.provider_user_id,
|
|
294
|
+
user_id=data.user_id,
|
|
295
|
+
provider_email=data.provider_email,
|
|
296
|
+
access_token=data.access_token,
|
|
297
|
+
refresh_token=data.refresh_token,
|
|
298
|
+
expires_at=data.expires_at,
|
|
299
|
+
)
|
|
300
|
+
session.add(record)
|
|
301
|
+
await session.commit()
|
|
302
|
+
return data
|
|
303
|
+
|
|
304
|
+
async def update_oauth_account(
|
|
305
|
+
self, provider: str, provider_user_id: str, data: dict[str, Any]
|
|
306
|
+
) -> OAuthAccount | None:
|
|
307
|
+
async with self._session_maker() as session:
|
|
308
|
+
await session.execute(
|
|
309
|
+
update(OAuthAccountModel)
|
|
310
|
+
.where(
|
|
311
|
+
OAuthAccountModel.provider == provider,
|
|
312
|
+
OAuthAccountModel.provider_user_id == provider_user_id,
|
|
313
|
+
)
|
|
314
|
+
.values(**data)
|
|
315
|
+
)
|
|
316
|
+
await session.commit()
|
|
317
|
+
return await self.get_oauth_account(provider, provider_user_id)
|
|
318
|
+
|
|
319
|
+
async def delete_oauth_account(self, provider: str, provider_user_id: str) -> None:
|
|
320
|
+
async with self._session_maker() as session:
|
|
321
|
+
result = await session.execute(
|
|
322
|
+
select(OAuthAccountModel).where(
|
|
323
|
+
OAuthAccountModel.provider == provider,
|
|
324
|
+
OAuthAccountModel.provider_user_id == provider_user_id,
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
row = result.scalars().first()
|
|
328
|
+
if row:
|
|
329
|
+
await session.delete(row)
|
|
330
|
+
await session.commit()
|