skrift 0.1.0a10__tar.gz → 0.1.0a11__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.
Files changed (74) hide show
  1. {skrift-0.1.0a10 → skrift-0.1.0a11}/PKG-INFO +1 -1
  2. {skrift-0.1.0a10 → skrift-0.1.0a11}/pyproject.toml +1 -1
  3. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/config.py +1 -0
  4. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/controllers/auth.py +68 -4
  5. {skrift-0.1.0a10 → skrift-0.1.0a11}/.gitignore +0 -0
  6. {skrift-0.1.0a10 → skrift-0.1.0a11}/README.md +0 -0
  7. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/__init__.py +0 -0
  8. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/__main__.py +0 -0
  9. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/admin/__init__.py +0 -0
  10. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/admin/controller.py +0 -0
  11. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/admin/navigation.py +0 -0
  12. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/env.py +0 -0
  13. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/script.py.mako +0 -0
  14. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +0 -0
  15. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +0 -0
  16. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +0 -0
  17. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +0 -0
  18. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260122_200000_add_settings_table.py +0 -0
  19. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260129_add_oauth_accounts.py +0 -0
  20. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260129_add_provider_metadata.py +0 -0
  21. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic.ini +0 -0
  22. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/asgi.py +0 -0
  23. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/auth/__init__.py +0 -0
  24. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/auth/guards.py +0 -0
  25. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/auth/roles.py +0 -0
  26. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/auth/services.py +0 -0
  27. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/cli.py +0 -0
  28. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/controllers/__init__.py +0 -0
  29. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/controllers/web.py +0 -0
  30. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/__init__.py +0 -0
  31. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/base.py +0 -0
  32. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/__init__.py +0 -0
  33. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/oauth_account.py +0 -0
  34. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/page.py +0 -0
  35. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/role.py +0 -0
  36. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/setting.py +0 -0
  37. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/user.py +0 -0
  38. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/services/__init__.py +0 -0
  39. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/services/oauth_service.py +0 -0
  40. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/services/page_service.py +0 -0
  41. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/services/setting_service.py +0 -0
  42. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/lib/__init__.py +0 -0
  43. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/lib/exceptions.py +0 -0
  44. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/lib/template.py +0 -0
  45. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/__init__.py +0 -0
  46. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/config_writer.py +0 -0
  47. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/controller.py +0 -0
  48. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/middleware.py +0 -0
  49. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/providers.py +0 -0
  50. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/state.py +0 -0
  51. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/static/css/style.css +0 -0
  52. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/admin.html +0 -0
  53. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/base.html +0 -0
  54. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/pages/edit.html +0 -0
  55. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/pages/list.html +0 -0
  56. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/settings/site.html +0 -0
  57. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/users/list.html +0 -0
  58. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/users/roles.html +0 -0
  59. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/auth/dummy_login.html +0 -0
  60. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/auth/login.html +0 -0
  61. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/base.html +0 -0
  62. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/error-404.html +0 -0
  63. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/error-500.html +0 -0
  64. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/error.html +0 -0
  65. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/index.html +0 -0
  66. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/page.html +0 -0
  67. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/admin.html +0 -0
  68. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/auth.html +0 -0
  69. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/base.html +0 -0
  70. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/complete.html +0 -0
  71. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/configuring.html +0 -0
  72. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/database.html +0 -0
  73. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/restart.html +0 -0
  74. {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/site.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skrift
3
- Version: 0.1.0a10
3
+ Version: 0.1.0a11
4
4
  Summary: A lightweight async Python CMS for crafting modern websites
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: advanced-alchemy>=0.26.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "skrift"
3
- version = "0.1.0a10"
3
+ version = "0.1.0a11"
4
4
  description = "A lightweight async Python CMS for crafting modern websites"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -137,6 +137,7 @@ class AuthConfig(BaseModel):
137
137
  """Authentication configuration."""
138
138
 
139
139
  redirect_base_url: str = "http://localhost:8000"
140
+ allowed_redirect_domains: list[str] = []
140
141
  providers: dict[str, ProviderConfig] = {}
141
142
 
142
143
  @classmethod
@@ -5,11 +5,12 @@ Also supports a development-only "dummy" provider for testing.
5
5
  """
6
6
 
7
7
  import base64
8
+ import fnmatch
8
9
  import hashlib
9
10
  import secrets
10
11
  from datetime import UTC, datetime
11
12
  from typing import Annotated
12
- from urllib.parse import urlencode
13
+ from urllib.parse import urlencode, urlparse
13
14
 
14
15
  import httpx
15
16
  from litestar import Controller, Request, get, post
@@ -26,6 +27,56 @@ from skrift.db.models.user import User
26
27
  from skrift.setup.providers import DUMMY_PROVIDER_KEY, OAUTH_PROVIDERS, get_provider_info
27
28
 
28
29
 
30
+ def _is_safe_redirect_url(url: str, allowed_domains: list[str]) -> bool:
31
+ """Check if URL is safe to redirect to.
32
+
33
+ Supports wildcard patterns using fnmatch-style matching:
34
+ - "*.example.com" matches any subdomain of example.com
35
+ - "app-*.example.com" matches app-foo.example.com, app-bar.example.com, etc.
36
+ - "example.com" (no wildcards) matches example.com and all subdomains
37
+ """
38
+ # Relative paths are always safe (but not protocol-relative //domain.com)
39
+ if url.startswith("/") and not url.startswith("//"):
40
+ return True
41
+
42
+ # Parse absolute URL
43
+ try:
44
+ parsed = urlparse(url)
45
+ except Exception:
46
+ return False
47
+
48
+ # Must have scheme and netloc
49
+ if not parsed.scheme or not parsed.netloc:
50
+ return False
51
+
52
+ # Only allow http/https
53
+ if parsed.scheme not in ("http", "https"):
54
+ return False
55
+
56
+ # Check if domain matches allowed list
57
+ host = parsed.netloc.lower().split(":")[0] # Remove port
58
+ for pattern in allowed_domains:
59
+ pattern = pattern.lower()
60
+ # If pattern contains wildcards, use fnmatch
61
+ if "*" in pattern or "?" in pattern:
62
+ if fnmatch.fnmatch(host, pattern):
63
+ return True
64
+ else:
65
+ # No wildcards: exact match or subdomain match
66
+ if host == pattern or host.endswith(f".{pattern}"):
67
+ return True
68
+
69
+ return False
70
+
71
+
72
+ def _get_safe_redirect_url(request: Request, allowed_domains: list[str], default: str = "/") -> str:
73
+ """Get the next redirect URL from session, validating it's safe."""
74
+ next_url = request.session.pop("auth_next", None)
75
+ if next_url and _is_safe_redirect_url(next_url, allowed_domains):
76
+ return next_url
77
+ return default
78
+
79
+
29
80
  def get_auth_url(provider: str, settings, state: str, code_challenge: str | None = None) -> str:
30
81
  """Build the OAuth authorization URL for a provider."""
31
82
  provider_info = get_provider_info(provider)
@@ -230,11 +281,16 @@ class AuthController(Controller):
230
281
  self,
231
282
  request: Request,
232
283
  provider: str,
284
+ next_url: Annotated[str | None, Parameter(query="next")] = None,
233
285
  ) -> Redirect | TemplateResponse:
