tachibana-shin / vue-auth3

This plugin is a combination of @websanova/vue-auth and Vue3 and Axios!
https://vue-auth3.js.org
MIT License
29 stars 13 forks source link

Not showing Previous/From Page when directly hitting a page that needs authorisation. #49

Open nimashoghi0 opened 1 month ago

nimashoghi0 commented 1 month ago

Hey there,

Thanks for building this this awesome plugin. My app needs to know the previous route before redirecting to the login page if unauthorised. I was thinking of storing this in a Store for safe keeping.

It seems, if I'm coming from a valid page, then hit a route with meta: {auth: true}, it redirects me and shows me the previous page fine. However if i directly hit that unauthorised page it comes back with an empty from, and also empty auth.redirect() object.

By directly I mean a new chrome tab - then hit https://domain/requires-auth, it doesn't show me the previous page, but if I'm in a https://domain/open-page, then click a link that takes me to https://domain/requires-auth it shows me the previous page.

This is the method I'm using to try and find the previous page:

router.beforeEach((to, from) => { console.log('from'); console.log(from); console.log(auth.redirect()) })

I hope this explanation makes sense. I must be doing something wrong?

Cheers

codeautopilot[bot] commented 1 month ago

Potential solution

To solve the issue of not showing the previous route when directly accessing a page that requires authorization, we need to ensure that the previous route information is stored and retrieved correctly. The solution involves updating the route guard logic to store the previous route in local storage and retrieve it when necessary.

What is causing this bug?

The bug is caused by the fact that when a user directly accesses a protected page (e.g., by opening a new browser tab and navigating to a URL that requires authentication), there is no previous route information available. This results in an empty from object and an empty auth.redirect() object. The current implementation does not handle this scenario, leading to the issue described.

Code

Update src/shims-router.d.ts

Add a property for storing the previous route information in the RouteMeta interface.

import "vue-router"
import Roles from "./type/Roles"

declare module "vue-router" {
  interface RouteMeta {
    auth?:
      | boolean
      | string
      | Roles[]
      | {
          roles?: Roles
          redirect?:
            | RouteLocationRaw
            | ((to: RouteLocationNormalized) => RouteLocationRaw)
          notFoundRedirect?:
            | RouteLocationRaw
            | ((to: RouteLocationNormalized) => RouteLocationRaw)
          forbiddenRedirect?:
            | RouteLocationRaw
            | ((to: RouteLocationNormalized) => RouteLocationRaw)
          rolesKey?: string
        }
    previousRoute?: string // Add this line to store the previous route information
  }
}

Update src/utils/index.ts

Add utility functions for storing and retrieving the previous route information from local storage.

export function storePreviousRoute(route: string): void {
  if (isLocalStorage()) {
    localStorage.setItem('previousRoute', route);
  }
}

export function getPreviousRoute(): string | null {
  if (isLocalStorage()) {
    return localStorage.getItem('previousRoute');
  }
  return null;
}

Update src/Auth.ts

Modify the authentication logic to store the previous route in local storage when redirecting to the login page.

this.options.plugins?.router?.beforeEach(async (to, from, next) => {
  if (from.name) {
    this.tPrev = from;
    storePreviousRoute(from.fullPath);
  } else {
    const storedPrevRoute = getPreviousRoute();
    this.tPrev = storedPrevRoute ? { fullPath: storedPrevRoute } : null;
  }
  this.tCurrent = to;

  await syncStorage(this);

  const authMeta = getAuthMeta(to);

  processTransitionEach(this, to, authMeta, (redirect) => {
    if (!redirect) {
      next();
      return;
    }

    next(redirect);
  });
});

Clear the previous route on logout.

function logout(auth: Auth, redirect?: RouteLocationRaw) {
  $cookie.remove(auth, auth.options.tokenImpersonateKey);
  $cookie.remove(auth, auth.options.tokenDefaultKey);

  $token.remove(auth, auth.options.tokenImpersonateKey);
  $token.remove(auth, auth.options.tokenDefaultKey);

  $token.remove(auth, auth.options.staySignedInKey);

  $token.remove(auth, auth.options.userKey);

  auth.state.loaded = true;
  auth.state.authenticated = false;
  auth.state.data = null;

  localStorage.removeItem('previousRoute');

  routerPush(auth, redirect);
}

Update src/index.ts

Ensure the previous route is stored and retrieved during redirects.

