adminforth 1.3.22 → 1.3.24

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/auth.ts CHANGED
@@ -30,10 +30,15 @@ class AdminForthAuth {
30
30
  response.setHeader('Set-Cookie', `adminforth_jwt=; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=Thu, 01 Jan 1970 00:00:00 GMT`);
31
31
  }
32
32
 
33
- setAuthCookie({ response, username, pk}: {
34
- response: any, username: string, pk: string | null
33
+ setAuthCookie({ expireInDays, response, username, pk}: {
34
+ expireInDays?: number,
35
+ response: any,
36
+ username: string,
37
+ pk: string | null
35
38
  }) {
36
- const token = this.issueJWT({ username, pk, }, 'auth');
39
+ const expiresIn: string = expireInDays ? `${expireInDays}d` : (process.env.ADMINFORTH_AUTH_EXPIRESIN || '24h');
40
+
41
+ const token = this.issueJWT({ username, pk}, 'auth', expiresIn);
37
42
  response.setHeader('Set-Cookie', `adminforth_jwt=${token}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict`);
38
43
  }
39
44
 
@@ -47,11 +52,8 @@ class AdminForthAuth {
47
52
  const {name,value,expiry,httpOnly} = payload
48
53
  response.setHeader('Set-Cookie', `adminforth_${name}=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + expiry).toUTCString() } `);
49
54
  }
50
-
51
55
 
52
-
53
-
54
- issueJWT(payload: Object, type: string) {
56
+ issueJWT(payload: Object, type: string, expiresIn: string = '24h'): string {
55
57
  // read ADMINFORH_SECRET from environment if not drop error
56
58
  const secret = process.env.ADMINFORTH_SECRET;
57
59
  if (!secret) {
@@ -59,7 +61,6 @@ class AdminForthAuth {
59
61
  }
60
62
 
61
63
  // issue JWT token
62
- const expiresIn = process.env.ADMINFORTH_AUTH_EXPIRESIN || '24h';
63
64
  return jwt.sign({...payload, t: type}, secret, { expiresIn });
64
65
  }
65
66
 
package/dist/auth.js CHANGED
@@ -30,8 +30,9 @@ class AdminForthAuth {
30
30
  removeAuthCookie(response) {
31
31
  response.setHeader('Set-Cookie', `adminforth_jwt=; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=Thu, 01 Jan 1970 00:00:00 GMT`);
32
32
  }
33
- setAuthCookie({ response, username, pk }) {
34
- const token = this.issueJWT({ username, pk, }, 'auth');
33
+ setAuthCookie({ expireInDays, response, username, pk }) {
34
+ const expiresIn = expireInDays ? `${expireInDays}d` : (process.env.ADMINFORTH_AUTH_EXPIRESIN || '24h');
35
+ const token = this.issueJWT({ username, pk }, 'auth', expiresIn);
35
36
  response.setHeader('Set-Cookie', `adminforth_jwt=${token}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict`);
36
37
  }
37
38
  removeCustomCookie({ response, name }) {
@@ -41,14 +42,13 @@ class AdminForthAuth {
41
42
  const { name, value, expiry, httpOnly } = payload;
42
43
  response.setHeader('Set-Cookie', `adminforth_${name}=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + expiry).toUTCString()} `);
43
44
  }
44
- issueJWT(payload, type) {
45
+ issueJWT(payload, type, expiresIn = '24h') {
45
46
  // read ADMINFORH_SECRET from environment if not drop error
46
47
  const secret = process.env.ADMINFORTH_SECRET;
47
48
  if (!secret) {
48
49
  throw new Error('ADMINFORTH_SECRET environment not set');
49
50
  }
50
51
  // issue JWT token
51
- const expiresIn = process.env.ADMINFORTH_AUTH_EXPIRESIN || '24h';
52
52
  return jwt.sign(Object.assign(Object.assign({}, payload), { t: type }), secret, { expiresIn });
53
53
  }
54
54
  verify(jwtToken_1, mustHaveType_1) {
@@ -28,7 +28,7 @@ export default class ConfigValidator {
28
28
  }
29
29
  return [];
30
30
  }
31
- validateComponent(component, errors, ignoreExistsCheck = false) {
31
+ validateComponent(component, errors) {
32
32
  if (!component) {
33
33
  return component;
34
34
  }
@@ -39,6 +39,12 @@ export default class ConfigValidator {
39
39
  else {
40
40
  obj = component;
41
41
  }
42
+ let ignoreExistsCheck = false;
43
+ if (this.adminforth.codeInjector.allComponentNames.hasOwnProperty(component.file)) {
44
+ // not obvious, but if we are in this if, it means that this is plugin component
45
+ // if component is plugin component, we don't need to check if it exists in users folder
46
+ ignoreExistsCheck = true;
47
+ }
42
48
  if (!ignoreExistsCheck) {
43
49
  errors.push(...this.checkCustomFileExists(obj.file));
44
50
  }
@@ -98,10 +104,7 @@ export default class ConfigValidator {
98
104
  }
99
105
  if (this.config.customization.customPages) {
100
106
  this.config.customization.customPages.forEach((page, i) => {
101
- // validate component if it's not plugin injection
102
- if (this.adminforth.codeInjector.allComponentNames.hasOwnProperty(page.component)) {
103
- const validatedPage = this.validateComponent(page.component, errors, true);
104
- }
107
+ this.validateComponent(page.component, errors);
105
108
  });
106
109
  }
107
110
  else {
@@ -298,9 +301,14 @@ export default class ConfigValidator {
298
301
  });
299
302
  res.options.bulkActions = bulkActions;
300
303
  // if pageInjection is a string, make array with one element. Also check file exists
301
- const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
304
+ const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems'];
305
+ const possiblePages = ['list', 'show', 'create', 'edit'];
302
306
  if (res.options.pageInjections) {
303
307
  Object.entries(res.options.pageInjections).map(([key, value]) => {
308
+ if (!possiblePages.includes(key)) {
309
+ const similar = suggestIfTypo(possiblePages, key);
310
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
311
+ }
304
312
  Object.entries(value).map(([injection, target]) => {
305
313
  if (possibleInjections.includes(injection)) {
306
314
  if (!Array.isArray(res.options.pageInjections[key][injection])) {
@@ -312,7 +320,8 @@ export default class ConfigValidator {
312
320
  });
313
321
  }
314
322
  else {
315
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')}`);
323
+ const similar = suggestIfTypo(possibleInjections, injection);
324
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
316
325
  }
317
326
  });
318
327
  });
@@ -417,13 +426,7 @@ export default class ConfigValidator {
417
426
  for (const column of resource.columns) {
418
427
  if (column.components) {
419
428
  for (const [key, comp] of Object.entries(column.components)) {
420
- let ignoreExistsCheck = false;
421
- if (this.adminforth.codeInjector.allComponentNames[comp.file]) {
422
- // not obvious, but if we are in this if, it means that this is plugin component
423
- // and there is no sense to check if it exists in users folder
424
- ignoreExistsCheck = true;
425
- }
426
- column.components[key] = this.validateComponent(comp, errors, ignoreExistsCheck);
429
+ column.components[key] = this.validateComponent(comp, errors);
427
430
  }
428
431
  }
429
432
  }
@@ -44,7 +44,7 @@ export default class AdminForthRestAPI {
44
44
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response }) {
45
45
  var _b, _c, _d, _e;
46
46
  const INVALID_MESSAGE = 'Invalid Username or Password';
47
- const { username, password } = body;
47
+ const { username, password, rememberMe } = body;
48
48
  let adminUser;
49
49
  let toReturn = { ok: true, allowedLogin: true };
50
50
  // get resource from db
@@ -93,7 +93,13 @@ export default class AdminForthRestAPI {
93
93
  }
94
94
  }
95
95
  if (toReturn.allowedLogin) {
96
- this.adminforth.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
96
+ const expireInDays = rememberMe && this.adminforth.config.auth.rememberMeDays;
97
+ this.adminforth.auth.setAuthCookie({
98
+ expireInDays,
99
+ response,
100
+ username,
101
+ pk: userRecord[userResource.columns.find((col) => col.primaryKey).name]
102
+ });
97
103
  }
98
104
  }
99
105
  else {
@@ -140,6 +146,7 @@ export default class AdminForthRestAPI {
140
146
  demoCredentials: this.adminforth.config.auth.demoCredentials,
141
147
  loginPromptHTML: this.adminforth.config.auth.loginPromptHTML,
142
148
  loginPageInjections: this.adminforth.config.customization.loginPageInjections,
149
+ rememberMeDays: this.adminforth.config.auth.rememberMeDays,
143
150
  };
144
151
  }),
145
152
  });
@@ -32,7 +32,8 @@ export default class ConfigValidator implements IConfigValidator {
32
32
  return [];
33
33
  }
34
34
 
35
- validateComponent(component: AdminForthComponentDeclaration, errors: Array<string>, ignoreExistsCheck: boolean = false): AdminForthComponentDeclaration {
35
+ validateComponent(component: AdminForthComponentDeclaration, errors: Array<string>): AdminForthComponentDeclaration {
36
+
36
37
  if (!component) {
37
38
  return component;
38
39
  }
@@ -42,6 +43,18 @@ export default class ConfigValidator implements IConfigValidator {
42
43
  } else {
43
44
  obj = component;
44
45
  }
46
+
47
+ let ignoreExistsCheck = false;
48
+ if (
49
+ this.adminforth.codeInjector.allComponentNames.hasOwnProperty(
50
+ (component as AdminForthComponentDeclarationFull).file)
51
+ ) {
52
+ // not obvious, but if we are in this if, it means that this is plugin component
53
+ // if component is plugin component, we don't need to check if it exists in users folder
54
+ ignoreExistsCheck = true;
55
+ }
56
+
57
+
45
58
  if (!ignoreExistsCheck) {
46
59
  errors.push(...this.checkCustomFileExists(obj.file));
47
60
  }
@@ -109,10 +122,7 @@ export default class ConfigValidator implements IConfigValidator {
109
122
 
110
123
  if (this.config.customization.customPages) {
111
124
  this.config.customization.customPages.forEach((page, i) => {
112
- // validate component if it's not plugin injection
113
- if (this.adminforth.codeInjector.allComponentNames.hasOwnProperty(page.component as PropertyKey)) {
114
- const validatedPage = this.validateComponent(page.component, errors, true);
115
- }
125
+ this.validateComponent(page.component, errors);
116
126
  });
117
127
  } else {
118
128
  this.config.customization.customPages = [];
@@ -341,9 +351,16 @@ export default class ConfigValidator implements IConfigValidator {
341
351
  res.options.bulkActions = bulkActions;
342
352
 
343
353
  // if pageInjection is a string, make array with one element. Also check file exists
344
- const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
354
+ const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems'];
355
+ const possiblePages = ['list', 'show', 'create', 'edit'];
356
+
345
357
  if (res.options.pageInjections) {
346
358
  Object.entries(res.options.pageInjections).map(([key, value]) => {
359
+ if (!possiblePages.includes(key)) {
360
+ const similar = suggestIfTypo(possiblePages, key);
361
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
362
+ }
363
+
347
364
  Object.entries(value).map(([injection, target]) => {
348
365
  if (possibleInjections.includes(injection)) {
349
366
  if (!Array.isArray(res.options.pageInjections[key][injection])) {
@@ -354,7 +371,8 @@ export default class ConfigValidator implements IConfigValidator {
354
371
  res.options.pageInjections[key][injection][i] = this.validateComponent(target, errors);
355
372
  });
356
373
  } else {
357
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')}`);
374
+ const similar = suggestIfTypo(possibleInjections, injection);
375
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
358
376
  }
359
377
  });
360
378
 
@@ -477,13 +495,8 @@ export default class ConfigValidator implements IConfigValidator {
477
495
  if (column.components) {
478
496
 
479
497
  for (const [key, comp] of Object.entries(column.components as Record<string, AdminForthComponentDeclarationFull>)) {
480
- let ignoreExistsCheck = false;
481
- if (this.adminforth.codeInjector.allComponentNames[comp.file]) {
482
- // not obvious, but if we are in this if, it means that this is plugin component
483
- // and there is no sense to check if it exists in users folder
484
- ignoreExistsCheck = true;
485
- }
486
- column.components[key] = this.validateComponent(comp, errors, ignoreExistsCheck);
498
+
499
+ column.components[key] = this.validateComponent(comp, errors);
487
500
  }
488
501
  }
489
502
  }
@@ -63,7 +63,7 @@ export default class AdminForthRestAPI {
63
63
  handler: async ({ body, response }) => {
64
64
 
65
65
  const INVALID_MESSAGE = 'Invalid Username or Password';
66
- const { username, password } = body;
66
+ const { username, password, rememberMe } = body;
67
67
  let adminUser: AdminUser;
68
68
  let toReturn: { ok: boolean, redirectTo?: string, allowedLogin:boolean } = { ok: true, allowedLogin:true};
69
69
 
@@ -120,7 +120,13 @@ export default class AdminForthRestAPI {
120
120
  }
121
121
  }
122
122
  if (toReturn.allowedLogin){
123
- this.adminforth.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
123
+ const expireInDays = rememberMe && this.adminforth.config.auth.rememberMeDays;
124
+ this.adminforth.auth.setAuthCookie({
125
+ expireInDays,
126
+ response,
127
+ username,
128
+ pk: userRecord[userResource.columns.find((col) => col.primaryKey).name]
129
+ });
124
130
  }
125
131
  } else {
126
132
  return { error: INVALID_MESSAGE };
@@ -172,6 +178,7 @@ export default class AdminForthRestAPI {
172
178
  demoCredentials: this.adminforth.config.auth.demoCredentials,
173
179
  loginPromptHTML: this.adminforth.config.auth.loginPromptHTML,
174
180
  loginPageInjections: this.adminforth.config.customization.loginPageInjections,
181
+ rememberMeDays: this.adminforth.config.auth.rememberMeDays,
175
182
  };
176
183
  },
177
184
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.3.22",
3
+ "version": "1.3.24",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/spa/src/App.vue CHANGED
@@ -165,7 +165,7 @@
165
165
  </div>
166
166
  </div>
167
167
  <AcceptModal />
168
- <div v-if="toastStore.toasts.length>0" class="fixed bottom-5 right-5 flex gap-1 flex-col-reverse">
168
+ <div v-if="toastStore.toasts.length>0" class="fixed bottom-5 right-5 flex gap-1 flex-col-reverse z-50">
169
169
  <transition-group
170
170
  name="fade"
171
171
  tag="div"
@@ -1,7 +1,8 @@
1
1
  <template>
2
2
 
3
3
 
4
- <div id="toast-default" class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert"
4
+ <div id="toast-default" class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
5
+ role="alert"
5
6
  :class="
6
7
  {
7
8
  'danger': 'bg-red-100',
@@ -1,6 +1,6 @@
1
1
  import { createRouter, createWebHistory } from 'vue-router';
2
2
  import ResourceParent from '@/views/ResourceParent.vue';
3
- import { useFiltersStore } from '@/stores/filters';
3
+
4
4
  /* IMPORTANT:ADMINFORTH ROUTES IMPORTS */
5
5
 
6
6
  const router = createRouter({
@@ -47,6 +47,10 @@ export const useCoreStore = defineStore('core', () => {
47
47
  if (!resource.value) {
48
48
  throw new Error('Columns not fetched yet');
49
49
  }
50
+ const col = resource.value.columns.find((col: AdminForthResourceColumn) => col.primaryKey);
51
+ if (!col) {
52
+ throw new Error(`Primary key not found in resource ${resourceId}`);
53
+ }
50
54
 
51
55
  const respData = await callAdminForthApi({
52
56
  path: '/get_resource_data',
@@ -56,7 +60,7 @@ export const useCoreStore = defineStore('core', () => {
56
60
  resourceId: resourceId,
57
61
  filters: [
58
62
  {
59
- field: resource.value.columns.find((col: AdminForthResourceColumn) => col.primaryKey).name,
63
+ field: col.name,
60
64
  operator: 'eq',
61
65
  value: primaryKey
62
66
  }
@@ -3,7 +3,14 @@ import { defineStore } from 'pinia';
3
3
 
4
4
  export const useFiltersStore = defineStore('filters', () => {
5
5
  const filters: Ref<any[]> = ref([]);
6
-
6
+ const sort: Ref<any> = ref({});
7
+
8
+ const setSort = (s: any) => {
9
+ sort.value = s;
10
+ }
11
+ const getSort = () => {
12
+ return sort.value;
13
+ }
7
14
  const setFilter = (filter: any) => {
8
15
  filters.value.push(filter);
9
16
  }
@@ -16,5 +23,5 @@ export const useFiltersStore = defineStore('filters', () => {
16
23
  const clearFilters = () => {
17
24
  filters.value = [];
18
25
  }
19
- return {setFilter, getFilters, clearFilters, filters, setFilters}
26
+ return {setFilter, getFilters, clearFilters, filters, setFilters, setSort, getSort}
20
27
  })
@@ -72,6 +72,33 @@
72
72
  {{ filtersStore.filters.length }}
73
73
  </span>
74
74
  </button>
75
+
76
+ <template v-if="coreStore.resourceOptions?.pageInjections?.list?.threeDotsDropdownItems">
77
+ <button id="dropdownMenuIconButton"
78
+ data-dropdown-toggle="dropdownDots"
79
+ class="flex items-center py-2 px-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
80
+ >
81
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 4 15">
82
+ <path d="M3.5 1.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 6.041a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 5.959a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"/>
83
+ </svg>
84
+ </button>
85
+
86
+ <!-- Dropdown menu -->
87
+ <div id="dropdownDots" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700 dark:divide-gray-600">
88
+ <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownMenuIconButton">
89
+ <li v-for="item in coreStore.resourceOptions?.pageInjections?.list?.threeDotsDropdownItems" :key="`dropdown-item-${item.label}`">
90
+ <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
91
+ <component :is="getCustomComponent(item)"
92
+ :meta="item.meta"
93
+ :resource="coreStore.resource"
94
+ :adminUser="coreStore.adminUser"
95
+ />
96
+ </a>
97
+ </li>
98
+ </ul>
99
+ </div>
100
+ </template>
101
+
75
102
  </BreadcrumbsWithButtons>
76
103
 
77
104
  <component
@@ -138,6 +165,11 @@ const page = ref(1);
138
165
  const columnsMinMax = ref({});
139
166
  const sort = ref([]);
140
167
 
168
+ watch(() => sort, async (to, from) => {
169
+ // in store sort might be needed for plugins
170
+ filtersStore.setSort(sort.value);
171
+ }, {deep: true});
172
+
141
173
  const rows = ref(null);
142
174
  const totalRows = ref(0);
143
175
  const checkboxes = ref([]);
@@ -224,6 +256,9 @@ async function init() {
224
256
  resourceId: route.params.resourceId
225
257
  });
226
258
 
259
+ initFlowbite();
260
+
261
+
227
262
  // !!! clear filters should be in same tick with sort assignment so that watch can catch it
228
263
  filtersStore.clearFilters();
229
264
  if (coreStore.resource.options?.defaultSort) {
@@ -56,9 +56,16 @@
56
56
  </button>
57
57
  </div>
58
58
 
59
- <div class="flex items-start mb-5 hidden">
59
+ <div v-if="coreStore.config.rememberMeDays"
60
+ class="flex items-start mb-5"
61
+ :title="`Stay logged in for ${coreStore.config.rememberMeDays} days`"
62
+ >
60
63
  <div class="flex items-center h-5">
61
- <input id="remember" type="checkbox" value="" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800" required />
64
+ <input id="remember"
65
+ ref="rememberInput"
66
+ type="checkbox"
67
+ value=""
68
+ class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800" />
62
69
  </div>
63
70
  <label for="remember" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Remember me</label>
64
71
  </div>
@@ -125,6 +132,7 @@ import { useRouter } from 'vue-router';
125
132
 
126
133
  const passwordInput = ref(null);
127
134
  const usernameInput = ref(null);
135
+ const rememberInput = ref(null);
128
136
 
129
137
  const router = useRouter();
130
138
  const inProgress = ref(false);
@@ -166,6 +174,7 @@ async function login() {
166
174
  body: {
167
175
  username,
168
176
  password,
177
+ rememberMe: rememberInput.value.checked,
169
178
  }
170
179
  });
171
180
  inProgress.value = false;
@@ -239,11 +239,11 @@ export interface IAdminForthDataSourceConnectorConstructor {
239
239
 
240
240
  export interface IAdminForthAuth {
241
241
  verify(jwt : string, mustHaveType: string, decodeUser?: boolean): Promise<any>;
242
- issueJWT(payload: Object, type: string): string;
242
+ issueJWT(payload: Object, type: string, expiresIn?: string): string;
243
243
 
244
244
  removeCustomCookie({response, name}: {response: any, name: string}): void;
245
245
 
246
- setAuthCookie({response, username, pk,}: {response: any, username: string, pk: string}): void;
246
+ setAuthCookie({expireInDays, response, username, pk,}: {expireInDays?: number, response: any, username: string, pk: string}): void;
247
247
 
248
248
  removeAuthCookie(response: any): void;
249
249
  }
@@ -1043,6 +1043,7 @@ export type AdminForthResource = {
1043
1043
  beforeBreadcrumbs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
1044
1044
  afterBreadcrumbs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
1045
1045
  bottom?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
1046
+ threeDotsDropdownItems?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
1046
1047
  },
1047
1048
 
1048
1049
  /**
@@ -1178,6 +1179,13 @@ export type AdminForthConfig = {
1178
1179
  * Any prompt to show users on login. Supports HTML.
1179
1180
  */
1180
1181
  loginPromptHTML?: string,
1182
+
1183
+ /**
1184
+ * Remember me days for "Remember Me" checkbox on login page.
1185
+ * If not set or set to null/0/undefined, "Remember Me" checkbox will not be displayed.
1186
+ * If rememberMeDays is set, then users who check "Remember Me" will be staying logged in for this amount of days.
1187
+ */
1188
+ rememberMeDays?: number,
1181
1189
  },
1182
1190
  /**
1183
1191
  * Array of resources which will be displayed in the admin panel.