Cleaned up authentication logic.
This commit is contained in:
parent
4ec30b37f8
commit
cde09d1b50
|
@ -1,6 +1,5 @@
|
|||
import { api } from 'src/api/main/index';
|
||||
import {AuthStoreType} from 'stores/auth-store';
|
||||
import Timeout = NodeJS.Timeout;
|
||||
import { WeightUnit } from 'src/api/main/submission';
|
||||
import {Page, PaginationOptions, toQueryParams} from 'src/api/main/models';
|
||||
|
||||
|
@ -67,41 +66,6 @@ export enum UserFollowResponse {
|
|||
}
|
||||
|
||||
class AuthModule {
|
||||
private static readonly TOKEN_REFRESH_INTERVAL_MS = 30000;
|
||||
|
||||
private tokenRefreshTimer?: Timeout;
|
||||
|
||||
/**
|
||||
* Attempts to use the given credentials to obtain an access token for
|
||||
* sending authenticated requests.
|
||||
* @param authStore The auth store to use to update app state.
|
||||
* @param credentials The credentials for logging in.
|
||||
*/
|
||||
public async login(authStore: AuthStoreType, credentials: TokenCredentials) {
|
||||
authStore.token = await this.getNewToken(credentials);
|
||||
authStore.user = await this.getMyUser(authStore);
|
||||
// Load the user's attached data right away too.
|
||||
const [personalDetails, preferences, roles] = await Promise.all([
|
||||
this.getMyPersonalDetails(authStore),
|
||||
this.getMyPreferences(authStore),
|
||||
this.getMyRoles(authStore)
|
||||
]);
|
||||
authStore.user.personalDetails = personalDetails;
|
||||
authStore.user.preferences = preferences;
|
||||
authStore.roles = roles;
|
||||
|
||||
clearTimeout(this.tokenRefreshTimer);
|
||||
this.tokenRefreshTimer = setInterval(
|
||||
() => this.refreshToken(authStore),
|
||||
AuthModule.TOKEN_REFRESH_INTERVAL_MS
|
||||
);
|
||||
}
|
||||
|
||||
public logout(authStore: AuthStoreType) {
|
||||
authStore.logOut();
|
||||
clearTimeout(this.tokenRefreshTimer);
|
||||
}
|
||||
|
||||
public async register(payload: UserCreationPayload): Promise<User> {
|
||||
const response = await api.post('/auth/register', payload);
|
||||
return response.data;
|
||||
|
@ -117,18 +81,9 @@ class AuthModule {
|
|||
return response.data.token;
|
||||
}
|
||||
|
||||
public async refreshToken(authStore: AuthStoreType) {
|
||||
try {
|
||||
const response = await api.get('/auth/token', authStore.axiosConfig);
|
||||
authStore.token = response.data.token;
|
||||
} catch (error: any) {
|
||||
authStore.logOut();
|
||||
if (error.response) {
|
||||
console.warn('Failed to refresh token: ', error.response);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
public async refreshToken(authStore: AuthStoreType): Promise<string> {
|
||||
const response = await api.get('/auth/token', authStore.axiosConfig);
|
||||
return response.data.token;
|
||||
}
|
||||
|
||||
public async getMyUser(authStore: AuthStoreType): Promise<User> {
|
||||
|
|
|
@ -22,7 +22,7 @@ account-related actions.
|
|||
<q-item-label>{{ $t('accountMenuItem.settings') }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="api.auth.logout(authStore)">
|
||||
<q-item clickable v-close-popup @click="authStore.logOut()">
|
||||
<q-item-section>
|
||||
<q-item-label>{{ $t('accountMenuItem.logOut') }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
@ -42,7 +42,6 @@ account-related actions.
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from 'stores/auth-store';
|
||||
import api from 'src/api/main';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getUserRoute } from 'src/router/user-routing';
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import {onMounted, ref} from 'vue';
|
||||
import AccountMenuItem from 'components/AccountMenuItem.vue';
|
||||
import {useAuthStore} from 'stores/auth-store';
|
||||
|
||||
|
@ -62,4 +62,8 @@ const leftDrawerOpen = ref(false);
|
|||
function toggleLeftDrawer() {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await authStore.tryLogInWithStoredToken();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/**
|
||||
* Module declaration for the "leaflet" library for showing openstreetmap content.
|
||||
*/
|
||||
declare module 'leaflet';
|
||||
|
||||
export {};
|
||||
|
|
|
@ -55,19 +55,17 @@
|
|||
<script setup lang="ts">
|
||||
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
|
||||
import SlimForm from 'components/SlimForm.vue';
|
||||
import { ref } from 'vue';
|
||||
import api from 'src/api/main';
|
||||
import { useAuthStore } from 'stores/auth-store';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { resolveLocale } from 'src/i18n';
|
||||
import { useQuasar } from 'quasar';
|
||||
import {ref} from 'vue';
|
||||
import {useAuthStore} from 'stores/auth-store';
|
||||
import {useRoute, useRouter} from 'vue-router';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {resolveLocale} from 'src/i18n';
|
||||
import {showApiErrorToast, showWarningToast} from 'src/utils';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const i18n = useI18n({ useScope: 'global' });
|
||||
const quasar = useQuasar();
|
||||
|
||||
const loginModel = ref({
|
||||
email: '',
|
||||
|
@ -84,7 +82,7 @@ const passwordVisible = ref(false);
|
|||
*/
|
||||
async function tryLogin() {
|
||||
try {
|
||||
await api.auth.login(authStore, loginModel.value);
|
||||
await authStore.logInWithCredentials(loginModel.value);
|
||||
|
||||
// Set the locale to the user's preferred locale.
|
||||
i18n.locale.value = resolveLocale(authStore.user?.preferences?.locale).value;
|
||||
|
@ -96,18 +94,9 @@ async function tryLogin() {
|
|||
await router.push(dest);
|
||||
} catch (error: any) {
|
||||
if (error.response && error.response.status === 401) {
|
||||
quasar.notify({
|
||||
message: i18n.t('loginPage.authFailed'),
|
||||
type: 'warning',
|
||||
position: 'top',
|
||||
});
|
||||
showWarningToast('loginPage.authFailed');
|
||||
} else {
|
||||
quasar.notify({
|
||||
message: i18n.t('generalErrors.apiError'),
|
||||
type: 'negative',
|
||||
position: 'top',
|
||||
});
|
||||
console.error(error);
|
||||
showApiErrorToast(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,19 +117,19 @@ let initialPreferences: UserPreferences | null = null;
|
|||
const newPassword = ref('');
|
||||
|
||||
onMounted(async () => {
|
||||
// Redirect away from the page if the user isn't viewing their own settings.
|
||||
const userId = route.params.userId as string;
|
||||
if (!authStore.user || authStore.user.id !== userId) {
|
||||
if (authStore.user && authStore.user.id === userId) {
|
||||
personalDetails.value = await api.auth.getMyPersonalDetails(authStore);
|
||||
initialPersonalDetails = structuredClone(toRaw(personalDetails.value));
|
||||
|
||||
preferences.value = await api.auth.getMyPreferences(authStore);
|
||||
initialPreferences = structuredClone(toRaw(preferences.value));
|
||||
|
||||
newPassword.value = '';
|
||||
} else {
|
||||
// Redirect away from the page if the user isn't viewing their own settings.
|
||||
await router.replace(`/users/${userId}`);
|
||||
}
|
||||
|
||||
personalDetails.value = await api.auth.getMyPersonalDetails(authStore);
|
||||
initialPersonalDetails = structuredClone(toRaw(personalDetails.value));
|
||||
|
||||
preferences.value = await api.auth.getMyPreferences(authStore);
|
||||
initialPreferences = structuredClone(toRaw(preferences.value));
|
||||
|
||||
newPassword.value = '';
|
||||
});
|
||||
|
||||
const personalDetailsChanged = computed(() => {
|
||||
|
|
|
@ -83,7 +83,12 @@ const isOwnUser = ref(false);
|
|||
// re-render.
|
||||
watch(route, async (updatedRoute) => {
|
||||
if (updatedRoute.params.userId && updatedRoute.params.userId.length > 0) {
|
||||
const userId = updatedRoute.params.userId[0];
|
||||
let userId;
|
||||
if (Array.isArray(updatedRoute.params.userId)) {
|
||||
userId = updatedRoute.params.userId[0];
|
||||
} else {
|
||||
userId = updatedRoute.params.userId;
|
||||
}
|
||||
if (!profile.value || (profile.value.id !== userId)) {
|
||||
await loadUser(userId);
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
*/
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import { User } from 'src/api/main/auth';
|
||||
import {TokenCredentials, User} from 'src/api/main/auth';
|
||||
import api from 'src/api/main';
|
||||
import {AxiosRequestConfig} from 'axios';
|
||||
import Timeout = NodeJS.Timeout;
|
||||
|
||||
interface AuthState {
|
||||
/**
|
||||
|
@ -20,18 +22,32 @@ interface AuthState {
|
|||
*/
|
||||
token: string | null;
|
||||
|
||||
tokenRefreshTimer: Timeout | null;
|
||||
|
||||
/**
|
||||
* The list of roles that the currently authenticated user has.
|
||||
*/
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
const TOKEN_REFRESH_INTERVAL = 60 * 1000 * 25;
|
||||
|
||||
export const useAuthStore = defineStore('authStore', {
|
||||
state: (): AuthState => {
|
||||
return { user: null, token: null, roles: [] };
|
||||
return { user: null, token: null, tokenRefreshTimer: null, roles: [] };
|
||||
},
|
||||
getters: {
|
||||
/**
|
||||
* Determines whether a user is currently logged in.
|
||||
* @param state The store's state.
|
||||
*/
|
||||
loggedIn: (state) => state.user !== null && state.token !== null,
|
||||
/**
|
||||
* Gets the axios config that can be applied to requests to authenticate
|
||||
* them as the current user. This will always return a valid request config
|
||||
* even when the user is not logged in.
|
||||
* @param state The store's state.
|
||||
*/
|
||||
axiosConfig(state): AxiosRequestConfig {
|
||||
if (this.token !== null) {
|
||||
return {
|
||||
|
@ -41,19 +57,80 @@ export const useAuthStore = defineStore('authStore', {
|
|||
return {};
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Getter that returns true if a user is authenticated, and the user is
|
||||
* an admin, meaning they have special access to additional data.
|
||||
* @param state The store's state.
|
||||
*/
|
||||
isAdmin: state => state.roles.indexOf('admin') !== -1,
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* Logs a user into the application.
|
||||
* @param user The user who was logged in.
|
||||
* @param token The token that was obtained.
|
||||
* @param roles The list of the user's roles.
|
||||
* Attempts to log in with the given token credentials.
|
||||
* @param credentials The credentials to use.
|
||||
*/
|
||||
logIn(user: User, token: string, roles: string[]) {
|
||||
this.user = user;
|
||||
this.token = token;
|
||||
async logInWithCredentials(credentials: TokenCredentials) {
|
||||
const token = await api.auth.getNewToken(credentials);
|
||||
await this._logInWithToken(token);
|
||||
},
|
||||
/**
|
||||
* Attempts to log in with a token that's stored in the browser's local
|
||||
* storage.
|
||||
*/
|
||||
async tryLogInWithStoredToken() {
|
||||
const token = localStorage.getItem('auth-token');
|
||||
if (token) {
|
||||
this.token = token; // Temporarily set our token to the one that was stored, so we can use it to request a new one.
|
||||
try {
|
||||
await this.refreshToken();
|
||||
if (this.token) { // If we were able to refresh the token, we can now go through with the login.
|
||||
await this._logInWithToken(token);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not log in with stored token: ', error);
|
||||
this.logOut();
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Initializes the auth state using a freshly-obtained token. This will
|
||||
* populate all the necessary data to consider the user to be logged in,
|
||||
* including fetching user data.
|
||||
*
|
||||
* Note: This method is intended to be used only internally.
|
||||
* @param token The token to use.
|
||||
*/
|
||||
async _logInWithToken(token: string) {
|
||||
this.updateToken(token);
|
||||
this.user = await api.auth.getMyUser(this);
|
||||
const [personalDetails, preferences, roles] = await Promise.all([
|
||||
api.auth.getMyPersonalDetails(this),
|
||||
api.auth.getMyPreferences(this),
|
||||
api.auth.getMyRoles(this)
|
||||
]);
|
||||
this.user.personalDetails = personalDetails;
|
||||
this.user.preferences = preferences;
|
||||
this.roles = roles;
|
||||
if (this.tokenRefreshTimer) {
|
||||
clearInterval(this.tokenRefreshTimer);
|
||||
}
|
||||
this.tokenRefreshTimer = setInterval(
|
||||
() => this.refreshToken(),
|
||||
TOKEN_REFRESH_INTERVAL
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Refreshes the existing token used for authentication. If this fails,
|
||||
* the auth state will reset to the nominal logged-out state.
|
||||
*/
|
||||
async refreshToken() {
|
||||
try {
|
||||
const newToken = await api.auth.refreshToken(this);
|
||||
this.updateToken(newToken);
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh token: ', error);
|
||||
this.logOut();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Logs a user out of the application, resetting the auth state.
|
||||
|
@ -61,7 +138,12 @@ export const useAuthStore = defineStore('authStore', {
|
|||
logOut() {
|
||||
this.user = null;
|
||||
this.token = null;
|
||||
if (this.tokenRefreshTimer) {
|
||||
clearInterval(this.tokenRefreshTimer);
|
||||
}
|
||||
this.tokenRefreshTimer = null;
|
||||
this.roles = [];
|
||||
localStorage.removeItem('auth-token');
|
||||
},
|
||||
/**
|
||||
* Updates the token that's stored for the currently authenticated user.
|
||||
|
@ -69,6 +151,7 @@ export const useAuthStore = defineStore('authStore', {
|
|||
*/
|
||||
updateToken(token: string) {
|
||||
this.token = token;
|
||||
localStorage.setItem('auth-token', this.token);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue