@hostlink/nuxt-light 1.0.2 → 1.0.4

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 (29) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/l-app-main.vue +8 -3
  3. package/dist/runtime/components/l-card.vue +1 -1
  4. package/dist/runtime/components/l-col.vue +1 -1
  5. package/dist/runtime/components/l-customizer.vue +3 -4
  6. package/dist/runtime/components/l-file-manager.vue +4 -4
  7. package/dist/runtime/components/l-input-xlsx.vue +8 -3
  8. package/dist/runtime/components/l-row.vue +4 -2
  9. package/dist/runtime/components/l-table.vue +5 -4
  10. package/dist/runtime/formkit/Form.vue +2 -2
  11. package/dist/runtime/locales/en.json +5 -12
  12. package/dist/runtime/locales/zh-hk.json +15 -2
  13. package/dist/runtime/pages/Permission/all.vue +1 -1
  14. package/dist/runtime/pages/Role/add.vue +1 -5
  15. package/dist/runtime/pages/Role/index.vue +21 -5
  16. package/dist/runtime/pages/System/database/backup.vue +25 -2
  17. package/dist/runtime/pages/System/database/table.vue +4 -2
  18. package/dist/runtime/pages/System/index.vue +3 -2
  19. package/dist/runtime/pages/System/package.vue +20 -1
  20. package/dist/runtime/pages/System/setting.vue +33 -4
  21. package/dist/runtime/pages/System/test.vue +1 -0
  22. package/dist/runtime/pages/System/view_as.vue +38 -11
  23. package/dist/runtime/pages/Translate/index.vue +5 -4
  24. package/dist/runtime/pages/User/_user_id/edit.vue +13 -12
  25. package/dist/runtime/pages/User/add.vue +18 -10
  26. package/dist/runtime/pages/User/setting/bio-auth.vue +4 -7
  27. package/dist/runtime/pages/User/setting/password.vue +29 -11
  28. package/dist/runtime/pages/User/setting/two-factor-auth.vue +13 -7
  29. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "light",
3
3
  "configKey": "light",
4
- "version": "1.0.2"
4
+ "version": "1.0.4"
5
5
  }
@@ -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", { i18nMessages: ["name", "value"] }],
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 v-if="tt.system.devMode">
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 2023 HostLink(HK). Build {{ light.getVersion() }}
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">
@@ -7,7 +7,7 @@ const props = defineProps({
7
7
  },
8
8
  gutter: {
9
9
  type: String,
10
- default: "none"
10
+ default: "md"
11
11
  }
12
12
  })
13
13
 
@@ -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 </q-item-label>
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') }} </q-item-section>
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)">
@@ -1,7 +1,7 @@
1
1
 
2
2
  <script setup>
3
3
  import * as XLSX from "xlsx";
4
- import { computed, ref, useAttrs, useSlots } from "vue";
4
+ import { ref, useAttrs, } from "vue";
5
5
  import { useLight } from "#imports"
6
6
 
7
7
  const emit = defineEmits(["update:modelValue"]);
@@ -50,7 +50,7 @@ const showView = ref(false);
50
50
  </script>
51
51
 
52
52
  <template>
53
- <q-field v-bind="attrs" :loading="loading" :model-value="localData" @update:model-value="onClear" :clearable="hasFile">
53
+ <q-field v-bind="attrs" :loading="loading" :model-value="localData" @update:model-value="onClear">
54
54
  <template v-slot:control>
55
55
  <template v-if="!hasFile">
56
56
  <input type="file" accept=".xlsx" @change="onChange" ref="file" />
@@ -66,10 +66,15 @@ const showView = ref(false);
66
66
  <q-icon name="sym_o_table_view" @click="showView = true" class="cursor-pointer" />
67
67
  </template>
68
68
 
69
+
70
+ <template v-slot:append v-if="hasFile">
71
+ <q-icon name="cancel" @click="onClear" class="cursor-pointer" />
72
+ </template>
73
+
69
74
  <q-dialog v-model="showView" full-width>
70
75
  <q-card>
71
76
  <q-card-section>
72
- <q-table :rows="localData" />
77
+ <q-table :rows="localData" dense separator="cell" flat bordered/>
73
78
  </q-card-section>
74
79
  </q-card>
75
80
  </q-dialog>
@@ -1,5 +1,7 @@
1
1
  <template>
2
- <div class="row q-col-gutter-md">
3
- <slot></slot>
2
+ <div>
3
+ <div class="row q-col-gutter-md">
4
+ <slot></slot>
5
+ </div>
4
6
  </div>
5
7
  </template>
@@ -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 v-model.number="filters[col.name]"
622
- @keydown.enter.prevent="onFilters" @clear="onFilters" mask="##########"></q-input>
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-col-gutter-${gutter}`">
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 color="primary" icon="sym_o_check" label="Submit" @click="onSubmit" :disabled="!context.state.dirty"
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
- "SystemBackup": "System backup",
3
- "SystemValue": "System value",
4
- "UserGroup": "User group",
5
- "UserLog": "User log",
6
- "FileManager": "File manager",
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, reactive } from "vue"
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
- comming soon
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" expand-separator>
18
+ <q-expansion-item :label="table.name" v-for="table in database.table">
19
19
  <div class=" q-ma-sm">
20
- <q-table dense :rows="table.columns" :rows-per-page-options="[0]" hide-pagination flat bordered></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="system.package" :rows-per-page-options="[0]" hide-pagination flat bordered></q-table>
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
- router.go(-1);
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,29 @@ 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 />
92
105
 
93
- </FormKit>
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
+ validation="required" />
109
+
110
+ <FormKit label="Auth lockout attempts" type="l-input" name="auth_lockout_attempts"
111
+ hint="The number of failed login attempts before the user is locked out. Default is 5 attempts."
112
+ validation="required" />
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
+ validation="required" />
117
+
118
+ <q-separator />
119
+
120
+ <FormKit label="Copyright name" type="l-input" name="copyright_name" validation="required" />
121
+ <FormKit label="Copyright year" type="l-input" name="copyright_year" validation="required" />
94
122
 
95
123
 
96
124
  </FormKit>
125
+
97
126
  </l-page>
98
127
  </template>
@@ -13,6 +13,7 @@ const onSave = async () => {
13
13
  })
14
14
 
15
15
  }
16
+
16
17
  </script>
17
18
  <template>
18
19
  <l-page>
@@ -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
- <q-table flat :columns="columns" :rows="users" :rows-per-page-options="[0]">
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 rounded outline color="primary" @click="onCickView(props.row.user_id)" label="view"
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 hide-bottom :rows-per-page-options="[0]" :columns="columns">
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
- v-model="obj[language.value]"></l-input>
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 languages = [
14
- { label: 'English', value: 'en' },
15
- { label: '中文', value: 'zh-hk' }
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" gutter="md">
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" gutter="md">
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 gutter="md">
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 { model } from "@hostlink/light";
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
- const languages = [
47
- { label: 'English', value: 'en' },
48
- { label: '中文', value: 'zh-hk' }
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" gutter="md">
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" gutter="md">
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 gutter="md">
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
- <div>
42
- <div>Password policy</div>
43
- <div>{{ system.passwordPolicy }}</div>
44
- </div>
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
- <el-link type="primary" target="_blank"
40
- href="https://play.google.com/store/apps/details?id=com.azure.authenticator">Authenticator</el-link>
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
- <el-link type="primary" target="_blank"
46
- href="https://apps.apple.com/us/app/microsoft-authenticator/id983156458">Authenticator</el-link>
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
- Your 2FA is enabled
56
- <l-btn @click="show = true" label="Reset Your 2FA" icon="sym_o_refresh" />
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostlink/nuxt-light",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "HostLink Nuxt Light Framework",
5
5
  "repository": "@hostlink/nuxt-light",
6
6
  "license": "MIT",