feat(auth): enforce login route with global middleware
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted } from "vue";
|
||||
import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.vue";
|
||||
import CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
|
||||
import CrmCalendarPanel from "~~/app/components/workspace/calendar/CrmCalendarPanel.vue";
|
||||
import CrmCommunicationsContextSidebar from "~~/app/components/workspace/communications/CrmCommunicationsContextSidebar.vue";
|
||||
import CrmCommunicationsListSidebar from "~~/app/components/workspace/communications/CrmCommunicationsListSidebar.vue";
|
||||
@@ -13,7 +12,6 @@ import meQuery from "~~/graphql/operations/me.graphql?raw";
|
||||
import chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw";
|
||||
import dashboardQuery from "~~/graphql/operations/dashboard.graphql?raw";
|
||||
import getClientTimelineQuery from "~~/graphql/operations/get-client-timeline.graphql?raw";
|
||||
import loginMutation from "~~/graphql/operations/login.graphql?raw";
|
||||
import logoutMutation from "~~/graphql/operations/logout.graphql?raw";
|
||||
import logPilotNoteMutation from "~~/graphql/operations/log-pilot-note.graphql?raw";
|
||||
import createCalendarEventMutation from "~~/graphql/operations/create-calendar-event.graphql?raw";
|
||||
@@ -533,10 +531,6 @@ const chatArchivingId = ref("");
|
||||
const chatThreadPickerOpen = ref(false);
|
||||
const commPinToggling = ref(false);
|
||||
const selectedChatId = ref("");
|
||||
const loginPhone = ref("");
|
||||
const loginPassword = ref("");
|
||||
const loginError = ref<string | null>(null);
|
||||
const loginBusy = ref(false);
|
||||
let pilotBackgroundPoll: ReturnType<typeof setInterval> | null = null;
|
||||
const lifecycleNowMs = ref(Date.now());
|
||||
let lifecycleClock: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -898,23 +892,7 @@ async function loadMe() {
|
||||
const authResolved = ref(false);
|
||||
|
||||
async function bootstrapSession() {
|
||||
try {
|
||||
await loadMe();
|
||||
if (!authMe.value) {
|
||||
stopCrmRealtime();
|
||||
pilotMessages.value = [];
|
||||
chatConversations.value = [];
|
||||
clientTimelineItems.value = [];
|
||||
telegramConnectStatus.value = "not_connected";
|
||||
telegramConnections.value = [];
|
||||
telegramConnectUrl.value = "";
|
||||
return;
|
||||
}
|
||||
await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData(), loadTelegramConnectStatus()]);
|
||||
if (process.client) {
|
||||
startCrmRealtime();
|
||||
}
|
||||
} catch {
|
||||
const resetAuthState = () => {
|
||||
stopCrmRealtime();
|
||||
authMe.value = null;
|
||||
pilotMessages.value = [];
|
||||
@@ -923,6 +901,26 @@ async function bootstrapSession() {
|
||||
telegramConnectStatus.value = "not_connected";
|
||||
telegramConnections.value = [];
|
||||
telegramConnectUrl.value = "";
|
||||
};
|
||||
|
||||
try {
|
||||
await loadMe();
|
||||
if (!authMe.value) {
|
||||
resetAuthState();
|
||||
if (process.client) {
|
||||
await navigateTo("/login", { replace: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData(), loadTelegramConnectStatus()]);
|
||||
if (process.client) {
|
||||
startCrmRealtime();
|
||||
}
|
||||
} catch {
|
||||
resetAuthState();
|
||||
if (process.client) {
|
||||
await navigateTo("/login", { replace: true });
|
||||
}
|
||||
} finally {
|
||||
authResolved.value = true;
|
||||
}
|
||||
@@ -963,25 +961,6 @@ async function archiveChatConversation(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function login() {
|
||||
loginError.value = null;
|
||||
loginBusy.value = true;
|
||||
try {
|
||||
await gqlFetch<{ login: { ok: boolean } }>(loginMutation, {
|
||||
phone: loginPhone.value,
|
||||
password: loginPassword.value,
|
||||
});
|
||||
await loadMe();
|
||||
startPilotBackgroundPolling();
|
||||
startCrmRealtime();
|
||||
await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData(), loadTelegramConnectStatus()]);
|
||||
} catch (e: any) {
|
||||
loginError.value = e?.data?.message || e?.message || "Login failed";
|
||||
} finally {
|
||||
loginBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await gqlFetch<{ logout: { ok: boolean } }>(logoutMutation);
|
||||
stopCrmRealtime();
|
||||
@@ -996,6 +975,9 @@ async function logout() {
|
||||
telegramConnectStatus.value = "not_connected";
|
||||
telegramConnections.value = [];
|
||||
telegramConnectUrl.value = "";
|
||||
if (process.client) {
|
||||
await navigateTo("/login", { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCrmData() {
|
||||
@@ -4781,18 +4763,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
|
||||
<template>
|
||||
<div class="h-[100dvh] overflow-hidden bg-base-200/35">
|
||||
<CrmAuthLoading v-if="!authResolved" />
|
||||
|
||||
<CrmAuthLoginForm
|
||||
v-else-if="!authMe"
|
||||
:phone="loginPhone"
|
||||
:password="loginPassword"
|
||||
:error="loginError"
|
||||
:busy="loginBusy"
|
||||
@update:phone="loginPhone = $event"
|
||||
@update:password="loginPassword = $event"
|
||||
@submit="login"
|
||||
/>
|
||||
<CrmAuthLoading v-if="!authResolved || !authMe" />
|
||||
|
||||
<template v-else>
|
||||
<div class="grid h-full min-h-0 grid-cols-1 gap-0 lg:grid-cols-[320px_minmax(0,1fr)]">
|
||||
|
||||
20
frontend/app/middleware/auth.global.ts
Normal file
20
frontend/app/middleware/auth.global.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const isLoginRoute = to.path === "/login";
|
||||
|
||||
const fetcher = import.meta.server ? useRequestFetch() : $fetch;
|
||||
let authenticated = false;
|
||||
try {
|
||||
await fetcher("/api/auth/session", { method: "GET" });
|
||||
authenticated = true;
|
||||
} catch {
|
||||
authenticated = false;
|
||||
}
|
||||
|
||||
if (!authenticated && !isLoginRoute) {
|
||||
return navigateTo("/login");
|
||||
}
|
||||
|
||||
if (authenticated && isLoginRoute) {
|
||||
return navigateTo("/");
|
||||
}
|
||||
});
|
||||
58
frontend/app/pages/login.vue
Normal file
58
frontend/app/pages/login.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
|
||||
import loginMutation from "~~/graphql/operations/login.graphql?raw";
|
||||
|
||||
const phone = ref("");
|
||||
const password = ref("");
|
||||
const error = ref<string | null>(null);
|
||||
const busy = ref(false);
|
||||
|
||||
async function gqlFetch<TData>(query: string, variables?: Record<string, unknown>) {
|
||||
const headers = process.server ? useRequestHeaders(["cookie"]) : undefined;
|
||||
const result = await $fetch<{ data?: TData; errors?: Array<{ message: string }> }>("/api/graphql", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: { query, variables },
|
||||
});
|
||||
|
||||
if (result.errors?.length) {
|
||||
throw new Error(result.errors[0]?.message || "GraphQL request failed");
|
||||
}
|
||||
|
||||
if (!result.data) {
|
||||
throw new Error("GraphQL returned empty payload");
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
error.value = null;
|
||||
busy.value = true;
|
||||
try {
|
||||
await gqlFetch<{ login: { ok: boolean } }>(loginMutation, {
|
||||
phone: phone.value,
|
||||
password: password.value,
|
||||
});
|
||||
await navigateTo("/", { replace: true });
|
||||
} catch (e: any) {
|
||||
error.value = e?.data?.message || e?.message || "Login failed";
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[100dvh] overflow-hidden bg-base-200/35">
|
||||
<CrmAuthLoginForm
|
||||
:phone="phone"
|
||||
:password="password"
|
||||
:error="error"
|
||||
:busy="busy"
|
||||
@update:phone="phone = $event"
|
||||
@update:password="password = $event"
|
||||
@submit="submit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
6
frontend/server/api/auth/session.get.ts
Normal file
6
frontend/server/api/auth/session.get.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { getAuthContext } from "../../utils/auth";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await getAuthContext(event);
|
||||
return { authenticated: true };
|
||||
});
|
||||
Reference in New Issue
Block a user