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
@@ -0,0 +1,110 @@
1
+ {% extends "setup/base.html" %}
2
+
3
+ {% block title %}Authentication Setup - Skrift{% endblock %}
4
+
5
+ {% block content %}
6
+ <h2>Authentication Providers</h2>
7
+ <p class="description">Configure at least one OAuth provider for user authentication.</p>
8
+
9
+ <form method="post" action="/setup/auth">
10
+ <div class="form-group">
11
+ <label for="redirect_base_url">OAuth Redirect Base URL</label>
12
+ <input type="url" id="redirect_base_url" name="redirect_base_url" value="{{ redirect_base_url }}" required>
13
+ <small>The base URL for OAuth callbacks (auto-detected from your current URL)</small>
14
+ </div>
15
+
16
+ <hr style="margin: 2rem 0;">
17
+
18
+ {% for key, provider in providers.items() %}
19
+ <div class="provider-card{% if key in configured_providers %} enabled{% endif %}" id="provider-{{ key }}">
20
+ <div class="provider-header" onclick="toggleProvider('{{ key }}')">
21
+ <input type="checkbox" name="{{ key }}_enabled" id="{{ key }}_enabled" {% if key in configured_providers %}checked{% endif %} onclick="event.stopPropagation(); toggleProvider('{{ key }}')">
22
+ <h3>{{ provider.name }}</h3>
23
+ </div>
24
+ <div class="provider-fields">
25
+ <div class="provider-instructions">
26
+ <details>
27
+ <summary>Setup Instructions</summary>
28
+ <pre>{{ provider.instructions }}</pre>
29
+ <p><a href="{{ provider.console_url }}" target="_blank" rel="noopener">Open Developer Console</a></p>
30
+ </details>
31
+ </div>
32
+
33
+ <div class="form-group">
34
+ <label>Redirect URI (copy this to your OAuth app settings)</label>
35
+ <div class="redirect-url" id="redirect-url-{{ key }}">{{ redirect_base_url }}/auth/{{ key }}/callback</div>
36
+ </div>
37
+
38
+ {% for field in provider.fields %}
39
+ <div class="form-group">
40
+ <label for="{{ key }}_{{ field.key }}">{{ field.label }}{% if not field.optional %} *{% endif %}</label>
41
+ <input type="{{ field.type }}"
42
+ id="{{ key }}_{{ field.key }}"
43
+ name="{{ key }}_{{ field.key }}"
44
+ value="{{ configured_providers.get(key, {}).get(field.key, '') }}"
45
+ {% if field.placeholder %}placeholder="{{ field.placeholder }}"{% endif %}
46
+ {% if not field.optional %}required{% endif %}>
47
+ <div class="form-group env-toggle">
48
+ <input type="checkbox" id="{{ key }}_{{ field.key }}_env" name="{{ key }}_{{ field.key }}_env">
49
+ <label for="{{ key }}_{{ field.key }}_env">Use environment variable (enter env var name above)</label>
50
+ </div>
51
+ </div>
52
+ {% endfor %}
53
+ </div>
54
+ </div>
55
+ {% endfor %}
56
+
57
+ <div class="form-actions">
58
+ <a href="/setup/database" class="btn-secondary" role="button">Back</a>
59
+ <button type="submit">Save &amp; Continue</button>
60
+ </div>
61
+ </form>
62
+ {% endblock %}
63
+
64
+ {% block scripts %}
65
+ <script>
66
+ function toggleProvider(key) {
67
+ const card = document.getElementById('provider-' + key);
68
+ const checkbox = document.getElementById(key + '_enabled');
69
+
70
+ // Toggle the checkbox when clicking the header (but not when clicking the checkbox itself)
71
+ if (event.target !== checkbox) {
72
+ checkbox.checked = !checkbox.checked;
73
+ }
74
+
75
+ card.classList.toggle('enabled', checkbox.checked);
76
+
77
+ // Toggle required attribute on fields
78
+ const fields = card.querySelectorAll('input[required]');
79
+ fields.forEach(field => {
80
+ if (checkbox.checked) {
81
+ field.setAttribute('required', '');
82
+ } else {
83
+ field.removeAttribute('required');
84
+ }
85
+ });
86
+ }
87
+
88
+ // Update redirect URLs when base URL changes
89
+ document.getElementById('redirect_base_url').addEventListener('input', function() {
90
+ const baseUrl = this.value;
91
+ {% for key in providers.keys() %}
92
+ document.getElementById('redirect-url-{{ key }}').textContent = baseUrl + '/auth/{{ key }}/callback';
93
+ {% endfor %}
94
+ });
95
+
96
+ // Initialize required attributes based on initial state
97
+ document.addEventListener('DOMContentLoaded', function() {
98
+ {% for key in providers.keys() %}
99
+ const card{{ key }} = document.getElementById('provider-{{ key }}');
100
+ const checkbox{{ key }} = document.getElementById('{{ key }}_enabled');
101
+ const fields{{ key }} = card{{ key }}.querySelectorAll('.provider-fields input[type="text"], .provider-fields input[type="password"]');
102
+ fields{{ key }}.forEach(field => {
103
+ if (!checkbox{{ key }}.checked && !field.closest('.form-group').querySelector('label').textContent.includes('optional')) {
104
+ field.removeAttribute('required');
105
+ }
106
+ });
107
+ {% endfor %}
108
+ });
109
+ </script>
110
+ {% endblock %}
@@ -0,0 +1,407 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}Setup - Skrift{% endblock %}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600&family=Lora:ital,wght@0,400;0,600;1,400;1,600&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="/static/css/style.css">
11
+ <style>
12
+ .setup-container {
13
+ max-width: 600px;
14
+ margin: 2rem auto;
15
+ padding: 0 1rem;
16
+ }
17
+
18
+ .setup-header {
19
+ text-align: center;
20
+ margin-bottom: 2rem;
21
+ }
22
+
23
+ .setup-header h1 {
24
+ font-size: 2rem;
25
+ margin-bottom: 0.5rem;
26
+ }
27
+
28
+ .setup-progress {
29
+ display: flex;
30
+ justify-content: center;
31
+ gap: 0.5rem;
32
+ margin-bottom: 2rem;
33
+ }
34
+
35
+ .setup-progress-step {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 0.5rem;
39
+ }
40
+
41
+ .setup-progress-dot {
42
+ width: 2rem;
43
+ height: 2rem;
44
+ border-radius: 50%;
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ font-weight: bold;
49
+ font-size: 0.875rem;
50
+ background: var(--color-surface);
51
+ color: var(--color-text-muted);
52
+ border: 1px solid var(--color-border);
53
+ }
54
+
55
+ .setup-progress-dot.active {
56
+ background: var(--color-primary);
57
+ color: var(--color-primary-text);
58
+ border-color: var(--color-primary);
59
+ }
60
+
61
+ .setup-progress-dot.completed {
62
+ background: var(--color-success);
63
+ color: white;
64
+ border-color: var(--color-success);
65
+ }
66
+
67
+ .setup-progress-line {
68
+ width: 2rem;
69
+ height: 2px;
70
+ background: var(--color-border);
71
+ }
72
+
73
+ .setup-progress-line.completed {
74
+ background: var(--color-success);
75
+ }
76
+
77
+ .setup-card {
78
+ background: var(--color-surface);
79
+ border-radius: var(--radius-md);
80
+ padding: 2rem;
81
+ border: 1px solid var(--color-border);
82
+ }
83
+
84
+ .setup-card h2 {
85
+ margin-top: 0;
86
+ margin-bottom: 0.5rem;
87
+ }
88
+
89
+ .setup-card p.description {
90
+ color: var(--color-text-muted);
91
+ margin-bottom: 1.5rem;
92
+ }
93
+
94
+ .form-group {
95
+ margin-bottom: 1.5rem;
96
+ }
97
+
98
+ .form-group label {
99
+ display: block;
100
+ margin-bottom: 0.5rem;
101
+ font-weight: 500;
102
+ }
103
+
104
+ .form-group small {
105
+ display: block;
106
+ margin-top: 0.25rem;
107
+ color: var(--color-text-muted);
108
+ }
109
+
110
+ .form-group.env-toggle {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: 0.5rem;
114
+ margin-top: 0.5rem;
115
+ }
116
+
117
+ .form-group.env-toggle label {
118
+ margin: 0;
119
+ font-weight: normal;
120
+ font-size: 0.875rem;
121
+ }
122
+
123
+ .form-group.env-toggle input[type="checkbox"] {
124
+ margin: 0;
125
+ width: 1.25rem;
126
+ height: 1.25rem;
127
+ flex-shrink: 0;
128
+ }
129
+
130
+ .radio-group {
131
+ display: flex;
132
+ gap: 1rem;
133
+ margin-bottom: 1rem;
134
+ }
135
+
136
+ .radio-option {
137
+ display: flex;
138
+ align-items: flex-start;
139
+ gap: 0.5rem;
140
+ padding: 1rem;
141
+ border: 2px solid var(--color-border);
142
+ border-radius: var(--radius-md);
143
+ cursor: pointer;
144
+ flex: 1;
145
+ background: var(--color-bg);
146
+ }
147
+
148
+ .radio-option:hover {
149
+ border-color: var(--color-primary);
150
+ }
151
+
152
+ .radio-option.selected {
153
+ border-color: var(--color-primary);
154
+ background: var(--color-surface);
155
+ }
156
+
157
+ .radio-option input[type="radio"] {
158
+ margin: 0;
159
+ }
160
+
161
+ .radio-option .radio-content {
162
+ flex: 1;
163
+ }
164
+
165
+ .radio-option .radio-content strong {
166
+ display: block;
167
+ margin-bottom: 0.25rem;
168
+ }
169
+
170
+ .radio-option .radio-content small {
171
+ color: var(--color-text-muted);
172
+ }
173
+
174
+ .conditional-fields {
175
+ display: none;
176
+ margin-top: 1.5rem;
177
+ padding-top: 1.5rem;
178
+ border-top: 1px solid var(--color-border);
179
+ }
180
+
181
+ .conditional-fields.visible {
182
+ display: block;
183
+ }
184
+
185
+ .form-actions {
186
+ display: flex;
187
+ justify-content: space-between;
188
+ margin-top: 2rem;
189
+ gap: 1rem;
190
+ }
191
+
192
+ .form-actions button,
193
+ .form-actions [role="button"] {
194
+ flex: 1;
195
+ margin-top: 0;
196
+ }
197
+
198
+ .alert {
199
+ padding: 1rem;
200
+ border-radius: var(--radius-md);
201
+ margin-bottom: 1rem;
202
+ }
203
+
204
+ .alert-error {
205
+ background: rgba(239, 68, 68, 0.1);
206
+ color: var(--color-error);
207
+ border: 1px solid var(--color-error);
208
+ }
209
+
210
+ .alert-success {
211
+ background: rgba(16, 185, 129, 0.1);
212
+ color: var(--color-success);
213
+ border: 1px solid var(--color-success);
214
+ }
215
+
216
+ .provider-card {
217
+ border: 2px solid var(--color-border);
218
+ border-radius: var(--radius-md);
219
+ margin-bottom: 1rem;
220
+ overflow: hidden;
221
+ background: var(--color-bg);
222
+ }
223
+
224
+ .provider-card.enabled {
225
+ border-color: var(--color-primary);
226
+ }
227
+
228
+ .provider-header {
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 1rem;
232
+ padding: 1rem;
233
+ cursor: pointer;
234
+ background: var(--color-surface);
235
+ }
236
+
237
+ .provider-header input[type="checkbox"] {
238
+ margin: 0;
239
+ }
240
+
241
+ .provider-header h3 {
242
+ margin: 0;
243
+ flex: 1;
244
+ font-size: 1rem;
245
+ }
246
+
247
+ .provider-fields {
248
+ display: none;
249
+ padding: 1rem;
250
+ border-top: 1px solid var(--color-border);
251
+ }
252
+
253
+ .provider-card.enabled .provider-fields {
254
+ display: block;
255
+ }
256
+
257
+ .provider-instructions {
258
+ margin-bottom: 1rem;
259
+ }
260
+
261
+ .provider-instructions details {
262
+ background: var(--color-surface);
263
+ padding: 0.5rem 1rem;
264
+ border-radius: var(--radius-sm);
265
+ }
266
+
267
+ .provider-instructions summary {
268
+ cursor: pointer;
269
+ font-weight: 500;
270
+ }
271
+
272
+ .provider-instructions pre {
273
+ white-space: pre-wrap;
274
+ font-size: 0.875rem;
275
+ margin: 0.5rem 0;
276
+ background: var(--color-bg);
277
+ padding: 0.75rem;
278
+ border-radius: var(--radius-sm);
279
+ }
280
+
281
+ .redirect-url {
282
+ background: var(--color-bg);
283
+ padding: 0.5rem;
284
+ border-radius: var(--radius-sm);
285
+ font-family: var(--font-family-mono);
286
+ font-size: 0.875rem;
287
+ word-break: break-all;
288
+ margin: 0.5rem 0 1rem;
289
+ border: 1px solid var(--color-border);
290
+ }
291
+
292
+ .oauth-buttons {
293
+ display: flex;
294
+ flex-direction: column;
295
+ gap: 1rem;
296
+ }
297
+
298
+ .oauth-btn {
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: center;
302
+ gap: 0.75rem;
303
+ padding: 1rem 1.5rem;
304
+ border-radius: var(--radius-pill);
305
+ font-size: 1rem;
306
+ font-weight: 600;
307
+ text-decoration: none;
308
+ transition: opacity 0.2s, transform 0.1s;
309
+ }
310
+
311
+ .oauth-btn:hover {
312
+ opacity: 0.9;
313
+ }
314
+
315
+ .oauth-btn:active {
316
+ transform: translateY(1px);
317
+ }
318
+
319
+ .oauth-btn::after {
320
+ display: none;
321
+ }
322
+
323
+ .oauth-btn-google {
324
+ background: #4285f4;
325
+ color: white;
326
+ }
327
+
328
+ .oauth-btn-github {
329
+ background: #24292e;
330
+ color: white;
331
+ }
332
+
333
+ .oauth-btn-microsoft {
334
+ background: #00a4ef;
335
+ color: white;
336
+ }
337
+
338
+ .oauth-btn-discord {
339
+ background: #5865f2;
340
+ color: white;
341
+ }
342
+
343
+ .oauth-btn-facebook {
344
+ background: #1877f2;
345
+ color: white;
346
+ }
347
+
348
+ .oauth-btn-twitter {
349
+ background: #000000;
350
+ color: white;
351
+ }
352
+
353
+ .complete-message {
354
+ text-align: center;
355
+ padding: 2rem;
356
+ }
357
+
358
+ .complete-message h2 {
359
+ color: var(--color-success);
360
+ }
361
+
362
+ .complete-icon {
363
+ font-size: 4rem;
364
+ margin-bottom: 1rem;
365
+ color: var(--color-success);
366
+ }
367
+ </style>
368
+ {% block head %}{% endblock %}
369
+ </head>
370
+ <body>
371
+ <main class="setup-container">
372
+ <div class="setup-header">
373
+ <h1>Skrift Setup</h1>
374
+ <p>Configure your site in a few easy steps</p>
375
+ </div>
376
+
377
+ {% if step is defined %}
378
+ <div class="setup-progress">
379
+ {% for i in range(1, total_steps + 1) %}
380
+ {% if i > 1 %}
381
+ <div class="setup-progress-line{% if i <= step %} completed{% endif %}"></div>
382
+ {% endif %}
383
+ <div class="setup-progress-step">
384
+ <div class="setup-progress-dot{% if i < step %} completed{% elif i == step %} active{% endif %}">
385
+ {% if i < step %}&#10003;{% else %}{{ i }}{% endif %}
386
+ </div>
387
+ </div>
388
+ {% endfor %}
389
+ </div>
390
+ {% endif %}
391
+
392
+ {% if flash %}
393
+ <div class="alert alert-success">{{ flash }}</div>
394
+ {% endif %}
395
+
396
+ {% if error %}
397
+ <div class="alert alert-error">{{ error }}</div>
398
+ {% endif %}
399
+
400
+ <div class="setup-card">
401
+ {% block content %}{% endblock %}
402
+ </div>
403
+ </main>
404
+
405
+ {% block scripts %}{% endblock %}
406
+ </body>
407
+ </html>
@@ -0,0 +1,17 @@
1
+ {% extends "setup/base.html" %}
2
+
3
+ {% block title %}Setup Complete - Skrift{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="complete-message">
7
+ <div class="complete-icon">&#10003;</div>
8
+ <h2>Setup Complete!</h2>
9
+ <p>Your Skrift site is now configured and ready to use.</p>
10
+ <p style="color: var(--muted-color, #666);">You can manage your site settings, users, and content from the admin panel.</p>
11
+
12
+ <div style="margin-top: 2rem;">
13
+ <a href="/" role="button" style="margin-right: 1rem;">Go to Site</a>
14
+ <a href="/admin" role="button" class="outline">Open Admin Panel</a>
15
+ </div>
16
+ </div>
17
+ {% endblock %}
@@ -0,0 +1,125 @@
1
+ {% extends "setup/base.html" %}
2
+
3
+ {% block title %}Database Setup - Skrift{% endblock %}
4
+
5
+ {% block content %}
6
+ <h2>Database Configuration</h2>
7
+ <p class="description">Choose your database type and configure the connection.</p>
8
+
9
+ <form method="post" action="/setup/database">
10
+ <div class="radio-group">
11
+ <label class="radio-option{% if db_type == 'sqlite' %} selected{% endif %}" id="sqlite-option">
12
+ <input type="radio" name="db_type" value="sqlite" {% if db_type == 'sqlite' %}checked{% endif %}>
13
+ <div class="radio-content">
14
+ <strong>SQLite</strong>
15
+ <small>Perfect for personal sites. No setup required.</small>
16
+ </div>
17
+ </label>
18
+ <label class="radio-option{% if db_type == 'postgresql' %} selected{% endif %}" id="postgresql-option">
19
+ <input type="radio" name="db_type" value="postgresql" {% if db_type == 'postgresql' %}checked{% endif %}>
20
+ <div class="radio-content">
21
+ <strong>PostgreSQL</strong>
22
+ <small>For production deployments.</small>
23
+ </div>
24
+ </label>
25
+ </div>
26
+
27
+ <div class="conditional-fields{% if db_type == 'sqlite' %} visible{% endif %}" id="sqlite-fields">
28
+ <div class="form-group">
29
+ <label for="sqlite_path">Database File Path</label>
30
+ <input type="text" id="sqlite_path" name="sqlite_path" value="./app.db" placeholder="./app.db">
31
+ <small>Path relative to the application directory</small>
32
+ </div>
33
+ <div class="form-group env-toggle">
34
+ <input type="checkbox" id="sqlite_path_env" name="sqlite_path_env">
35
+ <label for="sqlite_path_env">Use environment variable (enter env var name in field above)</label>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="conditional-fields{% if db_type == 'postgresql' %} visible{% endif %}" id="postgresql-fields">
40
+ <div class="form-group env-toggle" style="margin-bottom: 1rem;">
41
+ <input type="checkbox" id="pg_url_env" name="pg_url_env">
42
+ <label for="pg_url_env">Use a single DATABASE_URL environment variable</label>
43
+ </div>
44
+
45
+ <div id="pg-env-field" style="display: none;">
46
+ <div class="form-group">
47
+ <label for="pg_url_envvar">Environment Variable Name</label>
48
+ <input type="text" id="pg_url_envvar" name="pg_url_envvar" value="DATABASE_URL">
49
+ <small>The environment variable containing the full connection URL</small>
50
+ </div>
51
+ </div>
52
+
53
+ <div id="pg-manual-fields">
54
+ <div class="form-group">
55
+ <label for="pg_host">Host</label>
56
+ <input type="text" id="pg_host" name="pg_host" value="localhost" placeholder="localhost">
57
+ </div>
58
+
59
+ <div class="form-group">
60
+ <label for="pg_port">Port</label>
61
+ <input type="number" id="pg_port" name="pg_port" value="5432" placeholder="5432">
62
+ </div>
63
+
64
+ <div class="form-group">
65
+ <label for="pg_database">Database Name</label>
66
+ <input type="text" id="pg_database" name="pg_database" value="skrift" placeholder="skrift">
67
+ </div>
68
+
69
+ <div class="form-group">
70
+ <label for="pg_username">Username</label>
71
+ <input type="text" id="pg_username" name="pg_username" value="postgres" placeholder="postgres">
72
+ </div>
73
+
74
+ <div class="form-group">
75
+ <label for="pg_password">Password</label>
76
+ <input type="password" id="pg_password" name="pg_password" placeholder="Enter password">
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="form-actions">
82
+ <button type="submit">Test Connection &amp; Continue</button>
83
+ </div>
84
+ </form>
85
+ {% endblock %}
86
+
87
+ {% block scripts %}
88
+ <script>
89
+ document.addEventListener('DOMContentLoaded', function() {
90
+ const sqliteOption = document.getElementById('sqlite-option');
91
+ const postgresqlOption = document.getElementById('postgresql-option');
92
+ const sqliteFields = document.getElementById('sqlite-fields');
93
+ const postgresqlFields = document.getElementById('postgresql-fields');
94
+ const radioInputs = document.querySelectorAll('input[name="db_type"]');
95
+
96
+ function updateVisibility() {
97
+ const selected = document.querySelector('input[name="db_type"]:checked').value;
98
+
99
+ sqliteOption.classList.toggle('selected', selected === 'sqlite');
100
+ postgresqlOption.classList.toggle('selected', selected === 'postgresql');
101
+
102
+ sqliteFields.classList.toggle('visible', selected === 'sqlite');
103
+ postgresqlFields.classList.toggle('visible', selected === 'postgresql');
104
+ }
105
+
106
+ radioInputs.forEach(input => {
107
+ input.addEventListener('change', updateVisibility);
108
+ });
109
+
110
+ // PostgreSQL env var toggle
111
+ const pgEnvCheckbox = document.getElementById('pg_url_env');
112
+ const pgEnvField = document.getElementById('pg-env-field');
113
+ const pgManualFields = document.getElementById('pg-manual-fields');
114
+
115
+ function updatePgFields() {
116
+ const useEnv = pgEnvCheckbox.checked;
117
+ pgEnvField.style.display = useEnv ? 'block' : 'none';
118
+ pgManualFields.style.display = useEnv ? 'none' : 'block';
119
+ }
120
+
121
+ pgEnvCheckbox.addEventListener('change', updatePgFields);
122
+ updatePgFields();
123
+ });
124
+ </script>
125
+ {% endblock %}
@@ -0,0 +1,28 @@
1
+ {% extends "setup/base.html" %}
2
+
3
+ {% block title %}Restart Required - Skrift{% endblock %}
4
+
5
+ {% block content %}
6
+ <h2>Database Configured Successfully!</h2>
7
+ <p class="description">The database has been set up and migrations have been run. To continue with setup, you need to restart the server.</p>
8
+
9
+ <div style="background: var(--color-bg); padding: 1.5rem; border-radius: var(--radius-md); margin: 1.5rem 0; border: 1px solid var(--color-border);">
10
+ <h3 style="margin-top: 0;">Why restart?</h3>
11
+ <p style="margin-bottom: 0; color: var(--color-text-muted);">
12
+ The server needs to reload to enable authentication features that require the database connection you just configured.
13
+ </p>
14
+ </div>
15
+
16
+ <div style="background: var(--color-surface); padding: 1.5rem; border-radius: var(--radius-md); margin: 1.5rem 0; border: 1px solid var(--color-border);">
17
+ <h3 style="margin-top: 0;">To restart:</h3>
18
+ <ol style="margin-bottom: 0; padding-left: 1.5rem;">
19
+ <li>Stop the current server (press <code>Ctrl+C</code> in the terminal)</li>
20
+ <li>Start it again with the same command you used before</li>
21
+ <li>Return to this page - setup will continue automatically</li>
22
+ </ol>
23
+ </div>
24
+
25
+ <div class="form-actions">
26
+ <a href="/setup/restart" role="button" class="button">I've Restarted - Continue Setup</a>
27
+ </div>
28
+ {% endblock %}