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,19 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Admin - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block admin_content %}
|
|
6
|
+
<hgroup>
|
|
7
|
+
<h1>Admin Dashboard</h1>
|
|
8
|
+
<p>Welcome to the admin area</p>
|
|
9
|
+
</hgroup>
|
|
10
|
+
|
|
11
|
+
<section class="admin-quick-links">
|
|
12
|
+
{% for item in admin_nav %}
|
|
13
|
+
<a href="{{ item.path }}" class="admin-card">
|
|
14
|
+
<h3>{{ item.label }}</h3>
|
|
15
|
+
<p>Manage {{ item.label | lower }}</p>
|
|
16
|
+
</a>
|
|
17
|
+
{% endfor %}
|
|
18
|
+
</section>
|
|
19
|
+
{% endblock %}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block main_class %} admin-layout{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="admin-container">
|
|
7
|
+
<aside class="admin-sidebar">
|
|
8
|
+
<nav class="admin-nav">
|
|
9
|
+
<ul>
|
|
10
|
+
{% for item in admin_nav %}
|
|
11
|
+
<li>
|
|
12
|
+
<a href="{{ item.path }}" {% if current_path == item.path %}class="active"{% endif %}>
|
|
13
|
+
{{ item.label }}
|
|
14
|
+
</a>
|
|
15
|
+
</li>
|
|
16
|
+
{% endfor %}
|
|
17
|
+
</ul>
|
|
18
|
+
</nav>
|
|
19
|
+
</aside>
|
|
20
|
+
<div class="admin-content">
|
|
21
|
+
{% block admin_content %}{% endblock %}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
{% endblock %}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ "Edit" if page else "New" }} Page - Admin - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block admin_content %}
|
|
6
|
+
<hgroup>
|
|
7
|
+
<h1>{{ "Edit" if page else "New" }} Page</h1>
|
|
8
|
+
<p>{{ "Update page details" if page else "Create a new page" }}</p>
|
|
9
|
+
</hgroup>
|
|
10
|
+
|
|
11
|
+
<form method="post" action="{{ '/admin/pages/' ~ page.id ~ '/edit' if page else '/admin/pages/new' }}">
|
|
12
|
+
<label for="title">Title</label>
|
|
13
|
+
<input type="text" id="title" name="title" value="{{ page.title if page else '' }}" required>
|
|
14
|
+
|
|
15
|
+
<label for="slug">Slug</label>
|
|
16
|
+
<input type="text" id="slug" name="slug" value="{{ page.slug if page else '' }}" required placeholder="e.g., about or services/consulting">
|
|
17
|
+
<small class="text-muted">The URL path for this page (e.g., "about" becomes /about)</small>
|
|
18
|
+
|
|
19
|
+
<label for="content">Content</label>
|
|
20
|
+
<textarea id="content" name="content" rows="15">{{ page.content if page else '' }}</textarea>
|
|
21
|
+
|
|
22
|
+
<label>
|
|
23
|
+
<input type="checkbox" name="is_published" {% if page and page.is_published %}checked{% endif %}>
|
|
24
|
+
Published
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
<div class="form-actions">
|
|
28
|
+
<button type="submit">{{ "Update" if page else "Create" }} Page</button>
|
|
29
|
+
<a href="/admin/pages" role="button" class="outline">Cancel</a>
|
|
30
|
+
</div>
|
|
31
|
+
</form>
|
|
32
|
+
{% endblock %}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Pages - Admin - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block admin_content %}
|
|
6
|
+
<div class="admin-header">
|
|
7
|
+
<hgroup>
|
|
8
|
+
<h1>Pages</h1>
|
|
9
|
+
<p>Manage site pages</p>
|
|
10
|
+
</hgroup>
|
|
11
|
+
<a href="/admin/pages/new" role="button">New Page</a>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<table class="admin-table">
|
|
15
|
+
<thead>
|
|
16
|
+
<tr>
|
|
17
|
+
<th>Title</th>
|
|
18
|
+
<th>Slug</th>
|
|
19
|
+
<th>Status</th>
|
|
20
|
+
<th>Created</th>
|
|
21
|
+
<th>Actions</th>
|
|
22
|
+
</tr>
|
|
23
|
+
</thead>
|
|
24
|
+
<tbody>
|
|
25
|
+
{% for page in pages %}
|
|
26
|
+
<tr>
|
|
27
|
+
<td>
|
|
28
|
+
<a href="/{{ page.slug }}">{{ page.title }}</a>
|
|
29
|
+
</td>
|
|
30
|
+
<td class="text-muted">/{{ page.slug }}</td>
|
|
31
|
+
<td>
|
|
32
|
+
{% if page.is_published %}
|
|
33
|
+
<span class="status-badge status-published">Published</span>
|
|
34
|
+
{% else %}
|
|
35
|
+
<span class="status-badge status-draft">Draft</span>
|
|
36
|
+
{% endif %}
|
|
37
|
+
</td>
|
|
38
|
+
<td>{{ page.created_at.strftime('%Y-%m-%d') }}</td>
|
|
39
|
+
<td class="actions-cell">
|
|
40
|
+
<a href="/admin/pages/{{ page.id }}/edit" role="button" class="outline small">Edit</a>
|
|
41
|
+
{% if page.is_published %}
|
|
42
|
+
<form method="post" action="/admin/pages/{{ page.id }}/unpublish" class="inline-form">
|
|
43
|
+
<button type="submit" class="outline small">Unpublish</button>
|
|
44
|
+
</form>
|
|
45
|
+
{% else %}
|
|
46
|
+
<form method="post" action="/admin/pages/{{ page.id }}/publish" class="inline-form">
|
|
47
|
+
<button type="submit" class="outline small">Publish</button>
|
|
48
|
+
</form>
|
|
49
|
+
{% endif %}
|
|
50
|
+
<form method="post" action="/admin/pages/{{ page.id }}/delete" class="inline-form" onsubmit="return confirm('Are you sure you want to delete this page?');">
|
|
51
|
+
<button type="submit" class="outline small danger">Delete</button>
|
|
52
|
+
</form>
|
|
53
|
+
</td>
|
|
54
|
+
</tr>
|
|
55
|
+
{% endfor %}
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
|
|
59
|
+
{% if not pages %}
|
|
60
|
+
<p class="no-items">No pages found.</p>
|
|
61
|
+
{% endif %}
|
|
62
|
+
{% endblock %}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Site Settings - Admin - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block admin_content %}
|
|
6
|
+
<hgroup>
|
|
7
|
+
<h1>Site Settings</h1>
|
|
8
|
+
<p>Configure your site's name, tagline, and other metadata</p>
|
|
9
|
+
</hgroup>
|
|
10
|
+
|
|
11
|
+
<form method="post" action="/admin/settings">
|
|
12
|
+
<label for="site_name">Site Name</label>
|
|
13
|
+
<input type="text" id="site_name" name="site_name" value="{{ settings.site_name }}" required>
|
|
14
|
+
<small class="text-muted">The name displayed in the header and browser title</small>
|
|
15
|
+
|
|
16
|
+
<label for="site_tagline">Site Tagline</label>
|
|
17
|
+
<input type="text" id="site_tagline" name="site_tagline" value="{{ settings.site_tagline }}">
|
|
18
|
+
<small class="text-muted">A short description or slogan for your site</small>
|
|
19
|
+
|
|
20
|
+
<label for="site_copyright_holder">Copyright Holder</label>
|
|
21
|
+
<input type="text" id="site_copyright_holder" name="site_copyright_holder" value="{{ settings.site_copyright_holder }}">
|
|
22
|
+
<small class="text-muted">Name displayed in the copyright notice (defaults to site name if empty)</small>
|
|
23
|
+
|
|
24
|
+
<label for="site_copyright_start_year">Copyright Start Year</label>
|
|
25
|
+
<input type="number" id="site_copyright_start_year" name="site_copyright_start_year" value="{{ settings.site_copyright_start_year }}" min="1900" max="2100">
|
|
26
|
+
<small class="text-muted">If different from current year, will show a range (e.g., 2020-2026)</small>
|
|
27
|
+
|
|
28
|
+
<div class="form-actions">
|
|
29
|
+
<button type="submit">Save Settings</button>
|
|
30
|
+
</div>
|
|
31
|
+
</form>
|
|
32
|
+
{% endblock %}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Users - Admin - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block admin_content %}
|
|
6
|
+
<hgroup>
|
|
7
|
+
<h1>Users</h1>
|
|
8
|
+
<p>Manage user accounts and roles</p>
|
|
9
|
+
</hgroup>
|
|
10
|
+
|
|
11
|
+
<table class="admin-table">
|
|
12
|
+
<thead>
|
|
13
|
+
<tr>
|
|
14
|
+
<th>User</th>
|
|
15
|
+
<th>Email</th>
|
|
16
|
+
<th>Roles</th>
|
|
17
|
+
<th>Last Login</th>
|
|
18
|
+
<th>Actions</th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
{% for user in users %}
|
|
23
|
+
<tr>
|
|
24
|
+
<td class="user-cell">
|
|
25
|
+
{% if user.picture_url %}
|
|
26
|
+
<img src="{{ user.picture_url }}" alt="{{ user.name }}" class="user-avatar" referrerpolicy="no-referrer">
|
|
27
|
+
{% endif %}
|
|
28
|
+
<span>{{ user.name or "—" }}</span>
|
|
29
|
+
</td>
|
|
30
|
+
<td>{{ user.email }}</td>
|
|
31
|
+
<td>
|
|
32
|
+
{% if user.roles %}
|
|
33
|
+
{% for role in user.roles %}
|
|
34
|
+
<span class="role-badge">{{ role.display_name or role.name }}</span>
|
|
35
|
+
{% endfor %}
|
|
36
|
+
{% else %}
|
|
37
|
+
<span class="text-muted">No roles</span>
|
|
38
|
+
{% endif %}
|
|
39
|
+
</td>
|
|
40
|
+
<td>
|
|
41
|
+
{% if user.last_login_at %}
|
|
42
|
+
{{ user.last_login_at.strftime('%Y-%m-%d %H:%M') }}
|
|
43
|
+
{% else %}
|
|
44
|
+
<span class="text-muted">Never</span>
|
|
45
|
+
{% endif %}
|
|
46
|
+
</td>
|
|
47
|
+
<td>
|
|
48
|
+
<a href="/admin/users/{{ user.id }}/roles" role="button" class="outline small">Edit Roles</a>
|
|
49
|
+
</td>
|
|
50
|
+
</tr>
|
|
51
|
+
{% endfor %}
|
|
52
|
+
</tbody>
|
|
53
|
+
</table>
|
|
54
|
+
|
|
55
|
+
{% if not users %}
|
|
56
|
+
<p class="no-items">No users found.</p>
|
|
57
|
+
{% endif %}
|
|
58
|
+
{% endblock %}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Edit Roles - {{ target_user.name or target_user.email }} - Admin - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block admin_content %}
|
|
6
|
+
<hgroup>
|
|
7
|
+
<h1>Edit Roles</h1>
|
|
8
|
+
<p>Assign roles to {{ target_user.name or target_user.email }}</p>
|
|
9
|
+
</hgroup>
|
|
10
|
+
|
|
11
|
+
<div class="user-info-card">
|
|
12
|
+
{% if target_user.picture_url %}
|
|
13
|
+
<img src="{{ target_user.picture_url }}" alt="{{ target_user.name }}" class="user-avatar-large" referrerpolicy="no-referrer">
|
|
14
|
+
{% endif %}
|
|
15
|
+
<div>
|
|
16
|
+
<strong>{{ target_user.name or "—" }}</strong>
|
|
17
|
+
<p class="text-muted">{{ target_user.email }}</p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<form method="post" action="/admin/users/{{ target_user.id }}/roles">
|
|
22
|
+
<fieldset>
|
|
23
|
+
<legend>Roles</legend>
|
|
24
|
+
{% for role_name, role_def in available_roles.items() %}
|
|
25
|
+
<label>
|
|
26
|
+
<input type="checkbox" name="role_{{ role_name }}" {% if role_name in current_roles %}checked{% endif %}>
|
|
27
|
+
<span>
|
|
28
|
+
<strong>{{ role_def.display_name or role_name }}</strong>
|
|
29
|
+
{% if role_def.description %}
|
|
30
|
+
<small class="text-muted"> — {{ role_def.description }}</small>
|
|
31
|
+
{% endif %}
|
|
32
|
+
</span>
|
|
33
|
+
</label>
|
|
34
|
+
{% endfor %}
|
|
35
|
+
</fieldset>
|
|
36
|
+
|
|
37
|
+
<div class="form-actions">
|
|
38
|
+
<button type="submit">Save Roles</button>
|
|
39
|
+
<a href="/admin/users" role="button" class="outline">Cancel</a>
|
|
40
|
+
</div>
|
|
41
|
+
</form>
|
|
42
|
+
{% endblock %}
|
|
@@ -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 %}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Login - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block head %}
|
|
6
|
+
<style>
|
|
7
|
+
.login-container {
|
|
8
|
+
max-width: 400px;
|
|
9
|
+
margin: 2rem auto;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.oauth-buttons {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
gap: 1rem;
|
|
16
|
+
margin-top: 1.5rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.oauth-btn {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
gap: 0.75rem;
|
|
24
|
+
padding: 1rem 1.5rem;
|
|
25
|
+
border-radius: 0.5rem;
|
|
26
|
+
font-size: 1rem;
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
text-decoration: none;
|
|
29
|
+
transition: opacity 0.2s;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.oauth-btn:hover {
|
|
33
|
+
opacity: 0.9;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.oauth-btn-google {
|
|
37
|
+
background: #4285f4;
|
|
38
|
+
color: white;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.oauth-btn-github {
|
|
42
|
+
background: #24292e;
|
|
43
|
+
color: white;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.oauth-btn-microsoft {
|
|
47
|
+
background: #00a4ef;
|
|
48
|
+
color: white;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.oauth-btn-discord {
|
|
52
|
+
background: #5865f2;
|
|
53
|
+
color: white;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.oauth-btn-facebook {
|
|
57
|
+
background: #1877f2;
|
|
58
|
+
color: white;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.oauth-btn-twitter {
|
|
62
|
+
background: #000000;
|
|
63
|
+
color: white;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.oauth-btn svg {
|
|
67
|
+
width: 20px;
|
|
68
|
+
height: 20px;
|
|
69
|
+
fill: currentColor;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
72
|
+
{% endblock %}
|
|
73
|
+
|
|
74
|
+
{% block content %}
|
|
75
|
+
<div class="login-container">
|
|
76
|
+
<article>
|
|
77
|
+
<header>
|
|
78
|
+
<h1>Login</h1>
|
|
79
|
+
</header>
|
|
80
|
+
|
|
81
|
+
<p>Sign in to access your account.</p>
|
|
82
|
+
|
|
83
|
+
<div class="oauth-buttons">
|
|
84
|
+
{% for key, provider in providers.items() %}
|
|
85
|
+
<a href="/auth/{{ key }}/login" class="oauth-btn oauth-btn-{{ key }}">
|
|
86
|
+
{% if key == 'google' %}
|
|
87
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
|
88
|
+
<path fill="#fff" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
|
|
89
|
+
<path fill="#fff" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
|
|
90
|
+
<path fill="#fff" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
|
|
91
|
+
<path fill="#fff" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
|
|
92
|
+
</svg>
|
|
93
|
+
{% elif key == 'github' %}
|
|
94
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
95
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
96
|
+
</svg>
|
|
97
|
+
{% elif key == 'microsoft' %}
|
|
98
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21">
|
|
99
|
+
<rect x="1" y="1" width="9" height="9" fill="#f25022"/>
|
|
100
|
+
<rect x="1" y="11" width="9" height="9" fill="#00a4ef"/>
|
|
101
|
+
<rect x="11" y="1" width="9" height="9" fill="#7fba00"/>
|
|
102
|
+
<rect x="11" y="11" width="9" height="9" fill="#ffb900"/>
|
|
103
|
+
</svg>
|
|
104
|
+
{% elif key == 'discord' %}
|
|
105
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
106
|
+
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
|
107
|
+
</svg>
|
|
108
|
+
{% elif key == 'facebook' %}
|
|
109
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
110
|
+
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
|
111
|
+
</svg>
|
|
112
|
+
{% elif key == 'twitter' %}
|
|
113
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
114
|
+
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
|
115
|
+
</svg>
|
|
116
|
+
{% endif %}
|
|
117
|
+
Continue with {{ provider.name }}
|
|
118
|
+
</a>
|
|
119
|
+
{% else %}
|
|
120
|
+
<p>No authentication providers are configured. Please contact the site administrator.</p>
|
|
121
|
+
{% endfor %}
|
|
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 %}
|
|
137
|
+
</article>
|
|
138
|
+
</div>
|
|
139
|
+
{% endblock %}
|
|
@@ -0,0 +1,52 @@
|
|
|
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 %}{{ site_name() }}{% 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
|
+
{% block head %}{% endblock %}
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<header class="container">
|
|
15
|
+
<nav>
|
|
16
|
+
<ul>
|
|
17
|
+
<li><a href="/" class="site-name">{{ site_name() }}</a></li>
|
|
18
|
+
</ul>
|
|
19
|
+
<ul>
|
|
20
|
+
{% if user %}
|
|
21
|
+
<li class="user-info">
|
|
22
|
+
{% if user.picture_url %}
|
|
23
|
+
<img src="{{ user.picture_url }}" alt="{{ user.name }}" referrerpolicy="no-referrer">
|
|
24
|
+
{% endif %}
|
|
25
|
+
<span>{{ user.name or user.email }}</span>
|
|
26
|
+
</li>
|
|
27
|
+
<li><a href="/auth/logout" role="button" class="outline">Logout</a></li>
|
|
28
|
+
{% else %}
|
|
29
|
+
<li><a href="/auth/login" role="button">Login</a></li>
|
|
30
|
+
{% endif %}
|
|
31
|
+
</ul>
|
|
32
|
+
</nav>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<main class="container{% block main_class %}{% endblock %}">
|
|
36
|
+
{% if flash %}
|
|
37
|
+
<div class="flash-messages">
|
|
38
|
+
<article role="alert">{{ flash }}</article>
|
|
39
|
+
</div>
|
|
40
|
+
{% endif %}
|
|
41
|
+
|
|
42
|
+
{% block content %}{% endblock %}
|
|
43
|
+
</main>
|
|
44
|
+
|
|
45
|
+
<footer class="container">
|
|
46
|
+
<hr>
|
|
47
|
+
<p><small>© {% if site_copyright_start_year() and site_copyright_start_year() != now().year %}{{ site_copyright_start_year() }}-{% endif %}{{ now().year }} {{ site_copyright_holder() or site_name() }}. All rights reserved.</small></p>
|
|
48
|
+
</footer>
|
|
49
|
+
|
|
50
|
+
{% block scripts %}{% endblock %}
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Page Not Found - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block main_class %} error-page{% endblock %}
|
|
6
|
+
|
|
7
|
+
{% block content %}
|
|
8
|
+
<article>
|
|
9
|
+
<header>
|
|
10
|
+
<h1>Page Not Found</h1>
|
|
11
|
+
</header>
|
|
12
|
+
|
|
13
|
+
<p>The page you're looking for doesn't exist or has been moved.</p>
|
|
14
|
+
|
|
15
|
+
<footer>
|
|
16
|
+
<a href="/" role="button">Return Home</a>
|
|
17
|
+
</footer>
|
|
18
|
+
</article>
|
|
19
|
+
{% endblock %}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Server Error - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block main_class %} error-page{% endblock %}
|
|
6
|
+
|
|
7
|
+
{% block content %}
|
|
8
|
+
<article>
|
|
9
|
+
<header>
|
|
10
|
+
<h1>Server Error</h1>
|
|
11
|
+
</header>
|
|
12
|
+
|
|
13
|
+
<p>Something went wrong on our end. Please try again later.</p>
|
|
14
|
+
|
|
15
|
+
<footer>
|
|
16
|
+
<a href="/" role="button">Return Home</a>
|
|
17
|
+
</footer>
|
|
18
|
+
</article>
|
|
19
|
+
{% endblock %}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Error {{ status_code }} - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block main_class %} error-page{% endblock %}
|
|
6
|
+
|
|
7
|
+
{% block content %}
|
|
8
|
+
<article>
|
|
9
|
+
<header>
|
|
10
|
+
<h1>Error {{ status_code }}</h1>
|
|
11
|
+
</header>
|
|
12
|
+
|
|
13
|
+
<p>{{ message or "An unexpected error occurred." }}</p>
|
|
14
|
+
|
|
15
|
+
<footer>
|
|
16
|
+
<a href="/" role="button">Return Home</a>
|
|
17
|
+
</footer>
|
|
18
|
+
</article>
|
|
19
|
+
{% endblock %}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ page.title }} - {{ site_name() }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<article>
|
|
7
|
+
<header>
|
|
8
|
+
<h1>{{ page.title }}</h1>
|
|
9
|
+
{% if page.published_at %}
|
|
10
|
+
<time datetime="{{ page.published_at.isoformat() }}">
|
|
11
|
+
Published: {{ page.published_at.strftime('%B %d, %Y') }}
|
|
12
|
+
</time>
|
|
13
|
+
{% endif %}
|
|
14
|
+
</header>
|
|
15
|
+
|
|
16
|
+
<div class="page-content">
|
|
17
|
+
{{ page.content | safe }}
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
{% if not page.is_published %}
|
|
21
|
+
<aside style="background: #fff3cd; padding: 1rem; border-left: 4px solid #ffc107; margin-top: 2rem;">
|
|
22
|
+
<strong>Draft:</strong> This page is not published and only visible to logged-in users.
|
|
23
|
+
</aside>
|
|
24
|
+
{% endif %}
|
|
25
|
+
</article>
|
|
26
|
+
{% endblock %}
|