adminforth 1.2.99 → 1.2.100

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.
@@ -67,73 +67,64 @@ export default class AdminForthRestAPI {
67
67
  let adminUser: AdminUser;
68
68
  let toReturn: { ok: boolean, redirectTo?: string, allowedLogin:boolean } = { ok: true, allowedLogin:true};
69
69
 
70
- let token;
71
- if (this.adminforth.config.rootUser
72
- && username === this.adminforth.config.rootUser.username
73
- && password === this.adminforth.config.rootUser.password
74
- ) {
75
- this.adminforth.auth.setAuthCookie({ response, username, pk: null });
76
- adminUser = { isRoot: true, dbUser: null, pk: null, username: this.adminforth.config.rootUser.username};
77
- } else {
78
- // get resource from db
79
- if (!this.adminforth.config.auth) {
80
- throw new Error('No config.auth defined');
81
- }
82
- const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.resourceId);
83
- // if there is no passwordHashField, in columns, add it, with backendOnly and showIn: []
84
- if (!userResource.dataSourceColumns.find((col) => col.name === this.adminforth.config.auth.passwordHashField)) {
85
- userResource.dataSourceColumns.push({
86
- name: this.adminforth.config.auth.passwordHashField,
87
- backendOnly: true,
88
- showIn: [],
89
- type: AdminForthDataTypes.STRING,
90
- });
91
- console.log('Adding passwordHashField to userResource', userResource)
92
- }
93
-
94
- const userRecord = (
95
- await this.adminforth.connectors[userResource.dataSource].getData({
96
- resource: userResource,
97
- filters: [
98
- { field: this.adminforth.config.auth.usernameField, operator: AdminForthFilterOperators.EQ, value: username },
99
- ],
100
- limit: 1,
101
- offset: 0,
102
- sort: [],
103
- })
104
- ).data?.[0];
105
-
106
- if (!userRecord) {
107
- return { error: 'User not found' };
108
- }
109
-
110
- const passwordHash = userRecord[this.adminforth.config.auth.passwordHashField];
111
- const valid = await AdminForthAuth.verifyPassword(password, passwordHash);
112
- if (valid) {
113
- adminUser = {
114
- isRoot: false, dbUser: userRecord,
115
- pk: userRecord[userResource.columns.find((col) => col.primaryKey).name],
116
- username,
117
- };
118
- const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation as (BeforeLoginConfirmationFunction[] | undefined);
119
- if (beforeLoginConfirmation?.length){
120
- for (const hook of beforeLoginConfirmation) {
121
- const resp = await hook({ adminUser, response });
122
-
123
- if (resp?.body?.redirectTo) {
124
- toReturn = {ok:resp.ok, redirectTo:resp?.body?.redirectTo, allowedLogin:resp?.body?.allowedLogin};
125
- break;
126
- }
70
+ // get resource from db
71
+ if (!this.adminforth.config.auth) {
72
+ throw new Error('No config.auth defined we need it to find user, please follow the docs');
73
+ }
74
+ const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId);
75
+ // if there is no passwordHashField, in columns, add it, with backendOnly and showIn: []
76
+ if (!userResource.dataSourceColumns.find((col) => col.name === this.adminforth.config.auth.passwordHashField)) {
77
+ userResource.dataSourceColumns.push({
78
+ name: this.adminforth.config.auth.passwordHashField,
79
+ backendOnly: true,
80
+ showIn: [],
81
+ type: AdminForthDataTypes.STRING,
82
+ });
83
+ console.log('Adding passwordHashField to userResource', userResource)
84
+ }
85
+
86
+ const userRecord = (
87
+ await this.adminforth.connectors[userResource.dataSource].getData({
88
+ resource: userResource,
89
+ filters: [
90
+ { field: this.adminforth.config.auth.usernameField, operator: AdminForthFilterOperators.EQ, value: username },
91
+ ],
92
+ limit: 1,
93
+ offset: 0,
94
+ sort: [],
95
+ })
96
+ ).data?.[0];
97
+
98
+ if (!userRecord) {
99
+ return { error: 'User not found' };
100
+ }
101
+
102
+ const passwordHash = userRecord[this.adminforth.config.auth.passwordHashField];
103
+ const valid = await AdminForthAuth.verifyPassword(password, passwordHash);
104
+ if (valid) {
105
+ adminUser = {
106
+ dbUser: userRecord,
107
+ pk: userRecord[userResource.columns.find((col) => col.primaryKey).name],
108
+ username,
109
+ };
110
+ const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation as (BeforeLoginConfirmationFunction[] | undefined);
111
+ if (beforeLoginConfirmation?.length){
112
+ for (const hook of beforeLoginConfirmation) {
113
+ const resp = await hook({ adminUser, response });
114
+
115
+ if (resp?.body?.redirectTo) {
116
+ toReturn = {ok:resp.ok, redirectTo:resp?.body?.redirectTo, allowedLogin:resp?.body?.allowedLogin};
117
+ break;
127
118
  }
128
119
  }
129
- if (toReturn.allowedLogin){
130
- this.adminforth.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
131
- }
132
- } else {
133
- return { error: INVALID_MESSAGE };
134
120
  }
135
-
121
+ if (toReturn.allowedLogin){
122
+ this.adminforth.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
123
+ }
124
+ } else {
125
+ return { error: INVALID_MESSAGE };
136
126
  }
127
+
137
128
 
138
129
  return toReturn;
139
130
  }
@@ -168,13 +159,14 @@ export default class AdminForthRestAPI {
168
159
  throw new Error('No config.auth defined');
169
160
  }
170
161
  const usernameField = this.adminforth.config.auth.usernameField;
171
- const resource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.resourceId);
162
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId);
172
163
  const usernameColumn = resource.columns.find((col) => col.name === usernameField);
