update base cms

parent 61480537
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
"http-status-codes": "^2.1.4", "http-status-codes": "^2.1.4",
"quasar": "^2.0.0-beta.1", "quasar": "^2.0.0-beta.1",
"secure-ls": "^1.2.6", "secure-ls": "^1.2.6",
"vue": "^3.0.11",
"vue-i18n": "^9.0.0-beta.0", "vue-i18n": "^9.0.0-beta.0",
"vuex-persistedstate": "^4.0.0-beta.3" "vuex-persistedstate": "^4.0.0-beta.3"
}, },
...@@ -36,7 +36,7 @@ module.exports = configure(function (ctx) { ...@@ -36,7 +36,7 @@ module.exports = configure(function (ctx) {
// https://github.com/quasarframework/quasar/tree/dev/extras // https://github.com/quasarframework/quasar/tree/dev/extras
extras: [ extras: [
// 'ionicons-v4', // 'ionicons-v4',
// 'mdi-v5', 'mdi-v5',
// 'fontawesome-v5', // 'fontawesome-v5',
// 'eva-icons', // 'eva-icons',
// 'themify', // 'themify',
...@@ -49,7 +49,7 @@ module.exports = configure(function (ctx) { ...@@ -49,7 +49,7 @@ module.exports = configure(function (ctx) {
// Full list of options: https://v2.quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build // Full list of options: https://v2.quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
build: { build: {
vueRouterMode: 'hash', // available values: 'hash', 'history' vueRouterMode: 'history', // available values: 'hash', 'history'
// transpile: false, // transpile: false,
......
...@@ -10,5 +10,16 @@ export const config = { ...@@ -10,5 +10,16 @@ export const config = {
TOKEN_EXPIRES: { TOKEN_EXPIRES: {
code: 2, code: 2,
}, },
'-1': {
code: -1,
}, },
},
CHANNEL: 'CMS',
}; };
export enum API_PATHS {
login = '/user/login',
getUserGroups = '/group/get_list',
getGroupInfo = '/group/get_info',
getListPages = '/page/list',
}
...@@ -20,12 +20,12 @@ declare module '@vue/runtime-core' { ...@@ -20,12 +20,12 @@ declare module '@vue/runtime-core' {
const api = axios.create({ baseURL: config.API_ENDPOINT }); const api = axios.create({ baseURL: config.API_ENDPOINT });
export type BaseResponseBody = { export type BaseResponseBody<T> = {
error: { error: {
code: number; code: number;
message: string; message: string;
}; };
data: unknown; data: T;
}; };
export default boot(({ app, store }) => { export default boot(({ app, store }) => {
...@@ -33,12 +33,20 @@ export default boot(({ app, store }) => { ...@@ -33,12 +33,20 @@ export default boot(({ app, store }) => {
const $store = store as Store<StateInterface>; const $store = store as Store<StateInterface>;
const onRequest = (config: AxiosRequestConfig) => { const onRequest = (axiosConfig: AxiosRequestConfig) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
config.headers.Authorization = `Bearer ${ axiosConfig.headers.Authorization = `Bearer ${
$store.state.authentication.token || '' $store.state.authentication.token || ''
}`; }`;
return config;
if (axiosConfig.method?.toUpperCase() === 'GET') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
axiosConfig.params.channel = config.CHANNEL;
} else if (axiosConfig.method?.toUpperCase() === 'POST') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
axiosConfig.data.channel = config.CHANNEL;
}
return axiosConfig;
}; };
const onRequestError = (error: Error) => { const onRequestError = (error: Error) => {
...@@ -50,7 +58,8 @@ export default boot(({ app, store }) => { ...@@ -50,7 +58,8 @@ export default boot(({ app, store }) => {
return Promise.reject(error); return Promise.reject(error);
}; };
const onResponse = (res: AxiosResponse<BaseResponseBody>) => { const onResponse = (res: AxiosResponse<BaseResponseBody<unknown>>) => {
if (res.data.error.code) {
if ( if (
res.data.error.code === config.API_RES_CODE.TOKEN_INVALID.code || res.data.error.code === config.API_RES_CODE.TOKEN_INVALID.code ||
res.data.error.code === config.API_RES_CODE.TOKEN_EXPIRES.code res.data.error.code === config.API_RES_CODE.TOKEN_EXPIRES.code
...@@ -61,6 +70,12 @@ export default boot(({ app, store }) => { ...@@ -61,6 +70,12 @@ export default boot(({ app, store }) => {
message: i18n.global.t('tokenInvalidMessage'), message: i18n.global.t('tokenInvalidMessage'),
}); });
// ... Logout // ... Logout
} else {
Notify.create({
type: 'warning',
message: i18n.global.t(`responseErrorMsg.msg${res.data.error.code}`),
});
}
} }
return res; return res;
}; };
...@@ -73,7 +88,9 @@ export default boot(({ app, store }) => { ...@@ -73,7 +88,9 @@ export default boot(({ app, store }) => {
}); });
} else { } else {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
const response = axiosError.response as AxiosResponse<BaseResponseBody>; const response = axiosError.response as AxiosResponse<
BaseResponseBody<unknown>
>;
if (response.status === StatusCodes.NOT_FOUND) { if (response.status === StatusCodes.NOT_FOUND) {
// ... // ...
......
import { defineComponent } from 'vue';
import MenuItemComponent from './menu-item/index.vue';
export const MenuListScript = defineComponent({
components: {
MenuItemComponent,
},
});
<template>
<q-list>
<MenuItemComponent
v-for="(menuItem, menuItemIdx) in $store.state.authentication.menuList"
:key="`menu-item-${menuItemIdx}-${menuItem.id}`"
:item="menuItem"
></MenuItemComponent>
</q-list>
</template>
<script lang="ts">
import { MenuListScript } from './MenuList';
export default MenuListScript;
</script>
import { MenuItem } from 'src/store/authentication/state';
import { defineComponent, PropType } from 'vue';
export const MenuItemScript = defineComponent({
name: 'MenuItemComponent',
props: {
item: {
type: Object as PropType<MenuItem>,
required: true,
},
},
});
<template>
<q-expansion-item
v-if="item.children && item.children.length"
:label="item.pageName"
:icon="item.pageIcon"
:header-inset-level="item.level - 1"
>
<MenuItemComponent
v-for="(menuItem, menuItemIdx) in item.children"
:key="`menu-item-${menuItemIdx}-${menuItem.id}`"
:item="menuItem"
></MenuItemComponent>
</q-expansion-item>
<q-expansion-item
v-else
expand-icon-class="hidden"
:label="item.pageName"
:icon="item.pageIcon"
:header-inset-level="item.level - 1"
:to="item.pageUrl"
></q-expansion-item>
</template>
<script lang="ts">
import { MenuItemScript } from './MenuItem';
export default MenuItemScript;
</script>
...@@ -7,4 +7,18 @@ export default { ...@@ -7,4 +7,18 @@ export default {
requestErrorMessage: 'Không thể kết nối đến server', requestErrorMessage: 'Không thể kết nối đến server',
responseErrorMessage: '', responseErrorMessage: '',
tokenInvalidMessage: 'Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại', tokenInvalidMessage: 'Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại',
loginSuccess: 'Đăng nhập thành công',
logoutSuccess: 'Đăng xuất thành công',
loginPage: {
pageTitle: 'Đăng nhập',
usernameInputLabel: 'Tên đăng nhập',
passwordInputLabel: 'Mật khẩu',
requireUsername: 'Vui lòng nhập tên đăng nhập',
requirePassword: 'Vui lòng nhập mật khẩu',
submitLoginBtnLabel: 'Đăng nhập',
loginSuccess: 'Đăng nhập thành công',
},
responseErrorMsg: {
'msg-1': 'Lỗi không xác định',
},
}; };
<template> <template>
<q-layout view="lHh Lpr lFf"> <q-layout view="lHh Lpr lFf">
<q-header elevated> <q-header v-if="showHeader" elevated>
<q-toolbar> <q-toolbar>
<q-btn <q-btn
flat flat
...@@ -11,34 +11,20 @@ ...@@ -11,34 +11,20 @@
@click="toggleLeftDrawer" @click="toggleLeftDrawer"
/> />
<q-toolbar-title> <q-toolbar-title> Quasar App </q-toolbar-title>
Quasar App
</q-toolbar-title>
<div>Quasar v{{ $q.version }}</div> <div>Quasar v{{ $q.version }}</div>
</q-toolbar> </q-toolbar>
</q-header> </q-header>
<q-drawer <q-drawer
v-if="showHeader"
v-model="leftDrawerOpen" v-model="leftDrawerOpen"
show-if-above show-if-above
bordered bordered
class="bg-grey-1" class="bg-grey-1"
> >
<q-list> <MenuListComponent />
<q-item-label
header
class="text-grey-8"
>
Essential Links
</q-item-label>
<EssentialLink
v-for="link in essentialLinks"
:key="link.title"
v-bind="link"
/>
</q-list>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>
...@@ -48,72 +34,42 @@ ...@@ -48,72 +34,42 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import EssentialLink from 'components/EssentialLink.vue' import { useRoute } from 'vue-router';
import { Pages } from '../router/routes';
const linksList = [ import MenuListComponent from 'components/menu-list/index.vue';
{
title: 'Docs',
caption: 'quasar.dev',
icon: 'school',
link: 'https://quasar.dev'
},
{
title: 'Github',
caption: 'github.com/quasarframework',
icon: 'code',
link: 'https://github.com/quasarframework'
},
{
title: 'Discord Chat Channel',
caption: 'chat.quasar.dev',
icon: 'chat',
link: 'https://chat.quasar.dev'
},
{
title: 'Forum',
caption: 'forum.quasar.dev',
icon: 'record_voice_over',
link: 'https://forum.quasar.dev'
},
{
title: 'Twitter',
caption: '@quasarframework',
icon: 'rss_feed',
link: 'https://twitter.quasar.dev'
},
{
title: 'Facebook',
caption: '@QuasarFramework',
icon: 'public',
link: 'https://facebook.quasar.dev'
},
{
title: 'Quasar Awesome',
caption: 'Community Quasar projects',
icon: 'favorite',
link: 'https://awesome.quasar.dev'
}
];
import { defineComponent, ref } from 'vue' import { defineComponent, ref, computed, onMounted } from 'vue';
import { useStore } from '../store';
export default defineComponent({ export default defineComponent({
name: 'MainLayout', name: 'MainLayout',
components: { components: {
EssentialLink MenuListComponent,
}, },
setup () { setup() {
const leftDrawerOpen = ref(false) const leftDrawerOpen = ref(false);
const route = useRoute();
const showHeader = computed(() => {
return route.name !== Pages.login;
});
const $store = useStore();
onMounted(async () => {
await $store.dispatch('authentication/getListPages');
});
return { return {
essentialLinks: linksList,
leftDrawerOpen, leftDrawerOpen,
toggleLeftDrawer () { showHeader,
leftDrawerOpen.value = !leftDrawerOpen.value toggleLeftDrawer() {
} leftDrawerOpen.value = !leftDrawerOpen.value;
} },
} };
}) },
});
</script> </script>
...@@ -5,25 +5,21 @@ ...@@ -5,25 +5,21 @@
active active
:todos="todos" :todos="todos"
:meta="meta" :meta="meta"
></example-component> >
</example-component>
{{ config }} <q-btn @click="$store.dispatch('authentication/logOut')">Logout</q-btn>
</q-page> </q-page>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Todo, Meta } from '../components/models'; import { Todo, Meta } from '../components/models';
import ExampleComponent from '../components/CompositionComponent.vue'; import ExampleComponent from '../components/CompositionComponent.vue';
import { defineComponent, ref, onMounted } from 'vue'; import { defineComponent, ref } from 'vue';
import { useStore } from '../store';
export default defineComponent({ export default defineComponent({
name: 'PageIndex', name: 'PageIndex',
components: { ExampleComponent }, components: { ExampleComponent },
setup() { setup() {
const $store = useStore();
onMounted(() => {
$store.commit('authentication/setToken');
});
const todos = ref<Todo[]>([ const todos = ref<Todo[]>([
{ {
id: 1, id: 1,
......
import { StateInterface } from './../../store/index';
import { defineComponent, ref } from 'vue';
import { Router, useRouter } from 'vue-router';
import { useStore } from 'src/store';
import { i18n } from 'src/boot/i18n';
import { Store } from 'vuex';
import { Pages } from 'src/router/routes';
const user_name = ref('');
const password = ref('');
const loggingIn = ref(false);
const login = async (router: Router, store: Store<StateInterface>) => {
try {
await store.dispatch('authentication/callAPILogin', {
userName: user_name.value,
password: password.value,
});
} catch (error) {}
};
export const Login = defineComponent({
name: Pages.login,
setup() {
const router = useRouter();
const store = useStore();
const usernameInputRules = [
(val: string) =>
(val && val.trim().length > 0) ||
i18n.global.t('loginPage.requireUsername'),
];
const passwordInputRules = [
(val: string) =>
(val && val.trim().length > 0) ||
i18n.global.t('loginPage.requirePassword'),
];
const resetForm = () => {
user_name.value = '';
password.value = '';
};
const onSubmit = async () => {
if (!loggingIn.value) {
loggingIn.value = true;
await login(router, store);
loggingIn.value = false;
// ...
}
};
return {
usernameInputRules,
passwordInputRules,
user_name,
password,
resetForm,
onSubmit,
loggingIn,
};
},
});
<template>
<q-page class="row items-center justify-evenly">
<q-card class="login-card">
<q-card-section class="text-h5 text-center">{{
$t('loginPage.pageTitle')
}}</q-card-section>
<q-card-section>
<q-form autofocus @submit="onSubmit" @reset="resetForm">
<q-input
filled
v-model="user_name"
:label="`${$t('loginPage.usernameInputLabel')} *`"
lazy-rules
:rules="usernameInputRules"
class="q-my-sm"
:disable="loggingIn"
/>
<q-input
filled
type="password"
v-model="password"
:label="`${$t('loginPage.passwordInputLabel')} *`"
lazy-rules
:rules="passwordInputRules"
class="q-my-sm"
:disable="loggingIn"
/>
<div>
<q-btn
:label="$t('loginPage.submitLoginBtnLabel')"
type="submit"
color="primary"
class="full-width"
unelevated
:loading="loggingIn"
/>
</div>
</q-form>
</q-card-section>
</q-card>
</q-page>
</template>
<script lang="ts">
import { Login } from './Login';
export default Login;
</script>
<style lang="scss" scoped>
.login-card {
min-width: 35rem;
}
</style>
...@@ -6,7 +6,7 @@ import { ...@@ -6,7 +6,7 @@ import {
createWebHistory, createWebHistory,
} from 'vue-router'; } from 'vue-router';
import { StateInterface } from '../store'; import { StateInterface } from '../store';
import routes from './routes'; import routes, { Pages } from './routes';
/* /*
* If not building with SSR mode, you can * If not building with SSR mode, you can
...@@ -17,15 +17,13 @@ import routes from './routes'; ...@@ -17,15 +17,13 @@ import routes from './routes';
* with the Router instance. * with the Router instance.
*/ */
export default route<StateInterface>(function (/* { store, ssrContext } */) { const createHistory = process.env.SERVER
const createHistory =
process.env.SERVER
? createMemoryHistory ? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history' : process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory ? createWebHistory
: createWebHashHistory; : createWebHashHistory;
const Router = createRouter({ const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
routes, routes,
...@@ -35,7 +33,19 @@ export default route<StateInterface>(function (/* { store, ssrContext } */) { ...@@ -35,7 +33,19 @@ export default route<StateInterface>(function (/* { store, ssrContext } */) {
history: createHistory( history: createHistory(
process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE
), ),
});
export default route<StateInterface>(function ({ store }) {
Router.beforeEach((to, from, next) => {
const isAuthenticated = store.state.authentication.token;
if (to.name !== Pages.login && !isAuthenticated) {
next({ name: Pages.login });
}
if (to.name === Pages.login && isAuthenticated) {
next({ name: Pages.home });
} else {
next();
}
}); });
return Router; return Router;
}); });
export { Router };
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
export enum Pages {
home = 'home',
login = 'login',
}
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
path: '/', path: '/',
component: () => import('layouts/MainLayout.vue'), component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/Index.vue') }], children: [
{
path: '',
component: () => import('pages/Index.vue'),
name: Pages.home,
},
{
path: '/login',
component: () => import('pages/login/index.vue'),
name: Pages.login,
},
],
}, },
// Always leave this as last one, // Always leave this as last one,
......
import { AxiosResponse } from 'axios';
import { Notify } from 'quasar';
import { API_PATHS } from 'src/assets/configurations';
import { api, BaseResponseBody } from 'src/boot/axios';
import { i18n } from 'src/boot/i18n';
import { Router } from 'src/router';
import { Pages } from 'src/router/routes';
import { ActionTree } from 'vuex'; import { ActionTree } from 'vuex';
import { StateInterface } from '../index'; import { StateInterface } from '../index';
import { AuthenticationState } from './state'; import { AuthenticationState, MenuItem, Page, PageRole } from './state';
const actions: ActionTree<AuthenticationState, StateInterface> = { const actions: ActionTree<AuthenticationState, StateInterface> = {
someAction (/* context */) { async callAPILogin(context, payload: { userName: string; password: string }) {
// your code try {
const response = (await api({
url: API_PATHS.login,
method: 'POST',
data: payload,
})) as AxiosResponse<BaseResponseBody<unknown>>;
if (!response.data.error.code) {
const res = response as AxiosResponse<
BaseResponseBody<AuthenticationState>
>;
const menuList = res.data.data.pageRoles.reduce(
(acc: MenuItem[], role) => {
if (!role.parentId) {
const menuItem: MenuItem = {
...role,
};
menuItem.children = [];
menuItem.children.push(
...res.data.data.pageRoles.filter((r) => r.parentId === role.id)
);
acc.push(menuItem);
}
return acc;
},
[]
);
context.commit('setMenuList', menuList);
context.commit('setToken', res.data.data.token);
context.commit('setUserInfo', res.data.data.user);
context.commit('setPageInfo', res.data.data.pageRoles);
context.commit('setGroupInfo', res.data.data.groups);
Notify.create({
type: 'positive',
message: i18n.global.t('loginSuccess'),
});
await Router.push({ name: Pages.home });
}
} catch (error) {}
},
async logOut(context) {
context.commit('setToken', undefined);
context.commit('setUserInfo', undefined);
context.commit('setPageInfo', undefined);
context.commit('setGroupInfo', undefined);
Notify.create({
type: 'positive',
message: i18n.global.t('logoutSuccess'),
});
await Router.push({ name: Pages.login });
},
async getListPages(context) {
try {
const response = (await api({
url: API_PATHS.getListPages,
method: 'GET',
params: {},
})) as AxiosResponse<BaseResponseBody<unknown>>;
if (!response.data.error.code) {
const res = response as AxiosResponse<
BaseResponseBody<{ pages: Page[]; roles: PageRole[] | null }>
>;
const pageList = res.data.data.pages.reduce((acc: Page[], page) => {
if (page.id === 1 || page.parentId) {
page.formatted_role_list = [];
if (res.data.data.roles) {
page.formatted_role_list.push(
...res.data.data.roles.filter(
(page_role) => page_role.pageId === page.id
)
);
}
acc.push(page);
}
return acc;
}, []);
context.commit('setPageList', pageList);
} }
} catch (error) {}
},
}; };
export default actions; export default actions;
import { MutationTree } from 'vuex'; import { MutationTree } from 'vuex';
import { AuthenticationState } from './state'; import { AuthenticationState, MenuItem, Page, RoleInfo } from './state';
const mutation: MutationTree<AuthenticationState> = { const mutation: MutationTree<AuthenticationState> = {
setToken(state) { setToken(state, payload: string) {
state.token = 'aksjhdkajshdk'; state.token = payload;
},
setUserInfo(
state,
payload: {
email: string;
fullName: string;
id: number;
}
) {
state.user = payload;
},
setPageInfo(state, payload: RoleInfo[]) {
state.pageRoles = payload;
},
setGroupInfo(
state,
payload: {
group_id: number;
group_name: string;
}[]
) {
state.groups = payload;
},
setPageList(state, payload: Page[]) {
state.pageList = payload;
},
setMenuList(state, payload: MenuItem[]) {
state.menuList = payload;
}, },
}; };
......
export type RoleInfo = {
description: null | string;
id: number;
level: number;
menuIndex: number;
pageIcon: string;
pageName: string;
pageUrl: null | string;
parentId: number;
roles: string;
};
export type Page = {
defineKey?: string | null;
id: number;
level: number;
menuIndex: number;
pageIcon: string;
pageName: string;
pageUrl: string;
parentId: number;
roleList?: string | null;
formatted_role_list?: PageRole[];
};
export type PageRole = {
defineKey?: string | null;
description: string | null;
pageId: number;
roleId: number;
roleName: string;
status: number;
};
export type MenuItem = {
roles: string;
level: number;
id: number;
pageName: string;
pageUrl: null | string;
pageIcon: string;
description: string | null;
parentId: number;
menuIndex: number;
children?: MenuItem[];
};
export interface AuthenticationState { export interface AuthenticationState {
token?: string token?: string;
user?: {
email: string;
fullName: string;
id: number;
};
pageRoles: RoleInfo[];
groups?: {
group_id: number;
group_name: string;
}[];
pageList: Page[];
menuList: MenuItem[];
} }
function state(): AuthenticationState { function state(): AuthenticationState {
return { return {
token: '' token: '',
pageList: [],
pageRoles: [],
menuList: [],
}; };
} }
......
...@@ -51,6 +51,7 @@ export default store(function (/* { ssrContext } */) { ...@@ -51,6 +51,7 @@ export default store(function (/* { ssrContext } */) {
plugins: [ plugins: [
// eslint-disable-next-line @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-call
createPersistedState({ createPersistedState({
paths: ['authentication'],
storage: { storage: {
// eslint-disable-next-line // eslint-disable-next-line
getItem: (key: string) => ls.get(key), getItem: (key: string) => ls.get(key),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment