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,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 %}