feat(auth): enforce login route with global middleware
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, onBeforeUnmount, onMounted } from "vue";
|
import { nextTick, onBeforeUnmount, onMounted } from "vue";
|
||||||
import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.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 CrmCalendarPanel from "~~/app/components/workspace/calendar/CrmCalendarPanel.vue";
|
||||||
import CrmCommunicationsContextSidebar from "~~/app/components/workspace/communications/CrmCommunicationsContextSidebar.vue";
|
import CrmCommunicationsContextSidebar from "~~/app/components/workspace/communications/CrmCommunicationsContextSidebar.vue";
|
||||||
import CrmCommunicationsListSidebar from "~~/app/components/workspace/communications/CrmCommunicationsListSidebar.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 chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw";
|
||||||
import dashboardQuery from "~~/graphql/operations/dashboard.graphql?raw";
|
import dashboardQuery from "~~/graphql/operations/dashboard.graphql?raw";
|
||||||
import getClientTimelineQuery from "~~/graphql/operations/get-client-timeline.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 logoutMutation from "~~/graphql/operations/logout.graphql?raw";
|
||||||
import logPilotNoteMutation from "~~/graphql/operations/log-pilot-note.graphql?raw";
|
import logPilotNoteMutation from "~~/graphql/operations/log-pilot-note.graphql?raw";
|
||||||
import createCalendarEventMutation from "~~/graphql/operations/create-calendar-event.graphql?raw";
|
import createCalendarEventMutation from "~~/graphql/operations/create-calendar-event.graphql?raw";
|
||||||
@@ -533,10 +531,6 @@ const chatArchivingId = ref("");
|
|||||||
const chatThreadPickerOpen = ref(false);
|
const chatThreadPickerOpen = ref(false);
|
||||||
const commPinToggling = ref(false);
|
const commPinToggling = ref(false);
|
||||||
const selectedChatId = ref("");
|
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;
|
let pilotBackgroundPoll: ReturnType<typeof setInterval> | null = null;
|
||||||
const lifecycleNowMs = ref(Date.now());
|
const lifecycleNowMs = ref(Date.now());
|
||||||
let lifecycleClock: ReturnType<typeof setInterval> | null = null;
|
let lifecycleClock: ReturnType<typeof setInterval> | null = null;
|
||||||
@@ -898,23 +892,7 @@ async function loadMe() {
|
|||||||
const authResolved = ref(false);
|
const authResolved = ref(false);
|
||||||
|
|
||||||
async function bootstrapSession() {
|
async function bootstrapSession() {
|
||||||
try {
|
const resetAuthState = () => {
|
||||||
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 {
|
|
||||||
stopCrmRealtime();
|
stopCrmRealtime();
|
||||||
authMe.value = null;
|
authMe.value = null;
|
||||||
pilotMessages.value = [];
|
pilotMessages.value = [];
|
||||||
@@ -923,6 +901,26 @@ async function bootstrapSession() {
|
|||||||
telegramConnectStatus.value = "not_connected";
|
telegramConnectStatus.value = "not_connected";
|
||||||
telegramConnections.value = [];
|
telegramConnections.value = [];
|
||||||
telegramConnectUrl.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 {
|
} finally {
|
||||||
authResolved.value = true;
|
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() {
|
async function logout() {
|
||||||
await gqlFetch<{ logout: { ok: boolean } }>(logoutMutation);
|
await gqlFetch<{ logout: { ok: boolean } }>(logoutMutation);
|
||||||
stopCrmRealtime();
|
stopCrmRealtime();
|
||||||
@@ -996,6 +975,9 @@ async function logout() {
|
|||||||
telegramConnectStatus.value = "not_connected";
|
telegramConnectStatus.value = "not_connected";
|
||||||
telegramConnections.value = [];
|
telegramConnections.value = [];
|
||||||
telegramConnectUrl.value = "";
|
telegramConnectUrl.value = "";
|
||||||
|
if (process.client) {
|
||||||
|
await navigateTo("/login", { replace: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshCrmData() {
|
async function refreshCrmData() {
|
||||||
@@ -4781,18 +4763,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[100dvh] overflow-hidden bg-base-200/35">
|
<div class="h-[100dvh] overflow-hidden bg-base-200/35">
|
||||||
<CrmAuthLoading v-if="!authResolved" />
|
<CrmAuthLoading v-if="!authResolved || !authMe" />
|
||||||
|
|
||||||
<CrmAuthLoginForm
|
|
||||||
v-else-if="!authMe"
|
|
||||||
:phone="loginPhone"
|
|
||||||
:password="loginPassword"
|
|
||||||
:error="loginError"
|
|
||||||
:busy="loginBusy"
|
|
||||||
@update:phone="loginPhone = $event"
|
|
||||||
@update:password="loginPassword = $event"
|
|
||||||
@submit="login"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="grid h-full min-h-0 grid-cols-1 gap-0 lg:grid-cols-[320px_minmax(0,1fr)]">
|
<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