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.
Files changed (128) hide show
  1. stk/__init__.py +19 -0
  2. stk/__main__.py +6 -0
  3. stk/agent_login.py +78 -0
  4. stk/app.py +323 -0
  5. stk/checks.py +634 -0
  6. stk/cli/__init__.py +49 -0
  7. stk/cli/agent.py +267 -0
  8. stk/cli/base.py +79 -0
  9. stk/cli/database.py +199 -0
  10. stk/cli/develop.py +63 -0
  11. stk/cli/doctor.py +262 -0
  12. stk/cli/main.py +101 -0
  13. stk/cli/reports.py +296 -0
  14. stk/cli/smoke.py +399 -0
  15. stk/cli/users.py +259 -0
  16. stk/cli/watch.py +68 -0
  17. stk/extensions.py +29 -0
  18. stk/migrations/__init__.py +130 -0
  19. stk/migrations/env.py +58 -0
  20. stk/migrations/script.py.mako +17 -0
  21. stk/migrations/versions/stk_0001_initial_schema.py +213 -0
  22. stk/portal/views.py +34 -0
  23. stk/public/__init__.py +0 -0
  24. stk/public/models.py +0 -0
  25. stk/public/views.py +344 -0
  26. stk/scaffold/__init__.py +1 -0
  27. stk/scaffold/app/CLAUDE.md +65 -0
  28. stk/scaffold/app/Dockerfile +41 -0
  29. stk/scaffold/app/README.md +16 -0
  30. stk/scaffold/app/__app__/__init__.py +1 -0
  31. stk/scaffold/app/__app__/app.py +8 -0
  32. stk/scaffold/app/__app__/settings.py +7 -0
  33. stk/scaffold/app/claude/skills/stk-api/SKILL.md +52 -0
  34. stk/scaffold/app/claude/skills/stk-blueprint/SKILL.md +48 -0
  35. stk/scaffold/app/claude/skills/stk-deploy/SKILL.md +61 -0
  36. stk/scaffold/app/claude/skills/stk-lessons/SKILL.md +58 -0
  37. stk/scaffold/app/claude/skills/stk-migrate/SKILL.md +96 -0
  38. stk/scaffold/app/docker-compose.yml +88 -0
  39. stk/scaffold/app/dockerignore +13 -0
  40. stk/scaffold/app/env-sample +78 -0
  41. stk/scaffold/app/gitignore +8 -0
  42. stk/scaffold/app/nginx/nginx.conf +46 -0
  43. stk/scaffold/app/nginx/stk.conf +36 -0
  44. stk/scaffold/app/postgres/init-db.sh +8 -0
  45. stk/scaffold/app/pyproject.toml +23 -0
  46. stk/scaffold/app/tests/__init__.py +0 -0
  47. stk/scaffold/app/tests/test_app.py +23 -0
  48. stk/scaffold/generator.py +209 -0
  49. stk/scaffold/templates.py +437 -0
  50. stk/security.py +145 -0
  51. stk/settings.py +147 -0
  52. stk/shell.py +284 -0
  53. stk/static/VERSIONS.txt +4 -0
  54. stk/static/css/app.css +229 -0
  55. stk/static/css/layout.css +330 -0
  56. stk/static/css/vuetify.min.css +6 -0
  57. stk/static/icons/fonts/tabler-icons.woff2 +0 -0
  58. stk/static/icons/tabler-icons.min.css +4 -0
  59. stk/static/img/auth-bg.webp +0 -0
  60. stk/static/img/favicon.ico +0 -0
  61. stk/static/img/stk.svg +1664 -0
  62. stk/static/js/axios.min.js +5 -0
  63. stk/static/js/components/NotificationDropdown.js +156 -0
  64. stk/static/js/components/ThemeSwitcher.js +68 -0
  65. stk/static/js/components/TransitionExpand.js +64 -0
  66. stk/static/js/components/VerticalNavGroup.js +119 -0
  67. stk/static/js/components/VerticalNavLink.js +36 -0
  68. stk/static/js/components/VerticalNavSectionTitle.js +21 -0
  69. stk/static/js/components/index.js +47 -0
  70. stk/static/js/config.js +150 -0
  71. stk/static/js/vue.min.js +13 -0
  72. stk/static/js/vuetify.min.js +3148 -0
  73. stk/static/robots.txt +1 -0
  74. stk/tasks/__init__.py +78 -0
  75. stk/templates/401.html +14 -0
  76. stk/templates/404.html +12 -0
  77. stk/templates/500.html +13 -0
  78. stk/templates/_brand.html +1 -0
  79. stk/templates/auth_layout.html +26 -0
  80. stk/templates/cms/activities.html +135 -0
  81. stk/templates/cms/roles.html +215 -0
  82. stk/templates/cms/users.html +312 -0
  83. stk/templates/dashboard.html +132 -0
  84. stk/templates/index.html +52 -0
  85. stk/templates/layout.html +328 -0
  86. stk/templates/login-layout.html +54 -0
  87. stk/templates/security/_macros.html +39 -0
  88. stk/templates/security/_messages.html +12 -0
  89. stk/templates/security/change_password.html +100 -0
  90. stk/templates/security/confirm_email.html +44 -0
  91. stk/templates/security/email/change_notice.html +4 -0
  92. stk/templates/security/email/change_notice.txt +5 -0
  93. stk/templates/security/email/confirmation_instructions.html +3 -0
  94. stk/templates/security/email/confirmation_instructions.txt +3 -0
  95. stk/templates/security/email/login_instructions.html +5 -0
  96. stk/templates/security/email/login_instructions.txt +5 -0
  97. stk/templates/security/email/reset_instructions.html +1 -0
  98. stk/templates/security/email/reset_instructions.txt +3 -0
  99. stk/templates/security/email/reset_notice.html +1 -0
  100. stk/templates/security/email/reset_notice.txt +1 -0
  101. stk/templates/security/email/welcome.html +7 -0
  102. stk/templates/security/email/welcome.txt +7 -0
  103. stk/templates/security/forgot_password.html +63 -0
  104. stk/templates/security/login_user.html +206 -0
  105. stk/templates/security/mf_recovery.html +67 -0
  106. stk/templates/security/mf_recovery_codes.html +119 -0
  107. stk/templates/security/register_user.html +263 -0
  108. stk/templates/security/reset_password.html +53 -0
  109. stk/templates/security/send_confirmation.html +48 -0
  110. stk/templates/security/send_login.html +39 -0
  111. stk/templates/security/two_factor_select.html +78 -0
  112. stk/templates/security/two_factor_setup.html +166 -0
  113. stk/templates/security/two_factor_verify_code.html +71 -0
  114. stk/templates/security/wan_register.html +179 -0
  115. stk/templates/security/wan_signin.html +123 -0
  116. stk/templates/security/wan_verify.html +109 -0
  117. stk/user/__init__.py +0 -0
  118. stk/user/forms.py +10 -0
  119. stk/user/models.py +308 -0
  120. stk/user/views.py +339 -0
  121. stk/utils/email.py +32 -0
  122. stk/utils/ratelimit.py +67 -0
  123. stk/websocket.py +133 -0
  124. stkit-17.0.0.dist-info/METADATA +135 -0
  125. stkit-17.0.0.dist-info/RECORD +128 -0
  126. stkit-17.0.0.dist-info/WHEEL +4 -0
  127. stkit-17.0.0.dist-info/entry_points.txt +2 -0
  128. 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 %}