ironauth 0.1.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.
- ironauth-0.1.0/.gitignore +52 -0
- ironauth-0.1.0/PKG-INFO +25 -0
- ironauth-0.1.0/README.md +0 -0
- ironauth-0.1.0/docs/api-references.md +54 -0
- ironauth-0.1.0/docs/configuration.md +65 -0
- ironauth-0.1.0/docs/getting-started.md +71 -0
- ironauth-0.1.0/docs/guides/oauth.md +46 -0
- ironauth-0.1.0/docs/guides/two-factor.md +56 -0
- ironauth-0.1.0/docs/index.md +41 -0
- ironauth-0.1.0/ironauth/__init__.py +0 -0
- ironauth-0.1.0/ironauth/adapters/__init__.py +0 -0
- ironauth-0.1.0/ironauth/adapters/database/__init__.py +0 -0
- ironauth-0.1.0/ironauth/adapters/database/sqlalchemy.py +91 -0
- ironauth-0.1.0/ironauth/adapters/frameworks/__init__.py +0 -0
- ironauth-0.1.0/ironauth/adapters/frameworks/fastapi.py +184 -0
- ironauth-0.1.0/ironauth/core/__init__.py +0 -0
- ironauth-0.1.0/ironauth/core/auth.py +71 -0
- ironauth-0.1.0/ironauth/core/config.py +35 -0
- ironauth-0.1.0/ironauth/core/password.py +41 -0
- ironauth-0.1.0/ironauth/core/session.py +98 -0
- ironauth-0.1.0/ironauth/models/__init__.py +0 -0
- ironauth-0.1.0/ironauth/models/user.py +47 -0
- ironauth-0.1.0/ironauth/plugins/__init__.py +0 -0
- ironauth-0.1.0/ironauth/plugins/oauth.py +142 -0
- ironauth-0.1.0/ironauth/plugins/two_factor.py +93 -0
- ironauth-0.1.0/main.py +6 -0
- ironauth-0.1.0/mkdocs.yml +64 -0
- ironauth-0.1.0/pyproject.toml +53 -0
- ironauth-0.1.0/tests/conftest.py +51 -0
- ironauth-0.1.0/tests/test_database.py +64 -0
- ironauth-0.1.0/tests/test_password.py +53 -0
- ironauth-0.1.0/tests/test_routes.py +98 -0
- ironauth-0.1.0/tests/test_session.py +41 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
*.so
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
eggs/
|
|
12
|
+
parts/
|
|
13
|
+
var/
|
|
14
|
+
sdist/
|
|
15
|
+
develop-eggs/
|
|
16
|
+
.installed.cfg
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
|
|
20
|
+
# uv
|
|
21
|
+
.venv/
|
|
22
|
+
.python-version
|
|
23
|
+
uv.lock
|
|
24
|
+
|
|
25
|
+
# Tests
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
.coverage
|
|
28
|
+
htmlcov/
|
|
29
|
+
.tox/
|
|
30
|
+
|
|
31
|
+
# MkDocs
|
|
32
|
+
site/
|
|
33
|
+
|
|
34
|
+
# Env
|
|
35
|
+
.env
|
|
36
|
+
.env.local
|
|
37
|
+
.env.*.local
|
|
38
|
+
|
|
39
|
+
# IDE
|
|
40
|
+
.vscode/
|
|
41
|
+
.idea/
|
|
42
|
+
*.swp
|
|
43
|
+
*.swo
|
|
44
|
+
|
|
45
|
+
# OS
|
|
46
|
+
.DS_Store
|
|
47
|
+
Thumbs.db
|
|
48
|
+
|
|
49
|
+
# Secrets
|
|
50
|
+
*.pem
|
|
51
|
+
*.key
|
|
52
|
+
.pypirc
|
ironauth-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ironauth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Framework-agnostic authentication library for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/sileyekounou-1/ironauth
|
|
6
|
+
Project-URL: Repository, https://github.com/sileyekounou-1/ironauth
|
|
7
|
+
Author-email: sileyekounou-1 <kounousileye@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: auth,authentication,fastapi,jwt,oauth
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: FastAPI
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: fastapi>=0.136.1
|
|
19
|
+
Requires-Dist: httpx>=0.28.1
|
|
20
|
+
Requires-Dist: passlib[argon2]>=1.7.4
|
|
21
|
+
Requires-Dist: pydantic[email]>=2.13.4
|
|
22
|
+
Requires-Dist: pyjwt>=2.12.1
|
|
23
|
+
Requires-Dist: pyotp>=2.9.0
|
|
24
|
+
Requires-Dist: qrcode[pil]>=8.2
|
|
25
|
+
Requires-Dist: sqlalchemy>=2.0.49
|
ironauth-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## ironauth
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
ironauth(
|
|
7
|
+
database: SQLAlchemyAdapter,
|
|
8
|
+
config: dict,
|
|
9
|
+
plugins: list = [],
|
|
10
|
+
adapter: dict | None = None,
|
|
11
|
+
)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
| Parameter | Type | Description |
|
|
15
|
+
| ---------- | ------------------- | ---------------------------------------------------------- |
|
|
16
|
+
| `database` | `SQLAlchemyAdapter` | Database adapter |
|
|
17
|
+
| `config` | `dict` | Configuration dict (see [Configuration](configuration.md)) |
|
|
18
|
+
| `plugins` | `list` | List of plugins |
|
|
19
|
+
| `adapter` | `dict` | Framework adapter |
|
|
20
|
+
|
|
21
|
+
### Methods
|
|
22
|
+
|
|
23
|
+
| Method | Description |
|
|
24
|
+
| ---------------------------------- | -------------------------- |
|
|
25
|
+
| `await auth.init()` | Initialize database tables |
|
|
26
|
+
| `auth.router` | FastAPI router to mount |
|
|
27
|
+
| `auth.current_user(required=True)` | FastAPI dependency |
|
|
28
|
+
|
|
29
|
+
## Routes
|
|
30
|
+
|
|
31
|
+
### Email / Password
|
|
32
|
+
|
|
33
|
+
| Method | Route | Body | Description |
|
|
34
|
+
| ------ | ---------------- | ------------------- | --------------- |
|
|
35
|
+
| `POST` | `/auth/register` | `{email, password}` | Register |
|
|
36
|
+
| `POST` | `/auth/login` | `{email, password}` | Login |
|
|
37
|
+
| `POST` | `/auth/logout` | — | Logout |
|
|
38
|
+
| `POST` | `/auth/refresh` | — | Refresh session |
|
|
39
|
+
|
|
40
|
+
### OAuth
|
|
41
|
+
|
|
42
|
+
| Method | Route | Description |
|
|
43
|
+
| ------ | --------------------------------- | ---------------- |
|
|
44
|
+
| `GET` | `/auth/oauth/{provider}` | Start OAuth flow |
|
|
45
|
+
| `GET` | `/auth/oauth/{provider}/callback` | OAuth callback |
|
|
46
|
+
|
|
47
|
+
### 2FA
|
|
48
|
+
|
|
49
|
+
| Method | Route | Body | Description |
|
|
50
|
+
| ------ | -------------------- | ----------------- | ---------------- |
|
|
51
|
+
| `POST` | `/auth/2fa/enable` | — | Generate QR code |
|
|
52
|
+
| `POST` | `/auth/2fa/confirm` | `{code}` | Activate 2FA |
|
|
53
|
+
| `POST` | `/auth/2fa/validate` | `{user_id, code}` | Validate code |
|
|
54
|
+
| `POST` | `/auth/2fa/disable` | `{code}` | Disable 2FA |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
## Full reference
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
auth = ironauth(
|
|
7
|
+
database=sqlalchemy_adapter("postgresql+asyncpg://..."),
|
|
8
|
+
adapter=fastapi_adapter(),
|
|
9
|
+
plugins=[...],
|
|
10
|
+
config={
|
|
11
|
+
# Required
|
|
12
|
+
"secret_key": "your-secret-key",
|
|
13
|
+
|
|
14
|
+
# Token settings (optional)
|
|
15
|
+
"token": {
|
|
16
|
+
"access_token_expiry": 900, # 15 minutes
|
|
17
|
+
"refresh_token_expiry": 604800, # 7 days
|
|
18
|
+
"algorithm": "HS256",
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
# Cookie settings (optional)
|
|
22
|
+
"cookie": {
|
|
23
|
+
"http_only": True,
|
|
24
|
+
"secure": True,
|
|
25
|
+
"same_site": "lax",
|
|
26
|
+
"access_token_name": "af_access_token",
|
|
27
|
+
"refresh_token_name": "af_refresh_token",
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
# OAuth (required if using oauth plugin)
|
|
31
|
+
"oauth": {
|
|
32
|
+
"google": {
|
|
33
|
+
"client_id": "...",
|
|
34
|
+
"client_secret": "...",
|
|
35
|
+
"redirect_uri": "http://localhost:8000/auth/oauth/google/callback",
|
|
36
|
+
},
|
|
37
|
+
"github": {
|
|
38
|
+
"client_id": "...",
|
|
39
|
+
"client_secret": "...",
|
|
40
|
+
"redirect_uri": "http://localhost:8000/auth/oauth/github/callback",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Security defaults
|
|
48
|
+
|
|
49
|
+
ironauth ships with secure defaults out of the box.
|
|
50
|
+
|
|
51
|
+
| Setting | Default | Why |
|
|
52
|
+
| ---------------------- | ------- | -------------------------------------- |
|
|
53
|
+
| Password hashing | Argon2 | Winner of Password Hashing Competition |
|
|
54
|
+
| Cookie `HttpOnly` | `True` | Prevents XSS token theft |
|
|
55
|
+
| Cookie `Secure` | `True` | HTTPS only |
|
|
56
|
+
| Cookie `SameSite` | `lax` | CSRF protection |
|
|
57
|
+
| Access token expiry | 15 min | Limits exposure window |
|
|
58
|
+
| Refresh token rotation | Always | Detects token theft |
|
|
59
|
+
|
|
60
|
+
!!! warning "Secret key"
|
|
61
|
+
Always use a long random secret key in production.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -c "import secrets; print(secrets.token_urlsafe(64))"
|
|
65
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install ironauth
|
|
7
|
+
|
|
8
|
+
# With uv
|
|
9
|
+
uv add ironauth
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Minimal setup
|
|
13
|
+
|
|
14
|
+
### 1. Create your ironauth instance
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
# auth.py
|
|
18
|
+
from ironauth import ironauth
|
|
19
|
+
from ironauth.adapters.database.sqlalchemy import sqlalchemy_adapter
|
|
20
|
+
from ironauth.adapters.frameworks.fastapi import fastapi_adapter
|
|
21
|
+
|
|
22
|
+
auth = ironauth(
|
|
23
|
+
database=sqlalchemy_adapter("sqlite+aiosqlite:///./db.sqlite3"),
|
|
24
|
+
adapter=fastapi_adapter(),
|
|
25
|
+
config={"secret_key": "change-me-in-production"},
|
|
26
|
+
)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Mount on your FastAPI app
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# main.py
|
|
33
|
+
from fastapi import FastAPI, Depends
|
|
34
|
+
from auth import auth
|
|
35
|
+
|
|
36
|
+
app = FastAPI()
|
|
37
|
+
|
|
38
|
+
@app.on_event("startup")
|
|
39
|
+
async def startup():
|
|
40
|
+
await auth.init() # Creates tables automatically
|
|
41
|
+
|
|
42
|
+
app.include_router(auth.router)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Protect a route
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
@app.get("/profile")
|
|
49
|
+
async def profile(user=Depends(auth.current_user())):
|
|
50
|
+
return {"email": user.email}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
That's it. You now have the following routes available:
|
|
54
|
+
|
|
55
|
+
| Method | Route | Description |
|
|
56
|
+
| ------ | ---------------- | ------------------------------ |
|
|
57
|
+
| `POST` | `/auth/register` | Register with email + password |
|
|
58
|
+
| `POST` | `/auth/login` | Login |
|
|
59
|
+
| `POST` | `/auth/logout` | Logout |
|
|
60
|
+
| `POST` | `/auth/refresh` | Refresh session |
|
|
61
|
+
|
|
62
|
+
## Optional routes
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# Unprotected route (user may or may not be logged in)
|
|
66
|
+
@app.get("/public")
|
|
67
|
+
async def public(user=Depends(auth.current_user(required=False))):
|
|
68
|
+
if user:
|
|
69
|
+
return {"message": f"Hello {user.email}"}
|
|
70
|
+
return {"message": "Hello stranger"}
|
|
71
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# OAuth
|
|
2
|
+
|
|
3
|
+
ironauth supports OAuth2 with Google and GitHub out of the box.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### 1. Get your credentials
|
|
8
|
+
|
|
9
|
+
=== "Google" 1. Go to [Google Cloud Console](https://console.cloud.google.com) 2. Create a project → APIs & Services → Credentials 3. Create OAuth 2.0 Client ID 4. Add `http://localhost:8000/auth/oauth/google/callback` to authorized redirect URIs
|
|
10
|
+
|
|
11
|
+
=== "GitHub" 1. Go to GitHub → Settings → Developer settings → OAuth Apps 2. Create a new OAuth App 3. Set callback URL to `http://localhost:8000/auth/oauth/github/callback`
|
|
12
|
+
|
|
13
|
+
### 2. Configure ironauth
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from ironauth.plugins.oauth import oauth
|
|
17
|
+
|
|
18
|
+
auth = ironauth(
|
|
19
|
+
...
|
|
20
|
+
plugins=[oauth(providers=["google", "github"])],
|
|
21
|
+
config={
|
|
22
|
+
"secret_key": "...",
|
|
23
|
+
"oauth": {
|
|
24
|
+
"google": {
|
|
25
|
+
"client_id": "YOUR_CLIENT_ID",
|
|
26
|
+
"client_secret": "YOUR_CLIENT_SECRET",
|
|
27
|
+
"redirect_uri": "http://localhost:8000/auth/oauth/google/callback",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 3. Redirect your users
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# Frontend: redirect to this URL to start OAuth flow
|
|
38
|
+
GET /auth/oauth/google
|
|
39
|
+
GET /auth/oauth/github
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
ironauth handles the callback automatically and sets session cookies.
|
|
43
|
+
|
|
44
|
+
## Account linking
|
|
45
|
+
|
|
46
|
+
If a user signs in with OAuth using an email that already exists in your database, ironauth automatically links the OAuth account to the existing user — no duplicate accounts.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Two-Factor Authentication (TOTP)
|
|
2
|
+
|
|
3
|
+
ironauth supports TOTP-based 2FA compatible with Google Authenticator, Authy, and any TOTP app.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from ironauth.plugins.two_factor import two_factor
|
|
9
|
+
|
|
10
|
+
auth = ironauth(
|
|
11
|
+
...
|
|
12
|
+
plugins=[two_factor()],
|
|
13
|
+
)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Flow
|
|
17
|
+
|
|
18
|
+
### Enable 2FA
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
# 1. Request setup — returns QR code + secret
|
|
22
|
+
POST /auth/2fa/enable
|
|
23
|
+
# Response:
|
|
24
|
+
{
|
|
25
|
+
"secret": "BASE32SECRET",
|
|
26
|
+
"qr_code": "<base64 PNG>"
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Display the QR code to your user so they can scan it with their TOTP app.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# 2. Confirm with first code
|
|
34
|
+
POST /auth/2fa/confirm
|
|
35
|
+
{ "code": "123456" }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Validate at login
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
POST /auth/2fa/validate
|
|
42
|
+
{
|
|
43
|
+
"user_id": "...",
|
|
44
|
+
"code": "123456"
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Disable 2FA
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
POST /auth/2fa/disable
|
|
52
|
+
{ "code": "123456" }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
!!! info "TOTP window"
|
|
56
|
+
ironauth accepts codes valid within a 30-second window before and after the current time to account for clock drift.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ironauth
|
|
2
|
+
|
|
3
|
+
**ironauth** is a framework-agnostic authentication library for Python — simple to use, secure by default, and extensible via plugins.
|
|
4
|
+
|
|
5
|
+
## Why ironauth?
|
|
6
|
+
|
|
7
|
+
The Python ecosystem has authentication solutions, but none that feel like a single cohesive tool across frameworks. ironauth changes that.
|
|
8
|
+
|
|
9
|
+
- **Framework-agnostic** — FastAPI today, Django and Flask coming soon
|
|
10
|
+
- **Secure by default** — Argon2 hashing, HTTP-only cookies, JWT rotation
|
|
11
|
+
- **Plugin system** — OAuth, 2FA, and more
|
|
12
|
+
- **Fully async** — built for modern Python
|
|
13
|
+
|
|
14
|
+
## Quick example
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from ironauth import ironauth
|
|
18
|
+
from ironauth.adapters.database import sqlalchemy_adapter
|
|
19
|
+
from ironauth.adapters.frameworks import fastapi_adapter
|
|
20
|
+
from ironauth.plugins import oauth, two_factor
|
|
21
|
+
|
|
22
|
+
auth = ironauth(
|
|
23
|
+
database=sqlalchemy_adapter("postgresql+asyncpg://user:pass@localhost/db"),
|
|
24
|
+
adapter=fastapi_adapter(),
|
|
25
|
+
plugins=[
|
|
26
|
+
oauth(providers=["google", "github"]),
|
|
27
|
+
two_factor(),
|
|
28
|
+
],
|
|
29
|
+
config={"secret_key": "your-secret-key"},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
app.include_router(auth.router)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install ironauth
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
[Get started →](getting-started.md)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from ironauth.models.user import Base, OAuthAccount, User
|
|
4
|
+
from sqlalchemy import delete, select, update
|
|
5
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SQLAlchemyAdapter:
|
|
9
|
+
def __init__(self, database_url: str):
|
|
10
|
+
self.engine = create_async_engine(database_url, echo=False)
|
|
11
|
+
self.session_factory = async_sessionmaker(self.engine, expire_on_commit=False)
|
|
12
|
+
|
|
13
|
+
async def init(self):
|
|
14
|
+
async with self.engine.begin() as conn:
|
|
15
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
16
|
+
|
|
17
|
+
# --- User ---
|
|
18
|
+
|
|
19
|
+
async def create_user(
|
|
20
|
+
self, email: str, hashed_password: Optional[str] = None
|
|
21
|
+
) -> User:
|
|
22
|
+
async with self.session_factory() as session:
|
|
23
|
+
user = User(email=email, hashed_password=hashed_password)
|
|
24
|
+
session.add(user)
|
|
25
|
+
await session.commit()
|
|
26
|
+
await session.refresh(user)
|
|
27
|
+
return user
|
|
28
|
+
|
|
29
|
+
async def get_user_by_email(self, email: str) -> Optional[User]:
|
|
30
|
+
async with self.session_factory() as session:
|
|
31
|
+
result = await session.execute(select(User).where(User.email == email))
|
|
32
|
+
return result.scalar_one_or_none()
|
|
33
|
+
|
|
34
|
+
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
35
|
+
async with self.session_factory() as session:
|
|
36
|
+
result = await session.execute(select(User).where(User.id == user_id))
|
|
37
|
+
return result.scalar_one_or_none()
|
|
38
|
+
|
|
39
|
+
async def update_user(self, user_id: str, **kwargs: Any) -> Optional[User]:
|
|
40
|
+
async with self.session_factory() as session:
|
|
41
|
+
await session.execute(
|
|
42
|
+
update(User).where(User.id == user_id).values(**kwargs)
|
|
43
|
+
)
|
|
44
|
+
await session.commit()
|
|
45
|
+
return await self.get_user_by_id(user_id)
|
|
46
|
+
|
|
47
|
+
async def delete_user(self, user_id: str) -> None:
|
|
48
|
+
async with self.session_factory() as session:
|
|
49
|
+
await session.execute(delete(User).where(User.id == user_id))
|
|
50
|
+
await session.commit()
|
|
51
|
+
|
|
52
|
+
# --- OAuth ---
|
|
53
|
+
|
|
54
|
+
async def create_oauth_account(
|
|
55
|
+
self,
|
|
56
|
+
user_id: str,
|
|
57
|
+
provider: str,
|
|
58
|
+
provider_user_id: str,
|
|
59
|
+
access_token: str,
|
|
60
|
+
refresh_token: Optional[str] = None,
|
|
61
|
+
expires_at=None,
|
|
62
|
+
) -> OAuthAccount:
|
|
63
|
+
async with self.session_factory() as session:
|
|
64
|
+
account = OAuthAccount(
|
|
65
|
+
user_id=user_id,
|
|
66
|
+
provider=provider,
|
|
67
|
+
provider_user_id=provider_user_id,
|
|
68
|
+
access_token=access_token,
|
|
69
|
+
refresh_token=refresh_token,
|
|
70
|
+
expires_at=expires_at,
|
|
71
|
+
)
|
|
72
|
+
session.add(account)
|
|
73
|
+
await session.commit()
|
|
74
|
+
await session.refresh(account)
|
|
75
|
+
return account
|
|
76
|
+
|
|
77
|
+
async def get_oauth_account(
|
|
78
|
+
self, provider: str, provider_user_id: str
|
|
79
|
+
) -> Optional[OAuthAccount]:
|
|
80
|
+
async with self.session_factory() as session:
|
|
81
|
+
result = await session.execute(
|
|
82
|
+
select(OAuthAccount).where(
|
|
83
|
+
OAuthAccount.provider == provider,
|
|
84
|
+
OAuthAccount.provider_user_id == provider_user_id,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return result.scalar_one_or_none()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def sqlalchemy_adapter(database_url: str) -> SQLAlchemyAdapter:
|
|
91
|
+
return SQLAlchemyAdapter(database_url)
|
|
File without changes
|