234
286
  """Redirect to OAuth provider consent screen, or show dummy login form."""
235
287
  settings = get_settings()
236
288
  provider_info = get_provider_info(provider)
237
289
 
290
+ # Store next URL in session if provided and valid
291
+ if next_url and _is_safe_redirect_url(next_url, settings.auth.allowed_redirect_domains):
292
+ request.session["auth_next"] = next_url
293
+
238
294
  if not provider_info:
239
295
  raise NotFoundException(f"Unknown provider: {provider}")
240
296
 
@@ -392,14 +448,22 @@ class AuthController(Controller):
392
448
  request.session["user_picture_url"] = user.picture_url
393
449
  request.session["flash"] = "Successfully logged in!"
394
450
 
395
- return Redirect(path="/")
451
+ return Redirect(path=_get_safe_redirect_url(request, settings.auth.allowed_redirect_domains))
396
452
 
397
453
  @get("/login")
398
- async def login_page(self, request: Request) -> TemplateResponse:
454
+ async def login_page(
455
+ self,
456
+ request: Request,
457
+ next_url: Annotated[str | None, Parameter(query="next")] = None,
458
+ ) -> TemplateResponse:
399
459
  """Show login page with available providers."""
400
460
  flash = request.session.pop("flash", None)
401
461
  settings = get_settings()
402
462
 
463
+ # Store next URL in session if provided and valid
464
+ if next_url and _is_safe_redirect_url(next_url, settings.auth.allowed_redirect_domains):
465
+ request.session["auth_next"] = next_url
466
+
403
467
  # Get configured providers (excluding dummy from main list)
404
468
  configured_providers = list(settings.auth.providers.keys())
405
469
  providers = {
@@ -522,7 +586,7 @@ class AuthController(Controller):
522
586
  request.session["user_picture_url"] = user.picture_url
523
587
  request.session["flash"] = "Successfully logged in!"
524
588
 
525
- return Redirect(path="/")
589
+ return Redirect(path=_get_safe_redirect_url(request, settings.auth.allowed_redirect_domains))
526
590
 
527
591
  @get("/logout")
528
592
  async def logout(self, request: Request) -> Redirect:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes