diff --git a/src/core/endpoints/auth.ts b/src/core/endpoints/auth.ts new file mode 100644 index 0000000..f9c67e9 --- /dev/null +++ b/src/core/endpoints/auth.ts @@ -0,0 +1,7 @@ +export const authAPI = { + csrfGenerate: "users/auth/csrf/", + login: "users/auth/login/", + refresh: "users/auth/refresh/", + logout: "users/auth/logout/", + self: "users/self/", +}; diff --git a/src/core/endpoints/dartachalani.ts b/src/core/endpoints/dartachalani.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/dtos/User/auth.d.ts b/src/dtos/User/auth.d.ts new file mode 100644 index 0000000..c5129f5 --- /dev/null +++ b/src/dtos/User/auth.d.ts @@ -0,0 +1,8 @@ +export type LoginPayload = { + username: string; + password: string; +}; + +export type LoginResponse = { + access: string; +}; diff --git a/src/dtos/User/user.d.ts b/src/dtos/User/user.d.ts new file mode 100644 index 0000000..170b99e --- /dev/null +++ b/src/dtos/User/user.d.ts @@ -0,0 +1,6 @@ +export type UserDetail = { + id: string; + username: string; + is_active: boolean; + is_superuser?: boolean; +}; diff --git a/src/main.ts b/src/main.ts index 2909c5f..95f01f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,40 @@ -import { createApp } from 'vue' -import { createPinia } from 'pinia' +import "./assets/css/main.css"; -import App from './App.vue' -import router from './router' +import "@/assets/ts/main.ts"; -const app = createApp(App) +import { createApp } from "vue"; +import { createPinia } from "pinia"; -app.use(createPinia()) -app.use(router) +import App from "./App.vue"; +import { loadComponents } from "./utils/setup/component"; +import initRouter from "./utils/setup/routerSetup"; +import { loadDirectives } from "./utils/setup/directives"; +import { loadPlugins } from "./utils/setup/plugins"; -app.mount('#app') \ No newline at end of file +interface AppConfig { + API_URL: string; + APP_ENVIRONMENT: "PRODUCTION" | "DEVELOPMENT"; + + CLIENT_NAME: string; + CLIENT_LOGO: string; + CLIENT_PAGE_TITLE: string; + CLIENT_PAGE_TITLE_LOGO: string; + WEBSOCKET_URL: string; +} +declare global { + interface Window { + APP_CONFIG: AppConfig; + } +} + +export const initAPP = () => { + const app = createApp(App); + app.use(createPinia()); + + initRouter(app); + loadComponents(app); + loadDirectives(app); + loadPlugins(app); + + app.mount("#app"); +}; diff --git a/src/services/API/api.ts b/src/services/API/api.ts new file mode 100644 index 0000000..d6bdb02 --- /dev/null +++ b/src/services/API/api.ts @@ -0,0 +1,74 @@ +import axios from "axios"; +import { authAPI } from "@/core/endpoints/auth"; +import { getCSRFTokenFromCookie } from "./utilities"; +import { useUser } from "@/stores/User/User"; + +const apiURL: string = window.APP_CONFIG.API_URL; + +const api = axios.create({ + baseURL: apiURL, + headers: { + "Content-Type": "application/json", + }, + withCredentials: true, +}); + +api.interceptors.request.use( + function (config) { + // const authStore = useAuth(); + // const { token } = storeToRefs(authStore); + // //Attach the Bearer Token if it exist during api request + // if (token.value) { + // config.headers["Authorization"] = `Bearer ${token.value}`; + // } + const csrfToken = getCSRFTokenFromCookie(); + if (["post", "put", "patch", "delete"].includes(config.method || "")) { + config.headers["X-CSRFToken"] = csrfToken; + } + return config; + }, + function (error) { + return Promise.reject(error); + } +); + +api.interceptors.response.use( + function (response) { + return response; + }, + async function (error) { + const { + config, + response: { status }, + } = error; + const originalRequest = config; + if (status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + const userStore = useUser(); + const csrfToken = getCSRFTokenFromCookie(); + if (["post", "put", "patch", "delete"].includes(config.method || "")) { + originalRequest.headers["X-CSRFToken"] = csrfToken; + } + try { + await axios.post(apiURL + authAPI.refresh, null, { + withCredentials: true, + headers: { + "X-CSRFToken": csrfToken, + }, + }); + userStore.isAuthenticated = true; + // const token = response.data.data.access; + // authStore.token = token; + // originalRequest.headers["Authorization"] = `Bearer ${token}`; + return await axios(originalRequest); + } catch (e) { + console.error("Token refresh failed", e); + userStore.isAuthenticated = false; + throw e; + } + } + return Promise.reject(error); + } +); + +export default api; diff --git a/src/services/API/utilities.ts b/src/services/API/utilities.ts new file mode 100644 index 0000000..77a9fd3 --- /dev/null +++ b/src/services/API/utilities.ts @@ -0,0 +1,16 @@ +export function getCSRFTokenFromCookie(): string | null { + const name = "csrftoken="; + const cookies = document.cookie.split(";"); + + for (let i = 0; i < cookies.length; i++) { + const c = cookies[i]; + if (c) { + const cookie = c.trim(); + if (cookie.startsWith(name)) { + return decodeURIComponent(cookie.substring(name.length)); + } + } + } + + return null; +} diff --git a/src/stores/User/Auth.ts b/src/stores/User/Auth.ts new file mode 100644 index 0000000..48ad4ee --- /dev/null +++ b/src/stores/User/Auth.ts @@ -0,0 +1,85 @@ +import { authAPI } from "@/core/endpoints/auth"; +import api from "@/services/API/api"; +import type { LoginPayload } from "@/dtos/User/auth"; +import router from "@/router"; +import { acceptHMRUpdate, defineStore } from "pinia"; +import { useUser } from "./User"; +import { Toast } from "dolphin-components"; + +export const useAuth = defineStore("auth", { + state: () => ({ + loginDetails: { + username: "", + password: "", + } as LoginPayload, + token: null as null | string, + loginError: false, + }), + + getters: { + getLoginDetail(state) { + return state.loginDetails; + }, + getLoginError(state) { + return state.loginError; + }, + }, + + actions: { + async csrfGenerate() { + try { + await api.get(authAPI.csrfGenerate); + } catch (error) { + console.error(error); + } + }, + async login() { + try { + // const response = await api.post(authAPI.login, this.loginDetails); + await api.post(authAPI.login, this.loginDetails); + Toast.success("Logged in successfully."); + router.push({ name: "dashboard" }); + } catch { + Toast.error("Unable to login!"); + this.hasLoginError(); + } + }, + async logout() { + const userStore = useUser(); + try { + await api.post(authAPI.logout); + userStore.$reset(); + this.loginDetails.username = ""; + this.loginDetails.password = ""; + this.token = null; + Toast.success("Logged out successfully."); + router.push({ name: "login" }); + } catch (error) { + console.error(error); + } + }, + async refreshToken() { + try { + await api.post(authAPI.refresh); + // if (response.data) { + // this.token = response.data.data.access; + // } + return true; + } catch { + return false; + } + }, + hasLoginError(status: boolean = true) { + if (status) { + this.loginError = true; + setTimeout(() => { + this.loginError = false; + }, 5000); + } + }, + }, +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot)); +} diff --git a/src/stores/User/User.ts b/src/stores/User/User.ts new file mode 100644 index 0000000..7e24a30 --- /dev/null +++ b/src/stores/User/User.ts @@ -0,0 +1,36 @@ +import { authAPI } from "@/core/endpoints/auth"; +import api from "@/services/API/api"; +import type { UserDetail } from "@/dtos/User/user"; +// import { Toast } from "dolphin-components"; +import { acceptHMRUpdate, defineStore } from "pinia"; + +export const useUser = defineStore("user", { + state: () => ({ + isAuthenticated: false, + user: {} as UserDetail, + }), + getters: { + getUser(state) { + return state.user; + }, + }, + actions: { + async fetchUserDetail() { + try { + const response = await api.get(authAPI.self); + this.user = response.data.data; + this.isAuthenticated = true; + } catch (error: any) { + if (error.response.status == 403) { + // Toast.error("You do not have the permissions to perform this action."); + } else { + // Toast.error("Login failed!"); + } + } + }, + }, +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useUser, import.meta.hot)); +} diff --git a/src/utils/setup/SidebarItems.ts b/src/utils/setup/SidebarItems.ts new file mode 100644 index 0000000..738da9c --- /dev/null +++ b/src/utils/setup/SidebarItems.ts @@ -0,0 +1,13 @@ +import type { RouteRecordRaw } from "vue-router"; +const authChildren: Array = [ + { + path: "/", + name: "dashboard", + component: () => import("@/views/Dashboard/Components/Dashboard.vue"), + meta: { + permission: "", + }, + }, +] + +export default authChildren; \ No newline at end of file diff --git a/src/utils/setup/routerSetup.ts b/src/utils/setup/routerSetup.ts new file mode 100644 index 0000000..3dc9aa9 --- /dev/null +++ b/src/utils/setup/routerSetup.ts @@ -0,0 +1,46 @@ +import router from "@/router"; +// import { useAuth } from "@/stores/User/Auth"; +import { useUser } from "@/stores/User/User"; +import type { App } from "vue"; +// import { hasPermission } from "../common/permission"; + +const initRouter = (app: App) => { + const useUserStore = useUser(); + router.beforeEach(async (to, from, next) => { + await useUserStore.fetchUserDetail(); + if (to.name == "login") { + if (useUserStore.isAuthenticated) { + next({ + name: "dashboard", + }); + } else { + next(); + } + } else { + if (to.meta.requireAuth) { + if (useUserStore.isAuthenticated) { + if (to.meta.permission) { + // if (hasPermission(to.meta.permission)) { + // next(); + // } else { + // next({ + // name: "404", + // }); + // } + } else { + next(); + } + } else { + next({ + name: "login", + }); + } + } else { + next(); + } + } + }); + app.use(router); +}; + +export default initRouter; diff --git a/src/views/Auth/Login.vue b/src/views/Auth/Login.vue new file mode 100644 index 0000000..0e1b1c8 --- /dev/null +++ b/src/views/Auth/Login.vue @@ -0,0 +1,120 @@ + + + diff --git a/src/views/Dashboard/Components/Dashboard.vue b/src/views/Dashboard/Components/Dashboard.vue new file mode 100644 index 0000000..be11e25 --- /dev/null +++ b/src/views/Dashboard/Components/Dashboard.vue @@ -0,0 +1,107 @@ + + +