* refactor: abstract axios queries to a separate helper file * chore: delete unnecessary file * chore: remove unnecessary console logs * refactor: updated urls for api and authquery helpers * refactor: updated auth implementation * refactor: use default axios instance * chore: improve code readability * refactor: separate instances for rest and gql calls * refactor: removed async await from functions that do not need them * refactor: removed probable login and probable user from the auth system * refactor: better error handling in login component * chore: deleted unnecessary files and restructured some files * feat: new errors file with typed error message formats * refactor: removed unwanted usage of async await * refactor: optimizing the usage and return of promises in auth flow * refactor: convey boolean return type in a better way * chore: apply suggestions * refactor: handle case when mailcatcher is not active --------- Co-authored-by: nivedin <nivedinp@gmail.com> Co-authored-by: James George <jamesgeorge998001@gmail.com>
231 lines
6.3 KiB
TypeScript
231 lines
6.3 KiB
TypeScript
import { BehaviorSubject, Subject } from 'rxjs';
|
|
import {
|
|
getLocalConfig,
|
|
removeLocalConfig,
|
|
setLocalConfig,
|
|
} from './localpersistence';
|
|
import { Ref, ref } from 'vue';
|
|
import * as O from 'fp-ts/Option';
|
|
import authQuery from './backend/rest/authQuery';
|
|
import { COOKIES_NOT_FOUND, UNAUTHORIZED } from './errors';
|
|
|
|
/**
|
|
* A common (and required) set of fields that describe a user.
|
|
*/
|
|
export type HoppUser = {
|
|
/** A unique ID identifying the user */
|
|
uid: string;
|
|
|
|
/** The name to be displayed as the user's */
|
|
displayName: string | null;
|
|
|
|
/** The user's email address */
|
|
email: string | null;
|
|
|
|
/** URL to the profile picture of the user */
|
|
photoURL: string | null;
|
|
|
|
/** Name of the provider authenticating (NOTE: See notes on `platform/auth.ts`) */
|
|
provider?: string;
|
|
/** Access Token for the auth of the user against the given `provider`. */
|
|
accessToken?: string;
|
|
emailVerified: boolean;
|
|
/** Flag to check for admin status */
|
|
isAdmin: boolean;
|
|
};
|
|
|
|
export type AuthEvent =
|
|
| { event: 'login'; user: HoppUser } // We are authenticated
|
|
| { event: 'logout' } // No authentication and we have no previous state
|
|
| { event: 'token_refresh' }; // We have previous login state, but the app is waiting for authentication
|
|
|
|
export type GithubSignInResult =
|
|
| { type: 'success'; user: HoppUser } // The authentication was a success
|
|
| { type: 'account-exists-with-different-cred'; link: () => Promise<void> } // We authenticated correctly, but the provider didn't match, so we give the user the opportunity to link to continue completing auth
|
|
| { type: 'error'; err: unknown }; // Auth failed completely and we don't know why
|
|
|
|
export const authEvents$ = new Subject<
|
|
AuthEvent | { event: 'token_refresh' }
|
|
>();
|
|
|
|
const currentUser$ = new BehaviorSubject<HoppUser | null>(null);
|
|
|
|
const signOut = async (reloadWindow = false) => {
|
|
await authQuery.logout();
|
|
|
|
// Reload the window if both `access_token` and `refresh_token`is invalid
|
|
// there by the user is taken to the login page
|
|
if (reloadWindow) {
|
|
window.location.reload();
|
|
}
|
|
|
|
currentUser$.next(null);
|
|
removeLocalConfig('login_state');
|
|
|
|
authEvents$.next({
|
|
event: 'logout',
|
|
});
|
|
};
|
|
|
|
const getInitialUserDetails = async () => {
|
|
const res = await authQuery.getUserDetails();
|
|
return res.data;
|
|
};
|
|
const isGettingInitialUser: Ref<null | boolean> = ref(null);
|
|
|
|
const setUser = (user: HoppUser | null) => {
|
|
currentUser$.next(user);
|
|
setLocalConfig('login_state', JSON.stringify(user));
|
|
};
|
|
|
|
const setInitialUser = async () => {
|
|
isGettingInitialUser.value = true;
|
|
const res = await getInitialUserDetails();
|
|
|
|
if (res.errors?.[0]) {
|
|
const [error] = res.errors;
|
|
|
|
if (error.message === COOKIES_NOT_FOUND) {
|
|
setUser(null);
|
|
} else if (error.message === UNAUTHORIZED) {
|
|
const isRefreshSuccess = await refreshToken();
|
|
|
|
if (isRefreshSuccess) {
|
|
setInitialUser();
|
|
} else {
|
|
setUser(null);
|
|
signOut(true);
|
|
}
|
|
}
|
|
} else if (res.data?.me) {
|
|
const { uid, displayName, email, photoURL, isAdmin } = res.data.me;
|
|
|
|
const hoppUser: HoppUser = {
|
|
uid,
|
|
displayName,
|
|
email,
|
|
photoURL,
|
|
emailVerified: true,
|
|
isAdmin,
|
|
};
|
|
|
|
if (!hoppUser.isAdmin) {
|
|
hoppUser.isAdmin = await elevateUser();
|
|
}
|
|
|
|
setUser(hoppUser);
|
|
|
|
authEvents$.next({
|
|
event: 'login',
|
|
user: hoppUser,
|
|
});
|
|
}
|
|
|
|
isGettingInitialUser.value = false;
|
|
};
|
|
|
|
const refreshToken = async () => {
|
|
try {
|
|
const res = await authQuery.refreshToken();
|
|
authEvents$.next({
|
|
event: 'token_refresh',
|
|
});
|
|
return res.status === 200;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const elevateUser = async () => {
|
|
const res = await authQuery.elevateUser();
|
|
return Boolean(res.data?.isAdmin);
|
|
};
|
|
|
|
const sendMagicLink = async (email: string) => {
|
|
const res = await authQuery.sendMagicLink(email);
|
|
if (!res.data?.deviceIdentifier) {
|
|
throw new Error('test: does not get device identifier');
|
|
}
|
|
setLocalConfig('deviceIdentifier', res.data.deviceIdentifier);
|
|
return res.data;
|
|
};
|
|
|
|
export const auth = {
|
|
getCurrentUserStream: () => currentUser$,
|
|
getAuthEventsStream: () => authEvents$,
|
|
getCurrentUser: () => currentUser$.value,
|
|
|
|
performAuthInit: () => {
|
|
const currentUser = JSON.parse(getLocalConfig('login_state') ?? 'null');
|
|
currentUser$.next(currentUser);
|
|
return setInitialUser();
|
|
},
|
|
|
|
signInWithEmail: (email: string) => sendMagicLink(email),
|
|
|
|
isSignInWithEmailLink: (url: string) => {
|
|
const urlObject = new URL(url);
|
|
const searchParams = new URLSearchParams(urlObject.search);
|
|
return Boolean(searchParams.get('token'));
|
|
},
|
|
|
|
signInUserWithGoogle: () => {
|
|
window.location.href = `${
|
|
import.meta.env.VITE_BACKEND_API_URL
|
|
}/auth/google?redirect_uri=${import.meta.env.VITE_ADMIN_URL}`;
|
|
},
|
|
|
|
signInUserWithGithub: () => {
|
|
window.location.href = `${
|
|
import.meta.env.VITE_BACKEND_API_URL
|
|
}/auth/github?redirect_uri=${import.meta.env.VITE_ADMIN_URL}`;
|
|
},
|
|
|
|
signInUserWithMicrosoft: () => {
|
|
window.location.href = `${
|
|
import.meta.env.VITE_BACKEND_API_URL
|
|
}/auth/microsoft?redirect_uri=${import.meta.env.VITE_ADMIN_URL}`;
|
|
},
|
|
|
|
signInWithEmailLink: (url: string) => {
|
|
const urlObject = new URL(url);
|
|
const searchParams = new URLSearchParams(urlObject.search);
|
|
const token = searchParams.get('token');
|
|
const deviceIdentifier = getLocalConfig('deviceIdentifier');
|
|
|
|
return authQuery.signInWithEmailLink(token, deviceIdentifier);
|
|
},
|
|
|
|
performAuthRefresh: async () => {
|
|
const isRefreshSuccess = await refreshToken();
|
|
|
|
if (isRefreshSuccess) {
|
|
setInitialUser();
|
|
return O.some(true);
|
|
} else {
|
|
setUser(null);
|
|
isGettingInitialUser.value = false;
|
|
return O.none;
|
|
}
|
|
},
|
|
|
|
signOutUser: (reloadWindow = false) => signOut(reloadWindow),
|
|
|
|
processMagicLink: async () => {
|
|
if (auth.isSignInWithEmailLink(window.location.href)) {
|
|
const deviceIdentifier = getLocalConfig('deviceIdentifier');
|
|
|
|
if (!deviceIdentifier) {
|
|
throw new Error(
|
|
'Device Identifier not found, you can only signin from the browser you generated the magic link'
|
|
);
|
|
}
|
|
|
|
await auth.signInWithEmailLink(window.location.href);
|
|
|
|
removeLocalConfig('deviceIdentifier');
|
|
window.location.href = import.meta.env.VITE_ADMIN_URL;
|
|
}
|
|
},
|
|
};
|