working on auth

This commit is contained in:
prabidhi 2025-12-10 14:22:08 +05:45
parent 9705e0d150
commit 7e944ac917
13 changed files with 554 additions and 8 deletions

View File

@ -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/",
};

View File

8
src/dtos/User/auth.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
export type LoginPayload = {
username: string;
password: string;
};
export type LoginResponse = {
access: string;
};

6
src/dtos/User/user.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
export type UserDetail = {
id: string;
username: string;
is_active: boolean;
is_superuser?: boolean;
};

View File

@ -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')
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");
};

74
src/services/API/api.ts Normal file
View File

@ -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;

View File

@ -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;
}

85
src/stores/User/Auth.ts Normal file
View File

@ -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));
}

36
src/stores/User/User.ts Normal file
View File

@ -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));
}

View File

@ -0,0 +1,13 @@
import type { RouteRecordRaw } from "vue-router";
const authChildren: Array<RouteRecordRaw> = [
{
path: "/",
name: "dashboard",
component: () => import("@/views/Dashboard/Components/Dashboard.vue"),
meta: {
permission: "",
},
},
]
export default authChildren;

View File

@ -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;

120
src/views/Auth/Login.vue Normal file
View File

@ -0,0 +1,120 @@
<template>
<div style="height: 100vh; background-image: url(/svg/large-triangles.svg); background-size: cover">
<div class="w-[400px] mx-auto">
<div class="flex justify-center">
<img src="/img/Dolphin/dolphin-logo.png" class="w-36 my-5" />
</div>
<div class="border border-secondary-100 rounded-xs px-10 py-10 bg-white select-none">
<div class="text-center" :class="getLoginError ? '' : 'mb-[10px]'">
<p class="text-[30px] text-semibold">Sign In</p>
<div class="text-sm font-normal">Fill your detail to sign in to Dolphin DartaChalani.</div>
</div>
<div class="mt-[12px] mb-[-19px]" v-if="getLoginError">
<div class="text-sm text-red-700 font-semibold text-center flex gap-[15px]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5 -mr-3"
>
<path
fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
clip-rule="evenodd"
/>
</svg>
The username or password you entered is incorrect.
</div>
</div>
<form @submit.prevent="onLogin" class="pt-10">
<div class="">
<label for="username">Username</label>
<div class="my-1">
<input
v-model="getLoginDetail.username"
id="username"
name="username"
type="text"
autocomplete="username"
required
class="p-[10px] max-h-[40px]! h-[40px]! text-[14px] w-full"
/>
</div>
</div>
<div class="my-[15px]">
<label for="username">Password</label>
<div class="my-1 flex">
<!--When show password is false-->
<input
v-model="getLoginDetail.password"
id="password"
name="password"
type="password"
v-if="!showPassword"
autocomplete="current-password"
required
class="p-[10px] max-h-[40px]! h-[40px]! text-[14px] w-full"
/>
<!--When show password is true-->
<input
v-model="getLoginDetail.password"
id="password"
name="password"
type="text"
v-else
autocomplete="current-password"
required
class="p-[10px] max-h-[40px]! h-[40px]! text-[14px] w-full"
/>
<span
class="bg-gray-100 flex w-[50px] border-r border-t border-b border-gray-300 hover:bg-gray-200 cursor-pointer transition-all"
@click="showPassword = !showPassword"
>
<div class="m-auto">
<Icons
name="EyeOff"
size="20"
class="text-lg text-gray-900 mt-1 mr-[-1px]"
v-if="!showPassword"
/>
<Icons name="Eye" class="text-lg text-gray-900 mt-1" size="20" v-else />
</div>
</span>
</div>
</div>
<button type="submit" class="btn w-full max-h-[40px]! h-[40px]! text-lg!">Sign In</button>
</form>
</div>
<div class="text-xs text-center my-5 select-none">
<div>
© 2024
<a href="https://mavorion.com" class="text-[#224CAD] underline">Mavorion Systems</a>
. All rights reserved.
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useAuth } from "@/stores/User/Auth";
import { storeToRefs } from "pinia";
import { onMounted, ref } from "vue";
const authStore = useAuth();
const showPassword = ref(false);
const { getLoginDetail, getLoginError } = storeToRefs(authStore);
const onLogin = () => {
authStore.login();
};
onMounted(() => {
authStore.csrfGenerate();
});
</script>