173
164
 
174
165
  return {
175
166
  brandName: this.adminforth.config.customization.brandName,
176
167
  usernameFieldName: usernameColumn.label,
177
168
  loginBackgroundImage: this.adminforth.config.auth.loginBackgroundImage,
169
+ loginBackgroundPosition: this.adminforth.config.auth.loginBackgroundPosition,
178
170
  title: this.adminforth.config.customization?.title,
179
171
  demoCredentials: this.adminforth.config.auth.demoCredentials,
180
172
  loginPromptHTML: this.adminforth.config.auth.loginPromptHTML,
@@ -188,14 +180,10 @@ export default class AdminForthRestAPI {
188
180
  handler: async ({input, adminUser, cookies}) => {
189
181
  let username = ''
190
182
  let userFullName = ''
191
- if (adminUser.isRoot) {
192
- // isRoot can be in JWT token still when rootUser deleted, so we check for rootUser?"
193
- username = this.adminforth.config.rootUser?.username || 'RootUser';
194
- } else {
195
- const dbUser = adminUser.dbUser;
196
- username = dbUser[this.adminforth.config.auth.usernameField];
197
- userFullName =dbUser[this.adminforth.config.auth.userFullNameField];
198
- }
183
+
184
+ const dbUser = adminUser.dbUser;
185
+ username = dbUser[this.adminforth.config.auth.usernameField];
186
+ userFullName =dbUser[this.adminforth.config.auth.userFullNameField];
199
187
 
200
188
  const userData = {
201
189
  [this.adminforth.config.auth.usernameField]: username,
@@ -257,6 +245,7 @@ export default class AdminForthRestAPI {
257
245
  menu: newMenu,
258
246
  config: {
259
247
  brandName: this.adminforth.config.customization.brandName,
248
+ showBrandNameInSidebar: this.adminforth.config.customization.showBrandNameInSidebar,
260
249
  brandLogo: this.adminforth.config.customization.brandLogo,
261
250
  datesFormat: this.adminforth.config.customization.datesFormat,
262
251
  deleteConfirmation: this.adminforth.config.deleteConfirmation,
@@ -272,8 +261,6 @@ export default class AdminForthRestAPI {
272
261
  },
273
262
  });
274
263
 
275
-
276
-
277
264
  function checkAccess(action: AllowedActionsEnum, allowedActions: AllowedActions): { allowed: boolean, error?: string } {
278
265
  const allowed = (allowedActions[action] as boolean | string | undefined);
279
266
  if (allowed !== true) {
@@ -331,6 +318,7 @@ export default class AdminForthRestAPI {
331
318
  method: 'POST',
332
319
  path: '/get_resource_data',
333
320
  handler: async ({ body, adminUser }) => {
321
+
334
322
  const { resourceId, source } = body;
335
323
  if (['show', 'list'].includes(source) === false) {
336
324
  return { error: 'Invalid source, should be list or show' };
@@ -394,6 +382,7 @@ export default class AdminForthRestAPI {
394
382
  offset,
395
383
  filters,
396
384
  sort,
385
+ getTotals: true,
397
386
  });
398
387
  // for foreign keys, add references
399
388
  await Promise.all(
@@ -581,12 +570,13 @@ export default class AdminForthRestAPI {
581
570
 
582
571
  const response = await this.adminforth.createResourceRecord({ resource, record, adminUser });
583
572
  if (response.error) {
584
- return { error: response.error };
573
+ return { error: response.error, ok: false };
585
574
  }
586
575
  const connector = this.adminforth.connectors[resource.dataSource];
587
576
 
588
577
  return {
589
- newRecordId: record[connector.getPrimaryKey(resource)]
578
+ newRecordId: response.createdRecord[connector.getPrimaryKey(resource)],
579
+ ok: true
590
580
  }
591
581
  }
592
582
  });
@@ -617,7 +607,12 @@ export default class AdminForthRestAPI {
617
607
 
618
608
  // execute hook if needed
619
609
  for (const hook of listify(resource.hooks?.edit?.beforeSave as BeforeSaveFunction[])) {
620
- const resp = await hook({ resource, record, adminUser });
610
+ const resp = await hook({
611
+ recordId,
612
+ resource,
613
+ record,
614
+ adminUser
615
+ });
621
616
  if (!resp || (!resp.ok && !resp.error)) {
622
617
  throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
623
618
  }
@@ -647,7 +642,13 @@ export default class AdminForthRestAPI {
647
642
 
648
643
  // execute hook if needed
649
644
  for (const hook of listify(resource.hooks?.edit?.afterSave as AfterSaveFunction[])) {
650
- const resp = await hook({ resource, record, adminUser, oldRecord });
645
+ const resp = await hook({
646
+ resource,
647
+ record,
648
+ adminUser,
649
+ oldRecord,
650
+ recordId,
651
+ });
651
652
  if (!resp || (!resp.ok && !resp.error)) {
652
653
  throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
653
654
  }
@@ -687,7 +688,12 @@ export default class AdminForthRestAPI {
687
688
 
688
689
  // execute hook if needed
689
690
  for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
690
- const resp = await hook({ resource, record, adminUser });
691
+ const resp = await hook({
692
+ resource,
693
+ record,
694
+ adminUser,
695
+ recordId: body['primaryKey']
696
+ });
691
697
  if (!resp || (!resp.ok && !resp.error)) {
692
698
  throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
693
699
  }
@@ -702,7 +708,12 @@ export default class AdminForthRestAPI {
702
708
 
703
709
  // execute hook if needed
704
710
  for (const hook of listify(resource.hooks?.delete?.afterSave as BeforeSaveFunction[])) {
705
- const resp = await hook({ resource, record, adminUser });
711
+ const resp = await hook({
712
+ resource,
713
+ record,
714
+ adminUser,
715
+ recordId: body['primaryKey']
716
+ });
706
717
  if (!resp || (!resp.ok && !resp.error)) {
707
718
  throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
708
719
  }
package/modules/utils.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import path from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import fs from 'fs';
4
+ import Fuse from 'fuse.js';
4
5
  // @ts-ignore-next-line
5
6
 
6
7
 
@@ -335,3 +336,22 @@ export function inverseRGBA(rgba) {
335
336
  return brightness > 128 ? 'rgba(0,0,0,1)' : 'rgba(255,255,255,1)';
336
337
  }
337
338
 
339
+
340
+ export function suggestIfTypo(names: string[], name: string): string {
341
+ if (!name) {
342
+ return null;
343
+ }
344
+ const options = {
345
+ includeScore: true, // Includes score in the results to see how close matches are
346
+ threshold: 0.3, // Defines the fuzziness (lower values mean stricter matches)
347
+ };
348
+
349
+ const fuse = new Fuse(names.filter(
350
+ (n) => !!n
351
+ ), options);
352
+ // Search for a resource
353
+ const result = fuse.search(name);
354
+ if (result.length > 0) {
355
+ return result[0].item;
356
+ }
357
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.2.99",
3
+ "version": "1.2.100",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,9 +20,10 @@
20
20
  "@clickhouse/client": "^1.4.0",
21
21
  "better-sqlite3": "^10.0.0",
22
22
  "dayjs": "^1.11.11",
23
- "express": "^4.19.2",
23
+ "express": "^4.21.0",
24
24
  "filewatcher": "^3.0.1",
25
25
  "fs-extra": "^11.2.0",
26
+ "fuse.js": "^7.0.0",
26
27
  "jsonwebtoken": "^9.0.2",
27
28
  "mongodb": "6.6",
28
29
  "node-fetch": "^3.3.2",
package/spa/src/App.vue CHANGED
@@ -18,7 +18,12 @@
18
18
  </div>
19
19
  <div class="flex items-center">
20
20
  <div class="flex items-center ms-3 ">
21
+ <span @click="toggleTheme" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black hover:bg-lightHtml dark:text-darkSidebarTextHover dark:hover:bg-darkHtml dark:hover:text-darkSidebarTextActive" role="menuitem">
22
+ <IconMoonSolid class="w-5 h-5 text-blue-300" v-if="theme !== 'dark'" />
23
+ <IconSunSolid class="w-5 h-5 text-yellow-300" v-else />
24
+ </span>
21
25
  <div>
26
+
22
27
  <button type="button" class="flex text-sm bg- rounded-full focus:ring-4 focus:ring-lightSidebarDevider dark:focus:ring-darkSidebarDevider dark:bg-" aria-expanded="false" data-dropdown-toggle="dropdown-user">
23
28
  <span class="sr-only">Open user menu</span>
24
29
  <svg class="w-8 h-8 text-lightNavbarIcons dark:text-darkNavbarIcons" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
@@ -36,14 +41,9 @@
36
41
  </p>
37
42
  </div>
38
43
  <ul class="py-1" role="none">
39
- <li>
40
- <span @click="toggleTheme" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black hover:bg-lightHtml dark:text-darkSidebarTextHover dark:hover:bg-darkHtml dark:hover:text-darkSidebarTextActive" role="menuitem">
41
- {{ theme === 'dark' ? 'Light' : 'Dark' }}
42
- <IconMoonSolid class="w-5 h-5 text-blue-300" v-if="theme !== 'dark'" />
43
- <IconSunSolid class="w-5 h-5 text-yellow-300" v-else />
44
- mode
45
- </span>
46
- </li>
44
+ <!-- <li>
45
+
46
+ </li> -->
47
47
  <li>
48
48
  <button @click="logout" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black hover:bg-html dark:text-darkSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextActive w-full" role="menuitem">Sign out</button>
49
49
  </li>
@@ -68,7 +68,10 @@
68
68
  <div class="h-full px-3 pb-4 overflow-y-auto bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder dark:border-darkSidebarBorder">
69
69
  <div class="flex ms-2 md:me-24 m-4 ">
70
70
  <img :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="h-8 me-3" />
71
- <span class="self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText">
71
+ <span
72
+ v-if="coreStore.config?.showBrandNameInSidebar"
73
+ class="self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText"
74
+ >
72
75
  {{ coreStore.config?.brandName }}
73
76
  </span>
74
77
  </div>
@@ -115,7 +115,7 @@
115
115
  </template>
116
116
 
117
117
  <script setup>
118
- import { watch, computed, ref, onMounted } from 'vue'
118
+ import { watch, computed } from 'vue'
119
119
  import Dropdown from '@/components/Dropdown.vue';
120
120
  import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
121
121
  import { callAdminForthApi } from '@/utils';
@@ -130,7 +130,6 @@
130
130
  </button>
131
131
  </template>
132
132
  <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
133
-
134
133
  <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
135
134
 
136
135
  </td>
@@ -154,10 +153,10 @@ import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@ic
154
153
  import { computedAsync } from '@vueuse/core';
155
154
  import { initFlowbite } from 'flowbite';
156
155
  import { computed, onMounted, ref, watch } from 'vue';
157
- import { useRouter } from 'vue-router';
156
+ import { useRouter, useRoute } from 'vue-router';
158
157
 
159
158
  const router = useRouter();
160
-
159
+ const route = useRoute();
161
160
  const props = defineProps({
162
161
  loading: Boolean,
163
162
  resource: Object,
@@ -168,7 +167,7 @@ const props = defineProps({
168
167
 
169
168
  const unmasked = ref({});
170
169
 
171
- const mode = computed(() => props.record && Object.keys(props.record).length ? 'edit' : 'create');
170
+ const mode = computed(() => route.name === 'resource-create' ? 'create' : 'edit');
172
171
 
173
172
  const emit = defineEmits(['update:record', 'update:isValid']);
174
173
 
@@ -245,6 +244,10 @@ const setCurrentValue = (key, value) => {
245
244
  } else {
246
245
  currentValues.value[key] = value;
247
246
  }
247
+ if (['text', 'richtext', 'string'].includes(col.type) && col.enforceLowerCase) {
248
+ currentValues.value[key] = currentValues.value[key].toLowerCase();
249
+ }
250
+
248
251
  currentValues.value = { ...currentValues.value };
249
252
  console.log('3️⃣ setCurrentValue', key, value);
250
253
  emit('update:record', currentValues.value);