fix: Re-write Nuxt auth backend and get rid of sidebase auth (#6322)

This commit is contained in:
Michael Genson
2025-10-05 20:43:38 -05:00
committed by GitHub
parent fffe7b05e0
commit 6895b49543
30 changed files with 182 additions and 78 deletions

View File

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

View File

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

View 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,
};
};

View File

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

View File

@@ -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: {

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
definePageMeta({
middleware: ["sidebase-auth", "admin-only"],
middleware: ["admin-only"],
});
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ export default defineNuxtComponent({
components: {
RecipeOrganizerPage,
},
middleware: ["sidebase-auth", "group-only"],
middleware: ["group-only"],
setup() {
const $auth = useMealieAuth();
const toolStore = useToolStore();

View File

@@ -36,7 +36,7 @@
<script lang="ts">
export default defineNuxtComponent({
middleware: ["sidebase-auth", "can-organize-only"],
middleware: ["can-organize-only"],
setup() {
const i18n = useI18n();
const buttonLookup: { [key: string]: string } = {

View File

@@ -47,7 +47,7 @@
import { useGroupSelf } from "~/composables/use-groups";
export default defineNuxtComponent({
middleware: ["sidebase-auth", "can-manage-only"],
middleware: ["can-manage-only"],
setup() {
const { group, actions: groupActions } = useGroupSelf();
const i18n = useI18n();

View File

@@ -127,7 +127,7 @@ const MIGRATIONS = {
};
export default defineNuxtComponent({
middleware: ["sidebase-auth", "advanced-only"],
middleware: ["advanced-only"],
setup() {
const i18n = useI18n();
const { $globals } = useNuxtApp();

View File

@@ -52,7 +52,6 @@ import { useUserApi } from "~/composables/api";
import type { ReportOut } from "~/lib/api/types/reports";
export default defineNuxtComponent({
middleware: "sidebase-auth",
setup() {
const route = useRoute();
const id = route.params.id as string;

View File

@@ -43,7 +43,7 @@ export default defineNuxtComponent({
components: {
HouseholdPreferencesEditor,
},
middleware: ["sidebase-auth", "can-manage-household-only"],
middleware: ["can-manage-household-only"],
setup() {
const { household, actions: householdActions } = useHouseholdSelf();
const i18n = useI18n();

View File

@@ -75,7 +75,6 @@ import { useMealplans } from "~/composables/use-group-mealplan";
import { useUserMealPlanPreferences } from "~/composables/use-users/preferences";
export default defineNuxtComponent({
middleware: ["sidebase-auth"],
setup() {
const route = useRoute();
const router = useRouter();

View File

@@ -183,7 +183,6 @@ export default defineNuxtComponent({
GroupMealPlanRuleForm,
RecipeChips,
},
middleware: ["sidebase-auth"],
props: {
modelValue: {
type: Boolean,

View File

@@ -124,7 +124,6 @@ export default defineNuxtComponent({
components: {
UserAvatar,
},
middleware: ["sidebase-auth"],
setup() {
const $auth = useMealieAuth();
const api = useUserApi();

View File

@@ -199,7 +199,7 @@ interface OptionSection {
}
export default defineNuxtComponent({
middleware: ["sidebase-auth", "advanced-only"],
middleware: ["advanced-only"],
setup() {
const api = useUserApi();
const i18n = useI18n();

View File

@@ -75,7 +75,7 @@ import { alert } from "~/composables/use-toast";
export default defineNuxtComponent({
components: { GroupWebhookEditor },
middleware: ["sidebase-auth", "advanced-only"],
middleware: ["advanced-only"],
setup() {
const i18n = useI18n();
const { actions, webhooks } = useGroupWebhooks();

View File

@@ -304,7 +304,6 @@ export default defineNuxtComponent({
oidcLoggingIn.value = true;
try {
await $auth.oauthSignIn();
window.location.href = "/"; // Reload the app to get the new user
}
catch (error) {
await router.replace("/login?direct=1");
@@ -330,8 +329,7 @@ export default defineNuxtComponent({
formData.append("remember_me", String(form.remember));
try {
await $auth.signIn(formData, { redirect: false });
window.location.href = "/"; // Reload the app to get the new user
await $auth.signIn(formData);
}
catch (error) {
console.log(error);

View File

@@ -131,7 +131,6 @@ import { useShoppingListPreferences } from "~/composables/use-users/preferences"
import type { UserOut } from "~/lib/api/types/user";
export default defineNuxtComponent({
middleware: "sidebase-auth",
setup() {
const $auth = useMealieAuth();
const i18n = useI18n();

View File

@@ -21,7 +21,6 @@ import { useLoggedInState } from "~/composables/use-logged-in-state";
export default defineNuxtComponent({
components: { RecipeCardSection },
middleware: "sidebase-auth",
setup() {
const route = useRoute();
const i18n = useI18n();

View File

@@ -111,7 +111,7 @@ import { useUserApi } from "~/composables/api";
import type { VForm } from "~/types/auto-forms";
export default defineNuxtComponent({
middleware: ["sidebase-auth", "advanced-only"],
middleware: ["advanced-only"],
setup() {
const i18n = useI18n();
const $auth = useMealieAuth();

View File

@@ -213,7 +213,6 @@ export default defineNuxtComponent({
UserAvatar,
UserPasswordStrength,
},
middleware: "sidebase-auth",
setup() {
const i18n = useI18n();
const $auth = useMealieAuth();

View File

@@ -291,7 +291,6 @@ export default defineNuxtComponent({
UserAvatar,
StatsCards,
},
middleware: "sidebase-auth",
scrollToTop: true,
async setup() {
const i18n = useI18n();

View File

@@ -1989,7 +1989,7 @@
unplugin "^2.3.3"
unstorage "^1.16.0"
"@nuxt/kit@3.19.2", "@nuxt/kit@^3.12.4", "@nuxt/kit@^3.15.4", "@nuxt/kit@^3.17.2", "@nuxt/kit@^3.17.3", "@nuxt/kit@^3.17.6", "@nuxt/kit@^3.19.2", "@nuxt/kit@^3.5.0", "@nuxt/kit@^3.9.0":
"@nuxt/kit@3.19.2", "@nuxt/kit@^3.12.4", "@nuxt/kit@^3.15.4", "@nuxt/kit@^3.17.2", "@nuxt/kit@^3.17.3", "@nuxt/kit@^3.19.2", "@nuxt/kit@^3.5.0", "@nuxt/kit@^3.9.0":
version "3.19.2"
resolved "https://registry.yarnpkg.com/@nuxt/kit/-/kit-3.19.2.tgz#e49bd5e6672fdf21289f24603151e42a77b97e51"
integrity sha512-+QiqO0WcIxsKLUqXdVn3m4rzTRm2fO9MZgd330utCAaagGmHsgiMJp67kE14boJEPutnikfz3qOmrzBnDIHUUg==
@@ -2927,20 +2927,6 @@
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
"@sidebase/nuxt-auth@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@sidebase/nuxt-auth/-/nuxt-auth-1.1.0.tgz#c5ad29d5703f8a75503761120f9817314eb3d807"
integrity sha512-2Lj8dmlWE1tIA2CdGlekQsVUpgy2W56jaN9WDukJWxmuSDZRNK+CqxSXReYEHF+gb2tDRLMdhmyk0JUPfnRANg==
dependencies:
"@nuxt/kit" "^3.17.6"
defu "^6.1.4"
h3 "^1.15.3"
knitwork "^1.2.0"
nitropack "^2.11.13"
requrl "^3.0.2"
scule "^1.3.0"
ufo "^1.6.1"
"@sinclair/typebox@^0.27.8":
version "0.27.8"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
@@ -7957,7 +7943,7 @@ next-auth@~4.24.0:
preact-render-to-string "^5.1.19"
uuid "^8.3.2"
nitropack@^2.11.13, nitropack@^2.12.5:
nitropack@^2.12.5:
version "2.12.6"
resolved "https://registry.yarnpkg.com/nitropack/-/nitropack-2.12.6.tgz#7743f88c2c9710347f982341c356a4bf56cbce6d"
integrity sha512-DEq31s0SP4/Z5DIoVBRo9DbWFPWwIoYD4cQMEz7eE+iJMiAP+1k9A3B9kcc6Ihc0jDJmfUcHYyh6h2XlynCx6g==
@@ -9224,11 +9210,6 @@ require-from-string@^2.0.2:
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
requrl@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/requrl/-/requrl-3.0.2.tgz#d376104193b02a2d874dde68454c2db2dfeb0fac"
integrity sha512-f3gjR6d8MhOpn46PP+DSJywbmxi95fxQm3coXBFwognjFLla9X6tr8BdNyaIKNOEkaRbRcm0/zYAqN19N1oyhg==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"