mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-10-26 15:54:20 -04:00
fix: Re-write Nuxt auth backend and get rid of sidebase auth (#6322)
This commit is contained in:
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ["sidebase-auth", "admin-only"],
|
||||
middleware: ["admin-only"],
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -28,7 +28,7 @@ export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeOrganizerPage,
|
||||
},
|
||||
middleware: ["sidebase-auth", "group-only"],
|
||||
middleware: ["group-only"],
|
||||
setup() {
|
||||
const $auth = useMealieAuth();
|
||||
const toolStore = useToolStore();
|
||||
|
||||
@@ -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 } = {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -127,7 +127,7 @@ const MIGRATIONS = {
|
||||
};
|
||||
|
||||
export default defineNuxtComponent({
|
||||
middleware: ["sidebase-auth", "advanced-only"],
|
||||
middleware: ["advanced-only"],
|
||||
setup() {
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -183,7 +183,6 @@ export default defineNuxtComponent({
|
||||
GroupMealPlanRuleForm,
|
||||
RecipeChips,
|
||||
},
|
||||
middleware: ["sidebase-auth"],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -124,7 +124,6 @@ export default defineNuxtComponent({
|
||||
components: {
|
||||
UserAvatar,
|
||||
},
|
||||
middleware: ["sidebase-auth"],
|
||||
setup() {
|
||||
const $auth = useMealieAuth();
|
||||
const api = useUserApi();
|
||||
|
||||
@@ -199,7 +199,7 @@ interface OptionSection {
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
middleware: ["sidebase-auth", "advanced-only"],
|
||||
middleware: ["advanced-only"],
|
||||
setup() {
|
||||
const api = useUserApi();
|
||||
const i18n = useI18n();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -213,7 +213,6 @@ export default defineNuxtComponent({
|
||||
UserAvatar,
|
||||
UserPasswordStrength,
|
||||
},
|
||||
middleware: "sidebase-auth",
|
||||
setup() {
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
|
||||
@@ -291,7 +291,6 @@ export default defineNuxtComponent({
|
||||
UserAvatar,
|
||||
StatsCards,
|
||||
},
|
||||
middleware: "sidebase-auth",
|
||||
scrollToTop: true,
|
||||
async setup() {
|
||||
const i18n = useI18n();
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user