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,263 @@
|
|
|
1
|
+
{% extends 'auth_layout.html' %}
|
|
2
|
+
|
|
3
|
+
{% block css %}
|
|
4
|
+
<style>
|
|
5
|
+
html, body {
|
|
6
|
+
height: 100%;
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 0;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
}
|
|
11
|
+
#app, .v-application, .v-application__wrap {
|
|
12
|
+
height: 100vh;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
}
|
|
15
|
+
.auth-image {
|
|
16
|
+
background-image: url('/static/img/auth-bg.webp');
|
|
17
|
+
background-size: cover;
|
|
18
|
+
background-position: center;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
}
|
|
21
|
+
.auth-overlay {
|
|
22
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
23
|
+
height: 100%;
|
|
24
|
+
}
|
|
25
|
+
@media (max-width: 960px) {
|
|
26
|
+
.auth-image {
|
|
27
|
+
height: 40vh;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
31
|
+
{% endblock %}
|
|
32
|
+
|
|
33
|
+
{% block content %}
|
|
34
|
+
<div id="app">
|
|
35
|
+
<v-container fluid class="pa-0 ma-0 h-100" style="height: 100vh;">
|
|
36
|
+
<v-row no-gutters style="height: 100vh;">
|
|
37
|
+
<!-- Register Form Column -->
|
|
38
|
+
<v-col cols="12" md="6" class="d-flex align-center justify-center">
|
|
39
|
+
<v-card width="500" elevation="0" class="mx-auto">
|
|
40
|
+
<v-card-title>
|
|
41
|
+
<div class="d-flex align-center ga-3 mb-5">
|
|
42
|
+
{% include "_brand.html" %}
|
|
43
|
+
<span class="text-headline-small font-weight-bold">Create an account</span>
|
|
44
|
+
</div>
|
|
45
|
+
</v-card-title>
|
|
46
|
+
|
|
47
|
+
<v-card-text>
|
|
48
|
+
{% set has_social_auth = config.GOOGLE_AUTH_ENABLED or config.GITHUB_AUTH_ENABLED %}
|
|
49
|
+
|
|
50
|
+
{% if has_social_auth %}
|
|
51
|
+
<!-- Social Login Buttons -->
|
|
52
|
+
<div class="d-flex flex-column gap-3 mb-4">
|
|
53
|
+
{% if config.GOOGLE_AUTH_ENABLED %}
|
|
54
|
+
<v-btn
|
|
55
|
+
variant="outlined"
|
|
56
|
+
block
|
|
57
|
+
href="{{ url_for('public.google_login') }}"
|
|
58
|
+
class="mb-2"
|
|
59
|
+
height="48"
|
|
60
|
+
>
|
|
61
|
+
<template v-slot:prepend>
|
|
62
|
+
<svg width="18" height="18" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="mr-2">
|
|
63
|
+
<path d="M17 8.18371C17 7.63989 16.9551 7.09314 16.8591 6.55814H9.16046V9.63879H13.5691C13.3862 10.6324 12.7983 11.5113 11.9376 12.0698V14.0687H14.5678C16.1123 12.6754 17 10.6177 17 8.18371Z" fill="#4285F4"></path>
|
|
64
|
+
<path d="M9.16042 16C11.3617 16 13.2182 15.2916 14.5707 14.0687L11.9406 12.0698C11.2088 12.5578 10.2641 12.8341 9.16342 12.8341C7.03409 12.8341 5.22865 11.4261 4.58085 9.53299H1.8667V11.5936C3.25227 14.2951 6.07438 16 9.16042 16V16Z" fill="#34A853"></path>
|
|
65
|
+
<path d="M4.57786 9.53298C4.23596 8.53941 4.23596 7.46353 4.57786 6.46996V4.40933H1.8667C0.709065 6.66985 0.709065 9.33309 1.8667 11.5936L4.57786 9.53298V9.53298Z" fill="#FBBC04"></path>
|
|
66
|
+
<path d="M9.16042 3.16589C10.3241 3.14825 11.4487 3.57743 12.2914 4.36523L14.6217 2.0812C13.1462 0.72312 11.1878 -0.0235267 9.16042 -1.02057e-05C6.07438 -1.02057e-05 3.25227 1.70493 1.8667 4.40932L4.57785 6.46995C5.22265 4.57394 7.03109 3.16589 9.16042 3.16589V3.16589Z" fill="#EA4335"></path>
|
|
67
|
+
</svg>
|
|
68
|
+
</template>
|
|
69
|
+
Continue with Google
|
|
70
|
+
</v-btn>
|
|
71
|
+
{% endif %}
|
|
72
|
+
|
|
73
|
+
{% if config.GITHUB_AUTH_ENABLED %}
|
|
74
|
+
<v-btn
|
|
75
|
+
variant="outlined"
|
|
76
|
+
block
|
|
77
|
+
href="{{ url_for('public.github_login') }}"
|
|
78
|
+
class="mb-2"
|
|
79
|
+
height="48"
|
|
80
|
+
>
|
|
81
|
+
<template v-slot:prepend>
|
|
82
|
+
<svg width="18" height="18" viewBox="0 0 16 16" version="1.1" class="mr-2">
|
|
83
|
+
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
|
|
84
|
+
</svg>
|
|
85
|
+
</template>
|
|
86
|
+
Continue with GitHub
|
|
87
|
+
</v-btn>
|
|
88
|
+
{% endif %}
|
|
89
|
+
|
|
90
|
+
<div class="d-flex align-center my-4">
|
|
91
|
+
<v-divider></v-divider>
|
|
92
|
+
<span class="mx-3 text-medium-emphasis text-body-small">or</span>
|
|
93
|
+
<v-divider></v-divider>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<v-btn
|
|
97
|
+
variant="outlined"
|
|
98
|
+
block
|
|
99
|
+
height="48"
|
|
100
|
+
@click="showEmailForm = true"
|
|
101
|
+
v-if="!showEmailForm"
|
|
102
|
+
>
|
|
103
|
+
<template v-slot:prepend>
|
|
104
|
+
<i class="ti ti-mail mr-2" style="font-size: 20px;"></i>
|
|
105
|
+
</template>
|
|
106
|
+
Continue with Email
|
|
107
|
+
</v-btn>
|
|
108
|
+
</div>
|
|
109
|
+
{% endif %}
|
|
110
|
+
|
|
111
|
+
<!-- Email/Password Form -->
|
|
112
|
+
<v-form
|
|
113
|
+
action="{{ url_for_security('register') }}"
|
|
114
|
+
method="post"
|
|
115
|
+
id="registerForm"
|
|
116
|
+
ref="form"
|
|
117
|
+
v-if="showEmailForm || !hasSocialAuth"
|
|
118
|
+
>
|
|
119
|
+
{{ register_user_form.hidden_tag() }}
|
|
120
|
+
|
|
121
|
+
<v-row class="mb-6">
|
|
122
|
+
<v-col cols="6" class="py-0">
|
|
123
|
+
<v-text-field
|
|
124
|
+
label="First Name"
|
|
125
|
+
name="first_name"
|
|
126
|
+
type="text"
|
|
127
|
+
hide-details
|
|
128
|
+
required
|
|
129
|
+
></v-text-field>
|
|
130
|
+
</v-col>
|
|
131
|
+
<v-col cols="6" class="py-0">
|
|
132
|
+
<v-text-field
|
|
133
|
+
label="Last Name"
|
|
134
|
+
name="last_name"
|
|
135
|
+
type="text"
|
|
136
|
+
hide-details
|
|
137
|
+
required
|
|
138
|
+
></v-text-field>
|
|
139
|
+
</v-col>
|
|
140
|
+
</v-row>
|
|
141
|
+
|
|
142
|
+
<v-text-field
|
|
143
|
+
label="Email"
|
|
144
|
+
name="email"
|
|
145
|
+
type="email"
|
|
146
|
+
required
|
|
147
|
+
></v-text-field>
|
|
148
|
+
|
|
149
|
+
<v-text-field
|
|
150
|
+
id="password"
|
|
151
|
+
label="Password"
|
|
152
|
+
name="password"
|
|
153
|
+
type="password"
|
|
154
|
+
required
|
|
155
|
+
></v-text-field>
|
|
156
|
+
|
|
157
|
+
{% if register_user_form.password_confirm %}
|
|
158
|
+
<v-text-field
|
|
159
|
+
id="confirm-password"
|
|
160
|
+
label="Confirm Password"
|
|
161
|
+
name="password_confirm"
|
|
162
|
+
type="password"
|
|
163
|
+
required
|
|
164
|
+
></v-text-field>
|
|
165
|
+
{% endif %}
|
|
166
|
+
|
|
167
|
+
{% if register_user_form.email.errors or register_user_form.password.errors %}
|
|
168
|
+
<v-alert type="error" density="compact" class="mb-4">
|
|
169
|
+
Please check the form for errors: <br>
|
|
170
|
+
{{ register_user_form.errors }}
|
|
171
|
+
</v-alert>
|
|
172
|
+
{% endif %}
|
|
173
|
+
|
|
174
|
+
<v-btn
|
|
175
|
+
block
|
|
176
|
+
color="primary"
|
|
177
|
+
size="large"
|
|
178
|
+
@click="submit"
|
|
179
|
+
class="mb-4"
|
|
180
|
+
>
|
|
181
|
+
Create Account
|
|
182
|
+
</v-btn>
|
|
183
|
+
</v-form>
|
|
184
|
+
|
|
185
|
+
<div class="text-center mt-6">
|
|
186
|
+
<span class="text-medium-emphasis">Already have an account? </span>
|
|
187
|
+
<a href="{{ url_for_security('login') }}" class="text-decoration-none">Log in</a>
|
|
188
|
+
</div>
|
|
189
|
+
</v-card-text>
|
|
190
|
+
</v-card>
|
|
191
|
+
</v-col>
|
|
192
|
+
|
|
193
|
+
<!-- Image Column -->
|
|
194
|
+
<v-col cols="12" md="6" class="auth-image d-none d-md-block">
|
|
195
|
+
<v-container fluid class="auth-overlay pa-0 ma-0">
|
|
196
|
+
<v-row align="center" justify="center" style="height: 100vh;">
|
|
197
|
+
<v-col cols="12" class="text-center">
|
|
198
|
+
<div class="text-display-large font-weight-bold text-white">{{ config.APP_TAGLINE }}</div>
|
|
199
|
+
{% if config.APP_DESCRIPTION %}<p class="text-body-large text-white mt-4">{{ config.APP_DESCRIPTION }}</p>{% endif %}
|
|
200
|
+
</v-col>
|
|
201
|
+
</v-row>
|
|
202
|
+
</v-container>
|
|
203
|
+
</v-col>
|
|
204
|
+
</v-row>
|
|
205
|
+
</v-container>
|
|
206
|
+
|
|
207
|
+
<v-snackbar
|
|
208
|
+
v-model="snackBar"
|
|
209
|
+
:timeout="3000"
|
|
210
|
+
>
|
|
211
|
+
${snackMessage}
|
|
212
|
+
<template v-slot:actions>
|
|
213
|
+
<v-btn
|
|
214
|
+
color="primary"
|
|
215
|
+
variant="text"
|
|
216
|
+
@click="snackBar = false"
|
|
217
|
+
>
|
|
218
|
+
Close
|
|
219
|
+
</v-btn>
|
|
220
|
+
</template>
|
|
221
|
+
</v-snackbar>
|
|
222
|
+
</div>
|
|
223
|
+
{% endblock %}
|
|
224
|
+
|
|
225
|
+
{% block js %}
|
|
226
|
+
<script>
|
|
227
|
+
const {createApp} = Vue;
|
|
228
|
+
const {createVuetify} = Vuetify;
|
|
229
|
+
const vuetify = createVuetify(config.vuetifyConfig);
|
|
230
|
+
|
|
231
|
+
const app = createApp({
|
|
232
|
+
data() {
|
|
233
|
+
return {
|
|
234
|
+
menu: null,
|
|
235
|
+
errors: '',
|
|
236
|
+
snackBar: false,
|
|
237
|
+
snackMessage: '',
|
|
238
|
+
showEmailForm: false,
|
|
239
|
+
hasSocialAuth: {% if config.GOOGLE_AUTH_ENABLED or config.GITHUB_AUTH_ENABLED %}true{% else %}false{% endif %}
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
methods: {
|
|
243
|
+
showSnack(message) {
|
|
244
|
+
this.snackMessage = message;
|
|
245
|
+
this.snackBar = true;
|
|
246
|
+
},
|
|
247
|
+
submit() {
|
|
248
|
+
document.getElementById('registerForm').submit();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
mounted() {
|
|
252
|
+
// Check if there are form errors, if so, show the email form
|
|
253
|
+
const hasErrors = {% if register_user_form.email.errors or register_user_form.password.errors %}true{% else %}false{% endif %};
|
|
254
|
+
if (hasErrors) {
|
|
255
|
+
this.showEmailForm = true;
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
delimiters: ['${', '}']
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
app.use(vuetify).mount('#app');
|
|
262
|
+
</script>
|
|
263
|
+
{% endblock %}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
<v-card class="pa-5" width="600">
|
|
7
|
+
<v-card-title>Reset Password</v-card-title>
|
|
8
|
+
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
|
9
|
+
{% include "security/_messages.html" %}
|
|
10
|
+
|
|
11
|
+
<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST"
|
|
12
|
+
name="reset_password_form">
|
|
13
|
+
{{ reset_password_form.hidden_tag() }}
|
|
14
|
+
<v-text-field required name="password" type="password" label="New Password"></v-text-field>
|
|
15
|
+
<v-text-field required name="password_confirm" type="password"
|
|
16
|
+
label="Confirm Password"></v-text-field>
|
|
17
|
+
|
|
18
|
+
<v-btn type="submit" color="primary">Reset Password</v-btn>
|
|
19
|
+
</form>
|
|
20
|
+
</v-card>
|
|
21
|
+
</v-container>
|
|
22
|
+
<v-snackbar absolute button v-model="snackBar">
|
|
23
|
+
${snackMessage}
|
|
24
|
+
<v-btn text color="primary" @click.native="value = false">Close</v-btn>
|
|
25
|
+
</v-snackbar>
|
|
26
|
+
</v-app>
|
|
27
|
+
{% endblock %}
|
|
28
|
+
|
|
29
|
+
{% block js %}
|
|
30
|
+
<script>
|
|
31
|
+
const {createApp} = Vue;
|
|
32
|
+
const {createVuetify} = Vuetify;
|
|
33
|
+
const vuetify = createVuetify(config.vuetifyConfig);
|
|
34
|
+
|
|
35
|
+
const app = createApp({
|
|
36
|
+
data() {
|
|
37
|
+
return {
|
|
38
|
+
snackBar: false,
|
|
39
|
+
snackMessage: '',
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
methods: {
|
|
43
|
+
showSnack(message) {
|
|
44
|
+
this.snackMessage = message;
|
|
45
|
+
this.snackBar = true;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
delimiters: ['${', '}']
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
app.use(vuetify).mount('#app');
|
|
52
|
+
</script>
|
|
53
|
+
{% endblock %}
|
|
@@ -0,0 +1,48 @@
|
|
|
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>Resend confirmation instructions</h2>
|
|
8
|
+
|
|
9
|
+
<form action="{{ url_for_security('send_confirmation') }}" method="POST" name="send_confirmation_form">
|
|
10
|
+
{{ send_confirmation_form.hidden_tag() }}
|
|
11
|
+
<v-text-field required label="Email" name="email" type="email"></v-text-field>
|
|
12
|
+
|
|
13
|
+
<v-btn type="submit" color="primary">Resend Instructions</v-btn>
|
|
14
|
+
</form>
|
|
15
|
+
</div>
|
|
16
|
+
<v-snackbar absolute button v-model="snackBar">
|
|
17
|
+
${snackMessage}
|
|
18
|
+
<v-btn text color="primary" @click.native="value = false">Close</v-btn>
|
|
19
|
+
</v-snackbar>
|
|
20
|
+
</v-container>
|
|
21
|
+
</v-app>
|
|
22
|
+
{% endblock %}
|
|
23
|
+
|
|
24
|
+
{% block js %}
|
|
25
|
+
<script>
|
|
26
|
+
const {createApp} = Vue;
|
|
27
|
+
const {createVuetify} = Vuetify;
|
|
28
|
+
const vuetify = createVuetify(config.vuetifyConfig);
|
|
29
|
+
|
|
30
|
+
const app = createApp({
|
|
31
|
+
data() {
|
|
32
|
+
return {
|
|
33
|
+
snackBar: false,
|
|
34
|
+
snackMessage: '',
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
methods: {
|
|
38
|
+
showSnack(message) {
|
|
39
|
+
this.snackMessage = message;
|
|
40
|
+
this.snackBar = true;
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
delimiters: ['${', '}']
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
app.use(vuetify).mount('#app');
|
|
47
|
+
</script>
|
|
48
|
+
{% endblock %}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{% extends 'layout.html' %}
|
|
2
|
+
{% block content %}
|
|
3
|
+
<div class="main">
|
|
4
|
+
<h2>Login</h2>
|
|
5
|
+
|
|
6
|
+
<form action="{{ url_for_security('login') }}" method="POST" name="send_login_form">
|
|
7
|
+
{{ send_login_form.hidden_tag() }}
|
|
8
|
+
<v-text-field required label="Email" name="email" type="email"></v-text-field>
|
|
9
|
+
<v-btn type="submit" color="primary">Login</v-btn>
|
|
10
|
+
</form>
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
{% endblock %}
|
|
14
|
+
|
|
15
|
+
{% block js %}
|
|
16
|
+
<script>
|
|
17
|
+
const { createApp } = Vue;
|
|
18
|
+
const { createVuetify } = Vuetify;
|
|
19
|
+
const vuetify = createVuetify(config.vuetifyConfig);
|
|
20
|
+
|
|
21
|
+
const app = createApp({
|
|
22
|
+
data() {
|
|
23
|
+
return {
|
|
24
|
+
snackBar: false,
|
|
25
|
+
snackMessage: '',
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
methods: {
|
|
29
|
+
showSnack(message) {
|
|
30
|
+
this.snackMessage = message;
|
|
31
|
+
this.snackBar = true;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
delimiters: ['${', '}']
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.use(vuetify).mount('#app');
|
|
38
|
+
</script>
|
|
39
|
+
{% endblock %}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{% extends 'login-layout.html' %}
|
|
2
|
+
{% from "security/_macros.html" import prop_next %}
|
|
3
|
+
|
|
4
|
+
{% block content %}
|
|
5
|
+
<v-main class="d-flex flex-column align-center justify-center" style="min-height: 100vh; background: rgb(var(--v-theme-background));">
|
|
6
|
+
<v-card class="mx-auto pa-8 w-100" rounded="lg" style="max-width: 424px;">
|
|
7
|
+
<div class="text-center mb-6">
|
|
8
|
+
<div class="mb-4">{% include "_brand.html" %}</div>
|
|
9
|
+
<div class="text-headline-small font-weight-bold">Verify Your Identity</div>
|
|
10
|
+
<div class="text-body-medium text-medium-emphasis">Choose a verification method</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<form action="{{ url_for_security('tf_select') }}{{ prop_next() }}" method="post" id="tf-select-form">
|
|
14
|
+
{{ two_factor_select_form.hidden_tag() }}
|
|
15
|
+
|
|
16
|
+
<div class="d-flex flex-column ga-3 mb-4">
|
|
17
|
+
{% for value, label in two_factor_select_form.which.choices %}
|
|
18
|
+
<v-btn
|
|
19
|
+
:variant="selected === '{{ value }}' ? 'flat' : 'outlined'"
|
|
20
|
+
:color="selected === '{{ value }}' ? 'primary' : undefined"
|
|
21
|
+
size="large"
|
|
22
|
+
block
|
|
23
|
+
@click.prevent="select('{{ value }}')"
|
|
24
|
+
>
|
|
25
|
+
<template v-slot:prepend>
|
|
26
|
+
<i class="{{ 'ti ti-device-mobile' if value == 'authenticator' else 'ti ti-fingerprint' }}" style="font-size: 20px;"></i>
|
|
27
|
+
</template>
|
|
28
|
+
{{ 'Authenticator App' if value == 'authenticator' else 'Passkey' }}
|
|
29
|
+
</v-btn>
|
|
30
|
+
{% endfor %}
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<input type="hidden" name="{{ two_factor_select_form.which.name }}" :value="selected">
|
|
34
|
+
|
|
35
|
+
{% include "security/_messages.html" %}
|
|
36
|
+
|
|
37
|
+
<v-divider class="my-4"></v-divider>
|
|
38
|
+
|
|
39
|
+
<v-btn variant="outlined" block href="{{ url_for_security('mf_recovery') }}">
|
|
40
|
+
<template v-slot:prepend>
|
|
41
|
+
<i class="ti ti-lifebuoy" style="font-size: 18px;"></i>
|
|
42
|
+
</template>
|
|
43
|
+
Use Recovery Code
|
|
44
|
+
</v-btn>
|
|
45
|
+
</form>
|
|
46
|
+
</v-card>
|
|
47
|
+
|
|
48
|
+
<div class="mt-6 w-100" style="max-width: 424px;">
|
|
49
|
+
<v-btn href="{{ url_for_security('login') }}" variant="text" color="primary">
|
|
50
|
+
<i class="ti ti-chevron-left mr-1"></i>
|
|
51
|
+
Back to Login
|
|
52
|
+
</v-btn>
|
|
53
|
+
</div>
|
|
54
|
+
</v-main>
|
|
55
|
+
{% endblock %}
|
|
56
|
+
|
|
57
|
+
{% block js %}
|
|
58
|
+
<script>
|
|
59
|
+
const { createApp } = Vue;
|
|
60
|
+
const { createVuetify } = Vuetify;
|
|
61
|
+
const vuetify = createVuetify(vuetifyConfig);
|
|
62
|
+
|
|
63
|
+
const app = createApp({
|
|
64
|
+
delimiters: delimiters,
|
|
65
|
+
data() {
|
|
66
|
+
return { selected: '{{ two_factor_select_form.which.data or "" }}' };
|
|
67
|
+
},
|
|
68
|
+
methods: {
|
|
69
|
+
select(value) {
|
|
70
|
+
this.selected = value;
|
|
71
|
+
this.$nextTick(() => document.getElementById('tf-select-form').submit());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
app.use(vuetify).mount('#app');
|
|
77
|
+
</script>
|
|
78
|
+
{% endblock %}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
{% extends 'layout.html' %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<v-container fluid class="pa-4">
|
|
5
|
+
<v-card max-width="600" class="mx-auto">
|
|
6
|
+
<v-card-title class="text-headline-small">
|
|
7
|
+
<i class="ti ti-shield-lock mr-2"></i>
|
|
8
|
+
Two-Factor Authentication
|
|
9
|
+
</v-card-title>
|
|
10
|
+
<v-card-subtitle>
|
|
11
|
+
Add an extra layer of security to your account
|
|
12
|
+
</v-card-subtitle>
|
|
13
|
+
|
|
14
|
+
<v-card-text>
|
|
15
|
+
{% include "security/_messages.html" %}
|
|
16
|
+
|
|
17
|
+
{% if primary_method != 'none' %}
|
|
18
|
+
<!-- Enabled State -->
|
|
19
|
+
<v-card color="primary" variant="tonal" class="mb-6">
|
|
20
|
+
<v-card-text class="d-flex align-center">
|
|
21
|
+
<v-avatar color="primary" class="mr-4">
|
|
22
|
+
<i class="ti ti-check" style="font-size: 24px;"></i>
|
|
23
|
+
</v-avatar>
|
|
24
|
+
<div class="flex-grow-1">
|
|
25
|
+
<div class="text-title-large text-uppercase">{{ primary_method }}</div>
|
|
26
|
+
<div class="text-body-small">Currently enabled</div>
|
|
27
|
+
</div>
|
|
28
|
+
<form action="{{ url_for_security('two_factor_setup') }}" method="post">
|
|
29
|
+
{{ two_factor_setup_form.hidden_tag() }}
|
|
30
|
+
<input type="hidden" name="action" value="disable">
|
|
31
|
+
<v-btn color="error" variant="outlined" type="submit">
|
|
32
|
+
<i class="ti ti-x mr-1"></i>
|
|
33
|
+
Disable
|
|
34
|
+
</v-btn>
|
|
35
|
+
</form>
|
|
36
|
+
</v-card-text>
|
|
37
|
+
</v-card>
|
|
38
|
+
{% endif %}
|
|
39
|
+
|
|
40
|
+
{% if chosen_method == "authenticator" %}
|
|
41
|
+
<!-- QR Code Setup -->
|
|
42
|
+
<v-card variant="outlined" class="mb-4">
|
|
43
|
+
<v-card-title class="text-title-medium">
|
|
44
|
+
<i class="ti ti-qrcode mr-2"></i>
|
|
45
|
+
1. Scan QR Code
|
|
46
|
+
</v-card-title>
|
|
47
|
+
<v-card-text>
|
|
48
|
+
<p class="mb-4">
|
|
49
|
+
Open your authenticator app (Google Authenticator, Authy, 1Password, etc.) and scan this code:
|
|
50
|
+
</p>
|
|
51
|
+
<div class="text-center mb-4">
|
|
52
|
+
<img width="200" alt="QR code" src="{{ authr_qrcode }}" class="qr-adaptive">
|
|
53
|
+
</div>
|
|
54
|
+
<v-text-field
|
|
55
|
+
readonly
|
|
56
|
+
label="Manual entry key"
|
|
57
|
+
:model-value="'{{ authr_key }}'"
|
|
58
|
+
append-inner-icon="ti ti-copy"
|
|
59
|
+
@click:append-inner="copyKey"
|
|
60
|
+
hint="Can't scan? Enter this key manually in your app"
|
|
61
|
+
persistent-hint
|
|
62
|
+
></v-text-field>
|
|
63
|
+
</v-card-text>
|
|
64
|
+
</v-card>
|
|
65
|
+
|
|
66
|
+
<v-card variant="outlined" class="mb-4">
|
|
67
|
+
<v-card-title class="text-title-medium">
|
|
68
|
+
<i class="ti ti-numbers mr-2"></i>
|
|
69
|
+
2. Enter Verification Code
|
|
70
|
+
</v-card-title>
|
|
71
|
+
<v-card-text>
|
|
72
|
+
<form id="verify-form" action="{{ url_for_security('two_factor_setup') }}" method="post">
|
|
73
|
+
{{ two_factor_setup_form.hidden_tag() }}
|
|
74
|
+
<input type="hidden" name="action" value="verify">
|
|
75
|
+
<v-text-field
|
|
76
|
+
name="token"
|
|
77
|
+
label="6-digit code from your app"
|
|
78
|
+
placeholder="000000"
|
|
79
|
+
maxlength="6"
|
|
80
|
+
autocomplete="one-time-code"
|
|
81
|
+
inputmode="numeric"
|
|
82
|
+
autofocus
|
|
83
|
+
></v-text-field>
|
|
84
|
+
<v-btn color="primary" type="submit" class="mt-2">
|
|
85
|
+
Verify & Enable
|
|
86
|
+
</v-btn>
|
|
87
|
+
</form>
|
|
88
|
+
</v-card-text>
|
|
89
|
+
</v-card>
|
|
90
|
+
|
|
91
|
+
{% elif primary_method == 'none' %}
|
|
92
|
+
<!-- Options: not yet enabled -->
|
|
93
|
+
<v-card variant="outlined" class="mb-3">
|
|
94
|
+
<v-card-text class="d-flex align-center">
|
|
95
|
+
<v-avatar color="primary" variant="tonal" class="mr-4">
|
|
96
|
+
<i class="ti ti-device-mobile" style="font-size: 24px;"></i>
|
|
97
|
+
</v-avatar>
|
|
98
|
+
<div class="flex-grow-1">
|
|
99
|
+
<div class="font-weight-medium">Authenticator App</div>
|
|
100
|
+
<div class="text-body-small text-medium-emphasis">Use Google Authenticator, Authy, or similar</div>
|
|
101
|
+
</div>
|
|
102
|
+
<v-btn variant="outlined" color="primary" href="{{ url_for_security('two_factor_setup') }}?setup=authenticator">
|
|
103
|
+
Setup
|
|
104
|
+
</v-btn>
|
|
105
|
+
</v-card-text>
|
|
106
|
+
</v-card>
|
|
107
|
+
{% endif %}
|
|
108
|
+
|
|
109
|
+
{% if security.webauthn and current_user.is_authenticated and not chosen_method %}
|
|
110
|
+
<v-card variant="outlined" class="mb-3">
|
|
111
|
+
<v-card-text class="d-flex align-center">
|
|
112
|
+
<v-avatar color="primary" variant="tonal" class="mr-4">
|
|
113
|
+
<i class="ti ti-fingerprint" style="font-size: 24px;"></i>
|
|
114
|
+
</v-avatar>
|
|
115
|
+
<div class="flex-grow-1">
|
|
116
|
+
<div class="font-weight-medium">Passkeys</div>
|
|
117
|
+
<div class="text-body-small text-medium-emphasis">Hardware keys or device passkeys for passwordless sign-in</div>
|
|
118
|
+
</div>
|
|
119
|
+
<v-btn variant="outlined" color="primary" href="{{ url_for_security('wan_register') }}">
|
|
120
|
+
Manage
|
|
121
|
+
</v-btn>
|
|
122
|
+
</v-card-text>
|
|
123
|
+
</v-card>
|
|
124
|
+
{% endif %}
|
|
125
|
+
|
|
126
|
+
{% if security.support_mfa and security.multi_factor_recovery_codes and not chosen_method %}
|
|
127
|
+
<v-card variant="outlined">
|
|
128
|
+
<v-card-text class="d-flex align-center">
|
|
129
|
+
<v-avatar color="warning" variant="tonal" class="mr-4">
|
|
130
|
+
<i class="ti ti-lifebuoy" style="font-size: 24px;"></i>
|
|
131
|
+
</v-avatar>
|
|
132
|
+
<div class="flex-grow-1">
|
|
133
|
+
<div class="font-weight-medium">Recovery Codes</div>
|
|
134
|
+
<div class="text-body-small text-medium-emphasis">Backup codes for account recovery</div>
|
|
135
|
+
</div>
|
|
136
|
+
<v-btn variant="outlined" color="primary" href="{{ url_for_security('mf_recovery_codes') }}">
|
|
137
|
+
Manage
|
|
138
|
+
</v-btn>
|
|
139
|
+
</v-card-text>
|
|
140
|
+
</v-card>
|
|
141
|
+
{% endif %}
|
|
142
|
+
</v-card-text>
|
|
143
|
+
</v-card>
|
|
144
|
+
</v-container>
|
|
145
|
+
{% endblock %}
|
|
146
|
+
|
|
147
|
+
{% block js %}
|
|
148
|
+
<script>
|
|
149
|
+
const { createApp } = Vue;
|
|
150
|
+
const { createVuetify } = Vuetify;
|
|
151
|
+
const vuetify = createVuetify(config.vuetifyConfig);
|
|
152
|
+
|
|
153
|
+
const app = createApp({
|
|
154
|
+
mixins: [layoutMixin],
|
|
155
|
+
methods: {
|
|
156
|
+
copyKey() {
|
|
157
|
+
navigator.clipboard.writeText('{{ authr_key | default("") }}');
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
delimiters: ['${', '}']
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
registerStkComponents(app);
|
|
164
|
+
app.use(vuetify).mount('#app');
|
|
165
|
+
</script>
|
|
166
|
+
{% endblock %}
|