skrift 0.1.0a1__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.
Files changed (68) hide show
  1. skrift/__init__.py +1 -0
  2. skrift/__main__.py +17 -0
  3. skrift/admin/__init__.py +11 -0
  4. skrift/admin/controller.py +452 -0
  5. skrift/admin/navigation.py +105 -0
  6. skrift/alembic/env.py +91 -0
  7. skrift/alembic/script.py.mako +26 -0
  8. skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +70 -0
  9. skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +57 -0
  10. skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +31 -0
  11. skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +43 -0
  12. skrift/alembic/versions/20260122_200000_add_settings_table.py +38 -0
  13. skrift/alembic.ini +77 -0
  14. skrift/asgi.py +545 -0
  15. skrift/auth/__init__.py +58 -0
  16. skrift/auth/guards.py +130 -0
  17. skrift/auth/roles.py +94 -0
  18. skrift/auth/services.py +184 -0
  19. skrift/cli.py +45 -0
  20. skrift/config.py +192 -0
  21. skrift/controllers/__init__.py +4 -0
  22. skrift/controllers/auth.py +371 -0
  23. skrift/controllers/web.py +67 -0
  24. skrift/db/__init__.py +3 -0
  25. skrift/db/base.py +7 -0
  26. skrift/db/models/__init__.py +6 -0
  27. skrift/db/models/page.py +26 -0
  28. skrift/db/models/role.py +56 -0
  29. skrift/db/models/setting.py +13 -0
  30. skrift/db/models/user.py +36 -0
  31. skrift/db/services/__init__.py +1 -0
  32. skrift/db/services/page_service.py +217 -0
  33. skrift/db/services/setting_service.py +206 -0
  34. skrift/lib/__init__.py +3 -0
  35. skrift/lib/exceptions.py +168 -0
  36. skrift/lib/template.py +108 -0
  37. skrift/setup/__init__.py +14 -0
  38. skrift/setup/config_writer.py +211 -0
  39. skrift/setup/controller.py +751 -0
  40. skrift/setup/middleware.py +89 -0
  41. skrift/setup/providers.py +163 -0
  42. skrift/setup/state.py +134 -0
  43. skrift/static/css/style.css +998 -0
  44. skrift/templates/admin/admin.html +19 -0
  45. skrift/templates/admin/base.html +24 -0
  46. skrift/templates/admin/pages/edit.html +32 -0
  47. skrift/templates/admin/pages/list.html +62 -0
  48. skrift/templates/admin/settings/site.html +32 -0
  49. skrift/templates/admin/users/list.html +58 -0
  50. skrift/templates/admin/users/roles.html +42 -0
  51. skrift/templates/auth/login.html +125 -0
  52. skrift/templates/base.html +52 -0
  53. skrift/templates/error-404.html +19 -0
  54. skrift/templates/error-500.html +19 -0
  55. skrift/templates/error.html +19 -0
  56. skrift/templates/index.html +9 -0
  57. skrift/templates/page.html +26 -0
  58. skrift/templates/setup/admin.html +24 -0
  59. skrift/templates/setup/auth.html +110 -0
  60. skrift/templates/setup/base.html +407 -0
  61. skrift/templates/setup/complete.html +17 -0
  62. skrift/templates/setup/database.html +125 -0
  63. skrift/templates/setup/restart.html +28 -0
  64. skrift/templates/setup/site.html +39 -0
  65. skrift-0.1.0a1.dist-info/METADATA +233 -0
  66. skrift-0.1.0a1.dist-info/RECORD +68 -0
  67. skrift-0.1.0a1.dist-info/WHEEL +4 -0
  68. skrift-0.1.0a1.dist-info/entry_points.txt +3 -0
