172 lines
5.6 KiB
TypeScript
172 lines
5.6 KiB
TypeScript
/**
|
|
* Service for all authentication- and authorization-related tasks.
|
|
*/
|
|
|
|
import { computed, reactive } from "vue";
|
|
import type { AuthState, Role, UserManagersForRoles } from "@/services/auth/auth.d.ts";
|
|
import { User, UserManager } from "oidc-client-ts";
|
|
import { AUTH_CONFIG_ENDPOINT, loadAuthConfig } from "@/services/auth/auth-config-loader.ts";
|
|
import authStorage from "./auth-storage.ts";
|
|
import { loginRoute } from "@/config.ts";
|
|
import apiClient from "@/services/api-client/api-client.ts";
|
|
import router from "@/router";
|
|
import type { AxiosError } from "axios";
|
|
|
|
async function getUserManagers(): Promise<UserManagersForRoles> {
|
|
const authConfig = await loadAuthConfig();
|
|
return {
|
|
student: new UserManager(authConfig.student),
|
|
teacher: new UserManager(authConfig.teacher),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Information about the current authentication state.
|
|
*/
|
|
const authState = reactive<AuthState>({
|
|
user: null,
|
|
accessToken: null,
|
|
activeRole: authStorage.getActiveRole() ?? null,
|
|
});
|
|
|
|
/**
|
|
* Load the information about who is currently logged in from the IDP.
|
|
*/
|
|
async function loadUser(): Promise<User | null> {
|
|
const activeRole = authStorage.getActiveRole();
|
|
if (!activeRole) {
|
|
return null;
|
|
}
|
|
|
|
const userManager = (await getUserManagers())[activeRole];
|
|
let user = await userManager.getUser(); // Load the user from the local storage.
|
|
if (!user) {
|
|
// If the user is not in the local storage, he could still be authenticated in Keycloak.
|
|
try {
|
|
user = await userManager.signinSilent();
|
|
} catch (_: unknown) {
|
|
// When the user was previously logged in and then logged out, signinSilent throws an error.
|
|
// In that case, the user is not authenticated anymore, so we set him to null.
|
|
user = null;
|
|
}
|
|
}
|
|
|
|
setUserAuthInfo(user);
|
|
authState.activeRole = activeRole ?? null;
|
|
return user;
|
|
}
|
|
|
|
const isLoggedIn = computed(() => authState.user !== null);
|
|
|
|
/**
|
|
* Clears all the cached information about the current authentication.
|
|
*/
|
|
function clearAuthState(): void {
|
|
authStorage.deleteActiveRole();
|
|
authState.accessToken = null;
|
|
authState.user = null;
|
|
authState.activeRole = null;
|
|
}
|
|
|
|
/**
|
|
* Sets the information about the currently logged-in user in the cache.
|
|
*/
|
|
function setUserAuthInfo(newUser: User | null): void {
|
|
authState.user = newUser;
|
|
authState.accessToken = newUser?.access_token ?? null;
|
|
}
|
|
|
|
/**
|
|
* Redirect the user to the login page where he/she can choose whether to log in as a student or teacher.
|
|
*/
|
|
async function initiateLogin(): Promise<void> {
|
|
if (isLoggedIn.value) {
|
|
clearAuthState();
|
|
}
|
|
await router.push(loginRoute);
|
|
}
|
|
|
|
/**
|
|
* Redirect the user to the IDP for the given role so that he can log in there.
|
|
* Only call this function when the user is not logged in yet!
|
|
*/
|
|
async function loginAs(role: Role): Promise<void> {
|
|
// Storing it in local storage so that it won't be lost when redirecting outside of the app.
|
|
authStorage.setActiveRole(role);
|
|
await (await getUserManagers())[role].signinRedirect();
|
|
}
|
|
|
|
/**
|
|
* To be called when the user is redirected to the callback-endpoint by the IDP after a successful login.
|
|
*/
|
|
async function handleLoginCallback(): Promise<void> {
|
|
const activeRole = authStorage.getActiveRole();
|
|
if (!activeRole) {
|
|
throw new Error("Login callback received, but the user is not logging in!");
|
|
}
|
|
authState.user = (await (await getUserManagers())[activeRole].signinCallback()) || null;
|
|
await apiClient.post("/auth/hello");
|
|
}
|
|
|
|
/**
|
|
* Refresh an expired authorization token.
|
|
*/
|
|
async function renewToken(): Promise<User | null> {
|
|
const activeRole = authStorage.getActiveRole();
|
|
if (!activeRole) {
|
|
await initiateLogin();
|
|
return null;
|
|
}
|
|
try {
|
|
const userManagerForRole = (await getUserManagers())[activeRole];
|
|
const user = await userManagerForRole.signinSilent();
|
|
setUserAuthInfo(user);
|
|
} catch (_error: unknown) {
|
|
await initiateLogin();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* End the session of the current user.
|
|
*/
|
|
async function logout(): Promise<void> {
|
|
const activeRole = authStorage.getActiveRole();
|
|
if (activeRole) {
|
|
await (await getUserManagers())[activeRole].signoutRedirect();
|
|
authStorage.deleteActiveRole();
|
|
clearAuthState();
|
|
}
|
|
}
|
|
|
|
// Registering interceptor to add the authorization header to each request when the user is logged in.
|
|
apiClient.interceptors.request.use(
|
|
async (reqConfig) => {
|
|
const token = authState?.user?.access_token;
|
|
if (token && reqConfig.url !== AUTH_CONFIG_ENDPOINT) {
|
|
reqConfig.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return reqConfig;
|
|
},
|
|
async (error) => Promise.reject(error),
|
|
);
|
|
|
|
// Registering interceptor to refresh the token when a request failed because it was expired.
|
|
apiClient.interceptors.response.use(
|
|
(response) => response,
|
|
async (error: AxiosError<{ message?: string; inner?: { message?: string } }>) => {
|
|
if (error.response?.status === 401) {
|
|
// If the user should already be logged in, his token is probably just expired.
|
|
if (isLoggedIn.value) {
|
|
await renewToken();
|
|
return apiClient(error.config!); // Retry the request
|
|
}
|
|
|
|
// Apparently, the user got a 401 because he was not logged in yet at all. Redirect him to login.
|
|
await initiateLogin();
|
|
}
|
|
return Promise.reject(error);
|
|
},
|
|
);
|
|
|
|
export default { authState, isLoggedIn, initiateLogin, loadUser, handleLoginCallback, loginAs, logout };
|