@hostlink/nuxt-light 1.57.2 → 1.59.0
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.
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -0
- package/dist/runtime/components/l-login.vue +17 -5
- package/dist/runtime/components/l-setup-2fa-dialog.d.vue.ts +13 -0
- package/dist/runtime/components/l-setup-2fa-dialog.vue +94 -0
- package/dist/runtime/components/l-setup-2fa-dialog.vue.d.ts +13 -0
- package/dist/runtime/pages/System/database/migrate.d.vue.ts +3 -0
- package/dist/runtime/pages/System/database/migrate.vue +179 -0
- package/dist/runtime/pages/System/database/migrate.vue.d.ts +3 -0
- package/dist/runtime/pages/User/setting/two-factor-auth.vue +33 -98
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -136,6 +136,11 @@ const routes = [
|
|
|
136
136
|
path: "/System/database/check",
|
|
137
137
|
file: "runtime/pages/System/database/check.vue"
|
|
138
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: "System-database-migrate",
|
|
141
|
+
path: "/System/database/migrate",
|
|
142
|
+
file: "runtime/pages/System/database/migrate.vue"
|
|
143
|
+
},
|
|
139
144
|
{
|
|
140
145
|
name: "System-database-process",
|
|
141
146
|
path: "/System/database/process",
|
|
@@ -84,16 +84,28 @@ const submit = async () => {
|
|
|
84
84
|
emits("login");
|
|
85
85
|
} catch (e) {
|
|
86
86
|
data.code = "";
|
|
87
|
-
|
|
87
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
88
|
+
if (errorMessage == "setup_2fa_required") {
|
|
89
|
+
await $q.dialog({
|
|
90
|
+
component: resolveComponent("l-setup-2fa-dialog"),
|
|
91
|
+
componentProps: {
|
|
92
|
+
username: data.username,
|
|
93
|
+
password: data.password
|
|
94
|
+
},
|
|
95
|
+
persistent: true
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (errorMessage == "two factor authentication code is required") {
|
|
88
100
|
loginWithCode(data.username, data.password);
|
|
89
101
|
return;
|
|
90
102
|
}
|
|
91
|
-
if (
|
|
103
|
+
if (errorMessage == "password is expired") {
|
|
92
104
|
passwordExpiredProcess(data.username, data.password);
|
|
93
105
|
return;
|
|
94
106
|
}
|
|
95
107
|
$q.notify({
|
|
96
|
-
message:
|
|
108
|
+
message: errorMessage,
|
|
97
109
|
color: "negative",
|
|
98
110
|
icon: "sym_o_error"
|
|
99
111
|
});
|
|
@@ -264,8 +276,8 @@ const facebookLogin = (accessToken) => {
|
|
|
264
276
|
<q-icon name="sym_o_lock" />
|
|
265
277
|
</template>
|
|
266
278
|
</l-input>
|
|
267
|
-
<l-input color="primary" v-if="twoFactorAuthentication" v-model="data.code" label="2FA code"
|
|
268
|
-
|
|
279
|
+
<l-input color="primary" v-if="twoFactorAuthentication" v-model="data.code" label="2FA code" type="text"
|
|
280
|
+
clearable stackLabel :outlined="false">
|
|
269
281
|
<template v-slot:prepend>
|
|
270
282
|
<q-icon name="sym_o_key" />
|
|
271
283
|
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare const _default: typeof __VLS_export;
|
|
2
|
+
export default _default;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {
|
|
4
|
+
$emit: typeof __VLS_emit;
|
|
5
|
+
$props: Partial<typeof props>;
|
|
6
|
+
username: string;
|
|
7
|
+
password: string;
|
|
8
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
9
|
+
declare const __VLS_emit: (event: "hide" | "ok", ...args: any[]) => void;
|
|
10
|
+
declare const props: {
|
|
11
|
+
readonly username: string;
|
|
12
|
+
readonly password: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useDialogPluginComponent } from "quasar";
|
|
3
|
+
import { onMounted, q, m } from "#imports";
|
|
4
|
+
import { ref } from "vue";
|
|
5
|
+
import { useQuasar } from "quasar";
|
|
6
|
+
const $q = useQuasar();
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
username: {
|
|
9
|
+
type: String,
|
|
10
|
+
required: true
|
|
11
|
+
},
|
|
12
|
+
password: {
|
|
13
|
+
type: String,
|
|
14
|
+
required: true
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const secret = ref("");
|
|
18
|
+
const code = ref("");
|
|
19
|
+
defineEmits([
|
|
20
|
+
// REQUIRED; need to specify some events that your
|
|
21
|
+
// component will emit through useDialogPluginComponent()
|
|
22
|
+
...useDialogPluginComponent.emits
|
|
23
|
+
]);
|
|
24
|
+
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
|
25
|
+
function onOKClick() {
|
|
26
|
+
m("updateTwoFactorAuthentication", {
|
|
27
|
+
username: props.username,
|
|
28
|
+
password: props.password,
|
|
29
|
+
secret: secret.value,
|
|
30
|
+
code: code.value
|
|
31
|
+
}).then(() => {
|
|
32
|
+
$q.notify({
|
|
33
|
+
type: "positive",
|
|
34
|
+
message: "Two-factor authentication has been successfully set up. Please use your authenticator app to log in."
|
|
35
|
+
});
|
|
36
|
+
onDialogOK();
|
|
37
|
+
}).catch((error) => {
|
|
38
|
+
$q.notify({
|
|
39
|
+
type: "negative",
|
|
40
|
+
message: error.message || "Failed to update two-factor authentication."
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const image = ref("");
|
|
45
|
+
onMounted(async () => {
|
|
46
|
+
const { app } = await q({
|
|
47
|
+
app: {
|
|
48
|
+
generate2FASecret: {
|
|
49
|
+
__args: {
|
|
50
|
+
username: props.username
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
image.value = app.generate2FASecret.image;
|
|
56
|
+
secret.value = app.generate2FASecret.secret;
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
62
|
+
<q-card class="q-dialog-plugin">
|
|
63
|
+
|
|
64
|
+
<q-card-section>
|
|
65
|
+
<div class="text-h6">Setup Two Factor Authentication</div>
|
|
66
|
+
<div>
|
|
67
|
+
To enhance the security of your account, please set up two-factor authentication (2FA) using an
|
|
68
|
+
authenticator app such as Google Authenticator or Microsoft Authenticator. Scan the QR code below
|
|
69
|
+
with your chosen app
|
|
70
|
+
to link it to your account.
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="q-mt-md q-mb-md flex flex-center" v-if="image">
|
|
74
|
+
<img :src="image" alt="2FA QR Code" />
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div>
|
|
78
|
+
<q-input label="Enter the 6-digit code from your authenticator app" type="text" v-model="code"
|
|
79
|
+
clearable stackLabel :outlined="false">
|
|
80
|
+
<template v-slot:prepend>
|
|
81
|
+
<q-icon name="sym_o_key" />
|
|
82
|
+
</template>
|
|
83
|
+
</q-input>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
</q-card-section>
|
|
87
|
+
|
|
88
|
+
<q-card-actions align="right">
|
|
89
|
+
<q-btn color="primary" label="OK" @click="onOKClick" />
|
|
90
|
+
<q-btn color="primary" label="Cancel" @click="onDialogCancel" />
|
|
91
|
+
</q-card-actions>
|
|
92
|
+
</q-card>
|
|
93
|
+
</q-dialog>
|
|
94
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare const _default: typeof __VLS_export;
|
|
2
|
+
export default _default;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {
|
|
4
|
+
$emit: typeof __VLS_emit;
|
|
5
|
+
$props: Partial<typeof props>;
|
|
6
|
+
username: string;
|
|
7
|
+
password: string;
|
|
8
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
9
|
+
declare const __VLS_emit: (event: "hide" | "ok", ...args: any[]) => void;
|
|
10
|
+
declare const props: {
|
|
11
|
+
readonly username: string;
|
|
12
|
+
readonly password: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { q, m } from "#imports";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import { Loading, Notify } from "quasar";
|
|
5
|
+
const isExporting = ref(false);
|
|
6
|
+
const onExportSchema = async () => {
|
|
7
|
+
isExporting.value = true;
|
|
8
|
+
Loading.show({ message: "\u6B63\u5728\u532F\u51FA Schema..." });
|
|
9
|
+
try {
|
|
10
|
+
const data = await q({
|
|
11
|
+
system: {
|
|
12
|
+
database: {
|
|
13
|
+
exportSchema: true
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-");
|
|
18
|
+
const filename = `schema-${timestamp}.json`;
|
|
19
|
+
const blob = new Blob(
|
|
20
|
+
[data.system.database.exportSchema],
|
|
21
|
+
{ type: "application/json;charset=utf-8" }
|
|
22
|
+
);
|
|
23
|
+
const url = URL.createObjectURL(blob);
|
|
24
|
+
const a = document.createElement("a");
|
|
25
|
+
a.download = filename;
|
|
26
|
+
a.href = url;
|
|
27
|
+
a.click();
|
|
28
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
29
|
+
Notify.create({
|
|
30
|
+
type: "positive",
|
|
31
|
+
message: "Schema \u532F\u51FA\u6210\u529F\uFF01",
|
|
32
|
+
position: "top"
|
|
33
|
+
});
|
|
34
|
+
} catch (e) {
|
|
35
|
+
Notify.create({
|
|
36
|
+
type: "negative",
|
|
37
|
+
message: "\u532F\u51FA\u5931\u6557\uFF1A" + e.message,
|
|
38
|
+
position: "top"
|
|
39
|
+
});
|
|
40
|
+
} finally {
|
|
41
|
+
Loading.hide();
|
|
42
|
+
isExporting.value = false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const selectedFile = ref(null);
|
|
46
|
+
const dryRun = ref(true);
|
|
47
|
+
const isImporting = ref(false);
|
|
48
|
+
const importResult = ref(null);
|
|
49
|
+
const onFileChange = () => {
|
|
50
|
+
importResult.value = null;
|
|
51
|
+
};
|
|
52
|
+
const clearFile = () => {
|
|
53
|
+
selectedFile.value = null;
|
|
54
|
+
importResult.value = null;
|
|
55
|
+
};
|
|
56
|
+
const onImportSchema = async () => {
|
|
57
|
+
if (!selectedFile.value) return;
|
|
58
|
+
isImporting.value = true;
|
|
59
|
+
Loading.show({
|
|
60
|
+
message: dryRun.value ? "\u6B63\u5728\u9810\u89BD\u8B8A\u66F4..." : "\u6B63\u5728\u532F\u5165 Schema..."
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
if (dryRun.value) {
|
|
64
|
+
const data = await q({
|
|
65
|
+
system: {
|
|
66
|
+
database: {
|
|
67
|
+
importSchema: {
|
|
68
|
+
__args: { schema: selectedFile.value }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
importResult.value = data.system.database.importSchema;
|
|
74
|
+
Notify.create({
|
|
75
|
+
type: "info",
|
|
76
|
+
message: "\u9810\u89BD\u5B8C\u6210\uFF0C\u8ACB\u6AA2\u8996\u8B8A\u66F4\u5167\u5BB9",
|
|
77
|
+
position: "top"
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
await m("importDatabaseSchema", { schema: selectedFile.value });
|
|
81
|
+
Notify.create({
|
|
82
|
+
type: "positive",
|
|
83
|
+
message: "Schema \u532F\u5165\u6210\u529F\uFF01",
|
|
84
|
+
position: "top"
|
|
85
|
+
});
|
|
86
|
+
importResult.value = null;
|
|
87
|
+
clearFile();
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
Notify.create({
|
|
91
|
+
type: "negative",
|
|
92
|
+
message: "\u64CD\u4F5C\u5931\u6557\uFF1A" + e.message,
|
|
93
|
+
position: "top"
|
|
94
|
+
});
|
|
95
|
+
} finally {
|
|
96
|
+
Loading.hide();
|
|
97
|
+
isImporting.value = false;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<template>
|
|
103
|
+
<l-page title="資料庫 Schema 遷移">
|
|
104
|
+
<div class="row q-col-gutter-md">
|
|
105
|
+
<!-- EXPORT 區塊 -->
|
|
106
|
+
<div class="col-12 col-md-6">
|
|
107
|
+
<l-card>
|
|
108
|
+
<q-card-section>
|
|
109
|
+
<div class="text-h6">
|
|
110
|
+
<q-icon name="sym_o_download" class="q-mr-sm" />
|
|
111
|
+
匯出 Schema
|
|
112
|
+
</div>
|
|
113
|
+
</q-card-section>
|
|
114
|
+
<q-card-section>
|
|
115
|
+
<p class="text-grey-7">
|
|
116
|
+
匯出目前的資料庫 Schema 為 JSON 檔案,可用於備份或遷移至其他環境。
|
|
117
|
+
</p>
|
|
118
|
+
<l-btn label="匯出 Schema" icon="sym_o_download" color="primary" :loading="isExporting"
|
|
119
|
+
:disable="isExporting" @click="onExportSchema" />
|
|
120
|
+
</q-card-section>
|
|
121
|
+
</l-card>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- IMPORT 區塊 -->
|
|
125
|
+
<div class="col-12 col-md-6">
|
|
126
|
+
<l-card>
|
|
127
|
+
<q-card-section>
|
|
128
|
+
<div class="text-h6">
|
|
129
|
+
<q-icon name="sym_o_upload" class="q-mr-sm" />
|
|
130
|
+
匯入 Schema
|
|
131
|
+
</div>
|
|
132
|
+
</q-card-section>
|
|
133
|
+
<q-card-section>
|
|
134
|
+
<p class="text-grey-7">
|
|
135
|
+
選擇 Schema JSON 檔案進行匯入,建議先使用 Dry Run 預覽變更。
|
|
136
|
+
</p>
|
|
137
|
+
|
|
138
|
+
<q-file v-model="selectedFile" label="選擇 Schema 檔案 (.json)" accept=".json,application/json"
|
|
139
|
+
clearable outlined class="q-mb-md" @update:model-value="onFileChange">
|
|
140
|
+
<template #prepend>
|
|
141
|
+
<q-icon name="sym_o_attach_file" />
|
|
142
|
+
</template>
|
|
143
|
+
</q-file>
|
|
144
|
+
|
|
145
|
+
<l-checkbox v-model="dryRun" label="Dry Run (僅預覽變更,不實際執行)" class="q-mb-md" />
|
|
146
|
+
|
|
147
|
+
<div class="row q-gutter-sm">
|
|
148
|
+
<l-btn v-if="dryRun" label="預覽變更" icon="sym_o_preview" color="primary"
|
|
149
|
+
:loading="isImporting" :disable="isImporting || !selectedFile"
|
|
150
|
+
@click="onImportSchema" />
|
|
151
|
+
<l-btn v-else label="執行匯入" icon="sym_o_upload" color="negative" :loading="isImporting"
|
|
152
|
+
:disable="isImporting || !selectedFile" confirm-message="確定要執行 Schema 匯入嗎?此操作將修改資料庫結構。"
|
|
153
|
+
@click="onImportSchema" />
|
|
154
|
+
</div>
|
|
155
|
+
</q-card-section>
|
|
156
|
+
</l-card>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<!-- 結果顯示區塊 -->
|
|
160
|
+
<div v-if="importResult" class="col-12">
|
|
161
|
+
<l-card>
|
|
162
|
+
<q-card-section>
|
|
163
|
+
<div class="text-h6">
|
|
164
|
+
<q-icon name="sym_o_list_alt" class="q-mr-sm" />
|
|
165
|
+
變更預覽結果
|
|
166
|
+
</div>
|
|
167
|
+
</q-card-section>
|
|
168
|
+
<q-card-section>
|
|
169
|
+
<pre class="import-result">{{ importResult }}</pre>
|
|
170
|
+
</q-card-section>
|
|
171
|
+
</l-card>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</l-page>
|
|
175
|
+
</template>
|
|
176
|
+
|
|
177
|
+
<style scoped>
|
|
178
|
+
.import-result{background-color:#f5f5f5;border-radius:8px;font-size:13px;line-height:1.5;max-height:500px;overflow-x:auto;overflow-y:auto;padding:16px}
|
|
179
|
+
</style>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, reactive } from "vue";
|
|
3
3
|
import { useQuasar } from "quasar";
|
|
4
|
-
import { q, m } from "#imports";
|
|
4
|
+
import { q, m, navigateTo } from "#imports";
|
|
5
5
|
const $q = useQuasar();
|
|
6
6
|
const { my } = await q({
|
|
7
7
|
my: {
|
|
@@ -85,8 +85,9 @@ const onCopy = async () => {
|
|
|
85
85
|
<q-stepper v-model="currentStep" vertical color="primary" animated flat>
|
|
86
86
|
<q-step :name="1" title="Download Authenticator App" icon="download" :done="currentStep > 1">
|
|
87
87
|
<div class="q-mb-md">
|
|
88
|
-
<p class="text-body1 q-mb-md">{{ $t('Choose and download an authenticator app for your device:')
|
|
89
|
-
|
|
88
|
+
<p class="text-body1 q-mb-md">{{ $t('Choose and download an authenticator app for your device:')
|
|
89
|
+
}}</p>
|
|
90
|
+
|
|
90
91
|
<!-- App Selection Cards -->
|
|
91
92
|
<div class="row q-gutter-md q-mb-lg">
|
|
92
93
|
<div class="col-12 col-md-5">
|
|
@@ -94,120 +95,69 @@ const onCopy = async () => {
|
|
|
94
95
|
<q-card-section class="q-pa-md">
|
|
95
96
|
<div class="text-h6 q-mb-sm">📱 Android</div>
|
|
96
97
|
<div class="q-gutter-sm">
|
|
97
|
-
<q-btn
|
|
98
|
-
outline
|
|
99
|
-
color="primary"
|
|
100
|
-
size="sm"
|
|
101
|
-
target="_blank"
|
|
98
|
+
<q-btn outline color="primary" size="sm" target="_blank"
|
|
102
99
|
href="https://play.google.com/store/apps/details?id=com.azure.authenticator"
|
|
103
|
-
label="Microsoft Authenticator"
|
|
104
|
-
|
|
105
|
-
/>
|
|
106
|
-
<q-btn
|
|
107
|
-
outline
|
|
108
|
-
color="primary"
|
|
109
|
-
size="sm"
|
|
110
|
-
target="_blank"
|
|
100
|
+
label="Microsoft Authenticator" class="full-width" />
|
|
101
|
+
<q-btn outline color="primary" size="sm" target="_blank"
|
|
111
102
|
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2"
|
|
112
|
-
label="Google Authenticator"
|
|
113
|
-
class="full-width"
|
|
114
|
-
/>
|
|
103
|
+
label="Google Authenticator" class="full-width" />
|
|
115
104
|
</div>
|
|
116
105
|
</q-card-section>
|
|
117
106
|
</q-card>
|
|
118
107
|
</div>
|
|
119
|
-
|
|
108
|
+
|
|
120
109
|
<div class="col-12 col-md-5">
|
|
121
110
|
<q-card flat bordered class="app-card">
|
|
122
111
|
<q-card-section class="q-pa-md">
|
|
123
112
|
<div class="text-h6 q-mb-sm">🍎 iOS</div>
|
|
124
113
|
<div class="q-gutter-sm">
|
|
125
|
-
<q-btn
|
|
126
|
-
outline
|
|
127
|
-
color="primary"
|
|
128
|
-
size="sm"
|
|
129
|
-
target="_blank"
|
|
114
|
+
<q-btn outline color="primary" size="sm" target="_blank"
|
|
130
115
|
href="https://apps.apple.com/us/app/microsoft-authenticator/id983156458"
|
|
131
|
-
label="Microsoft Authenticator"
|
|
132
|
-
|
|
133
|
-
/>
|
|
134
|
-
<q-btn
|
|
135
|
-
outline
|
|
136
|
-
color="primary"
|
|
137
|
-
size="sm"
|
|
138
|
-
target="_blank"
|
|
116
|
+
label="Microsoft Authenticator" class="full-width" />
|
|
117
|
+
<q-btn outline color="primary" size="sm" target="_blank"
|
|
139
118
|
href="https://apps.apple.com/us/app/google-authenticator/id388497605"
|
|
140
|
-
label="Google Authenticator"
|
|
141
|
-
class="full-width"
|
|
142
|
-
/>
|
|
119
|
+
label="Google Authenticator" class="full-width" />
|
|
143
120
|
</div>
|
|
144
121
|
</q-card-section>
|
|
145
122
|
</q-card>
|
|
146
123
|
</div>
|
|
147
124
|
</div>
|
|
148
125
|
|
|
149
|
-
<q-btn
|
|
150
|
-
|
|
151
|
-
@click="currentStep = 2"
|
|
152
|
-
:label="$t('I have installed the app')"
|
|
153
|
-
icon-right="arrow_forward"
|
|
154
|
-
/>
|
|
126
|
+
<q-btn color="primary" @click="currentStep = 2" :label="$t('I have installed the app')"
|
|
127
|
+
icon-right="arrow_forward" />
|
|
155
128
|
</div>
|
|
156
129
|
</q-step>
|
|
157
130
|
|
|
158
131
|
<q-step :name="2" title="Scan QR Code" icon="qr_code" :done="currentStep > 2">
|
|
159
132
|
<div class="text-center q-mb-md">
|
|
160
133
|
<p class="text-body1 q-mb-md">{{ $t('Scan this QR code with your authenticator app:') }}</p>
|
|
161
|
-
|
|
134
|
+
|
|
162
135
|
<q-card flat bordered class="qr-card inline-block q-pa-lg">
|
|
163
136
|
<q-img :src="my.my2FA.image" width="200px" class="q-mb-md" />
|
|
164
|
-
|
|
137
|
+
|
|
165
138
|
<!-- Manual Entry Option -->
|
|
166
|
-
<q-expansion-item
|
|
167
|
-
icon="key"
|
|
168
|
-
:label="$t('Enter manually instead')"
|
|
169
|
-
class="text-left"
|
|
170
|
-
>
|
|
139
|
+
<q-expansion-item icon="key" :label="$t('Enter manually instead')" class="text-left">
|
|
171
140
|
<div class="q-pa-md bg-grey-1 rounded-borders">
|
|
172
141
|
<div class="text-caption text-grey-7 q-mb-xs">{{ $t('Secret Key:') }}</div>
|
|
173
142
|
<div class="row items-center q-gutter-sm">
|
|
174
143
|
<div class="col">
|
|
175
|
-
<q-input
|
|
176
|
-
:model-value="secretVisible ? (my.my2FA?.secret || '') : '●'.repeat(my.my2FA?.secret?.length || 16)"
|
|
177
|
-
readonly
|
|
178
|
-
dense
|
|
179
|
-
outlined
|
|
180
|
-
class="secret-input"
|
|
181
|
-
/>
|
|
144
|
+
<q-input
|
|
145
|
+
:model-value="secretVisible ? (my.my2FA?.secret || '') : '●'.repeat(my.my2FA?.secret?.length || 16)"
|
|
146
|
+
readonly dense outlined class="secret-input" />
|
|
182
147
|
</div>
|
|
183
|
-
<q-btn
|
|
184
|
-
flat
|
|
185
|
-
round
|
|
186
|
-
dense
|
|
187
|
-
:icon="secretVisible ? 'visibility_off' : 'visibility'"
|
|
148
|
+
<q-btn flat round dense :icon="secretVisible ? 'visibility_off' : 'visibility'"
|
|
188
149
|
@click="secretVisible = !secretVisible"
|
|
189
|
-
:title="secretVisible ? $t('Hide') : $t('Show')"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
flat
|
|
193
|
-
round
|
|
194
|
-
dense
|
|
195
|
-
icon="content_copy"
|
|
196
|
-
@click="onCopy"
|
|
197
|
-
:title="$t('Copy to clipboard')"
|
|
198
|
-
/>
|
|
150
|
+
:title="secretVisible ? $t('Hide') : $t('Show')" />
|
|
151
|
+
<q-btn flat round dense icon="content_copy" @click="onCopy"
|
|
152
|
+
:title="$t('Copy to clipboard')" />
|
|
199
153
|
</div>
|
|
200
154
|
</div>
|
|
201
155
|
</q-expansion-item>
|
|
202
156
|
</q-card>
|
|
203
157
|
|
|
204
158
|
<div class="q-mt-md">
|
|
205
|
-
<q-btn
|
|
206
|
-
|
|
207
|
-
@click="currentStep = 3"
|
|
208
|
-
:label="$t('I have scanned the code')"
|
|
209
|
-
icon-right="arrow_forward"
|
|
210
|
-
/>
|
|
159
|
+
<q-btn color="primary" @click="currentStep = 3" :label="$t('I have scanned the code')"
|
|
160
|
+
icon-right="arrow_forward" />
|
|
211
161
|
</div>
|
|
212
162
|
</div>
|
|
213
163
|
</q-step>
|
|
@@ -217,21 +167,12 @@ const onCopy = async () => {
|
|
|
217
167
|
<p class="text-body1 q-mb-md">
|
|
218
168
|
{{ $t('Enter the 6-digit code from your authenticator app to complete setup:') }}
|
|
219
169
|
</p>
|
|
220
|
-
|
|
170
|
+
|
|
221
171
|
<div class="row justify-center q-mb-lg">
|
|
222
172
|
<div class="col-12 col-sm-8 col-md-6">
|
|
223
|
-
<q-input
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
hint="6-digit code from your authenticator app"
|
|
227
|
-
type="tel"
|
|
228
|
-
inputmode="numeric"
|
|
229
|
-
pattern="[0-9]*"
|
|
230
|
-
outlined
|
|
231
|
-
maxlength="6"
|
|
232
|
-
class="text-center code-input"
|
|
233
|
-
required
|
|
234
|
-
>
|
|
173
|
+
<q-input v-model="obj.code" :label="$t('Verification Code')"
|
|
174
|
+
hint="6-digit code from your authenticator app" type="tel" inputmode="numeric"
|
|
175
|
+
pattern="[0-9]*" outlined maxlength="6" class="text-center code-input" required>
|
|
235
176
|
<template v-slot:prepend>
|
|
236
177
|
<q-icon name="security" />
|
|
237
178
|
</template>
|
|
@@ -240,14 +181,8 @@ const onCopy = async () => {
|
|
|
240
181
|
</div>
|
|
241
182
|
|
|
242
183
|
<div class="text-center">
|
|
243
|
-
<q-btn
|
|
244
|
-
|
|
245
|
-
color="positive"
|
|
246
|
-
size="lg"
|
|
247
|
-
:label="$t('Complete Setup')"
|
|
248
|
-
icon="check_circle"
|
|
249
|
-
:loading="loading"
|
|
250
|
-
/>
|
|
184
|
+
<q-btn @click="save" color="positive" size="lg" :label="$t('Complete Setup')"
|
|
185
|
+
icon="check_circle" :loading="loading" />
|
|
251
186
|
</div>
|
|
252
187
|
</div>
|
|
253
188
|
</q-step>
|