skrift 0.1.0a1__py3-none-any.whl → 0.1.0a2__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.
- skrift/asgi.py +19 -11
- skrift/config.py +59 -5
- skrift/controllers/auth.py +84 -6
- skrift/setup/config_writer.py +4 -2
- skrift/setup/providers.py +53 -2
- skrift/setup/state.py +3 -2
- skrift/static/css/style.css +3 -3
- skrift/templates/auth/dummy_login.html +102 -0
- skrift/templates/auth/login.html +14 -0
- {skrift-0.1.0a1.dist-info → skrift-0.1.0a2.dist-info}/METADATA +3 -1
- {skrift-0.1.0a1.dist-info → skrift-0.1.0a2.dist-info}/RECORD +13 -12
- {skrift-0.1.0a1.dist-info → skrift-0.1.0a2.dist-info}/WHEEL +0 -0
- {skrift-0.1.0a1.dist-info → skrift-0.1.0a2.dist-info}/entry_points.txt +0 -0
skrift/asgi.py
CHANGED
|
@@ -29,7 +29,7 @@ from litestar.static_files import create_static_files_router
|
|
|
29
29
|
from litestar.template import TemplateConfig
|
|
30
30
|
from litestar.types import ASGIApp, Receive, Scope, Send
|
|
31
31
|
|
|
32
|
-
from skrift.config import get_settings, is_config_valid
|
|
32
|
+
from skrift.config import get_config_path, get_settings, is_config_valid
|
|
33
33
|
from skrift.db.base import Base
|
|
34
34
|
from skrift.db.services.setting_service import (
|
|
35
35
|
load_site_settings_cache,
|
|
@@ -45,7 +45,7 @@ from skrift.lib.exceptions import http_exception_handler, internal_server_error_
|
|
|
45
45
|
|
|
46
46
|
def load_controllers() -> list:
|
|
47
47
|
"""Load controllers from app.yaml configuration."""
|
|
48
|
-
config_path =
|
|
48
|
+
config_path = get_config_path()
|
|
49
49
|
|
|
50
50
|
if not config_path.exists():
|
|
51
51
|
return []
|
|
@@ -196,12 +196,7 @@ class AppDispatcher:
|
|
|
196
196
|
await self.setup_app(scope, receive, send)
|
|
197
197
|
return
|
|
198
198
|
|
|
199
|
-
#
|
|
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
|
|
199
|
+
# Check if setup is complete in DB
|
|
205
200
|
if await self._is_setup_complete_in_db():
|
|
206
201
|
# Setup complete - try to get/create main app
|
|
207
202
|
main_app = await self._get_or_create_main_app()
|
|
@@ -215,8 +210,13 @@ class AppDispatcher:
|
|
|
215
210
|
f"Setup complete but cannot start application: {self._main_app_error}"
|
|
216
211
|
)
|
|
217
212
|
else:
|
|
218
|
-
# Setup not complete
|
|
219
|
-
|
|
213
|
+
# Setup not complete
|
|
214
|
+
# Route /auth/* to setup app for OAuth callbacks during setup
|
|
215
|
+
if path.startswith("/auth"):
|
|
216
|
+
await self.setup_app(scope, receive, send)
|
|
217
|
+
else:
|
|
218
|
+
# Redirect other paths to /setup
|
|
219
|
+
await self._redirect(send, "/setup")
|
|
220
220
|
|
|
221
221
|
async def _is_setup_complete_in_db(self) -> bool:
|
|
222
222
|
"""Check if setup is complete in the database."""
|
|
@@ -270,6 +270,10 @@ def create_app() -> Litestar:
|
|
|
270
270
|
This app has all routes for normal operation. It is used by the dispatcher
|
|
271
271
|
after setup is complete.
|
|
272
272
|
"""
|
|
273
|
+
# CRITICAL: Check for dummy auth in production BEFORE anything else
|
|
274
|
+
from skrift.setup.providers import validate_no_dummy_auth_in_production
|
|
275
|
+
validate_no_dummy_auth_in_production()
|
|
276
|
+
|
|
273
277
|
settings = get_settings()
|
|
274
278
|
|
|
275
279
|
# Load controllers from app.yaml
|
|
@@ -404,7 +408,7 @@ def create_setup_app() -> Litestar:
|
|
|
404
408
|
|
|
405
409
|
# Also try to get the raw db URL from config (before env var resolution)
|
|
406
410
|
if not db_url:
|
|
407
|
-
config_path =
|
|
411
|
+
config_path = get_config_path()
|
|
408
412
|
if config_path.exists():
|
|
409
413
|
try:
|
|
410
414
|
with open(config_path, "r") as f:
|
|
@@ -493,6 +497,10 @@ def create_dispatcher() -> ASGIApp:
|
|
|
493
497
|
This is the main entry point. The dispatcher handles routing between
|
|
494
498
|
setup and main apps, with lazy creation of the main app after setup completes.
|
|
495
499
|
"""
|
|
500
|
+
# CRITICAL: Check for dummy auth in production BEFORE anything else
|
|
501
|
+
from skrift.setup.providers import validate_no_dummy_auth_in_production
|
|
502
|
+
validate_no_dummy_auth_in_production()
|
|
503
|
+
|
|
496
504
|
global _dispatcher
|
|
497
505
|
from skrift.setup.state import get_database_url_from_yaml
|
|
498
506
|
|
skrift/config.py
CHANGED
|
@@ -16,6 +16,31 @@ load_dotenv(_env_file)
|
|
|
16
16
|
# Pattern to match $VAR_NAME environment variable references
|
|
17
17
|
ENV_VAR_PATTERN = re.compile(r"\$([A-Z_][A-Z0-9_]*)")
|
|
18
18
|
|
|
19
|
+
# Environment configuration
|
|
20
|
+
SKRIFT_ENV = "SKRIFT_ENV"
|
|
21
|
+
DEFAULT_ENVIRONMENT = "production"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_environment() -> str:
|
|
25
|
+
"""Get the current environment name, normalized to lowercase.
|
|
26
|
+
|
|
27
|
+
Reads from SKRIFT_ENV environment variable. Defaults to "production".
|
|
28
|
+
"""
|
|
29
|
+
env = os.environ.get(SKRIFT_ENV, DEFAULT_ENVIRONMENT)
|
|
30
|
+
return env.lower().strip()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_config_path() -> Path:
|
|
34
|
+
"""Get the path to the environment-specific config file.
|
|
35
|
+
|
|
36
|
+
Production -> app.yaml
|
|
37
|
+
Other envs -> app.{env}.yaml (e.g., app.dev.yaml)
|
|
38
|
+
"""
|
|
39
|
+
env = get_environment()
|
|
40
|
+
if env == "production":
|
|
41
|
+
return Path.cwd() / "app.yaml"
|
|
42
|
+
return Path.cwd() / f"app.{env}.yaml"
|
|
43
|
+
|
|
19
44
|
|
|
20
45
|
def interpolate_env_vars(value, strict: bool = True):
|
|
21
46
|
"""Recursively replace $VAR_NAME with os.environ values.
|
|
@@ -54,10 +79,10 @@ def load_app_config(interpolate: bool = True, strict: bool = True) -> dict:
|
|
|
54
79
|
Returns:
|
|
55
80
|
Parsed configuration dictionary
|
|
56
81
|
"""
|
|
57
|
-
config_path =
|
|
82
|
+
config_path = get_config_path()
|
|
58
83
|
|
|
59
84
|
if not config_path.exists():
|
|
60
|
-
raise FileNotFoundError(f"
|
|
85
|
+
raise FileNotFoundError(f"{config_path.name} not found at {config_path}")
|
|
61
86
|
|
|
62
87
|
with open(config_path, "r") as f:
|
|
63
88
|
config = yaml.safe_load(f)
|
|
@@ -69,7 +94,7 @@ def load_app_config(interpolate: bool = True, strict: bool = True) -> dict:
|
|
|
69
94
|
|
|
70
95
|
def load_raw_app_config() -> dict | None:
|
|
71
96
|
"""Load app.yaml without any processing. Returns None if file doesn't exist."""
|
|
72
|
-
config_path =
|
|
97
|
+
config_path = get_config_path()
|
|
73
98
|
|
|
74
99
|
if not config_path.exists():
|
|
75
100
|
return None
|
|
@@ -98,11 +123,40 @@ class OAuthProviderConfig(BaseModel):
|
|
|
98
123
|
tenant_id: str | None = None
|
|
99
124
|
|
|
100
125
|
|
|
126
|
+
class DummyProviderConfig(BaseModel):
|
|
127
|
+
"""Dummy provider configuration (no credentials required)."""
|
|
128
|
+
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Union type for provider configs - dummy has no required fields
|
|
133
|
+
ProviderConfig = OAuthProviderConfig | DummyProviderConfig
|
|
134
|
+
|
|
135
|
+
|
|
101
136
|
class AuthConfig(BaseModel):
|
|
102
137
|
"""Authentication configuration."""
|
|
103
138
|
|
|
104
139
|
redirect_base_url: str = "http://localhost:8000"
|
|
105
|
-
providers: dict[str,
|
|
140
|
+
providers: dict[str, ProviderConfig] = {}
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def _parse_provider(cls, name: str, config: dict) -> ProviderConfig:
|
|
144
|
+
"""Parse a provider config, using the appropriate model based on provider name."""
|
|
145
|
+
if name == "dummy":
|
|
146
|
+
return DummyProviderConfig(**config)
|
|
147
|
+
return OAuthProviderConfig(**config)
|
|
148
|
+
|
|
149
|
+
def __init__(self, **data):
|
|
150
|
+
# Convert raw provider dicts to appropriate config objects
|
|
151
|
+
if "providers" in data and isinstance(data["providers"], dict):
|
|
152
|
+
parsed_providers = {}
|
|
153
|
+
for name, config in data["providers"].items():
|
|
154
|
+
if isinstance(config, dict):
|
|
155
|
+
parsed_providers[name] = self._parse_provider(name, config)
|
|
156
|
+
else:
|
|
157
|
+
parsed_providers[name] = config
|
|
158
|
+
data["providers"] = parsed_providers
|
|
159
|
+
super().__init__(**data)
|
|
106
160
|
|
|
107
161
|
def get_redirect_uri(self, provider: str) -> str:
|
|
108
162
|
"""Get the OAuth callback URL for a provider."""
|
|
@@ -137,7 +191,7 @@ def is_config_valid() -> tuple[bool, str | None]:
|
|
|
137
191
|
try:
|
|
138
192
|
config = load_raw_app_config()
|
|
139
193
|
if config is None:
|
|
140
|
-
return False, "
|
|
194
|
+
return False, f"{get_config_path().name} not found"
|
|
141
195
|
|
|
142
196
|
# Check database URL
|
|
143
197
|
db_config = config.get("db", {})
|
skrift/controllers/auth.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Authentication controller for OAuth login flows.
|
|
2
2
|
|
|
3
3
|
Supports multiple OAuth providers: Google, GitHub, Microsoft, Discord, Facebook, X (Twitter).
|
|
4
|
+
Also supports a development-only "dummy" provider for testing.
|
|
4
5
|
"""
|
|
5
6
|
|
|
6
7
|
import base64
|
|
@@ -11,7 +12,7 @@ from typing import Annotated
|
|
|
11
12
|
from urllib.parse import urlencode
|
|
12
13
|
|
|
13
14
|
import httpx
|
|
14
|
-
from litestar import Controller, Request, get
|
|
15
|
+
from litestar import Controller, Request, get, post
|
|
15
16
|
from litestar.exceptions import HTTPException, NotFoundException
|
|
16
17
|
from litestar.params import Parameter
|
|
17
18
|
from litestar.response import Redirect, Template as TemplateResponse
|
|
@@ -20,7 +21,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
20
21
|
|
|
21
22
|
from skrift.config import get_settings
|
|
22
23
|
from skrift.db.models.user import User
|
|
23
|
-
from skrift.setup.providers import OAUTH_PROVIDERS, get_provider_info
|
|
24
|
+
from skrift.setup.providers import DUMMY_PROVIDER_KEY, OAUTH_PROVIDERS, get_provider_info
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def get_auth_url(provider: str, settings, state: str, code_challenge: str | None = None) -> str:
|
|
@@ -227,8 +228,8 @@ class AuthController(Controller):
|
|
|
227
228
|
self,
|
|
228
229
|
request: Request,
|
|
229
230
|
provider: str,
|
|
230
|
-
) -> Redirect:
|
|
231
|
-
"""Redirect to OAuth provider consent screen."""
|
|
231
|
+
) -> Redirect | TemplateResponse:
|
|
232
|
+
"""Redirect to OAuth provider consent screen, or show dummy login form."""
|
|
232
233
|
settings = get_settings()
|
|
233
234
|
provider_info = get_provider_info(provider)
|
|
234
235
|
|
|
@@ -238,6 +239,14 @@ class AuthController(Controller):
|
|
|
238
239
|
if provider not in settings.auth.providers:
|
|
239
240
|
raise NotFoundException(f"Provider {provider} not configured")
|
|
240
241
|
|
|
242
|
+
# Dummy provider shows local login form instead of redirecting to OAuth
|
|
243
|
+
if provider == DUMMY_PROVIDER_KEY:
|
|
244
|
+
flash = request.session.pop("flash", None)
|
|
245
|
+
return TemplateResponse(
|
|
246
|
+
"auth/dummy_login.html",
|
|
247
|
+
context={"flash": flash},
|
|
248
|
+
)
|
|
249
|
+
|
|
241
250
|
# Generate CSRF state token
|
|
242
251
|
state = secrets.token_urlsafe(32)
|
|
243
252
|
request.session["oauth_state"] = state
|
|
@@ -348,22 +357,91 @@ class AuthController(Controller):
|
|
|
348
357
|
flash = request.session.pop("flash", None)
|
|
349
358
|
settings = get_settings()
|
|
350
359
|
|
|
351
|
-
# Get configured providers
|
|
360
|
+
# Get configured providers (excluding dummy from main list)
|
|
352
361
|
configured_providers = list(settings.auth.providers.keys())
|
|
353
362
|
providers = {
|
|
354
363
|
key: OAUTH_PROVIDERS[key]
|
|
355
364
|
for key in configured_providers
|
|
356
|
-
if key in OAUTH_PROVIDERS
|
|
365
|
+
if key in OAUTH_PROVIDERS and key != DUMMY_PROVIDER_KEY
|
|
357
366
|
}
|
|
358
367
|
|
|
368
|
+
# Check if dummy provider is configured
|
|
369
|
+
has_dummy = DUMMY_PROVIDER_KEY in settings.auth.providers
|
|
370
|
+
|
|
359
371
|
return TemplateResponse(
|
|
360
372
|
"auth/login.html",
|
|
361
373
|
context={
|
|
362
374
|
"flash": flash,
|
|
363
375
|
"providers": providers,
|
|
376
|
+
"has_dummy": has_dummy,
|
|
364
377
|
},
|
|
365
378
|
)
|
|
366
379
|
|
|
380
|
+
@post("/dummy-login")
|
|
381
|
+
async def dummy_login_submit(
|
|
382
|
+
self,
|
|
383
|
+
request: Request,
|
|
384
|
+
db_session: AsyncSession,
|
|
385
|
+
) -> Redirect:
|
|
386
|
+
"""Process dummy login form submission."""
|
|
387
|
+
settings = get_settings()
|
|
388
|
+
|
|
389
|
+
if DUMMY_PROVIDER_KEY not in settings.auth.providers:
|
|
390
|
+
raise NotFoundException("Dummy provider not configured")
|
|
391
|
+
|
|
392
|
+
# Parse form data from request
|
|
393
|
+
form_data = await request.form()
|
|
394
|
+
email = form_data.get("email", "").strip()
|
|
395
|
+
name = form_data.get("name", "").strip()
|
|
396
|
+
|
|
397
|
+
if not email:
|
|
398
|
+
request.session["flash"] = "Email is required"
|
|
399
|
+
return Redirect(path="/auth/dummy/login")
|
|
400
|
+
|
|
401
|
+
# Default name to email username if not provided
|
|
402
|
+
if not name:
|
|
403
|
+
name = email.split("@")[0]
|
|
404
|
+
|
|
405
|
+
# Generate deterministic oauth_id from email
|
|
406
|
+
oauth_id = f"dummy_{hashlib.sha256(email.encode()).hexdigest()[:16]}"
|
|
407
|
+
|
|
408
|
+
# Find or create user
|
|
409
|
+
result = await db_session.execute(
|
|
410
|
+
select(User).where(
|
|
411
|
+
User.oauth_id == oauth_id,
|
|
412
|
+
User.oauth_provider == DUMMY_PROVIDER_KEY,
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
user = result.scalar_one_or_none()
|
|
416
|
+
|
|
417
|
+
if user:
|
|
418
|
+
# Update existing user
|
|
419
|
+
user.name = name
|
|
420
|
+
user.email = email
|
|
421
|
+
user.last_login_at = datetime.now(UTC)
|
|
422
|
+
else:
|
|
423
|
+
# Create new user
|
|
424
|
+
user = User(
|
|
425
|
+
oauth_provider=DUMMY_PROVIDER_KEY,
|
|
426
|
+
oauth_id=oauth_id,
|
|
427
|
+
email=email,
|
|
428
|
+
name=name,
|
|
429
|
+
last_login_at=datetime.now(UTC),
|
|
430
|
+
)
|
|
431
|
+
db_session.add(user)
|
|
432
|
+
await db_session.flush()
|
|
433
|
+
|
|
434
|
+
await db_session.commit()
|
|
435
|
+
|
|
436
|
+
# Set session with user info
|
|
437
|
+
request.session["user_id"] = str(user.id)
|
|
438
|
+
request.session["user_name"] = user.name
|
|
439
|
+
request.session["user_email"] = user.email
|
|
440
|
+
request.session["user_picture_url"] = user.picture_url
|
|
441
|
+
request.session["flash"] = "Successfully logged in!"
|
|
442
|
+
|
|
443
|
+
return Redirect(path="/")
|
|
444
|
+
|
|
367
445
|
@get("/logout")
|
|
368
446
|
async def logout(self, request: Request) -> Redirect:
|
|
369
447
|
"""Clear session and redirect to home."""
|
skrift/setup/config_writer.py
CHANGED
|
@@ -7,6 +7,8 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from ruamel.yaml import YAML
|
|
9
9
|
|
|
10
|
+
from skrift.config import get_config_path as _get_config_path
|
|
11
|
+
|
|
10
12
|
# Default app.yaml structure
|
|
11
13
|
DEFAULT_CONFIG = {
|
|
12
14
|
"controllers": [
|
|
@@ -29,8 +31,8 @@ DEFAULT_CONFIG = {
|
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
def get_config_path() -> Path:
|
|
32
|
-
"""Get the path to
|
|
33
|
-
return
|
|
34
|
+
"""Get the path to the current environment's config file."""
|
|
35
|
+
return _get_config_path()
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def backup_config() -> Path | None:
|
skrift/setup/providers.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
|
+
DUMMY_PROVIDER_KEY = "dummy"
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
@dataclass
|
|
7
9
|
class OAuthProviderInfo:
|
|
@@ -150,6 +152,17 @@ OAUTH_PROVIDERS = {
|
|
|
150
152
|
""".strip(),
|
|
151
153
|
icon="twitter",
|
|
152
154
|
),
|
|
155
|
+
DUMMY_PROVIDER_KEY: OAuthProviderInfo(
|
|
156
|
+
name="Dummy (Development Only)",
|
|
157
|
+
auth_url="",
|
|
158
|
+
token_url="",
|
|
159
|
+
userinfo_url="",
|
|
160
|
+
scopes=[],
|
|
161
|
+
console_url="",
|
|
162
|
+
fields=[],
|
|
163
|
+
instructions="Development-only provider. DO NOT use in production.",
|
|
164
|
+
icon="dummy",
|
|
165
|
+
),
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
|
|
@@ -159,5 +172,43 @@ def get_provider_info(provider: str) -> OAuthProviderInfo | None:
|
|
|
159
172
|
|
|
160
173
|
|
|
161
174
|
def get_all_providers() -> dict[str, OAuthProviderInfo]:
|
|
162
|
-
"""Get all available OAuth providers."""
|
|
163
|
-
return OAUTH_PROVIDERS.
|
|
175
|
+
"""Get all available OAuth providers (excluding dev-only providers)."""
|
|
176
|
+
return {k: v for k, v in OAUTH_PROVIDERS.items() if k != DUMMY_PROVIDER_KEY}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def validate_no_dummy_auth_in_production() -> None:
|
|
180
|
+
"""Exit process if dummy auth is configured in production."""
|
|
181
|
+
import os
|
|
182
|
+
import signal
|
|
183
|
+
import sys
|
|
184
|
+
|
|
185
|
+
from skrift.config import get_environment, load_raw_app_config
|
|
186
|
+
|
|
187
|
+
if get_environment() != "production":
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
config = load_raw_app_config()
|
|
191
|
+
if config is None:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
providers = config.get("auth", {}).get("providers", {})
|
|
195
|
+
if DUMMY_PROVIDER_KEY in providers:
|
|
196
|
+
# Only print if we haven't already (use env var as cross-process flag)
|
|
197
|
+
if not os.environ.get("_SKRIFT_DUMMY_ERROR_PRINTED"):
|
|
198
|
+
os.environ["_SKRIFT_DUMMY_ERROR_PRINTED"] = "1"
|
|
199
|
+
sys.stderr.write(
|
|
200
|
+
"\n"
|
|
201
|
+
"======================================================================\n"
|
|
202
|
+
"SECURITY ERROR: Dummy auth provider is configured in production.\n"
|
|
203
|
+
"Remove 'dummy' from auth.providers in app.yaml.\n"
|
|
204
|
+
"Server will NOT start.\n"
|
|
205
|
+
"======================================================================\n\n"
|
|
206
|
+
)
|
|
207
|
+
sys.stderr.flush()
|
|
208
|
+
|
|
209
|
+
# Kill parent process (uvicorn reloader) to stop respawning
|
|
210
|
+
try:
|
|
211
|
+
os.kill(os.getppid(), signal.SIGTERM)
|
|
212
|
+
except (ProcessLookupError, PermissionError):
|
|
213
|
+
pass
|
|
214
|
+
os._exit(1)
|
skrift/setup/state.py
CHANGED
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from sqlalchemy import text
|
|
13
13
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
14
14
|
|
|
15
|
+
from skrift.config import get_config_path
|
|
15
16
|
from skrift.db.services.setting_service import SETUP_COMPLETED_AT_KEY, get_setting
|
|
16
17
|
|
|
17
18
|
|
|
@@ -27,7 +28,7 @@ class SetupStep(Enum):
|
|
|
27
28
|
|
|
28
29
|
def app_yaml_exists() -> bool:
|
|
29
30
|
"""Check if app.yaml exists in the current working directory."""
|
|
30
|
-
return (
|
|
31
|
+
return get_config_path().exists()
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def get_database_url_from_yaml() -> str | None:
|
|
@@ -38,7 +39,7 @@ def get_database_url_from_yaml() -> str | None:
|
|
|
38
39
|
"""
|
|
39
40
|
import yaml
|
|
40
41
|
|
|
41
|
-
config_path =
|
|
42
|
+
config_path = get_config_path()
|
|
42
43
|
if not config_path.exists():
|
|
43
44
|
return None
|
|
44
45
|
|
skrift/static/css/style.css
CHANGED
|
@@ -175,7 +175,7 @@ a {
|
|
|
175
175
|
position: relative;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
a:not([role="button"])::after {
|
|
178
|
+
a:not([role="button"]):not(.oauth-btn)::after {
|
|
179
179
|
content: '';
|
|
180
180
|
position: absolute;
|
|
181
181
|
left: 0;
|
|
@@ -188,11 +188,11 @@ a:not([role="button"])::after {
|
|
|
188
188
|
transition: transform 500ms ease-out;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
a:hover {
|
|
191
|
+
a:not([role="button"]):not(.oauth-btn):hover {
|
|
192
192
|
color: var(--color-primary-hover);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
a:not([role="button"]):hover::after {
|
|
195
|
+
a:not([role="button"]):not(.oauth-btn):hover::after {
|
|
196
196
|
transform: scaleX(1);
|
|
197
197
|
transform-origin: left;
|
|
198
198
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Dummy Login (Dev) - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block head %}
|
|
6
|
+
<style>
|
|
7
|
+
.login-container {
|
|
8
|
+
max-width: 400px;
|
|
9
|
+
margin: 2rem auto;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.warning-banner {
|
|
13
|
+
background: #fef3c7;
|
|
14
|
+
border: 1px solid #f59e0b;
|
|
15
|
+
color: #92400e;
|
|
16
|
+
padding: 1rem;
|
|
17
|
+
border-radius: 0.5rem;
|
|
18
|
+
margin-bottom: 1.5rem;
|
|
19
|
+
text-align: center;
|
|
20
|
+
font-weight: 600;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.form-group {
|
|
24
|
+
margin-bottom: 1rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.form-group label {
|
|
28
|
+
display: block;
|
|
29
|
+
margin-bottom: 0.5rem;
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.form-group input {
|
|
34
|
+
width: 100%;
|
|
35
|
+
padding: 0.75rem;
|
|
36
|
+
border: 1px solid var(--color-border, #ccc);
|
|
37
|
+
border-radius: 0.5rem;
|
|
38
|
+
font-size: 1rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.form-group small {
|
|
42
|
+
display: block;
|
|
43
|
+
margin-top: 0.25rem;
|
|
44
|
+
color: var(--color-text-muted, #666);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.submit-btn {
|
|
48
|
+
width: 100%;
|
|
49
|
+
padding: 1rem;
|
|
50
|
+
background: #6b7280;
|
|
51
|
+
color: white;
|
|
52
|
+
border: none;
|
|
53
|
+
border-radius: 0.5rem;
|
|
54
|
+
font-size: 1rem;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
margin-top: 1rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.submit-btn:hover {
|
|
61
|
+
background: #4b5563;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.back-link {
|
|
65
|
+
display: block;
|
|
66
|
+
text-align: center;
|
|
67
|
+
margin-top: 1.5rem;
|
|
68
|
+
color: var(--color-text-muted, #666);
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
71
|
+
{% endblock %}
|
|
72
|
+
|
|
73
|
+
{% block content %}
|
|
74
|
+
<div class="login-container">
|
|
75
|
+
<article>
|
|
76
|
+
<header>
|
|
77
|
+
<h1>Dummy Login</h1>
|
|
78
|
+
</header>
|
|
79
|
+
|
|
80
|
+
<div class="warning-banner">
|
|
81
|
+
DEVELOPMENT ONLY - Not for production use
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<form method="post" action="/auth/dummy-login">
|
|
85
|
+
<div class="form-group">
|
|
86
|
+
<label for="email">Email</label>
|
|
87
|
+
<input type="email" id="email" name="email" required placeholder="test@example.com">
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="form-group">
|
|
91
|
+
<label for="name">Name</label>
|
|
92
|
+
<input type="text" id="name" name="name" placeholder="Test User">
|
|
93
|
+
<small>Optional. Defaults to email username if not provided.</small>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<button type="submit" class="submit-btn">Login</button>
|
|
97
|
+
</form>
|
|
98
|
+
|
|
99
|
+
<a href="/auth/login" class="back-link">← Back to login options</a>
|
|
100
|
+
</article>
|
|
101
|
+
</div>
|
|
102
|
+
{% endblock %}
|
skrift/templates/auth/login.html
CHANGED
|
@@ -120,6 +120,20 @@
|
|
|
120
120
|
<p>No authentication providers are configured. Please contact the site administrator.</p>
|
|
121
121
|
{% endfor %}
|
|
122
122
|
</div>
|
|
123
|
+
|
|
124
|
+
{% if has_dummy %}
|
|
125
|
+
<div style="margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--color-border, #ccc);">
|
|
126
|
+
<p style="text-align: center; color: var(--color-text-muted, #666); font-size: 0.875rem; margin-bottom: 1rem;">
|
|
127
|
+
Development Only
|
|
128
|
+
</p>
|
|
129
|
+
<a href="/auth/dummy/login" class="oauth-btn" style="background: #6b7280; color: white;">
|
|
130
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
131
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/>
|
|
132
|
+
</svg>
|
|
133
|
+
Dummy Login (Dev)
|
|
134
|
+
</a>
|
|
135
|
+
</div>
|
|
136
|
+
{% endif %}
|
|
123
137
|
</article>
|
|
124
138
|
</div>
|
|
125
139
|
{% endblock %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skrift
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a2
|
|
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
|
|
@@ -15,6 +15,8 @@ Requires-Dist: pyyaml>=6.0.0
|
|
|
15
15
|
Requires-Dist: ruamel-yaml>=0.18.0
|
|
16
16
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.36
|
|
17
17
|
Requires-Dist: uvicorn>=0.34.0
|
|
18
|
+
Provides-Extra: docs
|
|
19
|
+
Requires-Dist: zensical>=0.0.19; extra == 'docs'
|
|
18
20
|
Description-Content-Type: text/markdown
|
|
19
21
|
|
|
20
22
|
# Skrift
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
skrift/__init__.py,sha256=eXE5PFVkJpH5XsV_ZlrTIeFPUPrmcHYAj4GpRS3R5PY,29
|
|
2
2
|
skrift/__main__.py,sha256=Fs17xxkqTjZpJn9MMC6pzkW4sOzNhV5RGLsA2ONPFww,271
|
|
3
3
|
skrift/alembic.ini,sha256=aq1sNDUgKvdON-N28lN9fncRcYtg430HE2DsuEMIDtk,1782
|
|
4
|
-
skrift/asgi.py,sha256=
|
|
4
|
+
skrift/asgi.py,sha256=U-9p_JlWlwjrhKJzAlBHpQ3DvTB2uIcvZe3-5Gs87Ec,19248
|
|
5
5
|
skrift/cli.py,sha256=rrm0cSc2LhQ1FA5-ucDN1sDTwFwKnDk1DCYf746SYyo,1522
|
|
6
|
-
skrift/config.py,sha256=
|
|
6
|
+
skrift/config.py,sha256=7Jqqm9jXff7rIRY83C4jchDVk_1UkpGdKATX3JgX974,7423
|
|
7
7
|
skrift/admin/__init__.py,sha256=x81Cj_ilVmv6slaMl16HHyT_AgrnLxKEWkS0RPa4V9s,289
|
|
8
8
|
skrift/admin/controller.py,sha256=5ZDypvKHXLNDESsKNsdsH2E3Si5OqlpzttFl7Ot8aF0,15651
|
|
9
9
|
skrift/admin/navigation.py,sha256=VwttFoIUIJy5rONKIkJd5w4CNkUpeK22_OfLGHecN34,3382
|
|
@@ -19,7 +19,7 @@ skrift/auth/guards.py,sha256=QePajHsGnJ4R_hlhzblr5IoAgZcY5jzeZ64bJwDL9hM,4451
|
|
|
19
19
|
skrift/auth/roles.py,sha256=jpqK4qaavqAhJRxhptm2x5mUb6KkwEALaml8sEH4Sug,2267
|
|
20
20
|
skrift/auth/services.py,sha256=h6GTXdN5UMRYglnaFz4asMoutVkSSAyL3_Vt56N26pA,5441
|
|
21
21
|
skrift/controllers/__init__.py,sha256=bVr0tsSGz7jBi002Lqd1AA1FQd7ZA_IagsqTKpiHiK0,147
|
|
22
|
-
skrift/controllers/auth.py,sha256=
|
|
22
|
+
skrift/controllers/auth.py,sha256=3rECj_bfpISngcoUfMI3TcqxsmnTlpjMHfVNpMjDG4I,16050
|
|
23
23
|
skrift/controllers/web.py,sha256=vmoBS1u5G9gCBu65S49yqZn_WBKlmsqlcvX5tYXTKnE,2348
|
|
24
24
|
skrift/db/__init__.py,sha256=uSghyDFT2K4SFiEqUzdjCGzWpS-Oy6Sd1FUappau-v0,52
|
|
25
25
|
skrift/db/base.py,sha256=QJplFj9235kZdScASEpvyNHln6YW2hqbHwJEYZ3OSsc,173
|
|
@@ -35,12 +35,12 @@ skrift/lib/__init__.py,sha256=PL66bS4_-90zJDMsQSnzqkkxwkO_3aLcYl9l9cDrE2Y,65
|
|
|
35
35
|
skrift/lib/exceptions.py,sha256=p8ceLIQCc7agCwW6-mhBDAuMAMxZDcf9TDLC6PfztU4,5803
|
|
36
36
|
skrift/lib/template.py,sha256=4_urkRfvth75yNeQ5TyGTHvkvs3vVef7TcwZx0k285k,4226
|
|
37
37
|
skrift/setup/__init__.py,sha256=3VjFPMES5y0M5cQ9R4C1xazqiEPEDqTPjX9-3rBMXnA,478
|
|
38
|
-
skrift/setup/config_writer.py,sha256=
|
|
38
|
+
skrift/setup/config_writer.py,sha256=YFH3FVjXN7Rum2fzGVPAQRkjdc9b0bHECDqMKYiEkhg,6347
|
|
39
39
|
skrift/setup/controller.py,sha256=HZs6Q2lNdvNh-JakmznMVL-1sj_6wD9axr3ESL1iIaA,29202
|
|
40
40
|
skrift/setup/middleware.py,sha256=Nai8ZG2vHldngmAhq7kWzAwKRNcP5tHKhJHa5dCh404,2941
|
|
41
|
-
skrift/setup/providers.py,sha256=
|
|
42
|
-
skrift/setup/state.py,sha256=
|
|
43
|
-
skrift/static/css/style.css,sha256=
|
|
41
|
+
skrift/setup/providers.py,sha256=0BFKB6168NcmtXxFF6ofHgEDMQD2FbXkexsqrARVtDI,7967
|
|
42
|
+
skrift/setup/state.py,sha256=cQT0LIBFHRHpTzbgsBUO5a4iqAlUSAyUeH3m1k6C95w,3739
|
|
43
|
+
skrift/static/css/style.css,sha256=sJ7-y8nrUdB5EB5_CyjWo1CTnqmYq0UgMZYMjxw0988,20824
|
|
44
44
|
skrift/templates/base.html,sha256=4bg4s4VdES0dSvhJYLgrfrN26ynqeq1-3jyKPkWWVWk,2065
|
|
45
45
|
skrift/templates/error-404.html,sha256=sJrDaF3Or3Nyki8mxo3wBxLLzgy4wkB9p9wdS8pRA6k,409
|
|
46
46
|
skrift/templates/error-500.html,sha256=MR0wJ1JKLqdmdvsoJbQnZxLkxDPE59LrlbtVPKLM8-A,401
|
|
@@ -54,7 +54,8 @@ skrift/templates/admin/pages/list.html,sha256=LYh1vEwSHkyXihjcy5PIo33iA7UeIxprw4
|
|
|
54
54
|
skrift/templates/admin/settings/site.html,sha256=xHZCZGj9XqyLKCe2eoBlpI3oFIx5VoG_5FP-VAF2mz4,1516
|
|
55
55
|
skrift/templates/admin/users/list.html,sha256=9GWql1la5Srm-OOgooHR9Eouk7ZL66K_FXY6HAAB5lA,1732
|
|
56
56
|
skrift/templates/admin/users/roles.html,sha256=pos-ZM-gXYCN_D8DZpzwEBEm_WvmOKMMPz-3xJqs7nY,1473
|
|
57
|
-
skrift/templates/auth/
|
|
57
|
+
skrift/templates/auth/dummy_login.html,sha256=G2ykWSiOK7XFdr7dzD-7VvKLTXzYT6Lduq5Pc6B21I0,2430
|
|
58
|
+
skrift/templates/auth/login.html,sha256=kBOfKRvKqn0L_POtXgOdl_rzUTNbr9F5NKtqxOMXQdk,7057
|
|
58
59
|
skrift/templates/setup/admin.html,sha256=BSIztZT2iqxVSW23Tfg7ZM51SdGrKL422CR05DPjNic,804
|
|
59
60
|
skrift/templates/setup/auth.html,sha256=0DVL0kU6DlJ2pWe4zwc3DsIVfBAdqpMJxPYvS4zm4OM,4953
|
|
60
61
|
skrift/templates/setup/base.html,sha256=LTXqbnHMvx1wDsxFvo4BSieBPD9pcLMj6NM4ZGzErFM,10372
|
|
@@ -62,7 +63,7 @@ skrift/templates/setup/complete.html,sha256=oyT-rYPl0uuyOjPXgNeLr8YoptW9QjHTlScZ
|
|
|
62
63
|
skrift/templates/setup/database.html,sha256=gU4-315-QraHa2Eq4Fh3b55QpOM2CkJzh27_Yz13frA,5495
|
|
63
64
|
skrift/templates/setup/restart.html,sha256=GHg31F_e2uLFhWUzJoalk0Y0oYLqsFWyZXWKX3mblbY,1355
|
|
64
65
|
skrift/templates/setup/site.html,sha256=PSOH-q1-ZBl47iSW9-Ad6lEfJn_fzdGD3Pk4vb3xgK4,1680
|
|
65
|
-
skrift-0.1.
|
|
66
|
-
skrift-0.1.
|
|
67
|
-
skrift-0.1.
|
|
68
|
-
skrift-0.1.
|
|
66
|
+
skrift-0.1.0a2.dist-info/METADATA,sha256=YhazEDBHS7K8MohN6aRwVbC5VlkbmM3X9vjghL79XR0,6435
|
|
67
|
+
skrift-0.1.0a2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
68
|
+
skrift-0.1.0a2.dist-info/entry_points.txt,sha256=4AIrmbeWKOdZnvTsKT3US6N3X9rrgk9jEDsYOPEZ1AE,74
|
|
69
|
+
skrift-0.1.0a2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|