import { computed, ComputedRef, inject } from "vue"
import Auth from "./Auth"
import { authKey } from "./injectionKey"
import Options from "./type/Options"
import AuthDriver, { defineAuthDriver } from "./type/drivers/AuthDriver"
import HttpDriver, { defineHttpDriver } from "./type/drivers/HttpDriver"
import OAuth2Driver, { defineOAuth2Driver } from "./type/drivers/OAuth2Driver"
import { storePreviousRoute } from "./utils/index"

export function createAuth(options: Options) {
  const auth = new Auth(options);

  router.beforeEach((to, from, next) => {
    if (to.meta.auth) {
      storePreviousRoute(from.fullPath);
    }
    next();
  });

  return auth;
}

export function useAuth(key: symbol | string = authKey): Auth {
  return inject(key) as Auth;
}

export function useUser<User extends object>(
  key: symbol | string = authKey
): ComputedRef<User | null> {
  const auth = useAuth(key);

  return computed(() => {
    return auth.user();
  });
}

export type {
  Options,
  Auth,
  AuthDriver,
  HttpDriver,
  OAuth2Driver,
  defineAuthDriver,
  defineHttpDriver,
  defineOAuth2Driver,
}

How to replicate the bug

  1. Open a new browser tab.
  2. Navigate directly to a URL that requires authentication (e.g., https://domain/requires-auth).
  3. Observe that the previous route information is not available, resulting in an empty from object and an empty auth.redirect() object.

By implementing the proposed changes, the previous route information will be correctly stored and retrieved, even when directly accessing a protected page. This should resolve the issue of the empty from object and auth.redirect() object.

Click here to create a Pull Request with the proposed solution

Files used for this task:

Changes on src/shims-router.d.ts # Analysis Report for `src/shims-router.d.ts` ## Overview The file `src/shims-router.d.ts` extends the `vue-router` module by adding custom properties to the `RouteMeta` interface. This interface is used to define metadata for routes in a Vue.js application. ## Current Implementation The `RouteMeta` interface currently includes the following properties: - `auth`: This can be a boolean, string, array of `Roles`, or an object with additional properties such as `roles`, `redirect`, `notFoundRedirect`, `forbiddenRedirect`, and `rolesKey`. ## Issue Analysis The issue described by the user is that when directly accessing a protected page (one that requires authentication), the previous route information is not available. This is problematic because the application needs to know the previous route to handle redirections properly. ## Proposed Solution To address this issue, we need to ensure that the `RouteMeta` interface includes properties for storing and retrieving the previous route information. This will allow the application to persist the previous route even when a user directly accesses a protected page. ## Changes Required 1. **Add a Property for Previous Route Information**: Extend the `RouteMeta` interface to include a property for storing the previous route. This can be a simple string or a more complex object depending on the requirements. ### Updated `src/shims-router.d.ts` ```typescript import "vue-router" import Roles from "./type/Roles" declare module "vue-router" { // eslint-disable-next-line functional/prefer-type-literal interface RouteMeta { auth?: | boolean | string | Roles[] | { roles?: Roles redirect?: | RouteLocationRaw | ((to: RouteLocationNormalized) => RouteLocationRaw) notFoundRedirect?: | RouteLocationRaw | ((to: RouteLocationNormalized) => RouteLocationRaw) forbiddenRedirect?: | RouteLocationRaw | ((to: RouteLocationNormalized) => RouteLocationRaw) rolesKey?: string } previousRoute?: string // Add this line to store the previous route information } } ``` ## Conclusion By adding the `previousRoute` property to the `RouteMeta` interface, we can store and retrieve the previous route information. This will help in handling redirections properly when a user directly accesses a protected page. The next steps involve updating the authentication logic and integrating these changes into the application.
Changes on src/utils/index.ts # Analysis Report for `src/utils/index.ts` ## Overview The file `src/utils/index.ts` contains utility functions that are used throughout the application. These functions include type checking, array conversion, object comparison, and storage capability checks. The current issue requires adding utility functions for storing and retrieving the previous route information from local storage or a Vuex store. ## Existing Functions 1. **isObject(val: any): boolean**: Checks if a value is an object. 2. **toArray(val: T): T[]**: Converts a value to an array. 3. **compare(one: O, two: T): boolean**: Compares two objects or arrays. 4. **isLocalStorage(): boolean**: Checks if local storage is available. 5. **isSessionStorage(): boolean**: Checks if session storage is available. 6. **isCookieStorage(): boolean**: Always returns true (placeholder for cookie storage check). 7. **getProperty(obj: any, desc: string): any**: Retrieves a nested property from an object using a dot-separated string. ## Required Additions To address the issue, we need to add utility functions for: 1. Storing the previous route information in local storage. 2. Retrieving the previous route information from local storage. ## Proposed Utility Functions ### 1. Storing Previous Route ```typescript export function storePreviousRoute(route: string): void { if (isLocalStorage()) { localStorage.setItem('previousRoute', route); } } ``` ### 2. Retrieving Previous Route ```typescript export function getPreviousRoute(): string | null { if (isLocalStorage()) { return localStorage.getItem('previousRoute'); } return null; } ``` ## Integration Plan 1. **Update `src/Auth.ts`**: Modify the authentication logic to call `storePreviousRoute` when redirecting to the login page. 2. **Update `src/index.ts`**: Ensure the previous route is retrieved using `getPreviousRoute` during redirects. 3. **Update `src/shims-router.d.ts`**: Ensure the `RouteMeta` interface includes properties for storing and retrieving the previous route information. ## Conclusion The file `src/utils/index.ts` is relevant to the issue as it will house the utility functions needed for storing and retrieving the previous route information. The proposed additions should help in resolving the issue where the previous route is not available when directly accessing a protected page.
Changes on src/Auth.ts ## Bug Analysis Report for `src/Auth.ts` ### Issue Description The user reported that when directly accessing a protected page (e.g., by opening a new browser tab and navigating to a URL that requires authentication), the application does not correctly store or retrieve the previous route information. This results in an empty `from` object and an empty `auth.redirect()` object. ### Relevant Code Analysis The `src/Auth.ts` file contains the authentication logic, including route guards and redirection handling. The key function related to the issue is the `beforeEach` navigation guard, which is responsible for handling route transitions and storing route information. ### Key Observations 1. **Route Guard Setup**: ```typescript this.options.plugins?.router?.beforeEach(async (to, from, next) => { this.tPrev = this.tCurrent; this.tCurrent = from; await syncStorage(this); const authMeta = getAuthMeta(to); processTransitionEach(this, to, authMeta, (redirect) => { if (!redirect) { next(); return; } next(redirect); }); }); ``` - The `beforeEach` hook updates `tPrev` and `tCurrent` with the current and previous routes, respectively. - The `syncStorage` function is called to ensure the authentication state is synchronized. - The `processTransitionEach` function handles the redirection logic based on the route's metadata. 2. **Redirection Handling**: ```typescript function processTransitionEach( auth: Auth, to: RouteLocationNormalized, authMeta: RouteMeta["auth"] | null, cb: (to?: RouteLocationRaw) => void ) { // ... existing logic ... if (!auth.check()) { auth.tStatusType = 401; if (typeof authRedirect === "function") { authRedirect = authRedirect(to); } cb(authRedirect); return; } // ... existing logic ... auth._redirect.value = auth.tStatusType ? { type: auth.tStatusType, from: auth.tPrev, to: auth.tCurrent, } : null; auth.tStatusType = null; cb(); } ``` - The `processTransitionEach` function sets the `_redirect` value based on the authentication status and route information. - The `auth.tPrev` and `auth.tCurrent` values are used to populate the `from` and `to` properties of the `_redirect` object. ### Potential Cause of the Bug - When directly accessing a protected page, the `from` route is not set correctly because there is no previous route in a new browser tab context. This results in `auth.tPrev` being `null`. - The current implementation does not handle the scenario where the `from` route is `null`, leading to an empty `from` object in the `auth.redirect()` result. ### Suggested Fix To address this issue, we need to persist the previous route information in a more reliable manner, such as using local storage. This way, even when directly accessing a protected page, we can retrieve the previous route from local storage. ### Proposed Code Changes 1. **Update `beforeEach` Hook**: ```typescript this.options.plugins?.router?.beforeEach(async (to, from, next) => { if (from.name) { this.tPrev = from; localStorage.setItem('previousRoute', JSON.stringify(from)); } else { const storedPrevRoute = localStorage.getItem('previousRoute'); this.tPrev = storedPrevRoute ? JSON.parse(storedPrevRoute) : null; } this.tCurrent = to; await syncStorage(this); const authMeta = getAuthMeta(to); processTransitionEach(this, to, authMeta, (redirect) => { if (!redirect) { next(); return; } next(redirect); }); }); ``` 2. **Clear Previous Route on Logout**: ```typescript function logout(auth: Auth, redirect?: RouteLocationRaw) { $cookie.remove(auth, auth.options.tokenImpersonateKey); $cookie.remove(auth, auth.options.tokenDefaultKey); $token.remove(auth, auth.options.tokenImpersonateKey); $token.remove(auth, auth.options.tokenDefaultKey); $token.remove(auth, auth.options.staySignedInKey); $token.remove(auth, auth.options.userKey); auth.state.loaded = true; auth.state.authenticated = false; auth.state.data = null; localStorage.removeItem('previousRoute'); routerPush(auth, redirect); } ``` ### Conclusion The proposed changes should ensure that the previous route information is correctly stored and retrieved, even when directly accessing a protected page. This should resolve the issue of the empty `from` object and `auth.redirect()` object.
Changes on src/index.ts ## Analysis of `src/index.ts` The `src/index.ts` file is primarily responsible for setting up and providing authentication-related functionalities. It includes functions to create and use an `Auth` instance, as well as to retrieve the current user. Here is a breakdown of the key components: 1. **Imports**: - Vue's `computed`, `ComputedRef`, and `inject` functions. - `Auth` class and `authKey` from local modules. - Types and drivers related to authentication (`Options`, `AuthDriver`, `HttpDriver`, `OAuth2Driver`). 2. **Functions**: - `createAuth(options: Options)`: Initializes and returns a new `Auth` instance. - `useAuth(key: symbol | string = authKey)`: Injects and returns the `Auth` instance using the provided key. - `useUser(key: symbol | string = authKey): ComputedRef`: Returns a computed reference to the current user. 3. **Exports**: - Functions: `createAuth`, `useAuth`, `useUser`. - Types: `Options`, `Auth`, `AuthDriver`, `HttpDriver`, `OAuth2Driver`, `defineAuthDriver`, `defineHttpDriver`, `defineOAuth2Driver`. ## Potential Cause of the Bug The issue described involves the inability to retrieve the previous route when directly accessing a protected page. The `src/index.ts` file does not currently handle the storage or retrieval of the previous route. This functionality needs to be integrated into the authentication logic. ## Suggested Changes To address the issue, the following changes should be made: 1. **Update `Auth` Class**: - Modify the `Auth` class to store the previous route in a persistent manner (e.g., local storage) when a user directly accesses a protected page. 2. **Integrate Updated Logic**: - Ensure that the `createAuth` and `useAuth` functions in `src/index.ts` utilize the updated `Auth` class to handle the previous route information. 3. **Utility Functions**: - Add utility functions for storing and retrieving the previous route information from local storage or a Vuex store. This can be done in the `src/utils/index.ts` file. ## Example Implementation Here is an example of how you might modify the `Auth` class and integrate it into `src/index.ts`: ### Update `Auth` Class (in `src/Auth.ts`) ```typescript class Auth { // Existing properties and methods... // New method to store the previous route storePreviousRoute(route: string) { localStorage.setItem('previousRoute', route); } // New method to retrieve the previous route getPreviousRoute(): string | null { return localStorage.getItem('previousRoute'); } } ``` ### Integrate in `src/index.ts` ```typescript import { computed, ComputedRef, inject } from "vue" import Auth from "./Auth" import { authKey } from "./injectionKey" import Options from "./type/Options" import AuthDriver, { defineAuthDriver } from "./type/drivers/AuthDriver" import HttpDriver, { defineHttpDriver } from "./type/drivers/HttpDriver" import OAuth2Driver, { defineOAuth2Driver } from "./type/drivers/OAuth2Driver" export function createAuth(options: Options) { const auth = new Auth(options); // Example of storing the previous route router.beforeEach((to, from, next) => { if (to.meta.auth) { auth.storePreviousRoute(from.fullPath); } next(); }); return auth; } export function useAuth(key: symbol | string = authKey): Auth { return inject(key) as Auth; } export function useUser( key: symbol | string = authKey ): ComputedRef { const auth = useAuth(key); return computed(() => { return auth.user(); }); } export type { Options, Auth, AuthDriver, HttpDriver, OAuth2Driver, defineAuthDriver, defineHttpDriver, defineOAuth2Driver, } ``` ## Conclusion The `src/index.ts` file itself does not directly cause the bug but needs to integrate with the updated `Auth` class to handle the previous route information. The primary changes should be made in the `Auth` class and then utilized in `src/index.ts` to ensure the previous route is correctly stored and retrieved during redirects.
tachibana-shin commented 1 month ago

.redirect() will only return the page doing the redirect when it comes from vue-router can you make me a minimal reproducible example?

nimashoghi0 commented 1 month ago

Hey @tachibana-shin, Thanks for getting in touch with me.

See here: https://github.com/nimashoghi0/vue-auth

When you hit /demo directly, vue-auth-3 redirects to the /login page, and there is no data on the previous page (/demo), vue router also doesn't show any info so that's probably why.

I need to capture this previous data, so I will write my own wrapper to check authorisation, where I can store the information in local storage, it would have been great to use your meta field checker though. Maybe something to consider in the future?

Cheers