zndraw-auth 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zndraw_auth/__init__.py +2 -0
- zndraw_auth/db.py +67 -1
- zndraw_auth/settings.py +21 -1
- zndraw_auth/users.py +8 -1
- {zndraw_auth-0.1.0.dist-info → zndraw_auth-0.2.0.dist-info}/METADATA +22 -1
- zndraw_auth-0.2.0.dist-info/RECORD +8 -0
- {zndraw_auth-0.1.0.dist-info → zndraw_auth-0.2.0.dist-info}/WHEEL +1 -1
- zndraw_auth-0.1.0.dist-info/RECORD +0 -8
zndraw_auth/__init__.py
CHANGED
|
@@ -29,6 +29,7 @@ from zndraw_auth.db import (
|
|
|
29
29
|
Base,
|
|
30
30
|
User,
|
|
31
31
|
create_db_and_tables,
|
|
32
|
+
ensure_default_admin,
|
|
32
33
|
get_async_session,
|
|
33
34
|
get_user_db,
|
|
34
35
|
)
|
|
@@ -51,6 +52,7 @@ __all__ = [
|
|
|
51
52
|
"User",
|
|
52
53
|
# Database
|
|
53
54
|
"create_db_and_tables",
|
|
55
|
+
"ensure_default_admin",
|
|
54
56
|
"get_async_session",
|
|
55
57
|
"get_user_db",
|
|
56
58
|
# Schemas
|
zndraw_auth/db.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Database models and session management."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import uuid
|
|
4
5
|
from collections.abc import AsyncGenerator
|
|
5
6
|
from functools import lru_cache
|
|
@@ -7,6 +8,8 @@ from typing import Annotated
|
|
|
7
8
|
|
|
8
9
|
from fastapi import Depends
|
|
9
10
|
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
|
|
11
|
+
from fastapi_users.password import PasswordHelper
|
|
12
|
+
from sqlalchemy import select
|
|
10
13
|
from sqlalchemy.ext.asyncio import (
|
|
11
14
|
AsyncEngine,
|
|
12
15
|
AsyncSession,
|
|
@@ -17,6 +20,8 @@ from sqlalchemy.orm import DeclarativeBase
|
|
|
17
20
|
|
|
18
21
|
from zndraw_auth.settings import AuthSettings, get_auth_settings
|
|
19
22
|
|
|
23
|
+
log = logging.getLogger(__name__)
|
|
24
|
+
|
|
20
25
|
|
|
21
26
|
class Base(DeclarativeBase):
|
|
22
27
|
"""SQLAlchemy declarative base."""
|
|
@@ -53,7 +58,7 @@ def get_session_maker(database_url: str) -> async_sessionmaker[AsyncSession]:
|
|
|
53
58
|
|
|
54
59
|
|
|
55
60
|
async def create_db_and_tables(settings: AuthSettings | None = None) -> None:
|
|
56
|
-
"""Create all database tables.
|
|
61
|
+
"""Create all database tables and ensure default admin exists.
|
|
57
62
|
|
|
58
63
|
Call this in your app's lifespan or startup.
|
|
59
64
|
"""
|
|
@@ -63,6 +68,67 @@ async def create_db_and_tables(settings: AuthSettings | None = None) -> None:
|
|
|
63
68
|
async with engine.begin() as conn:
|
|
64
69
|
await conn.run_sync(Base.metadata.create_all)
|
|
65
70
|
|
|
71
|
+
# Ensure default admin user exists (if configured)
|
|
72
|
+
await ensure_default_admin(settings)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def ensure_default_admin(settings: AuthSettings | None = None) -> None:
|
|
76
|
+
"""Create or promote the default admin user if configured.
|
|
77
|
+
|
|
78
|
+
If DEFAULT_ADMIN_EMAIL and DEFAULT_ADMIN_PASSWORD are set:
|
|
79
|
+
- Creates the user with is_superuser=True if they don't exist
|
|
80
|
+
- Promotes existing user to superuser if they exist but aren't one
|
|
81
|
+
|
|
82
|
+
If not configured, does nothing (dev mode - all users are superusers).
|
|
83
|
+
"""
|
|
84
|
+
if settings is None:
|
|
85
|
+
settings = get_auth_settings()
|
|
86
|
+
|
|
87
|
+
admin_email = settings.default_admin_email
|
|
88
|
+
admin_password = settings.default_admin_password
|
|
89
|
+
|
|
90
|
+
if admin_email is None:
|
|
91
|
+
log.info("No default admin configured - running in dev mode")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if admin_password is None:
|
|
95
|
+
log.warning(
|
|
96
|
+
"DEFAULT_ADMIN_EMAIL is set but DEFAULT_ADMIN_PASSWORD is not - "
|
|
97
|
+
"skipping admin creation"
|
|
98
|
+
)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
session_maker = get_session_maker(settings.database_url)
|
|
102
|
+
password_helper = PasswordHelper()
|
|
103
|
+
|
|
104
|
+
async with session_maker() as session:
|
|
105
|
+
# Check if user already exists
|
|
106
|
+
result = await session.execute(
|
|
107
|
+
select(User).where(User.email == admin_email) # type: ignore[arg-type]
|
|
108
|
+
)
|
|
109
|
+
existing_user = result.scalar_one_or_none()
|
|
110
|
+
|
|
111
|
+
if existing_user is None:
|
|
112
|
+
# Create new admin user
|
|
113
|
+
hashed_password = password_helper.hash(admin_password.get_secret_value())
|
|
114
|
+
admin_user = User(
|
|
115
|
+
email=admin_email,
|
|
116
|
+
hashed_password=hashed_password,
|
|
117
|
+
is_active=True,
|
|
118
|
+
is_superuser=True,
|
|
119
|
+
is_verified=True,
|
|
120
|
+
)
|
|
121
|
+
session.add(admin_user)
|
|
122
|
+
await session.commit()
|
|
123
|
+
log.info(f"Created default admin user: {admin_email}")
|
|
124
|
+
elif not existing_user.is_superuser:
|
|
125
|
+
# Promote existing user to superuser
|
|
126
|
+
existing_user.is_superuser = True
|
|
127
|
+
await session.commit()
|
|
128
|
+
log.info(f"Promoted user to superuser: {admin_email}")
|
|
129
|
+
else:
|
|
130
|
+
log.debug(f"Default admin already exists: {admin_email}")
|
|
131
|
+
|
|
66
132
|
|
|
67
133
|
async def get_async_session(
|
|
68
134
|
settings: Annotated[AuthSettings, Depends(get_auth_settings)],
|
zndraw_auth/settings.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from functools import lru_cache
|
|
4
4
|
|
|
5
|
-
from pydantic import SecretStr
|
|
5
|
+
from pydantic import SecretStr, computed_field
|
|
6
6
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
7
|
|
|
8
8
|
|
|
@@ -11,6 +11,13 @@ class AuthSettings(BaseSettings):
|
|
|
11
11
|
|
|
12
12
|
All settings can be overridden with ZNDRAW_AUTH_ prefix.
|
|
13
13
|
Example: ZNDRAW_AUTH_SECRET_KEY=your-secret-key
|
|
14
|
+
|
|
15
|
+
Admin mode vs Dev mode:
|
|
16
|
+
- If DEFAULT_ADMIN_EMAIL and DEFAULT_ADMIN_PASSWORD are set, the system
|
|
17
|
+
runs in "production mode": only the configured admin is a superuser,
|
|
18
|
+
and new users are created as regular users.
|
|
19
|
+
- If they are NOT set, the system runs in "dev mode": all newly
|
|
20
|
+
registered users are automatically granted superuser privileges.
|
|
14
21
|
"""
|
|
15
22
|
|
|
16
23
|
model_config = SettingsConfigDict(
|
|
@@ -30,6 +37,19 @@ class AuthSettings(BaseSettings):
|
|
|
30
37
|
reset_password_token_secret: SecretStr = SecretStr("CHANGE-ME-RESET")
|
|
31
38
|
verification_token_secret: SecretStr = SecretStr("CHANGE-ME-VERIFY")
|
|
32
39
|
|
|
40
|
+
# Default admin user (production mode)
|
|
41
|
+
default_admin_email: str | None = None
|
|
42
|
+
"""Email for the default admin user. If set, enables production mode."""
|
|
43
|
+
|
|
44
|
+
default_admin_password: SecretStr | None = None
|
|
45
|
+
"""Password for the default admin user."""
|
|
46
|
+
|
|
47
|
+
@computed_field # type: ignore[prop-decorator]
|
|
48
|
+
@property
|
|
49
|
+
def is_dev_mode(self) -> bool:
|
|
50
|
+
"""True if no admin credentials configured (all users become superusers)."""
|
|
51
|
+
return self.default_admin_email is None
|
|
52
|
+
|
|
33
53
|
|
|
34
54
|
@lru_cache
|
|
35
55
|
def get_auth_settings() -> AuthSettings:
|
zndraw_auth/users.py
CHANGED
|
@@ -28,6 +28,7 @@ from fastapi_users.authentication import (
|
|
|
28
28
|
from fastapi_users.db import SQLAlchemyUserDatabase
|
|
29
29
|
|
|
30
30
|
from zndraw_auth.db import User, get_user_db
|
|
31
|
+
from zndraw_auth.schemas import UserUpdate
|
|
31
32
|
from zndraw_auth.settings import AuthSettings, get_auth_settings
|
|
32
33
|
|
|
33
34
|
# --- User Manager ---
|
|
@@ -41,12 +42,17 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
|
|
41
42
|
|
|
42
43
|
reset_password_token_secret: str
|
|
43
44
|
verification_token_secret: str
|
|
45
|
+
is_dev_mode: bool = False
|
|
44
46
|
|
|
45
47
|
async def on_after_register(
|
|
46
48
|
self, user: User, request: Request | None = None
|
|
47
49
|
) -> None:
|
|
48
50
|
"""Called after successful registration."""
|
|
49
|
-
|
|
51
|
+
if self.is_dev_mode and not user.is_superuser:
|
|
52
|
+
await self.update(UserUpdate(is_superuser=True), user, safe=False)
|
|
53
|
+
print(f"User {user.id} has registered (granted superuser - dev mode).")
|
|
54
|
+
else:
|
|
55
|
+
print(f"User {user.id} has registered.")
|
|
50
56
|
|
|
51
57
|
async def on_after_forgot_password(
|
|
52
58
|
self, user: User, token: str, request: Request | None = None
|
|
@@ -73,6 +79,7 @@ async def get_user_manager(
|
|
|
73
79
|
manager.verification_token_secret = (
|
|
74
80
|
settings.verification_token_secret.get_secret_value()
|
|
75
81
|
)
|
|
82
|
+
manager.is_dev_mode = settings.is_dev_mode
|
|
76
83
|
yield manager
|
|
77
84
|
|
|
78
85
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: zndraw-auth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Shared authentication for ZnDraw using fastapi-users
|
|
5
5
|
Requires-Dist: fastapi>=0.128.0
|
|
6
6
|
Requires-Dist: fastapi-users[sqlalchemy]>=14.0.0
|
|
@@ -222,6 +222,27 @@ Settings are loaded from environment variables with the `ZNDRAW_AUTH_` prefix:
|
|
|
222
222
|
| `ZNDRAW_AUTH_DATABASE_URL` | `sqlite+aiosqlite:///./zndraw_auth.db` | Database connection URL |
|
|
223
223
|
| `ZNDRAW_AUTH_RESET_PASSWORD_TOKEN_SECRET` | `CHANGE-ME-RESET` | Password reset token secret |
|
|
224
224
|
| `ZNDRAW_AUTH_VERIFICATION_TOKEN_SECRET` | `CHANGE-ME-VERIFY` | Email verification token secret |
|
|
225
|
+
| `ZNDRAW_AUTH_DEFAULT_ADMIN_EMAIL` | `None` | Email for the default admin user |
|
|
226
|
+
| `ZNDRAW_AUTH_DEFAULT_ADMIN_PASSWORD` | `None` | Password for the default admin user |
|
|
227
|
+
|
|
228
|
+
### Dev Mode vs Production Mode
|
|
229
|
+
|
|
230
|
+
The system has two operating modes based on admin configuration:
|
|
231
|
+
|
|
232
|
+
**Dev Mode** (default - no admin configured):
|
|
233
|
+
- All newly registered users are automatically granted superuser privileges
|
|
234
|
+
- Useful for development and testing
|
|
235
|
+
|
|
236
|
+
**Production Mode** (admin configured):
|
|
237
|
+
- Set `ZNDRAW_AUTH_DEFAULT_ADMIN_EMAIL` and `ZNDRAW_AUTH_DEFAULT_ADMIN_PASSWORD`
|
|
238
|
+
- The configured admin user is created/promoted on startup
|
|
239
|
+
- New users are created as regular users (not superusers)
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# Production mode example
|
|
243
|
+
export ZNDRAW_AUTH_DEFAULT_ADMIN_EMAIL=admin@example.com
|
|
244
|
+
export ZNDRAW_AUTH_DEFAULT_ADMIN_PASSWORD=secure-password
|
|
245
|
+
```
|
|
225
246
|
|
|
226
247
|
## Exports
|
|
227
248
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
zndraw_auth/__init__.py,sha256=02v7zx7BxEc1HwsX9ll89HrwfrwGaw_hUn9NkWY44L0,1705
|
|
2
|
+
zndraw_auth/db.py,sha256=rAj5oqheAxoYIb4_kZiPdj1duyu8MDfq_ROyW4s309Q,4764
|
|
3
|
+
zndraw_auth/schemas.py,sha256=lG1c-donAxTqqyQ1-p_GqsNi9DIN2ryESbjVL25R4oQ,399
|
|
4
|
+
zndraw_auth/settings.py,sha256=FIVOJsRcz-4RA5i7R5g8y_iURQ7TDPUg3WTsxF8y7bg,1896
|
|
5
|
+
zndraw_auth/users.py,sha256=zKcng71VnsR_dD0Di5v6purHzDhR4gKmBykC310u8aU,4493
|
|
6
|
+
zndraw_auth-0.2.0.dist-info/WHEEL,sha256=5DEXXimM34_d4Gx1AuF9ysMr1_maoEtGKjaILM3s4w4,80
|
|
7
|
+
zndraw_auth-0.2.0.dist-info/METADATA,sha256=HMZ2LCSS8PaWpMwqpDxSLf98w0ofd4ErpJPh3yibXV0,7687
|
|
8
|
+
zndraw_auth-0.2.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
zndraw_auth/__init__.py,sha256=kHAQDlr6uxJCFFeIrJQWSfRqij6g8cSBj0iAfkqX1P0,1651
|
|
2
|
-
zndraw_auth/db.py,sha256=sxUZ_lfOP_DfZoSsTUhMPaomSF6MPLPD1NuEa_N4hLk,2383
|
|
3
|
-
zndraw_auth/schemas.py,sha256=lG1c-donAxTqqyQ1-p_GqsNi9DIN2ryESbjVL25R4oQ,399
|
|
4
|
-
zndraw_auth/settings.py,sha256=pNXX2-NlvBMobFiGytrEmDZ2EB65BUrvwKCXxrbYp8I,1012
|
|
5
|
-
zndraw_auth/users.py,sha256=LRI4LoY2xXUW24yQZIfa-v6oq1FcHovCEQDZIa2bz_0,4137
|
|
6
|
-
zndraw_auth-0.1.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
7
|
-
zndraw_auth-0.1.0.dist-info/METADATA,sha256=_iHPAJGDovbkX86AOtt8Jgsr_nTR71BUEnq1gp7rloc,6865
|
|
8
|
-
zndraw_auth-0.1.0.dist-info/RECORD,,
|