skrift 0.1.0a12__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/__init__.py +1 -0
- skrift/__main__.py +12 -0
- skrift/admin/__init__.py +11 -0
- skrift/admin/controller.py +452 -0
- skrift/admin/navigation.py +105 -0
- skrift/alembic/env.py +92 -0
- skrift/alembic/script.py.mako +26 -0
- skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +70 -0
- skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +57 -0
- skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +31 -0
- skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +43 -0
- skrift/alembic/versions/20260122_200000_add_settings_table.py +38 -0
- skrift/alembic/versions/20260129_add_oauth_accounts.py +141 -0
- skrift/alembic/versions/20260129_add_provider_metadata.py +29 -0
- skrift/alembic.ini +77 -0
- skrift/asgi.py +670 -0
- skrift/auth/__init__.py +58 -0
- skrift/auth/guards.py +130 -0
- skrift/auth/roles.py +129 -0
- skrift/auth/services.py +184 -0
- skrift/cli.py +143 -0
- skrift/config.py +259 -0
- skrift/controllers/__init__.py +4 -0
- skrift/controllers/auth.py +595 -0
- skrift/controllers/web.py +67 -0
- skrift/db/__init__.py +3 -0
- skrift/db/base.py +7 -0
- skrift/db/models/__init__.py +7 -0
- skrift/db/models/oauth_account.py +50 -0
- skrift/db/models/page.py +26 -0
- skrift/db/models/role.py +56 -0
- skrift/db/models/setting.py +13 -0
- skrift/db/models/user.py +36 -0
- skrift/db/services/__init__.py +1 -0
- skrift/db/services/oauth_service.py +195 -0
- skrift/db/services/page_service.py +217 -0
- skrift/db/services/setting_service.py +206 -0
- skrift/lib/__init__.py +3 -0
- skrift/lib/exceptions.py +168 -0
- skrift/lib/template.py +108 -0
- skrift/setup/__init__.py +14 -0
- skrift/setup/config_writer.py +213 -0
- skrift/setup/controller.py +888 -0
- skrift/setup/middleware.py +89 -0
- skrift/setup/providers.py +214 -0
- skrift/setup/state.py +315 -0
- skrift/static/css/style.css +1003 -0
- skrift/templates/admin/admin.html +19 -0
- skrift/templates/admin/base.html +24 -0
- skrift/templates/admin/pages/edit.html +32 -0
- skrift/templates/admin/pages/list.html +62 -0
- skrift/templates/admin/settings/site.html +32 -0
- skrift/templates/admin/users/list.html +58 -0
- skrift/templates/admin/users/roles.html +42 -0
- skrift/templates/auth/dummy_login.html +102 -0
- skrift/templates/auth/login.html +139 -0
- skrift/templates/base.html +52 -0
- skrift/templates/error-404.html +19 -0
- skrift/templates/error-500.html +19 -0
- skrift/templates/error.html +19 -0
- skrift/templates/index.html +9 -0
- skrift/templates/page.html +26 -0
- skrift/templates/setup/admin.html +24 -0
- skrift/templates/setup/auth.html +110 -0
- skrift/templates/setup/base.html +407 -0
- skrift/templates/setup/complete.html +17 -0
- skrift/templates/setup/configuring.html +158 -0
- skrift/templates/setup/database.html +125 -0
- skrift/templates/setup/restart.html +28 -0
- skrift/templates/setup/site.html +39 -0
- skrift-0.1.0a12.dist-info/METADATA +235 -0
- skrift-0.1.0a12.dist-info/RECORD +74 -0
- skrift-0.1.0a12.dist-info/WHEEL +4 -0
- skrift-0.1.0a12.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
{% extends "setup/base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Configuring Database - Skrift{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block head %}
|
|
6
|
+
<style>
|
|
7
|
+
.configuring-content {
|
|
8
|
+
text-align: center;
|
|
9
|
+
padding: 2rem 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.spinner {
|
|
13
|
+
width: 48px;
|
|
14
|
+
height: 48px;
|
|
15
|
+
border: 4px solid var(--color-border);
|
|
16
|
+
border-top-color: var(--color-primary);
|
|
17
|
+
border-radius: 50%;
|
|
18
|
+
animation: spin 1s linear infinite;
|
|
19
|
+
margin: 0 auto 1.5rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.spinner.complete {
|
|
23
|
+
border-color: var(--color-success);
|
|
24
|
+
border-top-color: var(--color-success);
|
|
25
|
+
animation: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.spinner.error {
|
|
29
|
+
border-color: var(--color-error);
|
|
30
|
+
border-top-color: var(--color-error);
|
|
31
|
+
animation: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@keyframes spin {
|
|
35
|
+
to { transform: rotate(360deg); }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.status-icon {
|
|
39
|
+
width: 48px;
|
|
40
|
+
height: 48px;
|
|
41
|
+
margin: 0 auto 1.5rem;
|
|
42
|
+
font-size: 48px;
|
|
43
|
+
line-height: 1;
|
|
44
|
+
display: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.status-icon.complete {
|
|
48
|
+
display: block;
|
|
49
|
+
color: var(--color-success);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.status-icon.error {
|
|
53
|
+
display: block;
|
|
54
|
+
color: var(--color-error);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.status-message {
|
|
58
|
+
font-size: 1.125rem;
|
|
59
|
+
margin-bottom: 0.5rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.status-detail {
|
|
63
|
+
color: var(--color-text-muted);
|
|
64
|
+
font-size: 0.875rem;
|
|
65
|
+
min-height: 1.25rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.form-actions button:disabled {
|
|
69
|
+
opacity: 0.5;
|
|
70
|
+
cursor: not-allowed;
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
73
|
+
{% endblock %}
|
|
74
|
+
|
|
75
|
+
{% block content %}
|
|
76
|
+
<h2>Configuring Database</h2>
|
|
77
|
+
<p class="description">Setting up your database and running migrations.</p>
|
|
78
|
+
|
|
79
|
+
<div class="configuring-content">
|
|
80
|
+
<div class="spinner" id="spinner"></div>
|
|
81
|
+
<div class="status-icon" id="status-icon"></div>
|
|
82
|
+
<div class="status-message" id="status-message">Initializing...</div>
|
|
83
|
+
<div class="status-detail" id="status-detail"></div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="form-actions">
|
|
87
|
+
<a href="/setup/database" role="button" class="btn-secondary">Back</a>
|
|
88
|
+
<button type="button" id="next-button" disabled>Continue</button>
|
|
89
|
+
</div>
|
|
90
|
+
{% endblock %}
|
|
91
|
+
|
|
92
|
+
{% block scripts %}
|
|
93
|
+
<script>
|
|
94
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
95
|
+
const spinner = document.getElementById('spinner');
|
|
96
|
+
const statusIcon = document.getElementById('status-icon');
|
|
97
|
+
const statusMessage = document.getElementById('status-message');
|
|
98
|
+
const statusDetail = document.getElementById('status-detail');
|
|
99
|
+
const nextButton = document.getElementById('next-button');
|
|
100
|
+
|
|
101
|
+
let nextStep = 'auth'; // Default fallback
|
|
102
|
+
|
|
103
|
+
function setComplete(step) {
|
|
104
|
+
spinner.style.display = 'none';
|
|
105
|
+
statusIcon.textContent = '\u2713';
|
|
106
|
+
statusIcon.classList.add('complete');
|
|
107
|
+
statusMessage.textContent = 'Database configured successfully!';
|
|
108
|
+
statusDetail.textContent = '';
|
|
109
|
+
nextButton.disabled = false;
|
|
110
|
+
if (step) {
|
|
111
|
+
nextStep = step;
|
|
112
|
+
}
|
|
113
|
+
nextButton.onclick = function() {
|
|
114
|
+
window.location.href = '/setup/' + nextStep;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function setError(message) {
|
|
119
|
+
spinner.style.display = 'none';
|
|
120
|
+
statusIcon.textContent = '\u2717';
|
|
121
|
+
statusIcon.classList.add('error');
|
|
122
|
+
statusMessage.textContent = 'Configuration failed';
|
|
123
|
+
statusDetail.textContent = message || 'An error occurred during setup.';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setStatus(message, detail) {
|
|
127
|
+
statusMessage.textContent = message;
|
|
128
|
+
statusDetail.textContent = detail || '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Connect to SSE endpoint
|
|
132
|
+
const eventSource = new EventSource('/setup/configuring/status');
|
|
133
|
+
|
|
134
|
+
eventSource.onmessage = function(event) {
|
|
135
|
+
const data = JSON.parse(event.data);
|
|
136
|
+
|
|
137
|
+
if (data.status === 'running') {
|
|
138
|
+
setStatus(data.message, data.detail);
|
|
139
|
+
} else if (data.status === 'complete') {
|
|
140
|
+
setComplete(data.next_step);
|
|
141
|
+
eventSource.close();
|
|
142
|
+
} else if (data.status === 'error') {
|
|
143
|
+
setError(data.message);
|
|
144
|
+
eventSource.close();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
eventSource.onerror = function() {
|
|
149
|
+
// Check if we were successful before the connection closed
|
|
150
|
+
if (!nextButton.disabled) {
|
|
151
|
+
return; // Already complete, ignore the error
|
|
152
|
+
}
|
|
153
|
+
setError('Connection lost. Please refresh the page.');
|
|
154
|
+
eventSource.close();
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
</script>
|
|
158
|
+
{% 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 & 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 %}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{% extends "setup/base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Site Settings - Skrift{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<h2>Site Settings</h2>
|
|
7
|
+
<p class="description">Configure your site's basic information.</p>
|
|
8
|
+
|
|
9
|
+
<form method="post" action="/setup/site">
|
|
10
|
+
<div class="form-group">
|
|
11
|
+
<label for="site_name">Site Name *</label>
|
|
12
|
+
<input type="text" id="site_name" name="site_name" value="{{ settings.site_name }}" required>
|
|
13
|
+
<small>The name displayed in the header and browser title</small>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="form-group">
|
|
17
|
+
<label for="site_tagline">Site Tagline</label>
|
|
18
|
+
<input type="text" id="site_tagline" name="site_tagline" value="{{ settings.site_tagline }}">
|
|
19
|
+
<small>A short description or slogan for your site</small>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="form-group">
|
|
23
|
+
<label for="site_copyright_holder">Copyright Holder</label>
|
|
24
|
+
<input type="text" id="site_copyright_holder" name="site_copyright_holder" value="{{ settings.site_copyright_holder }}">
|
|
25
|
+
<small>Name displayed in the copyright notice (defaults to site name if empty)</small>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="form-group">
|
|
29
|
+
<label for="site_copyright_start_year">Copyright Start Year</label>
|
|
30
|
+
<input type="number" id="site_copyright_start_year" name="site_copyright_start_year" value="{{ settings.site_copyright_start_year }}" min="1900" max="2100">
|
|
31
|
+
<small>If different from current year, will show a range (e.g., 2020-2026)</small>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="form-actions">
|
|
35
|
+
<a href="/setup/auth" class="btn-secondary" role="button">Back</a>
|
|
36
|
+
<button type="submit">Save & Continue</button>
|
|
37
|
+
</div>
|
|
38
|
+
</form>
|
|
39
|
+
{% endblock %}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skrift
|
|
3
|
+
Version: 0.1.0a12
|
|
4
|
+
Summary: A lightweight async Python CMS for crafting modern websites
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: advanced-alchemy>=0.26.0
|
|
7
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
8
|
+
Requires-Dist: alembic>=1.14.0
|
|
9
|
+
Requires-Dist: asyncpg>=0.30.0
|
|
10
|
+
Requires-Dist: httpx>=0.28.0
|
|
11
|
+
Requires-Dist: litestar[cryptography,jinja,standard]>=2.14.0
|
|
12
|
+
Requires-Dist: pydantic-settings>=2.7.0
|
|
13
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
14
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
15
|
+
Requires-Dist: ruamel-yaml>=0.18.0
|
|
16
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.36
|
|
17
|
+
Requires-Dist: uvicorn>=0.34.0
|
|
18
|
+
Provides-Extra: docs
|
|
19
|
+
Requires-Dist: zensical>=0.0.19; extra == 'docs'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Skrift
|
|
23
|
+
|
|
24
|
+
A modern Litestar-powered content management framework with multi-provider OAuth authentication, role-based access control, and WordPress-like template resolution.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Multi-Provider OAuth**: Authenticate with Google, GitHub, Microsoft, Discord, Facebook, or Twitter/X
|
|
29
|
+
- **Role-Based Access Control**: Flexible permission system with Admin, Editor, Author, and Moderator roles
|
|
30
|
+
- **Setup Wizard**: Guided first-time configuration without manual file editing
|
|
31
|
+
- **Admin Interface**: Web-based management for users, pages, and site settings
|
|
32
|
+
- **WordPress-like Templates**: Hierarchical template resolution for content pages
|
|
33
|
+
- **Dynamic Controllers**: Load controllers from `app.yaml` configuration
|
|
34
|
+
- **SQLAlchemy Integration**: Async database support with SQLite/PostgreSQL
|
|
35
|
+
- **Client-Side Sessions**: Encrypted cookie sessions for horizontal scalability
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
### Prerequisites
|
|
40
|
+
|
|
41
|
+
- Python 3.13+
|
|
42
|
+
|
|
43
|
+
### Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Install Skrift
|
|
47
|
+
pip install skrift
|
|
48
|
+
|
|
49
|
+
# Or install from git
|
|
50
|
+
pip install git+https://github.com/ZechCodes/skrift.git
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Getting Started
|
|
54
|
+
|
|
55
|
+
Create a project directory and set up your environment:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
mkdir mysite && cd mysite
|
|
59
|
+
|
|
60
|
+
# Create minimal environment file
|
|
61
|
+
echo "SECRET_KEY=$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" > .env
|
|
62
|
+
|
|
63
|
+
# Start Skrift
|
|
64
|
+
skrift
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Open http://localhost:8080 to launch the setup wizard.
|
|
68
|
+
|
|
69
|
+
### Setup Wizard
|
|
70
|
+
|
|
71
|
+
The setup wizard guides you through initial configuration:
|
|
72
|
+
|
|
73
|
+
1. **Database Configuration**: Choose SQLite (dev) or PostgreSQL (production)
|
|
74
|
+
2. **Authentication Providers**: Configure OAuth credentials
|
|
75
|
+
3. **Site Settings**: Set site name, tagline, and copyright info
|
|
76
|
+
4. **Admin Account**: Create your first admin user via OAuth login
|
|
77
|
+
|
|
78
|
+
After completing the wizard, an `app.yaml` configuration file is created in your project directory.
|
|
79
|
+
|
|
80
|
+
### Manual Configuration
|
|
81
|
+
|
|
82
|
+
Alternatively, create `app.yaml` manually:
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
controllers:
|
|
86
|
+
- skrift.controllers.auth:AuthController
|
|
87
|
+
- skrift.admin.controller:AdminController
|
|
88
|
+
- skrift.controllers.web:WebController
|
|
89
|
+
|
|
90
|
+
db:
|
|
91
|
+
url: sqlite+aiosqlite:///./app.db
|
|
92
|
+
|
|
93
|
+
auth:
|
|
94
|
+
redirect_base_url: http://localhost:8080
|
|
95
|
+
providers:
|
|
96
|
+
google:
|
|
97
|
+
client_id: $GOOGLE_CLIENT_ID
|
|
98
|
+
client_secret: $GOOGLE_CLIENT_SECRET
|
|
99
|
+
scopes: [openid, email, profile]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Then run migrations and start the server:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
skrift-db upgrade head
|
|
106
|
+
skrift
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Documentation
|
|
110
|
+
|
|
111
|
+
- **[Full Documentation](docs/README.md)**: Comprehensive guide covering all features
|
|
112
|
+
- **[Deployment Guide](docs/deployment.md)**: VPS, Docker, and Kubernetes deployment
|
|
113
|
+
- **[CSS Framework](docs/css-framework.md)**: Styling documentation
|
|
114
|
+
|
|
115
|
+
## Project Structure
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
skrift/
|
|
119
|
+
├── skrift/ # Main Python package
|
|
120
|
+
│ ├── asgi.py # Application factory
|
|
121
|
+
│ ├── config.py # Settings management
|
|
122
|
+
│ ├── controllers/ # Route handlers
|
|
123
|
+
│ ├── admin/ # Admin panel
|
|
124
|
+
│ ├── auth/ # RBAC and guards
|
|
125
|
+
│ ├── db/ # Models and services
|
|
126
|
+
│ ├── lib/ # Template resolver
|
|
127
|
+
│ └── setup/ # Setup wizard
|
|
128
|
+
├── templates/ # Jinja2 templates
|
|
129
|
+
├── static/ # Static assets
|
|
130
|
+
├── alembic/ # Database migrations
|
|
131
|
+
├── docs/ # Documentation
|
|
132
|
+
├── app.yaml # Application config (generated)
|
|
133
|
+
└── main.py # Development entry point
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Configuration
|
|
137
|
+
|
|
138
|
+
### Environment Variables
|
|
139
|
+
|
|
140
|
+
| Variable | Required | Description |
|
|
141
|
+
|----------|----------|-------------|
|
|
142
|
+
| `SECRET_KEY` | Yes | Session encryption key |
|
|
143
|
+
| `DEBUG` | No | Enable debug mode (default: false) |
|
|
144
|
+
| `DATABASE_URL` | No | Database connection string |
|
|
145
|
+
| `OAUTH_REDIRECT_BASE_URL` | No | OAuth callback base URL |
|
|
146
|
+
|
|
147
|
+
OAuth credentials are configured per-provider (e.g., `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`).
|
|
148
|
+
|
|
149
|
+
### app.yaml
|
|
150
|
+
|
|
151
|
+
Application configuration is stored in `app.yaml` (generated by setup wizard):
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
controllers:
|
|
155
|
+
- skrift.controllers.auth:AuthController
|
|
156
|
+
- skrift.admin.controller:AdminController
|
|
157
|
+
- skrift.controllers.web:WebController
|
|
158
|
+
|
|
159
|
+
db:
|
|
160
|
+
url: $DATABASE_URL
|
|
161
|
+
pool_size: 5
|
|
162
|
+
|
|
163
|
+
auth:
|
|
164
|
+
redirect_base_url: $OAUTH_REDIRECT_BASE_URL
|
|
165
|
+
providers:
|
|
166
|
+
google:
|
|
167
|
+
client_id: $GOOGLE_CLIENT_ID
|
|
168
|
+
client_secret: $GOOGLE_CLIENT_SECRET
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Environment variables (prefixed with `$`) are interpolated at runtime.
|
|
172
|
+
|
|
173
|
+
## Deployment
|
|
174
|
+
|
|
175
|
+
### Minimal VPS Deployment
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Install Skrift
|
|
179
|
+
pip install skrift
|
|
180
|
+
|
|
181
|
+
# Create project directory
|
|
182
|
+
mkdir -p /opt/skrift && cd /opt/skrift
|
|
183
|
+
|
|
184
|
+
# Configure environment
|
|
185
|
+
cat > .env << EOF
|
|
186
|
+
SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
|
|
187
|
+
DATABASE_URL=sqlite+aiosqlite:///./app.db
|
|
188
|
+
OAUTH_REDIRECT_BASE_URL=https://yourdomain.com
|
|
189
|
+
EOF
|
|
190
|
+
|
|
191
|
+
# Start server (use setup wizard or create app.yaml manually)
|
|
192
|
+
skrift
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Production with Gunicorn
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
pip install gunicorn
|
|
199
|
+
gunicorn skrift.asgi:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8080
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
See the [Deployment Guide](docs/deployment.md) for detailed instructions including Docker, Docker Compose, and Kubernetes deployments.
|
|
203
|
+
|
|
204
|
+
## Database Migrations
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Apply migrations
|
|
208
|
+
skrift-db upgrade head
|
|
209
|
+
|
|
210
|
+
# Create new migration
|
|
211
|
+
skrift-db revision --autogenerate -m "description"
|
|
212
|
+
|
|
213
|
+
# Rollback
|
|
214
|
+
skrift-db downgrade -1
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Template Resolution
|
|
218
|
+
|
|
219
|
+
Templates follow WordPress-like hierarchical resolution:
|
|
220
|
+
|
|
221
|
+
| URL Path | Templates Tried |
|
|
222
|
+
|----------|-----------------|
|
|
223
|
+
| `/about` | `page-about.html` -> `page.html` |
|
|
224
|
+
| `/services/web` | `page-services-web.html` -> `page-services.html` -> `page.html` |
|
|
225
|
+
|
|
226
|
+
## Contributing
|
|
227
|
+
|
|
228
|
+
1. Fork the repository
|
|
229
|
+
2. Create a feature branch
|
|
230
|
+
3. Make your changes
|
|
231
|
+
4. Submit a pull request
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
skrift/__init__.py,sha256=eXE5PFVkJpH5XsV_ZlrTIeFPUPrmcHYAj4GpRS3R5PY,29
|
|
2
|
+
skrift/__main__.py,sha256=wt6JZL9nBhKU36vdyurhOEtWy7w3C9zohyy24PLcKho,164
|
|
3
|
+
skrift/alembic.ini,sha256=mYguI6CbMCTyfHctsGiTyf9Z5gv21FdeI3qtfgOHO3A,1815
|
|
4
|
+
skrift/asgi.py,sha256=qtP0aq9C6-bIZgbXiwJeF_3dY8O0cM2U_5hkajKMjBc,23032
|
|
5
|
+
skrift/cli.py,sha256=aT-7pXvOuuZC-Eypx1h-xCiqaBKhIFjSqd5Ky_dHna0,4214
|
|
6
|
+
skrift/config.py,sha256=O37SnnYkqTme-iMicMs2k4aD7Fv0wt7v3E_7fsiF40Y,7772
|
|
7
|
+
skrift/admin/__init__.py,sha256=x81Cj_ilVmv6slaMl16HHyT_AgrnLxKEWkS0RPa4V9s,289
|
|
8
|
+
skrift/admin/controller.py,sha256=5ZDypvKHXLNDESsKNsdsH2E3Si5OqlpzttFl7Ot8aF0,15651
|
|
9
|
+
skrift/admin/navigation.py,sha256=VwttFoIUIJy5rONKIkJd5w4CNkUpeK22_OfLGHecN34,3382
|
|
10
|
+
skrift/alembic/env.py,sha256=GaQx7D-3f0zVTV4YJNhN0GOfqHL99N4VBfMzjZJz0Bc,2673
|
|
11
|
+
skrift/alembic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
|
|
12
|
+
skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py,sha256=X6w1vbVKFurhEcblTLGO4Nd_IKkMVtb8TsptV4gpAJ4,2750
|
|
13
|
+
skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py,sha256=yCGjnMTpfSMHVYQCsPTD6wdDI8pJSUtGRESO6bz0KfU,2672
|
|
14
|
+
skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py,sha256=96TCvvQbYk3RGpZdEQ0x4bIDXxBLc3Gv7VTItTq9T54,850
|
|
15
|
+
skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py,sha256=XJ55LhwzgN07MAREk6w4Bx2Vu3KvMe_bDkIjgE6EfqM,1223
|
|
16
|
+
skrift/alembic/versions/20260122_200000_add_settings_table.py,sha256=aG4pxp18fwonGAWt7EoT0XaiIEmu7zltSrj8e2a8QZU,1310
|
|
17
|
+
skrift/alembic/versions/20260129_add_oauth_accounts.py,sha256=KPuzJ5J13FEbSbyziuB-nfO67i4cRgBeQnygn-uNQx8,5412
|
|
18
|
+
skrift/alembic/versions/20260129_add_provider_metadata.py,sha256=prDfnYM0cODBSYb9xYqW5Dab2WDjHNkkEMRH-hI7BIA,846
|
|
19
|
+
skrift/auth/__init__.py,sha256=uHMqty3dgDSYlReVT96WhygzH6qNAWSVDWoxumzxmsA,1155
|
|
20
|
+
skrift/auth/guards.py,sha256=QePajHsGnJ4R_hlhzblr5IoAgZcY5jzeZ64bJwDL9hM,4451
|
|
21
|
+
skrift/auth/roles.py,sha256=Cvwv8v7Li6CJHhucTuzxRjaNmS530F9sbbQlCPRqhm4,3291
|
|
22
|
+
skrift/auth/services.py,sha256=h6GTXdN5UMRYglnaFz4asMoutVkSSAyL3_Vt56N26pA,5441
|
|
23
|
+
skrift/controllers/__init__.py,sha256=bVr0tsSGz7jBi002Lqd1AA1FQd7ZA_IagsqTKpiHiK0,147
|
|
24
|
+
skrift/controllers/auth.py,sha256=ohYjBJiRa5RDQKUQjL3nR6lzm4uUZcfIgnF2G-QY5o8,21905
|
|
25
|
+
skrift/controllers/web.py,sha256=vmoBS1u5G9gCBu65S49yqZn_WBKlmsqlcvX5tYXTKnE,2348
|
|
26
|
+
skrift/db/__init__.py,sha256=uSghyDFT2K4SFiEqUzdjCGzWpS-Oy6Sd1FUappau-v0,52
|
|
27
|
+
skrift/db/base.py,sha256=QJplFj9235kZdScASEpvyNHln6YW2hqbHwJEYZ3OSsc,173
|
|
28
|
+
skrift/db/models/__init__.py,sha256=wFF9YWe7rhIWIzRuMtAWVDcZdJsNcKnjfhKbLZw9JVs,341
|
|
29
|
+
skrift/db/models/oauth_account.py,sha256=fzmApdtrRYeEWLl35Y7H0YtF7CWjzrpG-lknBOuYRc8,1840
|
|
30
|
+
skrift/db/models/page.py,sha256=CI5W2sWq0zbKHxfwH5TpmGdJ4zlTicckzpdZtaqjJVE,1019
|
|
31
|
+
skrift/db/models/role.py,sha256=VkwkF3XWemmFtkUpQRk6RTIBfcukrU-0PUPCeo8x834,1768
|
|
32
|
+
skrift/db/models/setting.py,sha256=Am4HTyq2LFR408R6BZb--slys1xM8YLYq9HTq_gybD0,392
|
|
33
|
+
skrift/db/models/user.py,sha256=kkR3h0CphUyvOGJOpiZH6Qp28LqW2OriwVTfybs7vuY,1338
|
|
34
|
+
skrift/db/services/__init__.py,sha256=qAC24IPOYg6AampUWonmLeajljQRMwUw0lgXksQG6Nk,69
|
|
35
|
+
skrift/db/services/oauth_service.py,sha256=1065tYGvrSer0X28GblibrSz2BjwVjl3tndXk1plthE,5208
|
|
36
|
+
skrift/db/services/page_service.py,sha256=cXJ-urV7LjUDoVsT1P52f6wcOsur3T-pvqKkGG4xlvI,5246
|
|
37
|
+
skrift/db/services/setting_service.py,sha256=eqFxukn8QFrDHQbaBILOLjP1nr34JPIPFT_NnhmeaTY,5613
|
|
38
|
+
skrift/lib/__init__.py,sha256=PL66bS4_-90zJDMsQSnzqkkxwkO_3aLcYl9l9cDrE2Y,65
|
|
39
|
+
skrift/lib/exceptions.py,sha256=p8ceLIQCc7agCwW6-mhBDAuMAMxZDcf9TDLC6PfztU4,5803
|
|
40
|
+
skrift/lib/template.py,sha256=4_urkRfvth75yNeQ5TyGTHvkvs3vVef7TcwZx0k285k,4226
|
|
41
|
+
skrift/setup/__init__.py,sha256=3VjFPMES5y0M5cQ9R4C1xazqiEPEDqTPjX9-3rBMXnA,478
|
|
42
|
+
skrift/setup/config_writer.py,sha256=YFH3FVjXN7Rum2fzGVPAQRkjdc9b0bHECDqMKYiEkhg,6347
|
|
43
|
+
skrift/setup/controller.py,sha256=v0Ey8T7ptJ5A3vOqQ1TUAXH1bQwA0288J5uyUWMihsw,34250
|
|
44
|
+
skrift/setup/middleware.py,sha256=Nai8ZG2vHldngmAhq7kWzAwKRNcP5tHKhJHa5dCh404,2941
|
|
45
|
+
skrift/setup/providers.py,sha256=0BFKB6168NcmtXxFF6ofHgEDMQD2FbXkexsqrARVtDI,7967
|
|
46
|
+
skrift/setup/state.py,sha256=RMe9LtIjzDoOm9u-Nk5-KAnr_JBiQIjWDpTP9E30ezc,9304
|
|
47
|
+
skrift/static/css/style.css,sha256=-Ijp4PCugWzhHhtZTaRUAITHJDW-0AsbNpDCFI1njVQ,20943
|
|
48
|
+
skrift/templates/base.html,sha256=4bg4s4VdES0dSvhJYLgrfrN26ynqeq1-3jyKPkWWVWk,2065
|
|
49
|
+
skrift/templates/error-404.html,sha256=sJrDaF3Or3Nyki8mxo3wBxLLzgy4wkB9p9wdS8pRA6k,409
|
|
50
|
+
skrift/templates/error-500.html,sha256=MR0wJ1JKLqdmdvsoJbQnZxLkxDPE59LrlbtVPKLM8-A,401
|
|
51
|
+
skrift/templates/error.html,sha256=tg45Hj0wNc9QiZKBVVQ2WsqDiAkByXZnuehTdJPclXc,415
|
|
52
|
+
skrift/templates/index.html,sha256=lzZlnoZ2luY__OtRVa61mpQIgcopAbkKa2rS9XRfRlI,172
|
|
53
|
+
skrift/templates/page.html,sha256=kY1j7TH6dFnvWnYwBEGCIEvardjLhnFMnM3NbAnKszQ,759
|
|
54
|
+
skrift/templates/admin/admin.html,sha256=8sErpGKedl7Hf_CMeu53bUEHptH1FqdBX9LT3z-1HcU,454
|
|
55
|
+
skrift/templates/admin/base.html,sha256=JX1MPw9XTY8jCT2M6iHUwEj8NPNOdZPca2cDUEs9kfQ,659
|
|
56
|
+
skrift/templates/admin/pages/edit.html,sha256=s-uZ4BYG7BdnWYTBSS1mr5aFL8XfT8h-KNiSIraWmHw,1322
|
|
57
|
+
skrift/templates/admin/pages/list.html,sha256=LYh1vEwSHkyXihjcy5PIo33iA7UeIxprw4Gsn1WZMm4,2185
|
|
58
|
+
skrift/templates/admin/settings/site.html,sha256=xHZCZGj9XqyLKCe2eoBlpI3oFIx5VoG_5FP-VAF2mz4,1516
|
|
59
|
+
skrift/templates/admin/users/list.html,sha256=9GWql1la5Srm-OOgooHR9Eouk7ZL66K_FXY6HAAB5lA,1732
|
|
60
|
+
skrift/templates/admin/users/roles.html,sha256=pos-ZM-gXYCN_D8DZpzwEBEm_WvmOKMMPz-3xJqs7nY,1473
|
|
61
|
+
skrift/templates/auth/dummy_login.html,sha256=G2ykWSiOK7XFdr7dzD-7VvKLTXzYT6Lduq5Pc6B21I0,2430
|
|
62
|
+
skrift/templates/auth/login.html,sha256=kBOfKRvKqn0L_POtXgOdl_rzUTNbr9F5NKtqxOMXQdk,7057
|
|
63
|
+
skrift/templates/setup/admin.html,sha256=BSIztZT2iqxVSW23Tfg7ZM51SdGrKL422CR05DPjNic,804
|
|
64
|
+
skrift/templates/setup/auth.html,sha256=0DVL0kU6DlJ2pWe4zwc3DsIVfBAdqpMJxPYvS4zm4OM,4953
|
|
65
|
+
skrift/templates/setup/base.html,sha256=LTXqbnHMvx1wDsxFvo4BSieBPD9pcLMj6NM4ZGzErFM,10372
|
|
66
|
+
skrift/templates/setup/complete.html,sha256=oyT-rYPl0uuyOjPXgNeLr8YoptW9QjHTlScZSViDvTk,630
|
|
67
|
+
skrift/templates/setup/configuring.html,sha256=2KHW9h2BrJgL_kO5IizbAYs4pnFLyRf76IQvEj_cNRM,4607
|
|
68
|
+
skrift/templates/setup/database.html,sha256=gU4-315-QraHa2Eq4Fh3b55QpOM2CkJzh27_Yz13frA,5495
|
|
69
|
+
skrift/templates/setup/restart.html,sha256=GHg31F_e2uLFhWUzJoalk0Y0oYLqsFWyZXWKX3mblbY,1355
|
|
70
|
+
skrift/templates/setup/site.html,sha256=PSOH-q1-ZBl47iSW9-Ad6lEfJn_fzdGD3Pk4vb3xgK4,1680
|
|
71
|
+
skrift-0.1.0a12.dist-info/METADATA,sha256=Vqc35fHrSYSsEgJSZ1r46xjHr-mlE4YyEz_1H5RFutQ,6436
|
|
72
|
+
skrift-0.1.0a12.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
73
|
+
skrift-0.1.0a12.dist-info/entry_points.txt,sha256=uquZ5Mumqr0xwYTpTcNiJtFSITGfF6_QCCy2DZJSZig,42
|
|
74
|
+
skrift-0.1.0a12.dist-info/RECORD,,
|