simple-module-auth 0.0.1__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.
- auth/__init__.py +1 -0
- auth/contracts/__init__.py +5 -0
- auth/contracts/schemas.py +75 -0
- auth/deps.py +60 -0
- auth/locales/en.json +6 -0
- auth/locales/es.json +6 -0
- auth/module.py +26 -0
- auth/package.json +16 -0
- auth/py.typed +0 -0
- simple_module_auth-0.0.1.dist-info/METADATA +71 -0
- simple_module_auth-0.0.1.dist-info/RECORD +14 -0
- simple_module_auth-0.0.1.dist-info/WHEEL +4 -0
- simple_module_auth-0.0.1.dist-info/entry_points.txt +2 -0
- simple_module_auth-0.0.1.dist-info/licenses/LICENSE +21 -0
auth/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Auth module — shared contracts (UserContext, deps)."""
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Auth data types shared with other modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
# Runtime import would be circular: auth -> users -> auth.
|
|
10
|
+
# Only imported for type-hints, never at runtime.
|
|
11
|
+
from users.models import User
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class UserContext:
|
|
16
|
+
"""Authenticated user information for downstream handlers."""
|
|
17
|
+
|
|
18
|
+
id: str
|
|
19
|
+
email: str
|
|
20
|
+
name: str
|
|
21
|
+
roles: list[str] = field(default_factory=list)
|
|
22
|
+
tenant_id: str | None = None
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_user(cls, user: User | Any) -> UserContext:
|
|
26
|
+
"""Build a UserContext from a users.models.User with eagerly-loaded roles.
|
|
27
|
+
|
|
28
|
+
Duck-typed to avoid importing users.models at runtime — any object
|
|
29
|
+
exposing .id, .email, .full_name, .roles[*].name, .tenant_id works.
|
|
30
|
+
The caller is responsible for eager-loading roles (selectinload).
|
|
31
|
+
"""
|
|
32
|
+
return cls(
|
|
33
|
+
id=str(user.id),
|
|
34
|
+
email=user.email,
|
|
35
|
+
name=user.full_name or user.email,
|
|
36
|
+
roles=[r.name for r in user.roles],
|
|
37
|
+
tenant_id=user.tenant_id,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def to_session_dict(self) -> dict[str, Any]:
|
|
41
|
+
"""Serialize to a JSON-safe dict for the signed session cookie.
|
|
42
|
+
|
|
43
|
+
The inverse of :meth:`from_session_dict`. Adding a new field to
|
|
44
|
+
``UserContext`` requires updating both methods here — not the
|
|
45
|
+
AuthMiddleware cache helper, which stays schema-agnostic."""
|
|
46
|
+
return {
|
|
47
|
+
"id": self.id,
|
|
48
|
+
"email": self.email,
|
|
49
|
+
"name": self.name,
|
|
50
|
+
"roles": list(self.roles),
|
|
51
|
+
"tenant_id": self.tenant_id,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_session_dict(cls, payload: Any) -> UserContext | None:
|
|
56
|
+
"""Rebuild from :meth:`to_session_dict` output. Returns ``None`` on any
|
|
57
|
+
shape mismatch so callers can fall through to a fresh DB load."""
|
|
58
|
+
if not isinstance(payload, dict):
|
|
59
|
+
return None
|
|
60
|
+
try:
|
|
61
|
+
return cls(
|
|
62
|
+
id=str(payload["id"]),
|
|
63
|
+
email=str(payload["email"]),
|
|
64
|
+
name=str(payload["name"]),
|
|
65
|
+
roles=list(payload.get("roles") or []),
|
|
66
|
+
tenant_id=payload.get("tenant_id"),
|
|
67
|
+
)
|
|
68
|
+
except (KeyError, TypeError, ValueError):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def has_role(self, role: str) -> bool:
|
|
72
|
+
return role in self.roles
|
|
73
|
+
|
|
74
|
+
def has_any_role(self, roles: list[str]) -> bool:
|
|
75
|
+
return bool(set(self.roles) & set(roles))
|
auth/deps.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""FastAPI auth dependencies — get_current_user, require_permission."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from fastapi import Depends, HTTPException, Request
|
|
8
|
+
from simple_module_hosting.i18n_deps import TranslatorDep
|
|
9
|
+
|
|
10
|
+
from auth.contracts.schemas import UserContext
|
|
11
|
+
|
|
12
|
+
_ADMIN_ROLE = "admin"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def get_current_user(request: Request, t: TranslatorDep) -> UserContext:
|
|
16
|
+
"""Extract the authenticated user from request state.
|
|
17
|
+
|
|
18
|
+
The auth middleware must set ``request.state.user`` before this runs.
|
|
19
|
+
"""
|
|
20
|
+
user = getattr(request.state, "user", None)
|
|
21
|
+
if user is None:
|
|
22
|
+
raise HTTPException(status_code=401, detail=t.t("auth.errors.not_authenticated"))
|
|
23
|
+
return user
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
CurrentUser = Annotated[UserContext, Depends(get_current_user)]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def require_permission(*permissions: str):
|
|
30
|
+
"""Create a dependency that checks if the user has required permissions.
|
|
31
|
+
|
|
32
|
+
Usage::
|
|
33
|
+
|
|
34
|
+
@router.post("/", dependencies=[Depends(require_permission("products.create"))])
|
|
35
|
+
async def create_product(...): ...
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
async def check(
|
|
39
|
+
request: Request,
|
|
40
|
+
t: TranslatorDep,
|
|
41
|
+
user: UserContext = Depends(get_current_user),
|
|
42
|
+
):
|
|
43
|
+
# Admin role bypasses permission checks
|
|
44
|
+
if _ADMIN_ROLE in user.roles:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Get permission registry from app state
|
|
48
|
+
perm_registry = request.app.state.sm.permissions
|
|
49
|
+
user_perms = perm_registry.get_permissions_for_roles(user.roles)
|
|
50
|
+
|
|
51
|
+
if not any(p in user_perms for p in permissions):
|
|
52
|
+
raise HTTPException(
|
|
53
|
+
status_code=403,
|
|
54
|
+
detail=t.t(
|
|
55
|
+
"auth.errors.missing_permission",
|
|
56
|
+
permissions=", ".join(permissions),
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return Depends(check)
|
auth/locales/en.json
ADDED
auth/locales/es.json
ADDED
auth/module.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Auth module — shared contracts (UserContext, deps).
|
|
2
|
+
|
|
3
|
+
Intentionally minimal: this module owns the PUBLIC interface (UserContext,
|
|
4
|
+
get_current_user, CurrentUser, require_permission) that every other module
|
|
5
|
+
imports. Keeping it stable prevents churn when auth internals change.
|
|
6
|
+
|
|
7
|
+
All authentication logic (middleware, login, signup, OAuth) lives in the
|
|
8
|
+
users module.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.resources
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from simple_module_core.module import ModuleBase, ModuleMeta
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuthModule(ModuleBase):
|
|
20
|
+
meta = ModuleMeta(
|
|
21
|
+
name="Auth",
|
|
22
|
+
route_prefix="/auth",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def locale_dirs(self) -> dict[str, Path]:
|
|
26
|
+
return {"auth": Path(str(importlib.resources.files(__package__) / "locales"))}
|
auth/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simple-module-py/auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Frontend assets for the Auth module",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"react": "^19.0.0",
|
|
8
|
+
"react-dom": "^19.0.0",
|
|
9
|
+
"@inertiajs/react": "^2.0.0",
|
|
10
|
+
"@simple-module-py/ui": "*"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@simple-module-py/tsconfig": "*"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {}
|
|
16
|
+
}
|
auth/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple_module_auth
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Session-cookie authentication primitives — middleware, login/logout, redirect helpers for simple_module
|
|
5
|
+
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
|
+
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
7
|
+
Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Anto Subash <antosubash@live.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: authentication,cookie,fastapi,session,simple-module
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: itsdangerous>=2.2
|
|
25
|
+
Requires-Dist: simple-module-core==0.0.1
|
|
26
|
+
Requires-Dist: simple-module-db==0.0.1
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# simple_module_auth
|
|
30
|
+
|
|
31
|
+
Session-cookie authentication primitives for [simple_module](https://github.com/antosubash/simple_module_python) apps. Provides the `SessionMiddleware` wiring, login/logout helpers, and login-redirect handling used by the `simple_module_users` module.
|
|
32
|
+
|
|
33
|
+
**Heads up:** for most apps you don't install this directly — `simple_module_users` pulls it in and builds the email+password auth flow on top of these primitives.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install simple_module_auth
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What it provides
|
|
42
|
+
|
|
43
|
+
- Starlette `SessionMiddleware` configuration reading `SM_SECRET_KEY` and `SM_SESSION_COOKIE_*` env vars.
|
|
44
|
+
- `current_user_id` FastAPI dependency reading the signed session cookie.
|
|
45
|
+
- Redirect-to-login helpers for unauthenticated requests on Inertia routes.
|
|
46
|
+
- Login-required decorator / dependency for protecting routes without pulling in the heavier `simple_module_users` package.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from fastapi import APIRouter, Depends
|
|
52
|
+
from simple_module_auth import require_login
|
|
53
|
+
|
|
54
|
+
router = APIRouter()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@router.get("/me")
|
|
58
|
+
async def me(user_id: int = Depends(require_login)):
|
|
59
|
+
return {"user_id": user_id}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Routes that need more than just "logged in" (e.g. role/permission checks) should use `simple_module_permissions` instead.
|
|
63
|
+
|
|
64
|
+
## Depends on
|
|
65
|
+
|
|
66
|
+
- `simple_module_core`, `simple_module_db`
|
|
67
|
+
- `itsdangerous`
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT — see [LICENSE](https://github.com/antosubash/simple_module_python/blob/main/LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
auth/__init__.py,sha256=KNm7tP262uKG-dHYqVa_oIH_gSObAPH-vFL6hGGYsAw,60
|
|
2
|
+
auth/deps.py,sha256=ouFGjKZd9IOQo5oxLQ9UBSTkzgt_zoOhiEjOpNI_vXs,1805
|
|
3
|
+
auth/module.py,sha256=huMTDc_IeS8eiRQhnu-mvPcXY7xQ52GcdmqWdUFNJEA,767
|
|
4
|
+
auth/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
auth/contracts/__init__.py,sha256=7vR0kgDkcs9HDP7Gcrfmq8UCuWniErIxLUPyRnz-qlU,132
|
|
6
|
+
auth/contracts/schemas.py,sha256=Fi1-xqTaoWv3EEM9Oz-6Qst3PqVBrF7gp-_iXW1jjgo,2562
|
|
7
|
+
auth/locales/en.json,sha256=3a-d-xk1u4wUC6CZFWl3UhvTu7CTjZ0st9K_05FbY_o,139
|
|
8
|
+
auth/locales/es.json,sha256=9oeoOjEHrbkkgWSks4xSl4E1ePuEl-DHPsmjfntb9OM,135
|
|
9
|
+
auth/package.json,sha256=rSy_-0JLlNthQ25ru8_yk1b1-uBmki0wNiP426eWt_4,371
|
|
10
|
+
simple_module_auth-0.0.1.dist-info/METADATA,sha256=q4cnkxA4G7IB_B7Gqr4iqn2hkWjcQ9x7sk_nC2CEfZc,2748
|
|
11
|
+
simple_module_auth-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
+
simple_module_auth-0.0.1.dist-info/entry_points.txt,sha256=LpawiTLI4tijzuCnX4Judo1zha8cp87_Mx3_24qg4ZA,46
|
|
13
|
+
simple_module_auth-0.0.1.dist-info/licenses/LICENSE,sha256=Yn66lhLklsF5p7pa85_ksQrJ79Q-FgOaUAHevLBjer4,1068
|
|
14
|
+
simple_module_auth-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anto Subash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|