stkit 17.0.0__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.
- stk/__init__.py +19 -0
- stk/__main__.py +6 -0
- stk/agent_login.py +78 -0
- stk/app.py +323 -0
- stk/checks.py +634 -0
- stk/cli/__init__.py +49 -0
- stk/cli/agent.py +267 -0
- stk/cli/base.py +79 -0
- stk/cli/database.py +199 -0
- stk/cli/develop.py +63 -0
- stk/cli/doctor.py +262 -0
- stk/cli/main.py +101 -0
- stk/cli/reports.py +296 -0
- stk/cli/smoke.py +399 -0
- stk/cli/users.py +259 -0
- stk/cli/watch.py +68 -0
- stk/extensions.py +29 -0
- stk/migrations/__init__.py +130 -0
- stk/migrations/env.py +58 -0
- stk/migrations/script.py.mako +17 -0
- stk/migrations/versions/stk_0001_initial_schema.py +213 -0
- stk/portal/views.py +34 -0
- stk/public/__init__.py +0 -0
- stk/public/models.py +0 -0
- stk/public/views.py +344 -0
- stk/scaffold/__init__.py +1 -0
- stk/scaffold/app/CLAUDE.md +65 -0
- stk/scaffold/app/Dockerfile +41 -0
- stk/scaffold/app/README.md +16 -0
- stk/scaffold/app/__app__/__init__.py +1 -0
- stk/scaffold/app/__app__/app.py +8 -0
- stk/scaffold/app/__app__/settings.py +7 -0
- stk/scaffold/app/claude/skills/stk-api/SKILL.md +52 -0
- stk/scaffold/app/claude/skills/stk-blueprint/SKILL.md +48 -0
- stk/scaffold/app/claude/skills/stk-deploy/SKILL.md +61 -0
- stk/scaffold/app/claude/skills/stk-lessons/SKILL.md +58 -0
- stk/scaffold/app/claude/skills/stk-migrate/SKILL.md +96 -0
- stk/scaffold/app/docker-compose.yml +88 -0
- stk/scaffold/app/dockerignore +13 -0
- stk/scaffold/app/env-sample +78 -0
- stk/scaffold/app/gitignore +8 -0
- stk/scaffold/app/nginx/nginx.conf +46 -0
- stk/scaffold/app/nginx/stk.conf +36 -0
- stk/scaffold/app/postgres/init-db.sh +8 -0
- stk/scaffold/app/pyproject.toml +23 -0
- stk/scaffold/app/tests/__init__.py +0 -0
- stk/scaffold/app/tests/test_app.py +23 -0
- stk/scaffold/generator.py +209 -0
- stk/scaffold/templates.py +437 -0
- stk/security.py +145 -0
- stk/settings.py +147 -0
- stk/shell.py +284 -0
- stk/static/VERSIONS.txt +4 -0
- stk/static/css/app.css +229 -0
- stk/static/css/layout.css +330 -0
- stk/static/css/vuetify.min.css +6 -0
- stk/static/icons/fonts/tabler-icons.woff2 +0 -0
- stk/static/icons/tabler-icons.min.css +4 -0
- stk/static/img/auth-bg.webp +0 -0
- stk/static/img/favicon.ico +0 -0
- stk/static/img/stk.svg +1664 -0
- stk/static/js/axios.min.js +5 -0
- stk/static/js/components/NotificationDropdown.js +156 -0
- stk/static/js/components/ThemeSwitcher.js +68 -0
- stk/static/js/components/TransitionExpand.js +64 -0
- stk/static/js/components/VerticalNavGroup.js +119 -0
- stk/static/js/components/VerticalNavLink.js +36 -0
- stk/static/js/components/VerticalNavSectionTitle.js +21 -0
- stk/static/js/components/index.js +47 -0
- stk/static/js/config.js +150 -0
- stk/static/js/vue.min.js +13 -0
- stk/static/js/vuetify.min.js +3148 -0
- stk/static/robots.txt +1 -0
- stk/tasks/__init__.py +78 -0
- stk/templates/401.html +14 -0
- stk/templates/404.html +12 -0
- stk/templates/500.html +13 -0
- stk/templates/_brand.html +1 -0
- stk/templates/auth_layout.html +26 -0
- stk/templates/cms/activities.html +135 -0
- stk/templates/cms/roles.html +215 -0
- stk/templates/cms/users.html +312 -0
- stk/templates/dashboard.html +132 -0
- stk/templates/index.html +52 -0
- stk/templates/layout.html +328 -0
- stk/templates/login-layout.html +54 -0
- stk/templates/security/_macros.html +39 -0
- stk/templates/security/_messages.html +12 -0
- stk/templates/security/change_password.html +100 -0
- stk/templates/security/confirm_email.html +44 -0
- stk/templates/security/email/change_notice.html +4 -0
- stk/templates/security/email/change_notice.txt +5 -0
- stk/templates/security/email/confirmation_instructions.html +3 -0
- stk/templates/security/email/confirmation_instructions.txt +3 -0
- stk/templates/security/email/login_instructions.html +5 -0
- stk/templates/security/email/login_instructions.txt +5 -0
- stk/templates/security/email/reset_instructions.html +1 -0
- stk/templates/security/email/reset_instructions.txt +3 -0
- stk/templates/security/email/reset_notice.html +1 -0
- stk/templates/security/email/reset_notice.txt +1 -0
- stk/templates/security/email/welcome.html +7 -0
- stk/templates/security/email/welcome.txt +7 -0
- stk/templates/security/forgot_password.html +63 -0
- stk/templates/security/login_user.html +206 -0
- stk/templates/security/mf_recovery.html +67 -0
- stk/templates/security/mf_recovery_codes.html +119 -0
- stk/templates/security/register_user.html +263 -0
- stk/templates/security/reset_password.html +53 -0
- stk/templates/security/send_confirmation.html +48 -0
- stk/templates/security/send_login.html +39 -0
- stk/templates/security/two_factor_select.html +78 -0
- stk/templates/security/two_factor_setup.html +166 -0
- stk/templates/security/two_factor_verify_code.html +71 -0
- stk/templates/security/wan_register.html +179 -0
- stk/templates/security/wan_signin.html +123 -0
- stk/templates/security/wan_verify.html +109 -0
- stk/user/__init__.py +0 -0
- stk/user/forms.py +10 -0
- stk/user/models.py +308 -0
- stk/user/views.py +339 -0
- stk/utils/email.py +32 -0
- stk/utils/ratelimit.py +67 -0
- stk/websocket.py +133 -0
- stkit-17.0.0.dist-info/METADATA +135 -0
- stkit-17.0.0.dist-info/RECORD +128 -0
- stkit-17.0.0.dist-info/WHEEL +4 -0
- stkit-17.0.0.dist-info/entry_points.txt +2 -0
- stkit-17.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
7
|
+
<title>{% block title %}{{ config.APP_NAME }}{% endblock %}</title>
|
|
8
|
+
<meta name="description" content="A framework for the next decade">
|
|
9
|
+
<meta name="author" content="{{ config.APP_NAME }}">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<link rel="icon" href="/static/img/favicon.ico" type="image/x-icon">
|
|
12
|
+
|
|
13
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/vuetify.min.css') }}">
|
|
14
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='icons/tabler-icons.min.css') }}">
|
|
15
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/layout.css') }}">
|
|
16
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
|
|
17
|
+
|
|
18
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;700;800&family=Be+Vietnam+Pro:wght@400;500;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
19
|
+
<style>[v-cloak] { display: none !important; }</style>
|
|
20
|
+
|
|
21
|
+
{% block css %}{% endblock %}
|
|
22
|
+
{% block head_js %}{% endblock %}
|
|
23
|
+
</head>
|
|
24
|
+
|
|
25
|
+
<body class="{% block body_class %} {% endblock %}">
|
|
26
|
+
<v-app id="app" v-cloak>
|
|
27
|
+
<v-layout class="rounded rounded-md">
|
|
28
|
+
<!-- App Bar -->
|
|
29
|
+
<v-app-bar color="surface" density="comfortable" elevation="1">
|
|
30
|
+
{% if current_user.is_authenticated %}
|
|
31
|
+
<v-btn icon variant="text" @click="drawer = !drawer" class="d-md-none">
|
|
32
|
+
<i class="ti ti-menu-2" style="font-size: 24px;"></i>
|
|
33
|
+
</v-btn>
|
|
34
|
+
{% endif %}
|
|
35
|
+
|
|
36
|
+
<v-app-bar-title>
|
|
37
|
+
<a href="/" class="d-flex align-center text-decoration-none">
|
|
38
|
+
{% include "_brand.html" %}
|
|
39
|
+
</a>
|
|
40
|
+
</v-app-bar-title>
|
|
41
|
+
|
|
42
|
+
<v-spacer></v-spacer>
|
|
43
|
+
|
|
44
|
+
{% if not current_user.is_authenticated %}
|
|
45
|
+
<v-btn href="/login" variant="text">Sign in</v-btn>
|
|
46
|
+
{% else %}
|
|
47
|
+
<!-- Theme Switcher -->
|
|
48
|
+
<theme-switcher></theme-switcher>
|
|
49
|
+
|
|
50
|
+
<!-- Notifications -->
|
|
51
|
+
<notification-dropdown
|
|
52
|
+
:notifications="notifications"
|
|
53
|
+
@click="handleNotificationClick"
|
|
54
|
+
@mark-read="markNotificationRead"
|
|
55
|
+
@mark-all-read="markAllNotificationsRead"
|
|
56
|
+
@remove="removeNotification"
|
|
57
|
+
></notification-dropdown>
|
|
58
|
+
|
|
59
|
+
<!-- Connection Status -->
|
|
60
|
+
<v-tooltip :text="wsConnected ? 'Real-time connected' : 'Reconnecting...'" location="bottom">
|
|
61
|
+
<template v-slot:activator="{ props }">
|
|
62
|
+
<div v-bind="props" :style="{width: '8px', height: '8px', borderRadius: '50%', backgroundColor: wsConnected ? '#4caf50' : '#9e9e9e', transition: 'background-color 0.3s'}" class="mr-1"></div>
|
|
63
|
+
</template>
|
|
64
|
+
</v-tooltip>
|
|
65
|
+
|
|
66
|
+
<!-- User Menu -->
|
|
67
|
+
<v-menu :close-on-content-click="false" location="bottom end" transition="fade-transition">
|
|
68
|
+
<template v-slot:activator="{ props }">
|
|
69
|
+
<v-btn v-bind="props" icon variant="text" size="small" class="ml-1">
|
|
70
|
+
<v-avatar color="primary" size="32">
|
|
71
|
+
<span class="text-body-medium">{{ current_user.name[:2]|upper if current_user.name else 'U' }}</span>
|
|
72
|
+
</v-avatar>
|
|
73
|
+
</v-btn>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<v-card min-width="280">
|
|
77
|
+
<v-list>
|
|
78
|
+
<v-list-item
|
|
79
|
+
title="{{ current_user.name }}"
|
|
80
|
+
subtitle="{{ current_user.email }}"
|
|
81
|
+
>
|
|
82
|
+
<template v-slot:prepend><i class="ti ti-user mr-3" style="font-size: 20px;"></i></template>
|
|
83
|
+
</v-list-item>
|
|
84
|
+
</v-list>
|
|
85
|
+
|
|
86
|
+
<v-divider></v-divider>
|
|
87
|
+
|
|
88
|
+
<v-list density="compact">
|
|
89
|
+
<v-list-item href="/dashboard">
|
|
90
|
+
<template v-slot:prepend><i class="ti ti-layout-dashboard mr-3" style="font-size: 18px;"></i></template>
|
|
91
|
+
<v-list-item-title>My Account</v-list-item-title>
|
|
92
|
+
</v-list-item>
|
|
93
|
+
<v-list-item href="/change">
|
|
94
|
+
<template v-slot:prepend><i class="ti ti-key mr-3" style="font-size: 18px;"></i></template>
|
|
95
|
+
<v-list-item-title>Change Password</v-list-item-title>
|
|
96
|
+
</v-list-item>
|
|
97
|
+
<v-list-item href="/tf-setup">
|
|
98
|
+
<template v-slot:prepend><i class="ti ti-shield-lock mr-3" style="font-size: 18px;"></i></template>
|
|
99
|
+
<v-list-item-title>Two-Factor Auth</v-list-item-title>
|
|
100
|
+
</v-list-item>
|
|
101
|
+
</v-list>
|
|
102
|
+
|
|
103
|
+
<v-divider></v-divider>
|
|
104
|
+
|
|
105
|
+
<v-list density="compact">
|
|
106
|
+
<v-list-item @click="logout" base-color="error">
|
|
107
|
+
<template v-slot:prepend><i class="ti ti-logout mr-3" style="font-size: 18px;"></i></template>
|
|
108
|
+
<v-list-item-title>Logout</v-list-item-title>
|
|
109
|
+
</v-list-item>
|
|
110
|
+
</v-list>
|
|
111
|
+
</v-card>
|
|
112
|
+
</v-menu>
|
|
113
|
+
{% endif %}
|
|
114
|
+
</v-app-bar>
|
|
115
|
+
|
|
116
|
+
{% if current_user.is_authenticated %}
|
|
117
|
+
<!-- Navigation Drawer (authenticated users only) -->
|
|
118
|
+
<v-navigation-drawer v-model="drawer" :rail="isNavCollapsed" :rail-width="68" width="260">
|
|
119
|
+
<!-- Nav Header -->
|
|
120
|
+
<div class="nav-header">
|
|
121
|
+
<span v-show="!isNavCollapsed" class="app-title">Navigation</span>
|
|
122
|
+
<v-spacer></v-spacer>
|
|
123
|
+
<v-btn
|
|
124
|
+
v-show="!isMobile"
|
|
125
|
+
icon
|
|
126
|
+
variant="text"
|
|
127
|
+
size="small"
|
|
128
|
+
@click="toggleNavCollapse"
|
|
129
|
+
:title="isNavCollapsed ? 'Expand Sidebar' : 'Collapse Sidebar'"
|
|
130
|
+
>
|
|
131
|
+
<i :class="isNavCollapsed ? 'ti ti-layout-sidebar-right' : 'ti ti-layout-sidebar-left-collapse'" style="font-size: 20px;"></i>
|
|
132
|
+
</v-btn>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<v-divider></v-divider>
|
|
136
|
+
|
|
137
|
+
<!-- Navigation Items -->
|
|
138
|
+
<ul class="nav-items" style="overflow-y: auto; flex: 1;">
|
|
139
|
+
<component
|
|
140
|
+
v-for="item in filteredNavItems"
|
|
141
|
+
:key="item.title || item.heading"
|
|
142
|
+
:is="resolveNavComponent(item)"
|
|
143
|
+
:item="item"
|
|
144
|
+
/>
|
|
145
|
+
</ul>
|
|
146
|
+
</v-navigation-drawer>
|
|
147
|
+
{% endif %}
|
|
148
|
+
|
|
149
|
+
<!-- Main Content -->
|
|
150
|
+
<v-main class="d-flex align-center justify-center" style="min-height: 300px;">
|
|
151
|
+
{% block content %}
|
|
152
|
+
{% endblock %}
|
|
153
|
+
</v-main>
|
|
154
|
+
|
|
155
|
+
<!-- Flash messages (plain HTML - renders before Vue mounts) -->
|
|
156
|
+
{% set messages = get_flashed_messages(with_categories=true) %}
|
|
157
|
+
{% if messages %}
|
|
158
|
+
<div id="flash-messages" class="flash-container">
|
|
159
|
+
{% for category, message in messages %}
|
|
160
|
+
<div class="flash-message {{ category if category in ['success', 'warning', 'error', 'info'] else 'primary' }}">
|
|
161
|
+
<span>{{ message }}</span>
|
|
162
|
+
<button class="flash-close" onclick="this.parentElement.remove()">×</button>
|
|
163
|
+
</div>
|
|
164
|
+
{% endfor %}
|
|
165
|
+
</div>
|
|
166
|
+
{% endif %}
|
|
167
|
+
</v-layout>
|
|
168
|
+
</v-app>
|
|
169
|
+
|
|
170
|
+
{% if current_user.is_authenticated %}
|
|
171
|
+
<!-- Logout is POST-only with CSRF (quart-security >= 1.4.1) -->
|
|
172
|
+
<form id="logout-form" method="POST" action="/logout" hidden>
|
|
173
|
+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
174
|
+
</form>
|
|
175
|
+
{% endif %}
|
|
176
|
+
|
|
177
|
+
<!-- Flash message auto-hide (outside Vue template) -->
|
|
178
|
+
{% if messages %}
|
|
179
|
+
<script>
|
|
180
|
+
// Auto-hide flash messages after 5 seconds
|
|
181
|
+
setTimeout(function() {
|
|
182
|
+
const flashMessages = document.getElementById('flash-messages');
|
|
183
|
+
if (flashMessages) {
|
|
184
|
+
flashMessages.style.display = 'none';
|
|
185
|
+
}
|
|
186
|
+
}, 5000);
|
|
187
|
+
</script>
|
|
188
|
+
{% endif %}
|
|
189
|
+
|
|
190
|
+
<!-- Core Scripts -->
|
|
191
|
+
<script src="{{ url_for('static', filename='js/vue.min.js') }}"></script>
|
|
192
|
+
<script src="{{ url_for('static', filename='js/config.js') }}"></script>
|
|
193
|
+
<script src="{{ url_for('static', filename='js/vuetify.min.js') }}"></script>
|
|
194
|
+
<script src="{{ url_for('static', filename='js/axios.min.js') }}"></script>
|
|
195
|
+
<script>axios.defaults.headers.common["X-CSRFToken"] = document.querySelector('meta[name="csrf-token"]').content;</script>
|
|
196
|
+
|
|
197
|
+
<!-- Navigation Data: core entries plus the app's `app.navigation` -->
|
|
198
|
+
{% block navigation %}
|
|
199
|
+
<script>const stkNavigation = {{ stk_navigation() | tojson }};</script>
|
|
200
|
+
{% endblock %}
|
|
201
|
+
|
|
202
|
+
<!-- stk Components -->
|
|
203
|
+
<script src="{{ url_for('static', filename='js/components/TransitionExpand.js') }}"></script>
|
|
204
|
+
<script src="{{ url_for('static', filename='js/components/VerticalNavLink.js') }}"></script>
|
|
205
|
+
<script src="{{ url_for('static', filename='js/components/VerticalNavGroup.js') }}"></script>
|
|
206
|
+
<script src="{{ url_for('static', filename='js/components/VerticalNavSectionTitle.js') }}"></script>
|
|
207
|
+
<script src="{{ url_for('static', filename='js/components/NotificationDropdown.js') }}"></script>
|
|
208
|
+
<script src="{{ url_for('static', filename='js/components/ThemeSwitcher.js') }}"></script>
|
|
209
|
+
<script src="{{ url_for('static', filename='js/components/index.js') }}"></script>
|
|
210
|
+
|
|
211
|
+
<!-- Layout Base Script -->
|
|
212
|
+
<script>
|
|
213
|
+
// User roles passed from server
|
|
214
|
+
const userRoles = {{ current_user.roles | map(attribute='name') | list | tojson if current_user.is_authenticated else '[]' | safe }};
|
|
215
|
+
|
|
216
|
+
// Base app data and methods for layout
|
|
217
|
+
const layoutMixin = {
|
|
218
|
+
data() {
|
|
219
|
+
return {
|
|
220
|
+
drawer: window.innerWidth >= 960, // phones start with the menu closed
|
|
221
|
+
isNavCollapsed: false,
|
|
222
|
+
isMobile: window.innerWidth < 960,
|
|
223
|
+
navItems: typeof stkNavigation !== 'undefined' ? stkNavigation : [],
|
|
224
|
+
notifications: [],
|
|
225
|
+
wsConnected: false,
|
|
226
|
+
_ws: null,
|
|
227
|
+
_wsRetry: 1000,
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
computed: {
|
|
231
|
+
filteredNavItems() {
|
|
232
|
+
return filterNavByRole(this.navItems, userRoles);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
methods: {
|
|
236
|
+
resolveNavComponent,
|
|
237
|
+
logout() {
|
|
238
|
+
document.getElementById('logout-form').submit();
|
|
239
|
+
},
|
|
240
|
+
toggleNavCollapse() {
|
|
241
|
+
this.isNavCollapsed = !this.isNavCollapsed;
|
|
242
|
+
localStorage.setItem('stk-nav-collapsed', this.isNavCollapsed);
|
|
243
|
+
},
|
|
244
|
+
handleResize() {
|
|
245
|
+
this.isMobile = window.innerWidth < 960;
|
|
246
|
+
if (this.isMobile) {
|
|
247
|
+
this.isNavCollapsed = false;
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
// Notification methods
|
|
251
|
+
handleNotificationClick(notification) {
|
|
252
|
+
if (notification.to) {
|
|
253
|
+
window.location.href = notification.to;
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
markNotificationRead(id) {
|
|
257
|
+
const notification = this.notifications.find(n => n.id === id);
|
|
258
|
+
if (notification) {
|
|
259
|
+
notification.isSeen = true;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
markAllNotificationsRead() {
|
|
263
|
+
this.notifications.forEach(n => n.isSeen = true);
|
|
264
|
+
},
|
|
265
|
+
removeNotification(id) {
|
|
266
|
+
const index = this.notifications.findIndex(n => n.id === id);
|
|
267
|
+
if (index > -1) {
|
|
268
|
+
this.notifications.splice(index, 1);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
_connectWs() {
|
|
272
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
273
|
+
const ws = new WebSocket(`${proto}//${location.host}/ws`);
|
|
274
|
+
this._ws = ws;
|
|
275
|
+
ws.onopen = () => { this.wsConnected = true; this._wsRetry = 1000; };
|
|
276
|
+
ws.onclose = () => {
|
|
277
|
+
this.wsConnected = false;
|
|
278
|
+
setTimeout(() => this._connectWs(), Math.min(this._wsRetry *= 1.5, 30000));
|
|
279
|
+
};
|
|
280
|
+
ws.onmessage = (e) => { this._onWsMessage(JSON.parse(e.data)); };
|
|
281
|
+
},
|
|
282
|
+
_onWsMessage(msg) {
|
|
283
|
+
// Handle incoming messages — extend this for notifications, live updates, etc.
|
|
284
|
+
if (msg.type === 'notification') {
|
|
285
|
+
this.notifications.unshift(msg);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
mounted() {
|
|
290
|
+
// Restore nav collapsed state
|
|
291
|
+
const savedCollapsed = localStorage.getItem('stk-nav-collapsed');
|
|
292
|
+
if (savedCollapsed !== null && !this.isMobile) {
|
|
293
|
+
this.isNavCollapsed = savedCollapsed === 'true';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Handle resize
|
|
297
|
+
window.addEventListener('resize', this.handleResize);
|
|
298
|
+
|
|
299
|
+
// WebSocket connection (authenticated users only)
|
|
300
|
+
{% if current_user.is_authenticated %}
|
|
301
|
+
this._connectWs();
|
|
302
|
+
{% endif %}
|
|
303
|
+
},
|
|
304
|
+
beforeUnmount() {
|
|
305
|
+
window.removeEventListener('resize', this.handleResize);
|
|
306
|
+
if (this._ws) this._ws.close();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
</script>
|
|
310
|
+
|
|
311
|
+
{% block js %}{% endblock %}
|
|
312
|
+
|
|
313
|
+
<!-- Prefetch same-origin links on hover (Chrome). Skip mutating/auth routes. -->
|
|
314
|
+
<script type="speculationrules">
|
|
315
|
+
{
|
|
316
|
+
"prefetch": [{
|
|
317
|
+
"where": {
|
|
318
|
+
"and": [
|
|
319
|
+
{ "href_matches": "/*" },
|
|
320
|
+
{ "not": { "href_matches": ["/login/*", "/oauth/*", "/ws"] } }
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
"eagerness": "moderate"
|
|
324
|
+
}]
|
|
325
|
+
}
|
|
326
|
+
</script>
|
|
327
|
+
</body>
|
|
328
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>{{ config.APP_NAME }} - Security</title>
|
|
6
|
+
<meta name="description" content="A framework for the next decade">
|
|
7
|
+
<meta name="author" content="{{ config.APP_NAME }}">
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
9
|
+
<link rel="icon" href="/static/img/favicon.ico" type="image/x-icon">
|
|
10
|
+
|
|
11
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/vuetify.min.css') }}">
|
|
12
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='icons/tabler-icons.min.css') }}">
|
|
13
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
html, body { height: 100%; margin: 0; padding: 0; }
|
|
17
|
+
#app, .v-application, .v-application__wrap { min-height: 100vh; }
|
|
18
|
+
</style>
|
|
19
|
+
{% block css %}{% endblock %}
|
|
20
|
+
{% block head_js %}{% endblock %}
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<div id="app">
|
|
24
|
+
<v-app v-cloak>
|
|
25
|
+
{% block content %}{% endblock %}
|
|
26
|
+
</v-app>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<script src="{{ url_for('static', filename='js/vue.min.js') }}"></script>
|
|
30
|
+
<script src="{{ url_for('static', filename='js/vuetify.min.js') }}"></script>
|
|
31
|
+
<script src="{{ url_for('static', filename='js/axios.min.js') }}"></script>
|
|
32
|
+
<script src="{{ url_for('static', filename='js/config.js') }}"></script>
|
|
33
|
+
|
|
34
|
+
<script>
|
|
35
|
+
// Make config available globally for templates
|
|
36
|
+
const delimiters = config.delimiters;
|
|
37
|
+
const vuetifyConfig = config.vuetifyConfig;
|
|
38
|
+
|
|
39
|
+
// Helper for handling API request errors
|
|
40
|
+
function handleRequestError(error) {
|
|
41
|
+
if (error.response && error.response.data) {
|
|
42
|
+
const data = error.response.data;
|
|
43
|
+
if (data.response && data.response.errors) {
|
|
44
|
+
return Object.values(data.response.errors).flat().join(', ');
|
|
45
|
+
}
|
|
46
|
+
if (data.message) return data.message;
|
|
47
|
+
}
|
|
48
|
+
return error.message || 'An error occurred';
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
{% block js %}{% endblock %}
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{% macro render_field_with_errors(field) %}
|
|
2
|
+
<div class="fs-div" id="{{ field.id|default('fs-field') }}">
|
|
3
|
+
{{ field.label }} {{ field(**kwargs)|safe }}
|
|
4
|
+
{% if field.errors %}
|
|
5
|
+
<ul>
|
|
6
|
+
{% for error in field.errors %}<li class="fs-error-msg">{{ error }}</li>{% endfor %}
|
|
7
|
+
</ul>
|
|
8
|
+
{% endif %}
|
|
9
|
+
</div>
|
|
10
|
+
{% endmacro %}
|
|
11
|
+
|
|
12
|
+
{% macro render_field(field) %}
|
|
13
|
+
<div class="fs-div" id="{{ field.id|default('fs-field') }}">{{ field(**kwargs)|safe }}</div>
|
|
14
|
+
{% endmacro %}
|
|
15
|
+
|
|
16
|
+
{% macro render_field_errors(field) %}
|
|
17
|
+
<div class="fs-div" id="{{ field.id if field else 'fs-field' }}">
|
|
18
|
+
{% if field and field.errors %}
|
|
19
|
+
<ul>
|
|
20
|
+
{% for error in field.errors %}<li class="fs-error-msg">{{ error }}</li>{% endfor %}
|
|
21
|
+
</ul>
|
|
22
|
+
{% endif %}
|
|
23
|
+
</div>
|
|
24
|
+
{% endmacro %}
|
|
25
|
+
|
|
26
|
+
{# render WTForms (>3.0) form level errors #}
|
|
27
|
+
{% macro render_form_errors(form) %}
|
|
28
|
+
{% if form.form_errors %}
|
|
29
|
+
<div class="fs-div" id="fs-form-errors">
|
|
30
|
+
<ul>
|
|
31
|
+
{% for error in form.form_errors %}<li class="fs-error-msg">{{ error }}</li>{% endfor %}
|
|
32
|
+
</ul>
|
|
33
|
+
</div>
|
|
34
|
+
{% endif %}
|
|
35
|
+
{% endmacro %}
|
|
36
|
+
|
|
37
|
+
{% macro prop_next() -%}
|
|
38
|
+
{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}
|
|
39
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{%- with messages = get_flashed_messages(with_categories=true) -%}
|
|
2
|
+
{% if messages %}
|
|
3
|
+
{% for category, message in messages %}
|
|
4
|
+
<v-alert
|
|
5
|
+
type="{{ category if category in ['success', 'warning', 'error', 'info'] else 'info' }}"
|
|
6
|
+
variant="tonal"
|
|
7
|
+
density="compact"
|
|
8
|
+
class="mb-2"
|
|
9
|
+
>{{ message }}</v-alert>
|
|
10
|
+
{% endfor %}
|
|
11
|
+
{% endif %}
|
|
12
|
+
{%- endwith %}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{% extends 'layout.html' %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<v-container fluid class="pa-4">
|
|
5
|
+
<v-card max-width="500" class="mx-auto">
|
|
6
|
+
<v-card-title class="text-headline-small">
|
|
7
|
+
<i class="ti ti-key mr-2"></i>
|
|
8
|
+
Change Password
|
|
9
|
+
</v-card-title>
|
|
10
|
+
<v-card-subtitle>
|
|
11
|
+
Update your account password
|
|
12
|
+
</v-card-subtitle>
|
|
13
|
+
|
|
14
|
+
<v-card-text>
|
|
15
|
+
{% include "security/_messages.html" %}
|
|
16
|
+
|
|
17
|
+
<v-form
|
|
18
|
+
id="changePasswordForm"
|
|
19
|
+
action="{{ url_for_security('change_password') }}"
|
|
20
|
+
method="POST"
|
|
21
|
+
>
|
|
22
|
+
{{ change_password_form.hidden_tag() }}
|
|
23
|
+
|
|
24
|
+
{% if active_password and current_user.has_usable_password %}
|
|
25
|
+
<v-text-field
|
|
26
|
+
name="password"
|
|
27
|
+
label="Current Password"
|
|
28
|
+
type="password"
|
|
29
|
+
required
|
|
30
|
+
class="mb-2"
|
|
31
|
+
></v-text-field>
|
|
32
|
+
|
|
33
|
+
{% if change_password_form.password.errors %}
|
|
34
|
+
<v-alert type="error" variant="tonal" density="compact" class="mb-4">
|
|
35
|
+
{{ change_password_form.password.errors[0] }}
|
|
36
|
+
</v-alert>
|
|
37
|
+
{% endif %}
|
|
38
|
+
{% else %}
|
|
39
|
+
<v-alert type="info" variant="tonal" density="compact" class="mb-4">
|
|
40
|
+
Set a password to enable email login for your account.
|
|
41
|
+
</v-alert>
|
|
42
|
+
{% endif %}
|
|
43
|
+
|
|
44
|
+
<v-text-field
|
|
45
|
+
name="new_password"
|
|
46
|
+
label="New Password"
|
|
47
|
+
type="password"
|
|
48
|
+
required
|
|
49
|
+
class="mb-2"
|
|
50
|
+
></v-text-field>
|
|
51
|
+
|
|
52
|
+
{% if change_password_form.new_password.errors %}
|
|
53
|
+
<v-alert type="error" variant="tonal" density="compact" class="mb-4">
|
|
54
|
+
{{ change_password_form.new_password.errors[0] }}
|
|
55
|
+
</v-alert>
|
|
56
|
+
{% endif %}
|
|
57
|
+
|
|
58
|
+
<v-text-field
|
|
59
|
+
name="new_password_confirm"
|
|
60
|
+
label="Confirm New Password"
|
|
61
|
+
type="password"
|
|
62
|
+
required
|
|
63
|
+
class="mb-4"
|
|
64
|
+
></v-text-field>
|
|
65
|
+
|
|
66
|
+
{% if change_password_form.new_password_confirm.errors %}
|
|
67
|
+
<v-alert type="error" variant="tonal" density="compact" class="mb-4">
|
|
68
|
+
{{ change_password_form.new_password_confirm.errors[0] }}
|
|
69
|
+
</v-alert>
|
|
70
|
+
{% endif %}
|
|
71
|
+
|
|
72
|
+
<v-btn color="primary" @click="submit">
|
|
73
|
+
Change Password
|
|
74
|
+
</v-btn>
|
|
75
|
+
</v-form>
|
|
76
|
+
</v-card-text>
|
|
77
|
+
</v-card>
|
|
78
|
+
</v-container>
|
|
79
|
+
{% endblock %}
|
|
80
|
+
|
|
81
|
+
{% block js %}
|
|
82
|
+
<script>
|
|
83
|
+
const { createApp } = Vue;
|
|
84
|
+
const { createVuetify } = Vuetify;
|
|
85
|
+
const vuetify = createVuetify(config.vuetifyConfig);
|
|
86
|
+
|
|
87
|
+
const app = createApp({
|
|
88
|
+
mixins: [layoutMixin],
|
|
89
|
+
delimiters: ['${', '}'],
|
|
90
|
+
methods: {
|
|
91
|
+
submit() {
|
|
92
|
+
document.getElementById('changePasswordForm').submit();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
registerStkComponents(app);
|
|
98
|
+
app.use(vuetify).mount('#app');
|
|
99
|
+
</script>
|
|
100
|
+
{% endblock %}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{% extends 'layout.html' %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<v-app id="app" class="justify-center align-center min-vh-100 d-flex">
|
|
5
|
+
<v-container class="d-flex align-center justify-center">
|
|
6
|
+
<div class="main">
|
|
7
|
+
<h2>Thank for your registration</h2>
|
|
8
|
+
<p>
|
|
9
|
+
Please confirm your Email to continue.
|
|
10
|
+
</p>
|
|
11
|
+
</div>
|
|
12
|
+
</v-container>
|
|
13
|
+
<v-snackbar absolute button v-model="snackBar">
|
|
14
|
+
${snackMessage}
|
|
15
|
+
<v-btn text color="primary" @click.native="value = false">Close</v-btn>
|
|
16
|
+
</v-snackbar>
|
|
17
|
+
</v-app>
|
|
18
|
+
{% endblock %}
|
|
19
|
+
|
|
20
|
+
{% block js %}
|
|
21
|
+
<script>
|
|
22
|
+
const {createApp} = Vue;
|
|
23
|
+
const {createVuetify} = Vuetify;
|
|
24
|
+
const vuetify = createVuetify(config.vuetifyConfig);
|
|
25
|
+
|
|
26
|
+
const app = createApp({
|
|
27
|
+
data() {
|
|
28
|
+
return {
|
|
29
|
+
snackBar: false,
|
|
30
|
+
snackMessage: '',
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
methods: {
|
|
34
|
+
showSnack(message) {
|
|
35
|
+
this.snackMessage = message;
|
|
36
|
+
this.snackBar = true;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
delimiters: ['${', '}']
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
app.use(vuetify).mount('#app');
|
|
43
|
+
</script>
|
|
44
|
+
{% endblock %}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<p><a href="{{ reset_link }}">Click here to reset your password</a></p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<p>Your password has been reset</p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Your password has been reset
|