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,63 @@
|
|
|
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 mt-10" width="600">
|
|
7
|
+
<v-card-title class="mb-4">
|
|
8
|
+
Reset Password
|
|
9
|
+
</v-card-title>
|
|
10
|
+
<form action="{{ url_for_security('forgot_password') }}" method="POST" id="forgotPasswordForm"
|
|
11
|
+
ref="form">
|
|
12
|
+
<v-card-text>
|
|
13
|
+
{{ forgot_password_form.hidden_tag() }}
|
|
14
|
+
<v-text-field required type="email" name="email" label="Email"></v-text-field>
|
|
15
|
+
</v-card-text>
|
|
16
|
+
<v-card-text>
|
|
17
|
+
{% if forgot_password_form.errors %}
|
|
18
|
+
{% for k, err in forgot_password_form.errors.items() %}
|
|
19
|
+
<v-alert type="error" dense dismissible> {{ err[0] }}</v-alert>
|
|
20
|
+
{% endfor %}
|
|
21
|
+
{% endif %}
|
|
22
|
+
</v-card-text>
|
|
23
|
+
<v-card-actions class="justify-end">
|
|
24
|
+
<v-btn color="primary" type="submit">Send Instructions</v-btn>
|
|
25
|
+
</v-card-actions>
|
|
26
|
+
</form>
|
|
27
|
+
</v-card>
|
|
28
|
+
</v-container>
|
|
29
|
+
<v-snackbar absolute button v-model="snackBar">
|
|
30
|
+
${snackMessage}
|
|
31
|
+
<v-btn text color="primary" @click.native="value = false">Close</v-btn>
|
|
32
|
+
</v-snackbar>
|
|
33
|
+
</v-app>
|
|
34
|
+
{% endblock %}
|
|
35
|
+
|
|
36
|
+
{% block js %}
|
|
37
|
+
<script>
|
|
38
|
+
const {createApp} = Vue;
|
|
39
|
+
const {createVuetify} = Vuetify;
|
|
40
|
+
const vuetify = createVuetify(config.vuetifyConfig);
|
|
41
|
+
|
|
42
|
+
const app = createApp({
|
|
43
|
+
data() {
|
|
44
|
+
return {
|
|
45
|
+
snackBar: false,
|
|
46
|
+
snackMessage: '',
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
methods: {
|
|
50
|
+
showSnack(message) {
|
|
51
|
+
this.snackMessage = message;
|
|
52
|
+
this.snackBar = true;
|
|
53
|
+
},
|
|
54
|
+
submit() {
|
|
55
|
+
document.getElementById('forgotPasswordForm').submit();
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
delimiters: ['${', '}']
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
app.use(vuetify).mount('#app');
|
|
62
|
+
</script>
|
|
63
|
+
{% endblock %}
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
<!-- Login 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">Log in to your account</span>
|
|
44
|
+
</div>
|
|
45
|
+
</v-card-title>
|
|
46
|
+
|
|
47
|
+
<v-card-text>
|
|
48
|
+
{% include "security/_messages.html" %}
|
|
49
|
+
|
|
50
|
+
<!-- Email/Password Form -->
|
|
51
|
+
<v-form
|
|
52
|
+
action="{{ url_for_security('login') }}"
|
|
53
|
+
method="post"
|
|
54
|
+
id="loginForm"
|
|
55
|
+
ref="form"
|
|
56
|
+
>
|
|
57
|
+
{{ login_user_form.hidden_tag() }}
|
|
58
|
+
|
|
59
|
+
<v-text-field
|
|
60
|
+
label="Email"
|
|
61
|
+
name="email"
|
|
62
|
+
type="email"
|
|
63
|
+
required
|
|
64
|
+
></v-text-field>
|
|
65
|
+
|
|
66
|
+
<v-text-field
|
|
67
|
+
id="password"
|
|
68
|
+
label="Password"
|
|
69
|
+
name="password"
|
|
70
|
+
type="password"
|
|
71
|
+
required
|
|
72
|
+
></v-text-field>
|
|
73
|
+
|
|
74
|
+
{% if login_user_form.email.errors or login_user_form.password.errors %}
|
|
75
|
+
<v-alert type="error" density="compact" class="mb-4">Invalid email or password!</v-alert>
|
|
76
|
+
{% endif %}
|
|
77
|
+
|
|
78
|
+
{% if security.recoverable %}
|
|
79
|
+
<div class="d-flex justify-end mb-4">
|
|
80
|
+
<a href="{{ url_for_security('forgot_password') }}" class="text-decoration-none">Forgot password?</a>
|
|
81
|
+
</div>
|
|
82
|
+
{% endif %}
|
|
83
|
+
|
|
84
|
+
<v-btn
|
|
85
|
+
block
|
|
86
|
+
color="primary"
|
|
87
|
+
size="large"
|
|
88
|
+
@click="submit"
|
|
89
|
+
class="mb-4"
|
|
90
|
+
>
|
|
91
|
+
Log In
|
|
92
|
+
</v-btn>
|
|
93
|
+
</v-form>
|
|
94
|
+
|
|
95
|
+
{% if security.webauthn and security.wan_allow_as_first_factor or config.GOOGLE_AUTH_ENABLED or config.GITHUB_AUTH_ENABLED %}
|
|
96
|
+
<div class="d-flex align-center my-4">
|
|
97
|
+
<v-divider></v-divider>
|
|
98
|
+
<span class="mx-3 text-medium-emphasis text-body-small">or</span>
|
|
99
|
+
<v-divider></v-divider>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="d-flex flex-column gap-3 mb-4">
|
|
103
|
+
{% if security.webauthn and security.wan_allow_as_first_factor %}
|
|
104
|
+
<v-btn
|
|
105
|
+
variant="outlined"
|
|
106
|
+
block
|
|
107
|
+
href="{{ url_for_security('wan_signin') }}"
|
|
108
|
+
height="48"
|
|
109
|
+
>
|
|
110
|
+
<template v-slot:prepend>
|
|
111
|
+
<i class="ti ti-fingerprint" style="font-size: 20px;"></i>
|
|
112
|
+
</template>
|
|
113
|
+
Sign in with Passkey
|
|
114
|
+
</v-btn>
|
|
115
|
+
{% endif %}
|
|
116
|
+
|
|
117
|
+
{% if config.GOOGLE_AUTH_ENABLED %}
|
|
118
|
+
<v-btn
|
|
119
|
+
variant="outlined"
|
|
120
|
+
block
|
|
121
|
+
href="{{ url_for('public.google_login') }}"
|
|
122
|
+
class="mb-2"
|
|
123
|
+
height="48"
|
|
124
|
+
>
|
|
125
|
+
<template v-slot:prepend>
|
|
126
|
+
<svg width="18" height="18" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="mr-2">
|
|
127
|
+
<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>
|
|
128
|
+
<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>
|
|
129
|
+
<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>
|
|
130
|
+
<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>
|
|
131
|
+
</svg>
|
|
132
|
+
</template>
|
|
133
|
+
Continue with Google
|
|
134
|
+
</v-btn>
|
|
135
|
+
{% endif %}
|
|
136
|
+
|
|
137
|
+
{% if config.GITHUB_AUTH_ENABLED %}
|
|
138
|
+
<v-btn
|
|
139
|
+
variant="outlined"
|
|
140
|
+
block
|
|
141
|
+
href="{{ url_for('public.github_login') }}"
|
|
142
|
+
height="48"
|
|
143
|
+
>
|
|
144
|
+
<template v-slot:prepend>
|
|
145
|
+
<svg width="18" height="18" viewBox="0 0 16 16" version="1.1" class="mr-2">
|
|
146
|
+
<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>
|
|
147
|
+
</svg>
|
|
148
|
+
</template>
|
|
149
|
+
Continue with GitHub
|
|
150
|
+
</v-btn>
|
|
151
|
+
{% endif %}
|
|
152
|
+
</div>
|
|
153
|
+
{% endif %}
|
|
154
|
+
|
|
155
|
+
{% if security.registerable %}
|
|
156
|
+
<div class="text-center mt-6">
|
|
157
|
+
<span class="text-medium-emphasis">New to {{ config.APP_NAME }}? </span>
|
|
158
|
+
<a href="{{ url_for_security('register') }}" class="text-decoration-none">Sign up</a>
|
|
159
|
+
</div>
|
|
160
|
+
{% endif %}
|
|
161
|
+
</v-card-text>
|
|
162
|
+
</v-card>
|
|
163
|
+
</v-col>
|
|
164
|
+
|
|
165
|
+
<!-- Image Column -->
|
|
166
|
+
<v-col cols="12" md="6" class="auth-image d-none d-md-block">
|
|
167
|
+
<v-container fluid class="auth-overlay pa-0 ma-0">
|
|
168
|
+
<v-row align="center" justify="center" style="height: 100vh;">
|
|
169
|
+
<v-col cols="12" class="text-center">
|
|
170
|
+
<div class="text-display-large font-weight-bold text-white">{{ config.APP_TAGLINE }}</div>
|
|
171
|
+
{% if config.APP_DESCRIPTION %}<p class="text-body-large text-white mt-4">{{ config.APP_DESCRIPTION }}</p>{% endif %}
|
|
172
|
+
</v-col>
|
|
173
|
+
</v-row>
|
|
174
|
+
</v-container>
|
|
175
|
+
</v-col>
|
|
176
|
+
</v-row>
|
|
177
|
+
</v-container>
|
|
178
|
+
</div>
|
|
179
|
+
{% endblock %}
|
|
180
|
+
|
|
181
|
+
{% block js %}
|
|
182
|
+
<script>
|
|
183
|
+
const {createApp} = Vue;
|
|
184
|
+
const {createVuetify} = Vuetify;
|
|
185
|
+
const vuetify = createVuetify(config.vuetifyConfig);
|
|
186
|
+
|
|
187
|
+
const app = createApp({
|
|
188
|
+
data() {
|
|
189
|
+
return {
|
|
190
|
+
menu: null,
|
|
191
|
+
errors: '',
|
|
192
|
+
email: "",
|
|
193
|
+
password: ""
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
methods: {
|
|
197
|
+
submit() {
|
|
198
|
+
document.getElementById('loginForm').submit();
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
delimiters: ['${', '}']
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
app.use(vuetify).mount('#app');
|
|
205
|
+
</script>
|
|
206
|
+
{% endblock %}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{% extends 'login-layout.html' %}
|
|
2
|
+
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
|
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">Recovery Code</div>
|
|
10
|
+
<div class="text-body-medium text-medium-emphasis">Enter one of your recovery codes to sign in</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<form action="{{ url_for_security('mf_recovery') }}" method="POST" name="mf_recovery_form">
|
|
14
|
+
{{ recovery_form.hidden_tag() }}
|
|
15
|
+
|
|
16
|
+
<v-text-field
|
|
17
|
+
name="code"
|
|
18
|
+
label="Recovery Code"
|
|
19
|
+
placeholder="XXXX-XXXX-XXXX"
|
|
20
|
+
hint="Enter your 12-character recovery code including dashes"
|
|
21
|
+
persistent-hint
|
|
22
|
+
class="mb-4"
|
|
23
|
+
>
|
|
24
|
+
<template v-slot:prepend-inner>
|
|
25
|
+
<i class="ti ti-key" style="font-size: 18px;"></i>
|
|
26
|
+
</template>
|
|
27
|
+
</v-text-field>
|
|
28
|
+
|
|
29
|
+
{% include "security/_messages.html" %}
|
|
30
|
+
|
|
31
|
+
{% if recovery_form.errors and recovery_form.errors.code %}
|
|
32
|
+
<v-alert type="error" variant="tonal" class="mb-4">
|
|
33
|
+
{{ recovery_form.errors.code[0] }}
|
|
34
|
+
</v-alert>
|
|
35
|
+
{% endif %}
|
|
36
|
+
|
|
37
|
+
<v-btn
|
|
38
|
+
type="submit"
|
|
39
|
+
color="primary"
|
|
40
|
+
size="large"
|
|
41
|
+
block
|
|
42
|
+
class="mb-4"
|
|
43
|
+
>
|
|
44
|
+
Continue
|
|
45
|
+
</v-btn>
|
|
46
|
+
</form>
|
|
47
|
+
</v-card>
|
|
48
|
+
|
|
49
|
+
<div class="mt-6 w-100" style="max-width: 424px;">
|
|
50
|
+
<v-btn onclick="history.back()" variant="text" color="primary">
|
|
51
|
+
<i class="ti ti-chevron-left mr-1"></i>
|
|
52
|
+
Back
|
|
53
|
+
</v-btn>
|
|
54
|
+
</div>
|
|
55
|
+
</v-main>
|
|
56
|
+
{% endblock %}
|
|
57
|
+
|
|
58
|
+
{% block js %}
|
|
59
|
+
<script>
|
|
60
|
+
const { createApp } = Vue;
|
|
61
|
+
const { createVuetify } = Vuetify;
|
|
62
|
+
const vuetify = createVuetify(vuetifyConfig);
|
|
63
|
+
|
|
64
|
+
const app = createApp({ delimiters: delimiters });
|
|
65
|
+
app.use(vuetify).mount('#app');
|
|
66
|
+
</script>
|
|
67
|
+
{% endblock %}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{% extends 'layout.html' %}
|
|
2
|
+
{% from "security/_macros.html" import render_field_errors %}
|
|
3
|
+
|
|
4
|
+
{% block content %}
|
|
5
|
+
<v-container fluid class="pa-4">
|
|
6
|
+
<v-card max-width="600" class="mx-auto">
|
|
7
|
+
<v-card-title class="text-headline-small">
|
|
8
|
+
<i class="ti ti-lifebuoy mr-2"></i>
|
|
9
|
+
Recovery Codes
|
|
10
|
+
</v-card-title>
|
|
11
|
+
<v-card-subtitle>
|
|
12
|
+
Backup codes for account recovery when you can't use your authenticator
|
|
13
|
+
</v-card-subtitle>
|
|
14
|
+
|
|
15
|
+
<v-card-text>
|
|
16
|
+
{% include "security/_messages.html" %}
|
|
17
|
+
|
|
18
|
+
{% if recovery_codes %}
|
|
19
|
+
<v-alert type="success" variant="tonal" class="mb-4">
|
|
20
|
+
<strong>Save these codes!</strong> Each can only be used once. Store them securely.
|
|
21
|
+
</v-alert>
|
|
22
|
+
|
|
23
|
+
<v-card variant="outlined" class="mb-4">
|
|
24
|
+
<v-card-text class="d-flex flex-wrap ga-2 justify-center py-4">
|
|
25
|
+
{% for rc in recovery_codes %}
|
|
26
|
+
<v-chip size="large" label variant="outlined" class="text-title-large font-weight-mono">{{ rc }}</v-chip>
|
|
27
|
+
{% endfor %}
|
|
28
|
+
</v-card-text>
|
|
29
|
+
<v-divider></v-divider>
|
|
30
|
+
<v-card-actions>
|
|
31
|
+
<v-spacer></v-spacer>
|
|
32
|
+
<v-btn variant="text" @click="copyAll">
|
|
33
|
+
<i class="ti ti-copy mr-1"></i>
|
|
34
|
+
Copy
|
|
35
|
+
</v-btn>
|
|
36
|
+
<v-btn variant="text" @click="download">
|
|
37
|
+
<i class="ti ti-download mr-1"></i>
|
|
38
|
+
Download
|
|
39
|
+
</v-btn>
|
|
40
|
+
</v-card-actions>
|
|
41
|
+
</v-card>
|
|
42
|
+
{% elif has_codes %}
|
|
43
|
+
<v-card variant="outlined" class="mb-4">
|
|
44
|
+
<v-card-text class="text-center pa-6">
|
|
45
|
+
<i class="ti ti-eye-off mb-3" style="font-size: 48px; opacity: 0.5;"></i>
|
|
46
|
+
<div class="text-body-large">Recovery codes are active</div>
|
|
47
|
+
<div class="text-body-small text-medium-emphasis">Codes are shown only once when generated. Lost them? Regenerate below.</div>
|
|
48
|
+
</v-card-text>
|
|
49
|
+
</v-card>
|
|
50
|
+
{% else %}
|
|
51
|
+
<v-card variant="outlined" class="mb-4">
|
|
52
|
+
<v-card-text class="text-center pa-6">
|
|
53
|
+
<i class="ti ti-shield-off mb-3" style="font-size: 48px; opacity: 0.5;"></i>
|
|
54
|
+
<div class="text-body-large">No recovery codes generated yet</div>
|
|
55
|
+
<div class="text-body-small text-medium-emphasis mb-4">Generate codes below to use as backup when you can't access your authenticator</div>
|
|
56
|
+
</v-card-text>
|
|
57
|
+
</v-card>
|
|
58
|
+
{% endif %}
|
|
59
|
+
|
|
60
|
+
<v-divider class="my-4"></v-divider>
|
|
61
|
+
|
|
62
|
+
<v-card variant="outlined">
|
|
63
|
+
<v-card-text class="d-flex align-center">
|
|
64
|
+
<div class="flex-grow-1">
|
|
65
|
+
<div class="font-weight-medium">{{ 'Regenerate Codes' if has_codes else 'Generate Codes' }}</div>
|
|
66
|
+
<div class="text-body-small text-medium-emphasis">{{ 'This will invalidate existing codes' if has_codes else 'Create backup recovery codes' }}</div>
|
|
67
|
+
</div>
|
|
68
|
+
<form action="{{ url_for_security('mf_recovery_codes') }}" method="POST">
|
|
69
|
+
{{ mf_recovery_codes_form.hidden_tag() }}
|
|
70
|
+
{{ render_field_errors(mf_recovery_codes_form.csrf_token) }}
|
|
71
|
+
<v-btn color="{{ 'warning' if has_codes else 'primary' }}" variant="tonal" type="submit">
|
|
72
|
+
<i class="ti ti-refresh mr-1"></i>
|
|
73
|
+
Generate
|
|
74
|
+
</v-btn>
|
|
75
|
+
</form>
|
|
76
|
+
</v-card-text>
|
|
77
|
+
</v-card>
|
|
78
|
+
</v-card-text>
|
|
79
|
+
|
|
80
|
+
<v-card-actions class="pa-4">
|
|
81
|
+
<v-btn variant="text" href="{{ url_for_security('two_factor_setup') }}">
|
|
82
|
+
<i class="ti ti-chevron-left mr-1"></i>
|
|
83
|
+
Back to 2FA Settings
|
|
84
|
+
</v-btn>
|
|
85
|
+
</v-card-actions>
|
|
86
|
+
</v-card>
|
|
87
|
+
</v-container>
|
|
88
|
+
{% endblock %}
|
|
89
|
+
|
|
90
|
+
{% block js %}
|
|
91
|
+
<script>
|
|
92
|
+
const codes = [{% for rc in recovery_codes %}'{{ rc }}'{% if not loop.last %},{% endif %}{% endfor %}];
|
|
93
|
+
|
|
94
|
+
const { createApp } = Vue;
|
|
95
|
+
const { createVuetify } = Vuetify;
|
|
96
|
+
const vuetify = createVuetify(config.vuetifyConfig);
|
|
97
|
+
|
|
98
|
+
const app = createApp({
|
|
99
|
+
mixins: [layoutMixin],
|
|
100
|
+
methods: {
|
|
101
|
+
copyAll() {
|
|
102
|
+
navigator.clipboard.writeText(codes.join('\n'));
|
|
103
|
+
},
|
|
104
|
+
download() {
|
|
105
|
+
const text = {{ (config.APP_NAME ~ ' Recovery Codes')|tojson }} + '\n======================\n\n' + codes.join('\n');
|
|
106
|
+
const blob = new Blob([text], { type: 'text/plain' });
|
|
107
|
+
const a = document.createElement('a');
|
|
108
|
+
a.href = URL.createObjectURL(blob);
|
|
109
|
+
a.download = 'recovery-codes.txt';
|
|
110
|
+
a.click();
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
delimiters: ['${', '}']
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
registerStkComponents(app);
|
|
117
|
+
app.use(vuetify).mount('#app');
|
|
118
|
+
</script>
|
|
119
|
+
{% endblock %}
|