skrift/asgi.py ADDED
@@ -0,0 +1,545 @@
1
+ """ASGI application factory for Skrift.
2
+
3
+ This module handles application creation with setup wizard support.
4
+ The application uses a dispatcher architecture:
5
+ - AppDispatcher routes requests to either the setup app or the main app
6
+ - When setup completes, the dispatcher switches all traffic to the main app
7
+ - No server restart required after setup
8
+ """
9
+
10
+ import asyncio
11
+ import hashlib
12
+ import importlib
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+
16
+ import yaml
17
+ from advanced_alchemy.config import EngineConfig
18
+ from advanced_alchemy.extensions.litestar import (
19
+ AsyncSessionConfig,
20
+ SQLAlchemyAsyncConfig,
21
+ SQLAlchemyPlugin,
22
+ )
23
+ from litestar import Litestar
24
+ from litestar.config.compression import CompressionConfig
25
+ from litestar.contrib.jinja import JinjaTemplateEngine
26
+ from litestar.exceptions import HTTPException
27
+ from litestar.middleware.session.client_side import CookieBackendConfig
28
+ from litestar.static_files import create_static_files_router
29
+ from litestar.template import TemplateConfig
30
+ from litestar.types import ASGIApp, Receive, Scope, Send
31
+
32
+ from skrift.config import get_settings, is_config_valid
33
+ from skrift.db.base import Base
34
+ from skrift.db.services.setting_service import (
35
+ load_site_settings_cache,
36
+ get_cached_site_name,
37
+ get_cached_site_tagline,
38
+ get_cached_site_copyright_holder,
39
+ get_cached_site_copyright_start_year,
40
+ get_setting,
41
+ SETUP_COMPLETED_AT_KEY,
42
+ )
43
+ from skrift.lib.exceptions import http_exception_handler, internal_server_error_handler
44
+
45
+
46
+ def load_controllers() -> list:
47
+ """Load controllers from app.yaml configuration."""
48
+ config_path = Path.cwd() / "app.yaml"
49
+
50
+ if not config_path.exists():
51
+ return []
52
+
53
+ with open(config_path, "r") as f:
54
+ config = yaml.safe_load(f)
55
+
56
+ if not config:
57
+ return []
58
+
59
+ controllers = []
60
+ for controller_spec in config.get("controllers", []):
61
+ try:
62
+ module_path, class_name = controller_spec.split(":")
63
+ module = importlib.import_module(module_path)
64
+ controller_class = getattr(module, class_name)
65
+ controllers.append(controller_class)
66
+ except Exception:
67
+ # Skip controllers that can't be loaded during setup
68
+ pass
69
+
70
+ return controllers
71
+
72
+
73
+ async def check_setup_complete(db_config: SQLAlchemyAsyncConfig) -> bool:
74
+ """Check if setup has been completed."""
75
+ try:
76
+ async with db_config.get_session() as session:
77
+ value = await get_setting(session, SETUP_COMPLETED_AT_KEY)
78
+ return value is not None
79
+ except Exception:
80
+ return False
81
+
82
+
83
+ # Module-level reference to the dispatcher for state updates
84
+ _dispatcher: "AppDispatcher | None" = None
85
+
86
+
87
+ def lock_setup_in_dispatcher() -> None:
88
+ """Lock setup in the dispatcher, making /setup/* return 404.
89
+
90
+ This is called when setup is complete and user visits the main site.
91
+ """
92
+ global _dispatcher
93
+ if _dispatcher is not None:
94
+ _dispatcher.setup_locked = True
95
+
96
+
97
+ class AppDispatcher:
98
+ """ASGI dispatcher that routes between setup and main apps.
99
+
100
+ Uses a simple setup_locked flag:
101
+ - When True: /setup/* returns 404 (via main app), all traffic goes to main app
102
+ - When False: Setup routes work, check DB to determine routing for other paths
103
+
104
+ The main_app can be None at startup if config isn't valid yet. It will be
105
+ lazily created after setup completes.
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ setup_app: ASGIApp,
111
+ db_url: str | None = None,
112
+ main_app: Litestar | None = None,
113
+ ) -> None:
114
+ self._main_app = main_app
115
+ self._main_app_error: str | None = None
116
+ self._main_app_started = main_app is not None # Track if lifespan started
117
+ self.setup_app = setup_app
118
+ self.setup_locked = False # When True, setup is inaccessible
119
+ self._db_url = db_url
120
+ self._lifespan_task: asyncio.Task | None = None
121
+ self._shutdown_event: asyncio.Event | None = None
122
+
123
+ async def _get_or_create_main_app(self) -> Litestar | None:
124
+ """Get the main app, creating it lazily if needed."""
125
+ if self._main_app is None and self._main_app_error is None:
126
+ try:
127
+ self._main_app = create_app()
128
+ # Run lifespan startup for the newly created app
129
+ await self._start_main_app_lifespan()
130
+ except Exception as e:
131
+ self._main_app_error = str(e)
132
+ self._main_app = None
133
+ return self._main_app
134
+
135
+ async def _start_main_app_lifespan(self) -> None:
136
+ """Start the main app's lifespan (runs startup handlers)."""
137
+ if self._main_app is None or self._main_app_started:
138
+ return
139
+
140
+ startup_complete = asyncio.Event()
141
+ startup_failed: str | None = None
142
+ self._shutdown_event = asyncio.Event()
143
+ message_queue: asyncio.Queue = asyncio.Queue()
144
+
145
+ # Queue the startup message
146
+ await message_queue.put({"type": "lifespan.startup"})
147
+
148
+ async def receive():
149
+ # First return startup, then wait for shutdown signal
150
+ msg = await message_queue.get()
151
+ return msg
152
+
153
+ async def send(message):
154
+ nonlocal startup_failed
155
+ if message["type"] == "lifespan.startup.complete":
156
+ startup_complete.set()
157
+ elif message["type"] == "lifespan.startup.failed":
158
+ startup_failed = message.get("message", "Startup failed")
159
+ startup_complete.set()
160
+
161
+ scope = {"type": "lifespan", "asgi": {"version": "3.0"}, "state": {}}
162
+
163
+ async def run_lifespan():
164
+ try:
165
+ await self._main_app(scope, receive, send)
166
+ except Exception:
167
+ pass
168
+
169
+ # Start lifespan handler in background
170
+ self._lifespan_task = asyncio.create_task(run_lifespan())
171
+
172
+ # Wait for startup to complete
173
+ await startup_complete.wait()
174
+
175
+ if startup_failed:
176
+ raise RuntimeError(startup_failed)
177
+
178
+ self._main_app_started = True
179
+
180
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
181
+ if scope["type"] != "http":
182
+ # Lifespan events go to setup app if no main app yet
183
+ app = self._main_app or self.setup_app
184
+ await app(scope, receive, send)
185
+ return
186
+
187
+ path = scope.get("path", "")
188
+
189
+ # If setup is locked and we have main app, it handles EVERYTHING
190
+ if self.setup_locked and self._main_app:
191
+ await self._main_app(scope, receive, send)
192
+ return
193
+
194
+ # Setup not locked - /setup/* always goes to setup app
195
+ if path.startswith("/setup") or path.startswith("/static"):
196
+ await self.setup_app(scope, receive, send)
197
+ return
198
+
199
+ # For /auth/* during setup, route to setup app (OAuth callbacks)
200
+ if path.startswith("/auth"):
201
+ await self.setup_app(scope, receive, send)
202
+ return
203
+
204
+ # Non-setup path: check if setup is complete in DB
205
+ if await self._is_setup_complete_in_db():
206
+ # Setup complete - try to get/create main app
207
+ main_app = await self._get_or_create_main_app()
208
+ if main_app:
209
+ self.setup_locked = True
210
+ await main_app(scope, receive, send)
211
+ else:
212
+ # Can't create main app - show error
213
+ await self._error_response(
214
+ send,
215
+ f"Setup complete but cannot start application: {self._main_app_error}"
216
+ )
217
+ else:
218
+ # Setup not complete - redirect to /setup
219
+ await self._redirect(send, "/setup")
220
+
221
+ async def _is_setup_complete_in_db(self) -> bool:
222
+ """Check if setup is complete in the database."""
223
+ db_url = self._db_url
224
+
225
+ # Try to get db_url dynamically if not set at startup
226
+ # (setup may have configured the database after server started)
227
+ if not db_url:
228
+ try:
229
+ from skrift.setup.state import get_database_url_from_yaml
230
+ db_url = get_database_url_from_yaml()
231
+ if db_url:
232
+ self._db_url = db_url # Cache for future requests
233
+ except Exception:
234
+ pass
235
+
236
+ if not db_url:
237
+ return False
238
+
239
+ try:
240
+ return await check_setup_in_db(db_url)
241
+ except Exception:
242
+ return False
243
+
244
+ async def _redirect(self, send: Send, location: str) -> None:
245
+ """Send a redirect response."""
246
+ await send({
247
+ "type": "http.response.start",
248
+ "status": 302,
249
+ "headers": [(b"location", location.encode()), (b"content-length", b"0")],
250
+ })
251
+ await send({"type": "http.response.body", "body": b""})
252
+
253
+ async def _error_response(self, send: Send, message: str) -> None:
254
+ """Send an error response."""
255
+ body = f"<h1>Application Error</h1><p>{message}</p><p>Please check your configuration and restart the server.</p>".encode()
256
+ await send({
257
+ "type": "http.response.start",
258
+ "status": 500,
259
+ "headers": [
260
+ (b"content-type", b"text/html"),
261
+ (b"content-length", str(len(body)).encode()),
262
+ ],
263
+ })
264
+ await send({"type": "http.response.body", "body": body})
265
+
266
+
267
+ def create_app() -> Litestar:
268
+ """Create and configure the main Litestar application.
269
+
270
+ This app has all routes for normal operation. It is used by the dispatcher
271
+ after setup is complete.
272
+ """
273
+ settings = get_settings()
274
+
275
+ # Load controllers from app.yaml
276
+ controllers = load_controllers()
277
+
278
+ # Database configuration
279
+ if "sqlite" in settings.db.url:
280
+ engine_config = EngineConfig(echo=settings.db.echo)
281
+ else:
282
+ engine_config = EngineConfig(
283
+ pool_size=settings.db.pool_size,
284
+ max_overflow=settings.db.pool_overflow,
285
+ pool_timeout=settings.db.pool_timeout,
286
+ echo=settings.db.echo,
287
+ )
288
+
289
+ db_config = SQLAlchemyAsyncConfig(
290
+ connection_string=settings.db.url,
291
+ metadata=Base.metadata,
292
+ create_all=False,
293
+ session_config=AsyncSessionConfig(expire_on_commit=False),
294
+ engine_config=engine_config,
295
+ )
296
+
297
+ # Session configuration (client-side encrypted cookies)
298
+ session_secret = hashlib.sha256(settings.secret_key.encode()).digest()
299
+ session_config = CookieBackendConfig(
300
+ secret=session_secret,
301
+ max_age=60 * 60 * 24 * 7, # 7 days
302
+ httponly=True,
303
+ secure=not settings.debug,
304
+ samesite="lax",
305
+ )
306
+
307
+ # Template configuration
308
+ template_dir = Path(__file__).parent / "templates"
309
+ template_config = TemplateConfig(
310
+ directory=template_dir,
311
+ engine=JinjaTemplateEngine,
312
+ engine_callback=lambda engine: engine.engine.globals.update({
313
+ "now": datetime.now,
314
+ "site_name": get_cached_site_name,
315
+ "site_tagline": get_cached_site_tagline,
316
+ "site_copyright_holder": get_cached_site_copyright_holder,
317
+ "site_copyright_start_year": get_cached_site_copyright_start_year,
318
+ }),
319
+ )
320
+
321
+ # Static files
322
+ static_files_router = create_static_files_router(
323
+ path="/static",
324
+ directories=[Path(__file__).parent / "static"],
325
+ )
326
+
327
+ from skrift.auth import sync_roles_to_database
328
+
329
+ async def on_startup(_app: Litestar) -> None:
330
+ """Sync roles and load site settings on startup."""
331
+ try:
332
+ async with db_config.get_session() as session:
333
+ await sync_roles_to_database(session)
334
+ await load_site_settings_cache(session)
335
+ except Exception:
336
+ # Database might not exist yet during setup
337
+ pass
338
+
339
+ return Litestar(
340
+ on_startup=[on_startup],
341
+ route_handlers=[*controllers, static_files_router],
342
+ plugins=[SQLAlchemyPlugin(config=db_config)],
343
+ middleware=[session_config.middleware],
344
+ template_config=template_config,
345
+ compression_config=CompressionConfig(backend="gzip"),
346
+ exception_handlers={
347
+ HTTPException: http_exception_handler,
348
+ Exception: internal_server_error_handler,
349
+ },
350
+ debug=settings.debug,
351
+ )
352
+
353
+
354
+ def create_setup_app() -> Litestar:
355
+ """Create an application for the setup wizard.
356
+
357
+ This app handles only setup routes (/setup/*, /auth/*, /static/*).
358
+ The AppDispatcher handles routing non-setup paths.
359
+ """
360
+ from pydantic_settings import BaseSettings
361
+ from skrift.setup.state import get_database_url_from_yaml
362
+
363
+ class MinimalSettings(BaseSettings):
364
+ debug: bool = True
365
+ secret_key: str = "setup-wizard-temporary-secret-key-change-me"
366
+
367
+ settings = MinimalSettings()
368
+
369
+ # Session configuration
370
+ session_secret = hashlib.sha256(settings.secret_key.encode()).digest()
371
+ session_config = CookieBackendConfig(
372
+ secret=session_secret,
373
+ max_age=60 * 60 * 24, # 1 day
374
+ httponly=True,
375
+ secure=False,
376
+ samesite="lax",
377
+ )
378
+
379
+ # Template configuration
380
+ template_dir = Path(__file__).parent / "templates"
381
+ template_config = TemplateConfig(
382
+ directory=template_dir,
383
+ engine=JinjaTemplateEngine,
384
+ engine_callback=lambda engine: engine.engine.globals.update({
385
+ "now": datetime.now,
386
+ "site_name": lambda: "Skrift",
387
+ "site_tagline": lambda: "Setup",
388
+ "site_copyright_holder": lambda: "",
389
+ "site_copyright_start_year": lambda: None,
390
+ }),
391
+ )
392
+
393
+ # Static files
394
+ static_files_router = create_static_files_router(
395
+ path="/static",
396
+ directories=[Path(__file__).parent / "static"],
397
+ )
398
+
399
+ # Import controllers
400
+ from skrift.setup.controller import SetupController, SetupAuthController
401
+
402
+ # Check if database is configured - if so, include SQLAlchemy
403
+ db_url = get_database_url_from_yaml()
404
+
405
+ # Also try to get the raw db URL from config (before env var resolution)
406
+ if not db_url:
407
+ config_path = Path.cwd() / "app.yaml"
408
+ if config_path.exists():
409
+ try:
410
+ with open(config_path, "r") as f:
411
+ raw_config = yaml.safe_load(f)
412
+ raw_db_url = raw_config.get("db", {}).get("url", "")
413
+ # If it's an env var reference but env var isn't set,
414
+ # check if there's a local SQLite fallback we can use
415
+ if raw_db_url.startswith("$"):
416
+ for db_file in ["./app.db", "./data.db", "./skrift.db"]:
417
+ if Path(db_file).exists():
418
+ db_url = f"sqlite+aiosqlite:///{db_file}"
419
+ break
420
+ except Exception:
421
+ pass
422
+
423
+ plugins = []
424
+ route_handlers = [SetupController, SetupAuthController, static_files_router]
425
+ db_config: SQLAlchemyAsyncConfig | None = None
426
+
427
+ if db_url:
428
+ # Database is configured, add SQLAlchemy plugin
429
+ if "sqlite" in db_url:
430
+ engine_config = EngineConfig(echo=False)
431
+ else:
432
+ engine_config = EngineConfig(
433
+ pool_size=5,
434
+ max_overflow=10,
435
+ pool_timeout=30,
436
+ echo=False,
437
+ )
438
+
439
+ db_config = SQLAlchemyAsyncConfig(
440
+ connection_string=db_url,
441
+ metadata=Base.metadata,
442
+ create_all=False,
443
+ session_config=AsyncSessionConfig(expire_on_commit=False),
444
+ engine_config=engine_config,
445
+ )
446
+ plugins.append(SQLAlchemyPlugin(config=db_config))
447
+
448
+ async def on_startup(_app: Litestar) -> None:
449
+ """Initialize setup state and sync roles if database is available."""
450
+ if db_config is not None:
451
+ try:
452
+ from skrift.auth import sync_roles_to_database
453
+ async with db_config.get_session() as session:
454
+ await sync_roles_to_database(session)
455
+ except Exception:
456
+ pass
457
+
458
+ return Litestar(
459
+ on_startup=[on_startup],
460
+ route_handlers=route_handlers,
461
+ plugins=plugins,
462
+ middleware=[session_config.middleware],
463
+ template_config=template_config,
464
+ compression_config=CompressionConfig(backend="gzip"),
465
+ exception_handlers={
466
+ HTTPException: http_exception_handler,
467
+ Exception: internal_server_error_handler,
468
+ },
469
+ debug=settings.debug,
470
+ )
471
+
472
+
473
+ async def check_setup_in_db(db_url: str) -> bool:
474
+ """Check if setup is complete by querying the database directly."""
475
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
476
+
477
+ engine = create_async_engine(db_url)
478
+ async_session = async_sessionmaker(engine, expire_on_commit=False)
479
+
480
+ try:
481
+ async with async_session() as session:
482
+ value = await get_setting(session, SETUP_COMPLETED_AT_KEY)
483
+ return value is not None
484
+ except Exception:
485
+ return False
486
+ finally:
487
+ await engine.dispose()
488
+
489
+
490
+ def create_dispatcher() -> ASGIApp:
491
+ """Create the ASGI app dispatcher.
492
+
493
+ This is the main entry point. The dispatcher handles routing between
494
+ setup and main apps, with lazy creation of the main app after setup completes.
495
+ """
496
+ global _dispatcher
497
+ from skrift.setup.state import get_database_url_from_yaml
498
+
499
+ # Get database URL first
500
+ db_url: str | None = None
501
+ try:
502
+ db_url = get_database_url_from_yaml()
503
+ except Exception:
504
+ pass
505
+
506
+ # Check if setup is already complete
507
+ initial_setup_complete = False
508
+ if db_url:
509
+ try:
510
+ initial_setup_complete = asyncio.get_event_loop().run_until_complete(
511
+ check_setup_in_db(db_url)
512
+ )
513
+ except RuntimeError:
514
+ # No running event loop, try creating one
515
+ try:
516
+ initial_setup_complete = asyncio.run(check_setup_in_db(db_url))
517
+ except Exception:
518
+ pass
519
+ except Exception:
520
+ pass
521
+
522
+ # Also check if config is valid
523
+ config_valid, _ = is_config_valid()
524
+
525
+ if initial_setup_complete and config_valid:
526
+ # Setup already done - just return the main app directly
527
+ return create_app()
528
+
529
+ # Try to create main app if config is valid
530
+ main_app: Litestar | None = None
531
+ if config_valid:
532
+ try:
533
+ main_app = create_app()
534
+ except Exception:
535
+ pass
536
+
537
+ # Always use dispatcher - it handles lazy main app creation
538
+ setup_app = create_setup_app()
539
+ dispatcher = AppDispatcher(setup_app=setup_app, db_url=db_url, main_app=main_app)
540
+ dispatcher.setup_locked = initial_setup_complete
541
+ _dispatcher = dispatcher # Store reference for later updates
542
+ return dispatcher
543
+
544
+
545
+ app = create_dispatcher()
@@ -0,0 +1,58 @@
1
+ """Authentication and authorization module."""
2
+
3
+ from skrift.auth.guards import (
4
+ ADMINISTRATOR_PERMISSION,
5
+ AndRequirement,
6
+ AuthRequirement,
7
+ OrRequirement,
8
+ Permission,
9
+ Role,
10
+ auth_guard,
11
+ )
12
+ from skrift.auth.roles import (
13
+ ADMIN,
14
+ AUTHOR,
15
+ EDITOR,
16
+ MODERATOR,
17
+ ROLE_DEFINITIONS,
18
+ RoleDefinition,
19
+ create_role,
20
+ get_role_definition,
21
+ register_role,
22
+ )
23
+ from skrift.auth.services import (
24
+ UserPermissions,
25
+ assign_role_to_user,
26
+ get_user_permissions,
27
+ invalidate_user_permissions_cache,
28
+ remove_role_from_user,
29
+ sync_roles_to_database,
30
+ )
31
+
32
+ __all__ = [
33
+ # Guards
34
+ "ADMINISTRATOR_PERMISSION",
35
+ "AndRequirement",
36
+ "AuthRequirement",
37
+ "OrRequirement",
38
+ "Permission",
39
+ "Role",
40
+ "auth_guard",
41
+ # Roles
42
+ "ADMIN",
43
+ "AUTHOR",
44
+ "EDITOR",
45
+ "MODERATOR",
46
+ "ROLE_DEFINITIONS",
47
+ "RoleDefinition",
48
+ "create_role",
49
+ "get_role_definition",
50
+ "register_role",
51
+ # Services
52
+ "UserPermissions",
53
+ "assign_role_to_user",
54
+ "get_user_permissions",
55
+ "invalidate_user_permissions_cache",
56
+ "remove_role_from_user",
57
+ "sync_roles_to_database",
58
+ ]