@simitgroup/simpleapp-generator 1.1.28 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,6 +33,7 @@ SimpleApp generator is a typescript code generator for convert jsonschemas becom
33
33
  3. [frontend walk through](./docs/frontend.md)
34
34
  4. [End to End test](./docs/test.md)
35
35
  5. [BPMN walk through](./docs/bpmn.md)
36
+ 6. [Language](./doc/language.md)
36
37
 
37
38
  # Quick start
38
39
  1. Simpleapp implement database transaction, and require mongodb cluster, below setup 3 nodes
package/ReleaseNote.md CHANGED
@@ -1,3 +1,14 @@
1
+ [1.2.0]
2
+ 1. lot of house keeping
3
+ 2. frontend more stable login using keycloak
4
+ 3. reduce too much console log at backend
5
+ 4. build frontend/lang/df.ts instead of directly edit
6
+ 5. some improvement of simpleappinput, like add more probs and reduce console error
7
+ 6. simplified generated frontend page by using centralize tool bar
8
+ 7. support document status
9
+ 8. give frontend freedom to edit header bar and menu bar
10
+
11
+
1
12
  [1.1.24]
2
13
  1. fix cannot create tenant due to fullname miss-spell
3
14
  2. fix viewer create record auto redirect
@@ -0,0 +1,19 @@
1
+ # Introduction
2
+
3
+ To support multiple language in simpleapp project, you shall understand basic ideal of:
4
+ 1. i18n
5
+ 2. file & folder structures related to languages
6
+ 3. how to use language key in source code
7
+
8
+ # i18n
9
+ Simpleapp generator use @nuxt/i18n page, you can learn basic ideal from it
10
+
11
+ # file & folders
12
+ After project initialize correctly, everything is preconfigure as nuxt3 i18n practises. However, you shall know
13
+ 1. you shall define all default language key/message in `project-root/lang/default.json`, it will later copy into `frontend/lang/df.ts`.
14
+ 2. `fontend/lang/df.ts` consider as "unpolish" language file, will be override every time to frontend code regenerate, you shall maintain all language key as (1)
15
+ 3. after project completed, developer may pass `frontend/lang/df.ts` to suitable person (or chatgpt) to create proper `en.ts`, `cn.ts`
16
+ 4. put after translated file (`en.ts, cn.ts,...`) into `frontend/lang`
17
+
18
+ # how to use language key
19
+ in source code, just use syntax `t('yourkey')`, it will auto pick correct language file and language key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simitgroup/simpleapp-generator",
3
- "version": "1.1.28",
3
+ "version": "1.2.0",
4
4
  "description": "frontend nuxtjs and backend nests code generator using jsonschema",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
