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 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
- print(f"User {user.id} has registered.")
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.1.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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.28
2
+ Generator: uv 0.9.29
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,