View File

@ -0,0 +1,107 @@
<template>
<div class="text-[16px] font-bold pb-[15px]">Documents Details</div>
<div class="grid grid-cols-4 gap-[15px] rounded-default">
<div class="p-[15px] bg-white">
<div class="text-[#2f2f2f]">Total Documents</div>
<div class="text-[24px] text-[#333131]">
{{ data.total }}
</div>
</div>
<div class="p-[15px] bg-white relative">
<div class="text-[#2f2f2f]">Incoming (Darta)</div>
<div class="text-[24px] text-[#333131]">
{{ data.incoming }}
</div>
<!-- <div class="text-[#2f2f2f]">({{ getPercentage(data.total, data.scanned) }} % of {{ data.total }})</div>
<div class="w-[100px] h-[70px] ml-auto absolute right-0 top-[20px]">
<v-chart :option="getRadialOption(getPercentage(data.total, data.scanned))" />
</div> -->
</div>
<div class="p-[15px] bg-white relative">
<div class="w-fit">
<div class="text-[#2f2f2f]">Outgoing (Chalani)</div>
<div class="text-[24px] text-[#333131]">
{{ data.outgoing }}
</div>
<!-- <div class="text-[#2f2f2f]">({{ getPercentage(data.total, data.pending) }} % of {{ data.total }})</div> -->
</div>
<!-- <div class="w-[100px] h-[70px] ml-auto absolute right-0 top-[20px]">
<v-chart :option="getRadialOption(getPercentage(data.total, data.pending))" />
</div> -->
</div>
<div class="p-[15px] bg-white relative">
<div class="w-fit">
<div class="text-[#2f2f2f]">Tippani</div>
<div class="text-[24px] text-[#333131]">
{{ data.tippani }}
</div>
<!-- <div class="text-[#2f2f2f]">({{ getPercentage(data.total, data.pending) }} % of {{ data.total }})</div> -->
</div>
</div>
</div>
</template>
<script setup lang="ts">
// import api from "@/services/API/api";
import { ref, onMounted } from "vue";
// import VChart from "vue-echarts";
// import { use } from "echarts/core";
// import { CanvasRenderer } from "echarts/renderers";
// import { PieChart } from "echarts/charts";
// import { TitleComponent, TooltipComponent, LegendComponent } from "echarts/components";
// use([CanvasRenderer, PieChart, TitleComponent, TooltipComponent, LegendComponent]);
const data = ref({
total: 6,
incoming: 3,
outgoing: 2,
tippani: 1,
// schedule: {
// name: "",
// running: 0,
// },
});
// const getRadialOption = (percent: number) => ({
// series: [
// {
// type: "pie",
// radius: ["80%", "100%"],
// avoidLabelOverlap: false,
// silent: true,
// data: [
// { value: percent, itemStyle: { color: "#4a8e57" } },
// { value: 100 - percent, itemStyle: { color: "#e6edf7" } },
// ],
// label: {
// show: true,
// position: "center",
// formatter: `${Math.round(percent)}%`,
// fontSize: 12,
// color: "#6b7280", // Tailwind's gray-500
// fontWeight: 500,
// },
// labelLine: { show: false },
// emphasis: { disabled: true },
// hoverAnimation: false,
// },
// ],
// tooltip: { show: false },
// legend: { show: false },
// });
// onMounted(() => {
// api.get("/canteen/meal-report-count/").then((e) => {
// data.value = e.data.data;
// });
// });
// const getPercentage = (total: number, by: number) => {
// if (!by) {
// return 0;
// }
// return parseFloat(((by / total) * 100).toFixed(2));
// };
</script>