package/src/framework.ts CHANGED
@@ -71,7 +71,7 @@ export const prepareNest = (callback:Function)=>{
71
71
  if(!fs.existsSync(`${targetfolder}/.env`)){
72
72
 
73
73
 
74
- exec(`cd ${targetfolder};pnpm install --save @nestjs/event-emitter dayjs bpmn-server@2.0.6-alpha2 moment @casl/ability jsonpath yaml lodash @types/lodash nest-keycloak-connect keycloak-connect bpmn-client @nestjs/serve-static jsonwebtoken axios @darkwolf/base64url json-schema @wearenova/mongoose-tenant @nestjs/swagger @nestjs/mongoose mongoose ajv ajv-formats ajv-errors @nestjs/config`,async (error, stdout, stderr)=>{
74
+ exec(`cd ${targetfolder};pnpm install --save @nestjs/event-emitter dayjs bpmn-server@2.0.8-alpha2 moment @casl/ability jsonpath yaml lodash @types/lodash nest-keycloak-connect keycloak-connect bpmn-client @nestjs/serve-static jsonwebtoken axios @darkwolf/base64url json-schema @wearenova/mongoose-tenant @nestjs/swagger @nestjs/mongoose mongoose ajv ajv-formats ajv-errors @nestjs/config`,async (error, stdout, stderr)=>{
75
75
  // log.info(`dependency installed`)
76
76
  if(!error){
77
77
  // fs.mkdirSync(`${targetfolder}/public_html`,{recursive:true})
@@ -27,7 +27,7 @@ import { UserContext } from '../user.context';
27
27
  @Injectable()
28
28
  export class TenantMiddleware implements NestMiddleware {
29
29
  protected defaultxorg = Base64URL.encodeText('0-0-0');
30
- protected excludeXorgs = ['/profile', '/profile/tenant'];
30
+ protected excludeXorgs = ['/profile', '/profile/tenant','/profile/session'];
31
31
  protected logger = new Logger();
32
32
  protected transController;
33
33
  constructor(
@@ -71,7 +71,7 @@ export class DocnoformatService extends DocnoformatProcessor {
71
71
  undefined,
72
72
  {default:'desc'}
73
73
  );
74
- console.log('searchresult',searchresult);
74
+ //console.log('searchresult',searchresult);
75
75
  let data: any = [];
76
76
  for (let i = 0; i < searchresult.length; i++) {
77
77
  const s = searchresult[i];
@@ -12,6 +12,7 @@ watch(()=>useRoute().params['xorg'],(newval,oldvalue)=>{
12
12
  <template>
13
13
  <div>
14
14
  <NuxtLayout>
15
+ <SessionBlock/>
15
16
  <EventDocumentViewer></EventDocumentViewer>
16
17
  <EventDecision/>
17
18
  <EventNotification/>
@@ -0,0 +1,49 @@
1
+ <template>
2
+
3
+ <Dialog v-model:visible="showDialog" modal :pt="{root:{class:'w-1/3'}}"
4
+ :header="t('sessionExpire')">
5
+ <div>
6
+ {{ t('reloginMessage') }}
7
+ </div>
8
+ <div class="text-right">
9
+ <Button @click="relogin" class="btn-primary">
10
+ {{ t('login') }}
11
+ </Button>
12
+ </div>
13
+
14
+
15
+ </Dialog>
16
+
17
+
18
+
19
+
20
+ </template>
21
+ <script lang="ts" setup>
22
+ import {PROFILEApi} from '~/simpleapp/generate/openapi'
23
+ const showDialog = ref(false)
24
+ const tabvisible = ref('')
25
+
26
+
27
+ addEventListener("visibilitychange", async (event) => {
28
+ tabvisible.value = document.visibilityState
29
+
30
+ if(document.visibilityState ==='visible'){
31
+ //focus tab, try session still active?
32
+ const result = await getApiSession()
33
+ if(result){
34
+ showDialog.value=false
35
+ }else{
36
+ showDialog.value=true
37
+ }
38
+ }
39
+ });
40
+
41
+
42
+ const relogin= ()=>{
43
+ window.open(useRuntimeConfig().public.APP_URL+'/relogin')
44
+ }
45
+
46
+
47
+
48
+
49
+ </script>
@@ -14,14 +14,14 @@
14
14
  <InputText type="date" :pt="pt"
15
15
  v-else-if="inputType == SimpleAppInputType.date"
16
16
  :inputId="slotprops.uuid" :path="setting.instancepath"
17
- v-model="(datevalue as string)" @update:modelValue="updateDate" :readonly="isReadonly"
17
+ v-model="datevalue" @update:modelValue="updateDate" :readonly="isReadonly"
18
18
  :placeholder="placeholder"
19
19
  v-bind="(componentProps as InputTextProps)"/>
20
20
  <!-- calendar component -->
21
21
  <Calendar type="date" :pt="pt" class="flex flex-col"
22
22
  v-else-if="SimpleAppInputType.calendar==inputType"
23
23
  :inputId="slotprops.uuid" :path="setting.instancepath"
24
- v-model="(datevalue as string)" @update:modelValue="updateDate" :readonly="isReadonly"
24
+ v-model="datevalue" @update:modelValue="updateDate" :readonly="isReadonly"
25
25
  :placeholder="placeholder"
26
26
  v-bind="(componentProps as CalendarProps)"/>
27
27
  <!-- time component -->
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * This file was automatically generated by simpleapp generator during initialization.
3
3
  * DO NOT MODIFY IT BY HAND.
4
- * last change 2023-09-09
4
+ * last change 2024-01-24
5
5
  * author: Ks Tan
6
6
  */
7
7
 
@@ -16,7 +16,10 @@ const getAxiosConfig = () => {
16
16
  };
17
17
  return config
18
18
  }
19
- export const getApiSession = async ()=> await (new o.PROFILEApi(getAxiosConfig())).getSession()
19
+ export const getApiSession = async ()=> {
20
+ const res = await (new o.PROFILEApi(getAxiosConfig(),undefined,useNuxtApp().$axios)).getSession()
21
+ return res
22
+ }
20
23
 
21
24
  export const getDocumentApi = (documentName: string): any => {
22
25
  //const { csrf } = useCsrf()
@@ -25,11 +28,53 @@ export const getDocumentApi = (documentName: string): any => {
25
28
 
26
29
  const config = getAxiosConfig()
27
30
  const docsOpenapi: any = {
28
- <%for(let i=0; i<it.modules.length;i++){ %>
29
- <% const d = it.modules[i] %>
30
- '<%= d['docname'].toLowerCase() %>': new o.<%= d['doctype'].toUpperCase() %>Api(config),
31
+ 'tenant': new o.TENANTApi(config),
32
+
33
+ 'organization': new o.ORGApi(config),
34
+
35
+ 'branch': new o.BRANCHApi(config),
36
+
37
+ 'permission': new o.PERMApi(config),
38
+
39
+ 'autoincreament': new o.AUTOINCApi(config),
40
+
41
+ 'docnoformat': new o.DOCNOApi(config),
42
+
43
+ 'category': new o.CATApi(config),
44
+
45
+ 'customer': new o.CUSTApi(config),
46
+
47
+ 'enrollment': new o.ENROLLApi(config),
48
+
49
+ 'invoice': new o.INVApi(config),
50
+
51
+ 'level': new o.LVLApi(config),
52
+
53
+ 'payment': new o.PAYApi(config),
54
+
55
+ 'paymentmethod': new o.PAYMETHODApi(config),
56
+
57
+ 'period': new o.PERIODApi(config),
58
+
59
+ 'product': new o.PRDApi(config),
60
+
61
+ 'room': new o.ROOMApi(config),
62
+
63
+ 'schedule': new o.SCHEDULEApi(config),
64
+
65
+ 'school': new o.SCHOOLApi(config),
66
+
67
+ 'student': new o.STUApi(config),
68
+
69
+ 'studentgroup': new o.STUGROUPApi(config),
70
+
71
+ 'teacher': new o.TEACHERApi(config),
72
+
73
+ 'tuitionclass': new o.TUITIONApi(config),
74
+
75
+ 'user': new o.USERApi(config),
31
76
 
32
- <%}%>
77
+
33
78
  };
34
79
  if (!docsOpenapi[documentName]) {
35
80
  console.error(
@@ -7,11 +7,14 @@ export const reloadUserStore = async () =>{
7
7
  await $userstore.loadRemoteUserInfo()
8
8
  }
9
9
 
10
- export const getUserProfile = () =>{
10
+ export const getUserProfile = () =>{
11
+ const userstore = getUserStore()
12
+
11
13
  return getUserStore().getUserInfo()
12
14
  }
15
+
13
16
  export const getCurrentXorg = () =>{
14
- return getUserStore().getCurrentXorg()
17
+ return (useRoute().params.xorg) ? String(useRoute().params.xorg) : undefined
15
18
  }
16
19
  export const getPageBaseUrl = (resourcename:string) =>{
17
20
  return `/${getCurrentXorg()}/${resourcename}`;
@@ -1,22 +1,11 @@
1
- export const logout = async () => {
1
+ export const logout = async (redirecturl:string='') => {
2
+ const redirectdata = encodeURIComponent(redirecturl)
2
3
  const { signOut } = useAuth();
3
4
  const { data } = await <any>useFetch('/api/auth/logout');
4
- const signoutres = await signOut({redirect:false});
5
- console.log("signout data", signoutres)
6
- // console.log(data.value)
7
- if(data['value']&& data['value']['path']){
8
- // const logoutkeycloak = await <any>useFetch(data.value.path);
9
- // console.log('logoutkeycloak',data['value']['path'])
10
- if(data['value']['path']){
11
- navigateTo(data.value.path, { external: true })
12
- }
13
- //
14
- }
5
+ // remove session
6
+ await signOut({redirect:false});
7
+ let addPath = encodeURIComponent(`/login?callbackUrl=${redirectdata}`);
8
+ const tourl= `${data.value.path}${addPath}`
9
+ navigateTo(tourl, { external: true })
15
10
 
16
- // console.log('logout data',data.value)
17
-
18
-
19
- // const addPath = encodeURIComponent("/login");
20
- //
21
-
22
- };
11
+ };
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * This file was automatically generated by simpleapp generator. Every
3
3
  * MODIFICATION OVERRIDE BY GENERATEOR
4
- * last change 2023-10-28
4
+ * last change 2024-01-23
5
5
  * Author: Ks Tan
6
6
  */
7
7
  import { getUserProfile } from './../composables/getUserStore.generate';
8
8
  import {MenuData} from '~/types'
9
9
  export default defineNuxtRouteMiddleware((to, from) => {
10
10
 
11
+ // return true;
11
12
  const userprofile = getUserProfile()
12
-
13
-
14
- //is public resource
15
- if( getPublicResource(String(to.params['xorg'])).filter((item:MenuData)=>to.fullPath === item.url)){
13
+ if(to.path=='/relogin'){
14
+ return true
15
+ }else if( getPublicResource(String(to.params['xorg'])).filter((item:MenuData)=>to.fullPath === item.url)){
16
16
  return true
17
17
  }// no permission control needed yet
18
18
  else if(!to.params['xorg']){
@@ -33,7 +33,7 @@ tailwindcss: {
33
33
  'nuxt-primevue',
34
34
  // "nuxt-security", //temporary avoid nuxt-security cause cors
35
35
  '@vueuse/nuxt',
36
- '@nuxt/ui',
36
+ //'@nuxt/ui',
37
37
  '@nuxtjs/tailwindcss',
38
38
  ['@pinia/nuxt',{
39
39
  autoImports: [
@@ -1,27 +1,30 @@
1
+ <template>
2
+ <NuxtPage />
3
+ </template>
1
4
  <script setup lang="ts">
2
5
  /**
3
6
  * This file was automatically generated by simpleapp generator. Every
4
7
  * MODIFICATION OVERRIDE BY GENERATEOR
5
- * last change 2023-10-28
8
+ * last change 2024-01-22
6
9
  * Author: Ks Tan
7
10
  */
8
- definePageMeta({
9
- name: "Login",
11
+
12
+ definePageMeta({
13
+ name: 'Login',
10
14
  auth: false,
11
- });
15
+ })
12
16
 
13
- // const route = useRoute();
14
- const { signIn } = useAuth();
15
- onMounted(async () => {
16
- // logout();
17
- let callbackUrl = "/";
18
- // console.log("redirect to login");
19
- // if(route.params.redirect) {
20
- // callbackUrl = <string>route.params.redirect;
21
- // }
22
- await signIn("keycloak", { callbackUrl: callbackUrl });
23
- });
24
- </script>
25
- <template>
26
- <NuxtPage />
27
- </template>
17
+ const route = useRoute();
18
+ const { signIn } = useAuth()
19
+ const callbackUrl = ref('')
20
+ onMounted(async () => {
21
+ if(route.query.callbackUrl) {
22
+ callbackUrl.value = <string>route.query.callbackUrl;
23
+ }else{
24
+ callbackUrl.value = '/'
25
+ }
26
+ callbackUrl.value = decodeURIComponent(callbackUrl.value)
27
+ console.log("callbackUrlcallbackUrlcallbackUrl",callbackUrl.value)
28
+ await signIn('keycloak', { callbackUrl: callbackUrl.value })
29
+ })
30
+ </script>
@@ -0,0 +1,6 @@
1
+ <template>
2
+ <div>relogin success</div>
3
+ </template>
4
+ <script lang="ts" setup>
5
+ window.close()
6
+ </script>
@@ -31,8 +31,7 @@ export default defineNuxtPlugin( async(nuxtApp) => {
31
31
 
32
32
  }
33
33
  else if(error.response.status==401){
34
- console.error("axios 401 session expired, redirect to logout page1")
35
- logout()
34
+ console.error("axios 401 session expired, pop up for relogin")
36
35
  }else if(error.response && error.response.status){
37
36
  console.error("have error response")
38
37
  createError({
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * This file was automatically generated by simpleapp generator. Every
3
3
  * MODIFICATION OVERRIDE BY GENERATEOR
4
- * last change 2023-10-28
4
+ * last change 2024-01-23
5
5
  * Author: Ks Tan
6
6
  */
7
7
  import { defineNuxtPlugin } from "#app";
@@ -11,8 +11,7 @@ import _ from 'lodash'
11
11
 
12
12
 
13
13
  export default defineNuxtPlugin( async(nuxtApp) => {
14
-
15
-
14
+ console.log("start plugin 20")
16
15
  const useUserStore = defineStore('userstore', {
17
16
  state: ()=>({
18
17
  _id: ref(''),
@@ -91,10 +90,21 @@ export default defineNuxtPlugin( async(nuxtApp) => {
91
90
  return Promise.reject(errdata)
92
91
  }
93
92
  })
94
- },
93
+ },
95
94
  getCurrentXorg(){
96
95
  return (useRoute().params.xorg) ? String(useRoute().params.xorg) : undefined
97
96
  },
97
+ async pingSession(){
98
+ let xorg = this.getCurrentXorg()
99
+ let apiurl=''
100
+ if(xorg===undefined){
101
+ apiurl = `${useRuntimeConfig().public.APP_URL}/api`
102
+ }else{
103
+ apiurl = `${useRuntimeConfig().public.APP_URL}/api/${xorg}`
104
+ }
105
+ const {$axios} = useNuxtApp()
106
+ return await new PROFILEApi(undefined,apiurl,$axios).getSession()
107
+ },
98
108
  async decideInvitation(id:string,decision:string){
99
109
  const apiurl = `${useRuntimeConfig().public.APP_URL}/api`
100
110
  const {$axios} = useNuxtApp()
@@ -170,12 +180,17 @@ export default defineNuxtPlugin( async(nuxtApp) => {
170
180
  }
171
181
  }
172
182
  })
173
-
174
- const route = useRoute();
175
-
176
- if(route.meta.auth !==false){
183
+ console.log("after define user store")
184
+
185
+ if( useRoute().meta.auth !==false){
186
+ if(await useUserStore().pingSession()){
187
+ console.log("ping session ok")
177
188
  await useUserStore().loadRemoteUserInfo()
178
- }
189
+ }else{
190
+ console.log("No login session", useRoute().path)
191
+ await logout(useRoute().path)
192
+ }
193
+ }
179
194
 
180
195
  return {
181
196
 
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * This file was automatically generated by simpleapp generator. Every
3
3
  * MODIFICATION OVERRIDE BY GENERATEOR
4
- * last change 2023-10-28
4
+ * last change 2024-01-24
5
5
  * Author: Ks Tan
6
6
  */
7
7
  import { NuxtAuthHandler } from "#auth";
8
8
  import KeycloakProvider from "next-auth/providers/keycloak";
9
-
9
+ // import GithubProvider from "next-auth/providers/github";
10
10
 
11
11
  export default NuxtAuthHandler({
12
12
  secret: process.env.AUTH_SECRET_KEY,
@@ -14,7 +14,10 @@ export default NuxtAuthHandler({
14
14
  maxAge: 60 * 60 * 24 * 30,
15
15
  },
16
16
  providers: [
17
-
17
+ // GithubProvider.default({
18
+ // clientId: process.env.GITHUB_ID ?? '',
19
+ // clientSecret: process.env.GITHUB_SECRET ?? '',
20
+ // }),
18
21
  // @ts-expect-error
19
22
  KeycloakProvider.default({
20
23
  clientId: process.env.OAUTH2_CLIENTID ?? "",
@@ -28,7 +31,7 @@ export default NuxtAuthHandler({
28
31
  token: `${process.env.OAUTH2_CONFIGURL}/protocol/openid-connect/token`,
29
32
  logout:
30
33
  `${process.env.OAUTH2_CONFIGURL}/protocol/openid-connect/logout?redirect_uri=` +
31
- encodeURIComponent(String(process.env.APP_URL)),
34
+ encodeURIComponent(String(process.env.NUXT_PUBLIC_API_BASE)),
32
35
  },
33
36
  // accessTokenUrl: `${process.env.OAUTH2_CONFIGURL}/protocol/openid-connect/token`,
34
37
  // requestTokenUrl: `${process.env.OAUTH2_CONFIGURL}/protocol/openid-connect/token`,
@@ -49,8 +52,8 @@ export default NuxtAuthHandler({
49
52
  codeChallengeMethod: "S256",
50
53
  redirect: {
51
54
  logout: "/",
52
- callback: "/",
53
- home: "/",
55
+ // callback: "/",
56
+ // home: "/",
54
57
  },
55
58
  }),
56
59
  ],
@@ -58,17 +61,56 @@ export default NuxtAuthHandler({
58
61
  signIn: "/login",
59
62
  },
60
63
  callbacks: {
64
+ // async redirect({ url, baseUrl }) {
65
+
66
+ // console.log(url, baseUrl)
67
+ // return baseUrl
68
+ // },
61
69
  async jwt({ token, account }) {
70
+ // Persist the OAuth access_token to the token right after signin
62
71
  if (account) {
63
72
  token.accessToken = account.access_token;
73
+ token.refreshToken = account.refresh_token;
74
+ token.expiresAt = account.expires_at;
75
+ } else if (Date.now() < (<number>token.expiresAt) * 1000) {
76
+ return token;
77
+ }
78
+
79
+ // try refresh token
80
+ const response = await fetch(`${process.env.OAUTH2_CONFIGURL}/protocol/openid-connect/token`, {
81
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
82
+ body: new URLSearchParams({
83
+ client_id: process.env.OAUTH2_CLIENTID ?? "",
84
+ client_secret: process.env.OAUTH2_CLIENTSECRET ?? "",
85
+ grant_type: "refresh_token",
86
+ refresh_token: <string>token.refreshToken ?? "",
87
+ }),
88
+ method: "POST",
89
+ })
90
+
91
+ const newToken = await response.json();
92
+
93
+ if(!response.ok) {
94
+ /** @todo handle refresh token failed */
95
+ return token
96
+ }
97
+
98
+ return {
99
+ ...token,
100
+ accessToken: newToken.access_token,
101
+ // record next access token expires time
102
+ expiresAt: Math.floor(Date.now() / 1000 + newToken.expires_in),
103
+ // Fall back to old refresh token, but note that
104
+ // many providers may only allow using a refresh token once.
105
+ refreshToken: newToken.refresh_token ?? token.refresh_token,
64
106
  }
65
- return token;
66
107
  },
67
- async session({ session , token, user }) {
68
- const sessiondata:any = {...session}
108
+ async session({ session, token, user }) {
109
+ // console.log("session", session);
110
+ // Send properties to the client, like an access_token from a provider.
111
+ const sessiondata:any = {...session}
69
112
  sessiondata.accessToken = <string>token.accessToken;
70
113
  return sessiondata;
71
114
  },
72
-
73
115
  },
74
- });
116
+ });
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * This file was automatically generated by simpleapp generator. Every
3
3
  * MODIFICATION OVERRIDE BY GENERATEOR
4
- * last change 2023-10-28
4
+ * last change 2024-01-24
5
5
  * Author: Ks Tan
6
6
  */
7
7
  export default defineEventHandler(async (event) => {
8
8
  const path = `${
9
9
  process.env.OAUTH2_CONFIGURL
10
- }/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent(process.env.AUTH_ORIGIN??'')}`;
10
+ }/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent(
11
+ process.env.AUTH_ORIGIN ?? ""
12
+ )}`;
11
13
 
12
14
  return {
13
15
  path: path
@@ -29,4 +29,5 @@ export type EventType ={
29
29
  'RefreshUser' :string
30
30
  'RefreshDocumentList': RefreshDocumentList
31
31
  'ViewRecord': ViewRecord
32
+ 'SessionExpire': string
32
33
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "home":"Home",
2
3
  "welcome": "Welcome",
3
4
  "create": "Create",
4
5
  "update": "Update",