diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 695ac4710..d67f3de1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,7 +77,7 @@ jobs: sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/sqlite.md sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/postgres.md sed -i 's/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' pyproject.toml - sed -i 's/^\s*"version": "[^"]*"/"version": "${{ env.VERSION_NUM }}"/' frontend/package.json + sed -i 's/\("version": "\)[^"]*"/\1${{ env.VERSION_NUM }}"/' frontend/package.json - name: Create Pull Request uses: peter-evans/create-pull-request@v6 diff --git a/frontend/components/Layout/LayoutParts/AppHeader.vue b/frontend/components/Layout/LayoutParts/AppHeader.vue index 08c3e0002..6f61b121e 100644 --- a/frontend/components/Layout/LayoutParts/AppHeader.vue +++ b/frontend/components/Layout/LayoutParts/AppHeader.vue @@ -128,7 +128,7 @@ export default defineNuxtComponent({ async function logout() { try { - await $auth.signOut({ callbackUrl: "/login?direct=1" }); + await $auth.signOut("/login?direct=1"); } catch (e) { console.error(e); diff --git a/frontend/composables/useAuthBackend.ts b/frontend/composables/useAuthBackend.ts new file mode 100644 index 000000000..1d7660514 --- /dev/null +++ b/frontend/composables/useAuthBackend.ts @@ -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; + signOut: (callbackUrl?: string) => Promise; + refresh: () => Promise; + getSession: () => Promise; + setToken: (token: string | null) => void; +} + +const authUser = ref(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 { + if (!tokenCookie.value) { + authUser.value = null; + authStatus.value = "unauthenticated"; + return; + } + + authStatus.value = "loading"; + try { + const { data } = await $axios.get("/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 { + 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 { + 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 { + 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, + }; +}; diff --git a/frontend/composables/useMealieAuth.ts b/frontend/composables/useMealieAuth.ts index 6e73c1a0d..ac662c17d 100644 --- a/frontend/composables/useMealieAuth.ts +++ b/frontend/composables/useMealieAuth.ts @@ -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, }; diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 293a1a097..2a5513eb2 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -9,7 +9,6 @@ export default defineNuxtConfig({ modules: [ "@vite-pwa/nuxt", "@nuxtjs/i18n", - "@sidebase/nuxt-auth", "@nuxt/fonts", "vuetify-nuxt-module", "@nuxt/eslint", @@ -126,29 +125,6 @@ export default defineNuxtConfig({ baseURL: process.env.SUB_PATH || "", }, - auth: { - isEnabled: true, - // disableServerSideAuth: true, - originEnvKey: "AUTH_ORIGIN", - baseURL: "/api", - provider: { - type: "local", - endpoints: { - signIn: { path: "/auth/token", method: "post" }, - signOut: { path: "/auth/logout", method: "post" }, - getSession: { path: "/users/self", method: "get" }, - }, - token: { - signInResponseTokenPointer: "/access_token", - type: "Bearer", - cookieName: AUTH_TOKEN, - }, - pages: { - login: "/login", - }, - }, - }, - // eslint rules eslint: { config: { diff --git a/frontend/package.json b/frontend/package.json index 43197de99..bececf375 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mealie", -"version": "3.3.1", + "version": "3.3.1", "private": true, "scripts": { "dev": "nuxt dev", @@ -21,7 +21,6 @@ "@mdi/js": "^7.4.47", "@nuxt/fonts": "^0.11.4", "@nuxtjs/i18n": "^9.2.1", - "@sidebase/nuxt-auth": "^1.1.0", "@vite-pwa/nuxt": "^0.10.6", "@vueuse/core": "^12.7.0", "axios": "^1.8.1", diff --git a/frontend/pages/admin.vue b/frontend/pages/admin.vue index c71d8e422..99ff50c11 100644 --- a/frontend/pages/admin.vue +++ b/frontend/pages/admin.vue @@ -1,6 +1,6 @@ diff --git a/frontend/pages/g/[groupSlug]/cookbooks/index.vue b/frontend/pages/g/[groupSlug]/cookbooks/index.vue index 12f2f0281..9ecfd96e4 100644 --- a/frontend/pages/g/[groupSlug]/cookbooks/index.vue +++ b/frontend/pages/g/[groupSlug]/cookbooks/index.vue @@ -145,7 +145,7 @@ import { useCookbookPreferences } from "~/composables/use-users/preferences"; export default defineNuxtComponent({ components: { CookbookEditor, VueDraggable }, - middleware: ["sidebase-auth", "group-only"], + middleware: ["group-only"], setup() { const dialogStates = reactive({ create: false, diff --git a/frontend/pages/g/[groupSlug]/r/create.vue b/frontend/pages/g/[groupSlug]/r/create.vue index e541e2a6c..37ae21311 100644 --- a/frontend/pages/g/[groupSlug]/r/create.vue +++ b/frontend/pages/g/[groupSlug]/r/create.vue @@ -49,7 +49,7 @@ import AdvancedOnly from "~/components/global/AdvancedOnly.vue"; export default defineNuxtComponent({ components: { AdvancedOnly }, - middleware: ["sidebase-auth", "group-only"], + middleware: ["group-only"], setup() { const i18n = useI18n(); const $auth = useMealieAuth(); diff --git a/frontend/pages/g/[groupSlug]/recipes/categories/index.vue b/frontend/pages/g/[groupSlug]/recipes/categories/index.vue index dd247cfd7..f42dd9df8 100644 --- a/frontend/pages/g/[groupSlug]/recipes/categories/index.vue +++ b/frontend/pages/g/[groupSlug]/recipes/categories/index.vue @@ -23,7 +23,7 @@ export default defineNuxtComponent({ components: { RecipeOrganizerPage, }, - middleware: ["sidebase-auth", "group-only"], + middleware: ["group-only"], setup() { const { store, actions } = useCategoryStore(); const i18n = useI18n(); diff --git a/frontend/pages/g/[groupSlug]/recipes/tags/index.vue b/frontend/pages/g/[groupSlug]/recipes/tags/index.vue index 6aec3d1a8..863857f78 100644 --- a/frontend/pages/g/[groupSlug]/recipes/tags/index.vue +++ b/frontend/pages/g/[groupSlug]/recipes/tags/index.vue @@ -23,7 +23,7 @@ export default defineNuxtComponent({ components: { RecipeOrganizerPage, }, - middleware: ["sidebase-auth", "group-only"], + middleware: ["group-only"], setup() { const { store, actions } = useTagStore(); const i18n = useI18n(); diff --git a/frontend/pages/g/[groupSlug]/recipes/timeline.vue b/frontend/pages/g/[groupSlug]/recipes/timeline.vue index 896160cff..6f365b197 100644 --- a/frontend/pages/g/[groupSlug]/recipes/timeline.vue +++ b/frontend/pages/g/[groupSlug]/recipes/timeline.vue @@ -36,7 +36,7 @@ import RecipeTimeline from "~/components/Domain/Recipe/RecipeTimeline.vue"; export default defineNuxtComponent({ components: { RecipeTimeline }, - middleware: ["sidebase-auth", "group-only"], + middleware: ["group-only"], setup() { const i18n = useI18n(); const api = useUserApi(); diff --git a/frontend/pages/g/[groupSlug]/recipes/tools/index.vue b/frontend/pages/g/[groupSlug]/recipes/tools/index.vue index 04e2a5cef..b5f08caaf 100644 --- a/frontend/pages/g/[groupSlug]/recipes/tools/index.vue +++ b/frontend/pages/g/[groupSlug]/recipes/tools/index.vue @@ -28,7 +28,7 @@ export default defineNuxtComponent({ components: { RecipeOrganizerPage, }, - middleware: ["sidebase-auth", "group-only"], + middleware: ["group-only"], setup() { const $auth = useMealieAuth(); const toolStore = useToolStore(); diff --git a/frontend/pages/group/data.vue b/frontend/pages/group/data.vue index 66253db00..6bf455bc6 100644 --- a/frontend/pages/group/data.vue +++ b/frontend/pages/group/data.vue @@ -36,7 +36,7 @@