mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-10-27 00:04:23 -04:00
fix: Re-write Nuxt auth backend and get rid of sidebase auth (#6322)
This commit is contained in:
159
frontend/composables/useAuthBackend.ts
Normal file
159
frontend/composables/useAuthBackend.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { ref, computed } from "vue";
|
||||
import type { UserOut } from "~/lib/api/types/user";
|
||||
|
||||
interface AuthData {
|
||||
value: UserOut | null;
|
||||
}
|
||||
|
||||
interface AuthStatus {
|
||||
value: "loading" | "authenticated" | "unauthenticated";
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
data: AuthData;
|
||||
status: AuthStatus;
|
||||
signIn: (credentials: FormData, options?: { redirect?: boolean }) => Promise<void>;
|
||||
signOut: (callbackUrl?: string) => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
getSession: () => Promise<void>;
|
||||
setToken: (token: string | null) => void;
|
||||
}
|
||||
|
||||
const authUser = ref<UserOut | null>(null);
|
||||
const authStatus = ref<"loading" | "authenticated" | "unauthenticated">("unauthenticated");
|
||||
|
||||
export const useAuthBackend = function (): AuthState {
|
||||
const { $axios } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
const tokenName = useRuntimeConfig().public.AUTH_TOKEN;
|
||||
const tokenCookie = useCookie(tokenName);
|
||||
|
||||
function setToken(token: string | null) {
|
||||
tokenCookie.value = token;
|
||||
}
|
||||
|
||||
function handleAuthError(error: any, redirect = false) {
|
||||
// Only clear token on auth errors, not network errors
|
||||
if (error?.response?.status === 401) {
|
||||
setToken(null);
|
||||
authUser.value = null;
|
||||
authStatus.value = "unauthenticated";
|
||||
if (redirect) {
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getSession(): Promise<void> {
|
||||
if (!tokenCookie.value) {
|
||||
authUser.value = null;
|
||||
authStatus.value = "unauthenticated";
|
||||
return;
|
||||
}
|
||||
|
||||
authStatus.value = "loading";
|
||||
try {
|
||||
const { data } = await $axios.get<UserOut>("/api/users/self");
|
||||
authUser.value = data;
|
||||
authStatus.value = "authenticated";
|
||||
}
|
||||
catch (error: any) {
|
||||
handleAuthError(error);
|
||||
authStatus.value = "unauthenticated";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function signIn(credentials: FormData): Promise<void> {
|
||||
authStatus.value = "loading";
|
||||
|
||||
try {
|
||||
const response = await $axios.post("/api/auth/token", credentials, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
|
||||
const { access_token } = response.data;
|
||||
setToken(access_token);
|
||||
await getSession();
|
||||
}
|
||||
catch (error) {
|
||||
authStatus.value = "unauthenticated";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function signOut(callbackUrl: string = ""): Promise<void> {
|
||||
try {
|
||||
await $axios.post("/api/auth/logout");
|
||||
}
|
||||
catch (error) {
|
||||
// Continue with logout even if API call fails
|
||||
console.warn("Logout API call failed:", error);
|
||||
}
|
||||
finally {
|
||||
setToken(null);
|
||||
authUser.value = null;
|
||||
authStatus.value = "unauthenticated";
|
||||
await router.push(callbackUrl || "/login");
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
if (!tokenCookie.value) return;
|
||||
|
||||
try {
|
||||
const response = await $axios.get("/api/auth/refresh");
|
||||
const { access_token } = response.data;
|
||||
setToken(access_token);
|
||||
await getSession();
|
||||
}
|
||||
catch (error: any) {
|
||||
handleAuthError(error, true);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh user data periodically when authenticated
|
||||
if (import.meta.client) {
|
||||
let refreshInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
watch(() => authStatus.value, (status) => {
|
||||
if (status === "authenticated") {
|
||||
refreshInterval = setInterval(() => {
|
||||
if (tokenCookie.value) {
|
||||
getSession().catch(() => {
|
||||
// Ignore errors in background refresh
|
||||
});
|
||||
}
|
||||
}, 5 * 60 * 1000); // 5 minutes
|
||||
}
|
||||
else {
|
||||
// Clear interval when not authenticated
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
}
|
||||
|
||||
// Initialize auth state if token exists
|
||||
if (import.meta.client && tokenCookie.value && authStatus.value === "unauthenticated") {
|
||||
getSession().catch((error: any) => {
|
||||
handleAuthError(error);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data: computed(() => authUser.value),
|
||||
status: computed(() => authStatus.value),
|
||||
signIn,
|
||||
signOut,
|
||||
refresh,
|
||||
getSession,
|
||||
setToken,
|
||||
};
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { useAuthBackend } from "~/composables/useAuthBackend";
|
||||
import type { UserOut } from "~/lib/api/types/user";
|
||||
|
||||
export const useMealieAuth = function () {
|
||||
const auth = useAuth();
|
||||
const { setToken } = useAuthState();
|
||||
const auth = useAuthBackend();
|
||||
const { $axios } = useNuxtApp();
|
||||
|
||||
// User Management
|
||||
@@ -40,7 +40,7 @@ export const useMealieAuth = function () {
|
||||
async function oauthSignIn() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const { data: token } = await $axios.get<{ access_token: string; token_type: "bearer" }>("/api/auth/oauth/callback", { params });
|
||||
setToken(token.access_token);
|
||||
auth.setToken(token.access_token);
|
||||
await auth.getSession();
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ export const useMealieAuth = function () {
|
||||
loggedIn,
|
||||
signIn: auth.signIn,
|
||||
signOut: auth.signOut,
|
||||
signUp: auth.signUp,
|
||||
refresh: auth.refresh,
|
||||
oauthSignIn,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user