@hostlink/nuxt-light 1.0.2 → 1.0.3
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/runtime/components/l-app-main.vue +8 -3
- package/dist/runtime/components/l-card.vue +1 -1
- package/dist/runtime/components/l-col.vue +1 -1
- package/dist/runtime/components/l-customizer.vue +3 -4
- package/dist/runtime/components/l-file-manager.vue +4 -4
- package/dist/runtime/components/l-row.vue +4 -2
- package/dist/runtime/components/l-table.vue +5 -4
- package/dist/runtime/formkit/Form.vue +2 -2
- package/dist/runtime/locales/en.json +5 -12
- package/dist/runtime/locales/zh-hk.json +15 -2
- package/dist/runtime/pages/Permission/all.vue +1 -1
- package/dist/runtime/pages/Role/add.vue +1 -5
- package/dist/runtime/pages/Role/index.vue +21 -5
- package/dist/runtime/pages/System/database/backup.vue +25 -2
- package/dist/runtime/pages/System/database/table.vue +4 -2
- package/dist/runtime/pages/System/index.vue +3 -2
- package/dist/runtime/pages/System/package.vue +20 -1
- package/dist/runtime/pages/System/setting.vue +35 -4
- package/dist/runtime/pages/System/test.vue +1 -0
- package/dist/runtime/pages/System/view_as.vue +38 -11
- package/dist/runtime/pages/Translate/index.vue +5 -4
- package/dist/runtime/pages/User/_user_id/edit.vue +13 -12
- package/dist/runtime/pages/User/add.vue +18 -10
- package/dist/runtime/pages/User/setting/bio-auth.vue +4 -7
- package/dist/runtime/pages/User/setting/password.vue +29 -11
- package/dist/runtime/pages/User/setting/two-factor-auth.vue +13 -7
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -17,7 +17,10 @@ const appVersion = config.public.appVersion ?? '0.0.1';
|
|
|
17
17
|
const quasar = useQuasar();
|
|
18
18
|
const tt = await q({
|
|
19
19
|
system: ["devMode"],
|
|
20
|
-
app: ["menus", "viewAsMode", "languages",
|
|
20
|
+
app: ["menus", "viewAsMode", "languages",
|
|
21
|
+
"copyrightYear",
|
|
22
|
+
"copyrightName",
|
|
23
|
+
{ i18nMessages: ["name", "value"] }],
|
|
21
24
|
my: ['username', 'first_name', 'last_name', 'roles', "styles", "language", f('granted_storage:granted', { right: "system.storage" }, [])],
|
|
22
25
|
})
|
|
23
26
|
|
|
@@ -195,7 +198,7 @@ const containerStyle = computed(() => {
|
|
|
195
198
|
|
|
196
199
|
<q-toolbar-title>
|
|
197
200
|
{{ light.getCompany() }}
|
|
198
|
-
<template
|
|
201
|
+
<template v-if="tt.system.devMode">
|
|
199
202
|
- Development mode
|
|
200
203
|
</template>
|
|
201
204
|
</q-toolbar-title>
|
|
@@ -332,7 +335,9 @@ const containerStyle = computed(() => {
|
|
|
332
335
|
<q-footer bordered v-if="style.footer">
|
|
333
336
|
<q-item>
|
|
334
337
|
<q-item-section>
|
|
335
|
-
{{ light.getCompany() }} {{ appVersion }} - Copyright
|
|
338
|
+
{{ light.getCompany() }} {{ appVersion }} - Copyright {{ app.copyrightYear }} {{ app.copyrightName }}.
|
|
339
|
+
Build {{
|
|
340
|
+
light.getVersion() }}
|
|
336
341
|
</q-item-section>
|
|
337
342
|
</q-item>
|
|
338
343
|
</q-footer>
|
|
@@ -38,7 +38,7 @@ const showBody = computed(() => !minimize.value || fullScreen.value);
|
|
|
38
38
|
<template>
|
|
39
39
|
<q-card v-bind="attrs" :class="{ 'fullscreen': fullScreen, 'no-margin': fullScreen }">
|
|
40
40
|
<q-bar :class="cl" v-if="title">
|
|
41
|
-
<div>{{ title }}</div>
|
|
41
|
+
<div>{{ $t(title) }}</div>
|
|
42
42
|
<q-space />
|
|
43
43
|
<!-- q-btn-dropdown dense flat icon="sym_o_search" persistent>
|
|
44
44
|
<div class="q-ma-md">
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { defineModel } from 'vue'
|
|
3
2
|
const COLORS = [
|
|
4
3
|
'primary',
|
|
5
4
|
'secondary',
|
|
@@ -84,7 +83,7 @@ const props = defineProps({
|
|
|
84
83
|
<q-separator />
|
|
85
84
|
<q-item>
|
|
86
85
|
<q-item-section>
|
|
87
|
-
<q-item-label>Color</q-item-label>
|
|
86
|
+
<q-item-label>{{ $t('Color') }}</q-item-label>
|
|
88
87
|
<div class="row">
|
|
89
88
|
<div v-for="c in COLORS" :key="c" :class="`bg-${c}`" style="width: 1.5rem; height: 1.5rem;"
|
|
90
89
|
class="q-ma-xs cursor-pointer rounded-borders" @click="$emit('update:color', c)" />
|
|
@@ -115,7 +114,7 @@ const props = defineProps({
|
|
|
115
114
|
|
|
116
115
|
<q-item>
|
|
117
116
|
<q-item-section>
|
|
118
|
-
<q-item-label>Menu overlay header
|
|
117
|
+
<q-item-label>Menu overlay header</q-item-label>
|
|
119
118
|
</q-item-section>
|
|
120
119
|
<q-item-section side>
|
|
121
120
|
<q-toggle :model-value="menuOverlayHeader"
|
|
@@ -127,7 +126,7 @@ const props = defineProps({
|
|
|
127
126
|
|
|
128
127
|
<q-item>
|
|
129
128
|
<q-item-section>
|
|
130
|
-
<q-item-label>Show footer</q-item-label>
|
|
129
|
+
<q-item-label>{{ $t('Show footer') }}</q-item-label>
|
|
131
130
|
</q-item-section>
|
|
132
131
|
<q-item-section side>
|
|
133
132
|
<q-toggle :model-value="footer" @update:model-value="$emit('update:footer', $event)" />
|
|
@@ -464,7 +464,7 @@ const isDark = computed(() => light.isDarkMode());
|
|
|
464
464
|
</script>
|
|
465
465
|
<template>
|
|
466
466
|
<q-layout view="hHh lpR fFf" :class="isDark ? '' : 'bg-white'" container :style="{ 'min-height': height }">
|
|
467
|
-
<q-header bordered :class="isDark?'':'bg-white text-grey-8'" height-hint="64">
|
|
467
|
+
<q-header bordered :class="isDark ? '' : 'bg-white text-grey-8'" height-hint="64">
|
|
468
468
|
<q-toolbar>
|
|
469
469
|
<q-btn flat round @click="toggleLeftDrawer" aria-label="Menu" icon="menu" class="q-mr-sm" />
|
|
470
470
|
|
|
@@ -629,7 +629,7 @@ const isDark = computed(() => light.isDarkMode());
|
|
|
629
629
|
|
|
630
630
|
<q-table flat bordered :columns="columns" :rows="items" @row-dblclick="onDblclickRow" @row-click="onClickRow"
|
|
631
631
|
:pagination="pagination" row-key="path" selection="multiple" v-model:selected="selected" dense
|
|
632
|
-
:loading="loading">
|
|
632
|
+
:loading="loading" :loading-label="$t('Loading...')" :no-data-label="$t('No data available')">
|
|
633
633
|
<template #body-cell-icon="props">
|
|
634
634
|
<q-td auto-width>
|
|
635
635
|
<q-icon name="sym_o_folder" v-if="props.value == 'folder'" size="sm" />
|
|
@@ -646,14 +646,14 @@ const isDark = computed(() => light.isDarkMode());
|
|
|
646
646
|
<q-item-section avatar>
|
|
647
647
|
<q-icon name="sym_o_delete"></q-icon>
|
|
648
648
|
</q-item-section>
|
|
649
|
-
<q-item-section>{{ $t('Delete') }}
|
|
649
|
+
<q-item-section>{{ $t('Delete') }}</q-item-section>
|
|
650
650
|
</q-item>
|
|
651
651
|
|
|
652
652
|
<q-item v-if="props.row.type == 'file'" clickable v-close-popup @click="onDownloadRow(props.row)">
|
|
653
653
|
<q-item-section avatar>
|
|
654
654
|
<q-icon name="sym_o_download"></q-icon>
|
|
655
655
|
</q-item-section>
|
|
656
|
-
<q-item-section>Download</q-item-section>
|
|
656
|
+
<q-item-section>{{ $t('Download') }}</q-item-section>
|
|
657
657
|
</q-item>
|
|
658
658
|
|
|
659
659
|
<q-item clickable v-close-popup @click="onRenameRow(props.row)" v-if="canRenameRow(props.row)">
|
|
@@ -589,7 +589,7 @@ const getCellClass = (col: any, row: any) => {
|
|
|
589
589
|
|
|
590
590
|
|
|
591
591
|
<template #top-right="props" v-if="fullscreen || searchable">
|
|
592
|
-
<q-input v-if="searchable" outlined dense debounce="300" v-model="filter" placeholder="Search">
|
|
592
|
+
<q-input v-if="searchable" outlined dense debounce="300" v-model="filter" :placeholder="$t('Search')">
|
|
593
593
|
<template v-slot:append>
|
|
594
594
|
<q-icon name="search" />
|
|
595
595
|
</template>
|
|
@@ -618,8 +618,9 @@ const getCellClass = (col: any, row: any) => {
|
|
|
618
618
|
<template v-if="col.searchable">
|
|
619
619
|
|
|
620
620
|
<template v-if="col.searchType == 'number'">
|
|
621
|
-
<q-input dense clearable filled square
|
|
622
|
-
@keydown.enter.prevent="onFilters" @clear="onFilters"
|
|
621
|
+
<q-input style="min-width: 80px;" dense clearable filled square
|
|
622
|
+
v-model.number="filters[col.name]" @keydown.enter.prevent="onFilters" @clear="onFilters"
|
|
623
|
+
mask="##########"></q-input>
|
|
623
624
|
</template>
|
|
624
625
|
|
|
625
626
|
<template v-if="col.searchType == 'select'">
|
|
@@ -635,7 +636,7 @@ const getCellClass = (col: any, row: any) => {
|
|
|
635
636
|
</template>
|
|
636
637
|
|
|
637
638
|
<template v-if="!col.searchType">
|
|
638
|
-
<q-input dense clearable filled square v-model="filters[col.name]"
|
|
639
|
+
<q-input style="min-width: 80px;" dense clearable filled square v-model="filters[col.name]"
|
|
639
640
|
@keydown.enter.prevent="onFilters" @clear="onFilters" enterkeyhint="search"></q-input>
|
|
640
641
|
|
|
641
642
|
</template>
|
|
@@ -77,13 +77,13 @@ if (!props.context.onSubmit) {
|
|
|
77
77
|
<form v-bind="context.attrs">
|
|
78
78
|
<l-card :bordered="bordered">
|
|
79
79
|
<q-card-section>
|
|
80
|
-
<div :class="`q-
|
|
80
|
+
<div :class="`q-gutter-${gutter}`">
|
|
81
81
|
<slot v-bind="context"></slot>
|
|
82
82
|
</div>
|
|
83
83
|
</q-card-section>
|
|
84
84
|
|
|
85
85
|
<q-card-actions align="right">
|
|
86
|
-
<l-btn
|
|
86
|
+
<l-btn icon="sym_o_check" label="Submit" @click="onSubmit" :disabled="!context.state.dirty"
|
|
87
87
|
:loading="loading"></l-btn>
|
|
88
88
|
</q-card-actions>
|
|
89
89
|
</l-card>
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"mail-test": "Mail test",
|
|
8
|
-
"vx-table-message": "Showing {0} to {1} of {2} entries",
|
|
9
|
-
"vx-per-page": "Per page",
|
|
10
|
-
"Dashboard": "Dashboard",
|
|
11
|
-
"Theme Customizer": "Theme Customizer",
|
|
12
|
-
"Customize & Preview in Real Time": "Customize & Preview in Real Time",
|
|
13
|
-
"Permission": "Permission",
|
|
2
|
+
"Must contain at least {0} characters": "Must contain at least {0} characters",
|
|
3
|
+
"contains_uppercase": "Must contain at least one uppercase letter",
|
|
4
|
+
"contains_lowercase": "Must contain at least one lowercase letter",
|
|
5
|
+
"contains_numeric": "Must contain at least one number",
|
|
6
|
+
"contains_symbol": "Must contain at least one special character",
|
|
14
7
|
"storage_usage": "{0} free of {1}",
|
|
15
8
|
"input_required": "Please input {0}",
|
|
16
9
|
"input_min": "Please input at least {0} characters"
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
+
"Password policy": "密碼規則",
|
|
3
|
+
"Must contain at least {0} characters": "必須包含至少{0}個字元",
|
|
4
|
+
"contains_symbol": "必須包含符號",
|
|
5
|
+
"contains_numeric": "必須包含數字",
|
|
6
|
+
"contains_lowercase": "必須包含小寫字母",
|
|
7
|
+
"contains_uppercase": "必須包含大寫字母",
|
|
8
|
+
"Last access time": "最後存取時間",
|
|
9
|
+
"Default": "預設",
|
|
10
|
+
"Register": "註冊",
|
|
11
|
+
"Reset your 2FA": "重設雙重認證",
|
|
12
|
+
"Result": "結果",
|
|
13
|
+
"Update password": "更新密碼",
|
|
14
|
+
"Two factor auth": "雙重認證",
|
|
15
|
+
"Color": "顏色",
|
|
16
|
+
"Show footer": "顯示頁尾",
|
|
2
17
|
"Date": "日期",
|
|
3
18
|
"Online": "在線",
|
|
4
19
|
"Update": "更新",
|
|
@@ -108,8 +123,6 @@
|
|
|
108
123
|
"Rename": "重新命名",
|
|
109
124
|
"Database": "資料庫",
|
|
110
125
|
"Charset": "字元編碼",
|
|
111
|
-
"vx-table-message": "顯示 {0} 至 {1} 共 {2} 項資料",
|
|
112
|
-
"vx-per-page": "每頁顯示",
|
|
113
126
|
"Role": "角色",
|
|
114
127
|
"Permission": "權限",
|
|
115
128
|
"Old password": "舊密碼",
|
|
@@ -69,7 +69,7 @@ const onUpdate = (value, role, permission) => {
|
|
|
69
69
|
|
|
70
70
|
<template>
|
|
71
71
|
<l-page>
|
|
72
|
-
<q-table :columns="columns" flat bordered :rows="rows" :pagination="{ rowsPerPage: 0 }">
|
|
72
|
+
<q-table :columns="columns" flat bordered :rows="rows" :pagination="{ rowsPerPage: 0 }" dense>
|
|
73
73
|
<template #body="props">
|
|
74
74
|
<q-tr :props="props">
|
|
75
75
|
<q-td>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { q } from '
|
|
2
|
+
import { q } from '#imports'
|
|
3
3
|
|
|
4
4
|
let roles = await q("listRole", ["name"]);
|
|
5
5
|
roles = roles.map((role) => {
|
|
@@ -9,10 +9,6 @@ roles = roles.map((role) => {
|
|
|
9
9
|
};
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
const onSubmit = (e) => {
|
|
13
|
-
e.preventDefault();
|
|
14
|
-
console.log("submit");
|
|
15
|
-
}
|
|
16
12
|
</script>
|
|
17
13
|
|
|
18
14
|
<template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useQuasar } from 'quasar'
|
|
3
|
-
import { q, m } from '
|
|
4
|
-
import { ref
|
|
3
|
+
import { q, m, notify } from '#imports'
|
|
4
|
+
import { ref } from "vue"
|
|
5
5
|
|
|
6
6
|
const qua = useQuasar();
|
|
7
7
|
|
|
@@ -60,6 +60,10 @@ const onRemoveChild = async (value, child) => {
|
|
|
60
60
|
child: child.value
|
|
61
61
|
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
//notify
|
|
65
|
+
notify("Role removed from role")
|
|
66
|
+
|
|
63
67
|
//refresh
|
|
64
68
|
roles.value = await loadData();
|
|
65
69
|
}
|
|
@@ -70,6 +74,10 @@ const onAddChild = async (value, child) => {
|
|
|
70
74
|
child: child.value.value
|
|
71
75
|
|
|
72
76
|
});
|
|
77
|
+
|
|
78
|
+
//notify
|
|
79
|
+
notify("Role added to role")
|
|
80
|
+
|
|
73
81
|
//refresh
|
|
74
82
|
roles.value = await loadData();
|
|
75
83
|
}
|
|
@@ -80,6 +88,10 @@ const onAddUser = async (value, user) => {
|
|
|
80
88
|
user_id: user.value.user_id
|
|
81
89
|
|
|
82
90
|
});
|
|
91
|
+
|
|
92
|
+
//notify
|
|
93
|
+
notify("User added to role")
|
|
94
|
+
|
|
83
95
|
//refresh
|
|
84
96
|
roles.value = await loadData();
|
|
85
97
|
}
|
|
@@ -90,6 +102,10 @@ const onRemoveUser = async (value, user) => {
|
|
|
90
102
|
user_id: user.value.user_id
|
|
91
103
|
|
|
92
104
|
});
|
|
105
|
+
|
|
106
|
+
//notify
|
|
107
|
+
notify("User removed from role")
|
|
108
|
+
|
|
93
109
|
//refresh
|
|
94
110
|
roles.value = await loadData();
|
|
95
111
|
}
|
|
@@ -97,7 +113,7 @@ const onRemoveUser = async (value, user) => {
|
|
|
97
113
|
|
|
98
114
|
<template>
|
|
99
115
|
<l-page>
|
|
100
|
-
<q-table :rows="roles" flat bordered :columns="columns" :rows-per-page-options="[0]">
|
|
116
|
+
<q-table :rows="roles" flat bordered :columns="columns" :rows-per-page-options="[0]" dense>
|
|
101
117
|
<template #body-cell-_can_delete="props">
|
|
102
118
|
<q-td auto-width>
|
|
103
119
|
<q-btn v-if="props.row.canDelete" flat round dense icon="sym_o_delete"
|
|
@@ -108,7 +124,7 @@ const onRemoveUser = async (value, user) => {
|
|
|
108
124
|
|
|
109
125
|
<template #body-cell-children="props">
|
|
110
126
|
<q-td>
|
|
111
|
-
<q-select :options="role_options" v-model="props.row.children" multiple use-chips
|
|
127
|
+
<q-select :options="role_options" v-model="props.row.children" multiple use-chips dense
|
|
112
128
|
@remove="onRemoveChild(props.row.name, $event)" @add="onAddChild(props.row.name, $event)">
|
|
113
129
|
|
|
114
130
|
</q-select>
|
|
@@ -116,7 +132,7 @@ const onRemoveUser = async (value, user) => {
|
|
|
116
132
|
</template>
|
|
117
133
|
<template #body-cell-user="props">
|
|
118
134
|
<q-td>
|
|
119
|
-
<q-select :options="users" v-model="props.row.user" multiple use-chips option-label="name"
|
|
135
|
+
<q-select :options="users" v-model="props.row.user" multiple use-chips dense option-label="name"
|
|
120
136
|
option-value="user_id" @add="onAddUser(props.row.name, $event)"
|
|
121
137
|
@remove="onRemoveUser(props.row.name, $event)">
|
|
122
138
|
|
|
@@ -1,6 +1,29 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { q } from "#imports"
|
|
3
|
+
const onClickDownload = async () => {
|
|
4
|
+
|
|
5
|
+
const data = await q({
|
|
6
|
+
system: {
|
|
7
|
+
database: {
|
|
8
|
+
export: true
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const file = new File([data.system.database.export], "backup.sql", {
|
|
14
|
+
type: "text/plain;charset=utf-8"
|
|
15
|
+
});
|
|
16
|
+
//save to local
|
|
17
|
+
|
|
18
|
+
const a = document.createElement("a");
|
|
19
|
+
a.download = file.name;
|
|
20
|
+
a.href = URL.createObjectURL(file);
|
|
21
|
+
a.click();
|
|
22
|
+
URL.revokeObjectURL(a.href);
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
1
25
|
<template>
|
|
2
26
|
<l-page>
|
|
3
|
-
|
|
4
|
-
|
|
27
|
+
<q-btn color="primary" label="Download" icon="sym_o_download" @click="onClickDownload"></q-btn>
|
|
5
28
|
</l-page>
|
|
6
29
|
</template>
|
|
@@ -15,9 +15,11 @@ const { system: { database } } = await query({
|
|
|
15
15
|
<l-page>
|
|
16
16
|
<q-card flat bordered>
|
|
17
17
|
<q-list bordered class="rounded-borders" separator>
|
|
18
|
-
<q-expansion-item :label="table.name" v-for="table in database.table"
|
|
18
|
+
<q-expansion-item :label="table.name" v-for="table in database.table">
|
|
19
19
|
<div class=" q-ma-sm">
|
|
20
|
-
<q-table
|
|
20
|
+
<q-table
|
|
21
|
+
separator="cell"
|
|
22
|
+
dense :rows="table.columns" :rows-per-page-options="[0]" hide-pagination flat bordered></q-table>
|
|
21
23
|
</div>
|
|
22
24
|
|
|
23
25
|
</q-expansion-item>
|
|
@@ -5,7 +5,8 @@ const system = await q("system", ["server"])
|
|
|
5
5
|
const columns = [
|
|
6
6
|
{
|
|
7
7
|
name: "name",
|
|
8
|
-
label: "Name"
|
|
8
|
+
label: "Name",
|
|
9
|
+
sortable: true
|
|
9
10
|
},
|
|
10
11
|
{
|
|
11
12
|
name: "value",
|
|
@@ -17,6 +18,6 @@ const columns = [
|
|
|
17
18
|
</script>
|
|
18
19
|
<template>
|
|
19
20
|
<l-page>
|
|
20
|
-
<l-table :rows="system.server" :columns="columns" :rows-per-page-options="[0]" hide-pagination></l-table>
|
|
21
|
+
<l-table searchable :rows="system.server" :columns="columns" :rows-per-page-options="[0]" hide-pagination></l-table>
|
|
21
22
|
</l-page>
|
|
22
23
|
</template>
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
2
3
|
import { q } from '#imports'
|
|
3
4
|
const { system } = await q({ system: ["package"] })
|
|
5
|
+
const filter = ref("")
|
|
6
|
+
const filtered = computed(() => {
|
|
7
|
+
if (!filter.value) return system.package
|
|
8
|
+
return system.package.filter((row) => {
|
|
9
|
+
return Object.values(row).some((val) => {
|
|
10
|
+
return String(val).toLowerCase().includes(filter.value.toLowerCase())
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
4
15
|
</script>
|
|
5
16
|
<template>
|
|
6
17
|
<l-page>
|
|
7
|
-
<q-table dense :rows="
|
|
18
|
+
<q-table dense :rows="filtered" :rows-per-page-options="[0]" hide-pagination flat bordered separator="cell">
|
|
19
|
+
<template v-slot:top-right>
|
|
20
|
+
<q-input outlined dense debounce="300" v-model="filter" placeholder="Search">
|
|
21
|
+
<template v-slot:append>
|
|
22
|
+
<q-icon name="sym_o_search" />
|
|
23
|
+
</template>
|
|
24
|
+
</q-input>
|
|
25
|
+
</template>
|
|
26
|
+
</q-table>
|
|
8
27
|
</l-page>
|
|
9
28
|
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { Notify } from 'quasar'
|
|
3
|
+
import { reset } from "@formkit/core"
|
|
3
4
|
import { useRouter } from 'vue-router'
|
|
4
5
|
import { q, m } from '../../'
|
|
5
6
|
|
|
@@ -18,7 +19,13 @@ const fields = ["company", "company_logo",
|
|
|
18
19
|
"password_min_length",
|
|
19
20
|
"file_manager",
|
|
20
21
|
"two_factor_authentication",
|
|
21
|
-
"mode"
|
|
22
|
+
"mode",
|
|
23
|
+
"auth_lockout_duration",
|
|
24
|
+
"auth_lockout_attempts",
|
|
25
|
+
"access_token_expire",
|
|
26
|
+
"copyright_year",
|
|
27
|
+
"copyright_name"
|
|
28
|
+
];
|
|
22
29
|
|
|
23
30
|
//filter out fields that are not in the app.config table
|
|
24
31
|
Object.keys(obj).forEach((key) => {
|
|
@@ -43,7 +50,10 @@ const onSubmit = async (d, form) => {
|
|
|
43
50
|
color: "positive",
|
|
44
51
|
icon: "check"
|
|
45
52
|
})
|
|
46
|
-
|
|
53
|
+
|
|
54
|
+
reset(form, d)
|
|
55
|
+
|
|
56
|
+
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
|
|
@@ -57,11 +67,12 @@ if (obj.mode != 'prod') {
|
|
|
57
67
|
</script>
|
|
58
68
|
<template>
|
|
59
69
|
<l-page>
|
|
60
|
-
|
|
61
70
|
<FormKit type="l-form" :value="obj" #default="{ value }" @submit="onSubmit">
|
|
62
71
|
<FormKit type="l-input" label="Company" name="company" validation="required"></FormKit>
|
|
63
72
|
<FormKit type="l-input" label="Company logo" name="company_logo"></FormKit>
|
|
64
73
|
|
|
74
|
+
<q-separator />
|
|
75
|
+
|
|
65
76
|
<q-field label="Password policy" stack-label>
|
|
66
77
|
<FormKit type="q-checkbox" label="Upper Case" name="password_contains_uppercase" true-value="1"
|
|
67
78
|
false-value="0" />
|
|
@@ -88,11 +99,31 @@ if (obj.mode != 'prod') {
|
|
|
88
99
|
{ label: 'Development', value: 'dev' },
|
|
89
100
|
|
|
90
101
|
]" name="mode" validation="required">
|
|
102
|
+
</FormKit>
|
|
91
103
|
|
|
104
|
+
<q-separator />
|
|
105
|
+
|
|
106
|
+
<FormKit label="Auth lockout duration" type="l-input" name="auth_lockout_duration"
|
|
107
|
+
hint="The number of minutes the user is locked out after the maximum number of failed login attempts. Default is 15 minutes." />
|
|
108
|
+
|
|
109
|
+
<FormKit label="Auth lockout attempts" type="l-input" name="auth_lockout_attempts"
|
|
110
|
+
hint="The number of failed login attempts before the user is locked out. Default is 5 attempts." />
|
|
92
111
|
|
|
93
|
-
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
<FormKit label="Access token expiration" type="l-input" name="access_token_expire"
|
|
115
|
+
hint="The access token expiration time in seconds. Default is 28800 seconds (8 hours)." />
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
<q-separator />
|
|
121
|
+
|
|
122
|
+
<FormKit label="Copyright name" type="l-input" name="copyright_name" />
|
|
123
|
+
<FormKit label="Copyright year" type="l-input" name="copyright_year" validation="required" />
|
|
94
124
|
|
|
95
125
|
|
|
96
126
|
</FormKit>
|
|
127
|
+
|
|
97
128
|
</l-page>
|
|
98
129
|
</template>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { list, m } from '
|
|
2
|
+
import { list, m } from '#imports'
|
|
3
3
|
import { useRouter } from "vue-router"
|
|
4
|
+
import { ref, computed } from "vue"
|
|
5
|
+
import { Dialog } from 'quasar'
|
|
4
6
|
|
|
5
7
|
let { data: users } = await list("User", null, ["user_id", "username", "name", "roles"]);
|
|
6
8
|
|
|
@@ -31,26 +33,51 @@ let columns = [
|
|
|
31
33
|
|
|
32
34
|
const router = useRouter();
|
|
33
35
|
const onCickView = async (id) => {
|
|
36
|
+
try {
|
|
37
|
+
if (await m("viewAs", { user_id: id })) {
|
|
38
|
+
router.back();
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
Dialog.create({
|
|
42
|
+
title: "Error",
|
|
43
|
+
message: e.message,
|
|
44
|
+
color: "negative",
|
|
45
|
+
ok: true
|
|
46
|
+
})
|
|
34
47
|
|
|
35
|
-
if (await m("viewAs", { user_id: id })) {
|
|
36
|
-
router.back();
|
|
37
48
|
}
|
|
38
|
-
|
|
39
49
|
}
|
|
50
|
+
|
|
51
|
+
const filter = ref("")
|
|
52
|
+
const filtered = computed(() => {
|
|
53
|
+
if (!filter.value) return users
|
|
54
|
+
return users.filter((row) => {
|
|
55
|
+
return Object.values(row).some((val) => {
|
|
56
|
+
return String(val).toLowerCase().includes(filter.value.toLowerCase())
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
})
|
|
40
60
|
</script>
|
|
41
61
|
<template>
|
|
42
62
|
<l-page>
|
|
43
|
-
<
|
|
63
|
+
<p>
|
|
64
|
+
Use this page to view the system as another user. This is useful for testing permissions.
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
<q-table flat :columns="columns" :rows="filtered" :rows-per-page-options="[0]" dense>
|
|
68
|
+
<template v-slot:top-right>
|
|
69
|
+
<q-input outlined dense debounce="300" v-model="filter" placeholder="Search" clearable>
|
|
70
|
+
<template v-slot:append>
|
|
71
|
+
<q-icon name="sym_o_search" />
|
|
72
|
+
</template>
|
|
73
|
+
</q-input>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
44
76
|
<template #body-cell-view="props">
|
|
45
77
|
<q-td :props="props">
|
|
46
|
-
<q-btn
|
|
47
|
-
icon="sym_o_search"></q-btn>
|
|
78
|
+
<q-btn round circle flat dense @click="onCickView(props.row.user_id)" icon="sym_o_search"></q-btn>
|
|
48
79
|
</q-td>
|
|
49
|
-
|
|
50
80
|
</template>
|
|
51
|
-
|
|
52
81
|
</q-table>
|
|
53
|
-
|
|
54
|
-
|
|
55
82
|
</l-page>
|
|
56
83
|
</template>
|
|
@@ -98,7 +98,8 @@ const onDelete = async (name) => {
|
|
|
98
98
|
<l-card>
|
|
99
99
|
<q-splitter v-model="splitterModel" style="height:680px">
|
|
100
100
|
<template #before>
|
|
101
|
-
<q-table :rows="all" flat
|
|
101
|
+
<q-table :rows="all" flat :rows-per-page-options="[0]" :columns="columns" dense
|
|
102
|
+
separator="cell">
|
|
102
103
|
<template #body="props">
|
|
103
104
|
<q-tr :props="props">
|
|
104
105
|
<q-td key="_delete" auto-width>
|
|
@@ -122,9 +123,9 @@ const onDelete = async (name) => {
|
|
|
122
123
|
</template>
|
|
123
124
|
<template #after>
|
|
124
125
|
<l-form :bordered="false" @save="onSave">
|
|
125
|
-
<l-input label="Name" required v-model.trim="obj.name"></l-input>
|
|
126
|
-
<l-input v-for="language in app.languages" :label="language.name"
|
|
127
|
-
|
|
126
|
+
<l-input label="Name" required v-model.trim="obj.name" clearable></l-input>
|
|
127
|
+
<l-input v-for="language in app.languages" :label="language.name" v-model="obj[language.value]"
|
|
128
|
+
clearable></l-input>
|
|
128
129
|
</l-form>
|
|
129
130
|
</template>
|
|
130
131
|
</q-splitter>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { reactive } from 'vue'
|
|
3
|
-
import { getObject } from '
|
|
3
|
+
import { getObject, q } from '#imports'
|
|
4
4
|
const obj = reactive(await getObject(["username", "first_name", "last_name", "email", "phone",
|
|
5
5
|
"addr1", "addr2", "addr3", "join_date", "expiry_date", "status", "language", "default_page"
|
|
6
6
|
]))
|
|
@@ -10,11 +10,16 @@ const options = [
|
|
|
10
10
|
{ label: 'Inactive', value: 1 }
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
]
|
|
13
|
+
const tt = await q({
|
|
14
|
+
app: ["languages"],
|
|
15
|
+
})
|
|
17
16
|
|
|
17
|
+
const languages = tt.app.languages.map((lang) => {
|
|
18
|
+
return {
|
|
19
|
+
label: lang.name,
|
|
20
|
+
value: lang.value,
|
|
21
|
+
};
|
|
22
|
+
})
|
|
18
23
|
|
|
19
24
|
</script>
|
|
20
25
|
|
|
@@ -22,32 +27,28 @@ const languages = [
|
|
|
22
27
|
<l-page>
|
|
23
28
|
<FormKit type="l-form" :value="obj">
|
|
24
29
|
<l-row>
|
|
25
|
-
<l-col md="6"
|
|
30
|
+
<l-col md="6">
|
|
26
31
|
<FormKit type="l-input" label="Username" name="username" validation="required" />
|
|
27
32
|
<FormKit type="l-input" label="First name" name="first_name" validation="required" />
|
|
28
33
|
<FormKit type="l-input" label="Last name" name="last_name" />
|
|
29
34
|
<FormKit type="l-input" label="Email" name="email" validation="required|email" />
|
|
30
35
|
</l-col>
|
|
31
36
|
|
|
32
|
-
<l-col md="6"
|
|
37
|
+
<l-col md="6">
|
|
33
38
|
<FormKit type="l-input" label="Phone" name="phone" />
|
|
34
39
|
<FormKit type="l-input" label="Address1" name="addr1" />
|
|
35
40
|
<FormKit type="l-input" label="Address2" name="addr2" />
|
|
36
41
|
<FormKit type="l-input" label="Address3" name="addr3" />
|
|
37
42
|
</l-col>
|
|
38
43
|
|
|
39
|
-
<l-col
|
|
44
|
+
<l-col>
|
|
40
45
|
<FormKit type="l-date-picker" label="Join date" name="join_date" validation="required" />
|
|
41
46
|
<FormKit type="l-date-picker" label="Expiry date" name="expiry_date" />
|
|
42
47
|
<FormKit type="l-select" label="Status" name="status" :options="options" validation="required" />
|
|
43
48
|
<FormKit type="l-select" label="Language" name="language" :options="languages" validation="required" />
|
|
44
49
|
<FormKit type="l-input" label="Default page" name="default_page" />
|
|
45
|
-
|
|
46
50
|
</l-col>
|
|
47
51
|
</l-row>
|
|
48
|
-
|
|
49
52
|
</FormKit>
|
|
50
|
-
|
|
51
|
-
|
|
52
53
|
</l-page>
|
|
53
54
|
</template>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { reactive } from "vue"
|
|
3
3
|
import { useRouter } from "vue-router";
|
|
4
|
-
import {
|
|
5
|
-
import { q } from '../../'
|
|
4
|
+
import { q } from '#imports'
|
|
6
5
|
const router = useRouter()
|
|
7
6
|
const obj = reactive({
|
|
8
7
|
username: null,
|
|
@@ -43,10 +42,19 @@ const options = [
|
|
|
43
42
|
{ label: 'Inactive', value: 1 }
|
|
44
43
|
];
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
|
|
46
|
+
const tt = await q({
|
|
47
|
+
app: ["languages"],
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const languages = tt.app.languages.map((lang) => {
|
|
51
|
+
return {
|
|
52
|
+
label: lang.name,
|
|
53
|
+
value: lang.value,
|
|
54
|
+
};
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
|
|
50
58
|
|
|
51
59
|
|
|
52
60
|
</script>
|
|
@@ -54,23 +62,23 @@ const languages = [
|
|
|
54
62
|
<l-page>
|
|
55
63
|
<FormKit type="l-form" :value="obj">
|
|
56
64
|
<l-row>
|
|
57
|
-
<l-col md="6"
|
|
65
|
+
<l-col md="6">
|
|
58
66
|
<FormKit type="l-input" label="Username" name="username" validation="required" />
|
|
59
67
|
<FormKit type="l-input" label="Password" name="password" :validation="system.passwordPolicy"
|
|
60
|
-
input-type="password" />
|
|
68
|
+
input-type="password" autocomplete="new-password" />
|
|
61
69
|
<FormKit type="l-input" label="First name" name="first_name" validation="required" />
|
|
62
70
|
<FormKit type="l-input" label="Last name" name="last_name" />
|
|
63
71
|
<FormKit type="l-input" label="Email" name="email" validation="required|email" />
|
|
64
72
|
</l-col>
|
|
65
73
|
|
|
66
|
-
<l-col md="6"
|
|
74
|
+
<l-col md="6">
|
|
67
75
|
<FormKit type="l-input" label="Phone" name="phone" />
|
|
68
76
|
<FormKit type="l-input" label="Address1" name="addr1" />
|
|
69
77
|
<FormKit type="l-input" label="Address2" name="addr2" />
|
|
70
78
|
<FormKit type="l-input" label="Address3" name="addr3" />
|
|
71
79
|
</l-col>
|
|
72
80
|
|
|
73
|
-
<l-col
|
|
81
|
+
<l-col>
|
|
74
82
|
<FormKit type="l-date-picker" label="Join date" name="join_date" validation="required" />
|
|
75
83
|
<FormKit type="l-date-picker" label="Expiry date" name="expiry_date" />
|
|
76
84
|
<FormKit type="l-select" label="Status" name="status" :options="options" validation="required" />
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { Dialog } from "quasar";
|
|
3
3
|
import { ref } from "vue"
|
|
4
|
-
import { q, m, getCurrentUser } from '
|
|
4
|
+
import { q, m, getCurrentUser } from '#imports'
|
|
5
5
|
const app = await q("app", ["hasBioAuth"]);
|
|
6
6
|
|
|
7
7
|
import { webauthnRegister } from "@hostlink/light"
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
const data = ref(await q("listWebAuthn", ["uuid", "ip", "user_agent", "createdTime"]));
|
|
11
10
|
|
|
12
11
|
const register = async () => {
|
|
@@ -16,14 +15,11 @@ const register = async () => {
|
|
|
16
15
|
const user = await getCurrentUser();
|
|
17
16
|
localStorage.setItem("username", user.username);
|
|
18
17
|
} catch (e) {
|
|
19
|
-
|
|
20
18
|
Dialog.create({
|
|
21
19
|
title: "Error",
|
|
22
20
|
message: e.message,
|
|
23
|
-
ok: "OK"
|
|
21
|
+
ok: "OK"
|
|
24
22
|
})
|
|
25
|
-
|
|
26
|
-
|
|
27
23
|
}
|
|
28
24
|
}
|
|
29
25
|
|
|
@@ -55,6 +51,7 @@ const deleteItem = async (uuid) => {
|
|
|
55
51
|
//confirm
|
|
56
52
|
Dialog.create({
|
|
57
53
|
title: "Delete",
|
|
54
|
+
color: "negative",
|
|
58
55
|
message: "Are you sure you want to delete this item?",
|
|
59
56
|
ok: "Yes",
|
|
60
57
|
cancel: "No",
|
|
@@ -78,7 +75,7 @@ const deleteItem = async (uuid) => {
|
|
|
78
75
|
<l-btn label="Register" @click="register" icon="sym_o_add"></l-btn>
|
|
79
76
|
</q-card-section>
|
|
80
77
|
|
|
81
|
-
<q-table :rows="data" :columns="columns">
|
|
78
|
+
<q-table :rows="data" :columns="columns" dense :rows-per-page-options="[0]">
|
|
82
79
|
<template #body-cell-action="props">
|
|
83
80
|
<q-td :props="props" auto-width>
|
|
84
81
|
<q-btn @click="deleteItem(props.row.uuid)" icon="sym_o_delete" round flat dense></q-btn>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
|
|
3
|
-
import { Dialog } from 'quasar'
|
|
2
|
+
import { useI18n } from 'vue-i18n'
|
|
3
|
+
import { Dialog } from 'quasar'
|
|
4
4
|
import { reset } from "@formkit/core"
|
|
5
5
|
import { updatePassword } from "@hostlink/light"
|
|
6
|
-
|
|
6
|
+
import { computed } from 'vue'
|
|
7
7
|
import { q } from "#imports"
|
|
8
8
|
|
|
9
|
+
const { t } = useI18n()
|
|
10
|
+
|
|
9
11
|
const onSubmit = async (data, form) => {
|
|
10
12
|
if (await updatePassword(data.old_password, data.new_password)) {
|
|
11
13
|
reset(form);
|
|
@@ -21,25 +23,41 @@ const onSubmit = async (data, form) => {
|
|
|
21
23
|
message: "Old password is incorrect",
|
|
22
24
|
ok: "OK"
|
|
23
25
|
})
|
|
24
|
-
|
|
25
|
-
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const system = await q("system", ["passwordPolicy"])
|
|
30
30
|
|
|
31
|
+
const policies = computed(() => {
|
|
32
|
+
return system.passwordPolicy.split("|").map((policy) => {
|
|
33
|
+
let name = policy.split(":")[0]
|
|
34
|
+
|
|
35
|
+
if (name == "length") {
|
|
36
|
+
return t('Must contain at least {0} characters', [policy.split(":")[1]]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return t(name);
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
31
43
|
</script>
|
|
32
44
|
<template>
|
|
33
45
|
<FormKit type="l-form" :bordered="false" @submit="onSubmit">
|
|
34
|
-
<FormKit type="l-input" label="Old password" name="old_password" inputType="password" validation="required"
|
|
46
|
+
<FormKit type="l-input" label="Old password" name="old_password" inputType="password" validation="required"
|
|
47
|
+
autocomplete="current-password" />
|
|
35
48
|
<FormKit type="l-input" label="New password" name="new_password" inputType="password"
|
|
36
|
-
:validation="system.passwordPolicy" />
|
|
49
|
+
:validation="system.passwordPolicy" autocomplete="new-password" />
|
|
37
50
|
<FormKit type="l-input" label="Confirm password" name="new_password_confirm" inputType="password"
|
|
38
51
|
validation="required|confirm" />
|
|
39
52
|
</FormKit>
|
|
40
53
|
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
<q-card flat>
|
|
55
|
+
<q-card-section>
|
|
56
|
+
<div>{{ $t('Password policy') }}</div>
|
|
57
|
+
<ul>
|
|
58
|
+
<li v-for="policy in policies" :key="policy">{{ policy }}</li>
|
|
59
|
+
</ul>
|
|
60
|
+
|
|
61
|
+
</q-card-section>
|
|
62
|
+
</q-card>
|
|
45
63
|
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, reactive } from "vue"
|
|
3
|
-
import { q, m, notify } from '
|
|
3
|
+
import { q, m, notify } from '#imports'
|
|
4
4
|
|
|
5
5
|
const my = await q("my", ["twoFactorEnabled"])
|
|
6
6
|
const my2FA = await m("my2FA", [])
|
|
@@ -36,24 +36,30 @@ if (my.twoFactorEnabled) {
|
|
|
36
36
|
</p>
|
|
37
37
|
<p>
|
|
38
38
|
For Android user, install
|
|
39
|
-
<
|
|
40
|
-
href="https://play.google.com/store/apps/details?id=com.azure.authenticator">Authenticator</
|
|
39
|
+
<a type="primary" target="_blank"
|
|
40
|
+
href="https://play.google.com/store/apps/details?id=com.azure.authenticator">Authenticator</a>
|
|
41
41
|
</p>
|
|
42
42
|
|
|
43
43
|
<p>
|
|
44
44
|
For iOS user, install
|
|
45
|
-
<
|
|
46
|
-
href="https://apps.apple.com/us/app/microsoft-authenticator/id983156458">Authenticator</
|
|
45
|
+
<a type="primary" target="_blank"
|
|
46
|
+
href="https://apps.apple.com/us/app/microsoft-authenticator/id983156458">Authenticator</a>
|
|
47
47
|
</p>
|
|
48
48
|
</div>
|
|
49
49
|
<q-img :src="my2FA.image" width="250px" />
|
|
50
|
+
<p>
|
|
51
|
+
Secret : {{ my2FA.secret }}
|
|
52
|
+
</p>
|
|
53
|
+
|
|
50
54
|
<l-input v-model="obj.code" label="Code"
|
|
51
55
|
hint="Please scan the QR code with your authenticator app, and enter the code" required />
|
|
52
56
|
</l-form>
|
|
53
57
|
<div v-else>
|
|
54
58
|
<q-card-section>
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
<div>
|
|
60
|
+
Your 2FA is enabled
|
|
61
|
+
</div>
|
|
62
|
+
<l-btn @click="show = true" label="Reset your 2FA" icon="sym_o_refresh" />
|
|
57
63
|
</q-card-section>
|
|
58
64
|
|
|
59
65
|
</div>
|