feat(auth): rework auth system implementation to be more reliable and cleaner

This commit is contained in:
MiniDigger | Martin 2022-12-20 18:01:08 +01:00
parent 5c409f7831
commit 5470d5b07e
15 changed files with 268 additions and 277 deletions

View File

@ -54,8 +54,8 @@ public class LoginController extends HangarComponent {
config.checkDev();
UserTable fakeUser = authenticationService.loginAsFakeUser();
tokenService.issueRefreshAndAccessToken(fakeUser);
return new RedirectView(returnUrl);
tokenService.issueRefreshToken(fakeUser);
return addBaseAndRedirect(returnUrl);
} else {
response.addCookie(new Cookie("url", returnUrl));
return redirectToSso(ssoService.getLoginUrl(config.getBaseUrl() + "/login"));
@ -73,14 +73,14 @@ public class LoginController extends HangarComponent {
if (!validationService.isValidUsername(user.getName())) {
throw new HangarApiException("nav.user.error.invalidUsername");
}
tokenService.issueRefreshAndAccessToken(user);
tokenService.issueRefreshToken(user);
return addBaseAndRedirect(url);
}
@GetMapping("/refresh")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void refreshAccessToken(@CookieValue(name = SecurityConfig.REFRESH_COOKIE_NAME, required = false) String refreshToken) {
tokenService.refreshAccessToken(refreshToken);
public String refreshAccessToken(@CookieValue(name = SecurityConfig.REFRESH_COOKIE_NAME, required = false) String refreshToken) {
return tokenService.refreshAccessToken(refreshToken).accessToken();
}
@GetMapping("/invalidate")
@ -105,7 +105,7 @@ public class LoginController extends HangarComponent {
return redirectToSso(ssoService.getLogoutUrl(config.getBaseUrl() + "/handle-logout", principal.get()));
} else {
tokenService.invalidateToken(null);
return new RedirectView(returnUrl);
return addBaseAndRedirect(returnUrl);
}
}
}

View File

@ -26,6 +26,8 @@ import io.papermc.hangar.security.annotations.permission.PermissionRequired;
import io.papermc.hangar.security.annotations.ratelimit.RateLimit;
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
import io.papermc.hangar.security.authentication.HangarAuthenticationToken;
import io.papermc.hangar.security.configs.SecurityConfig;
import io.papermc.hangar.service.TokenService;
import io.papermc.hangar.service.api.UsersApiService;
import io.papermc.hangar.service.internal.perms.roles.OrganizationRoleService;
import io.papermc.hangar.service.internal.perms.roles.ProjectRoleService;
@ -45,6 +47,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@ -68,9 +71,10 @@ public class HangarUserController extends HangarComponent {
private final OrganizationRoleService organizationRoleService;
private final ProjectInviteService projectInviteService;
private final OrganizationInviteService organizationInviteService;
private final TokenService tokenService;
@Autowired
public HangarUserController(ObjectMapper mapper, UsersApiService usersApiService, UserService userService, NotificationService notificationService, ProjectRoleService projectRoleService, OrganizationRoleService organizationRoleService, ProjectInviteService projectInviteService, OrganizationInviteService organizationInviteService) {
public HangarUserController(ObjectMapper mapper, UsersApiService usersApiService, UserService userService, NotificationService notificationService, ProjectRoleService projectRoleService, OrganizationRoleService organizationRoleService, ProjectInviteService projectInviteService, OrganizationInviteService organizationInviteService, final TokenService tokenService) {
this.mapper = mapper;
this.usersApiService = usersApiService;
this.userService = userService;
@ -79,15 +83,38 @@ public class HangarUserController extends HangarComponent {
this.organizationRoleService = organizationRoleService;
this.projectInviteService = projectInviteService;
this.organizationInviteService = organizationInviteService;
this.tokenService = tokenService;
}
@Anyone
@GetMapping("/users/@me")
public ResponseEntity<?> getCurrentUser(HangarAuthenticationToken hangarAuthenticationToken) {
public ResponseEntity<?> getCurrentUser(HangarAuthenticationToken hangarAuthenticationToken, @CookieValue(name = SecurityConfig.REFRESH_COOKIE_NAME, required = false) String refreshToken) {
String token;
String name;
if (hangarAuthenticationToken == null) {
return ResponseEntity.noContent().build();
// if we don't have a token, lets see if we can get one via our refresh token
if (refreshToken == null) {
// neither token nor refresh token -> sorry no content
return ResponseEntity.noContent().build();
}
try {
TokenService.RefreshResponse refreshResponse = tokenService.refreshAccessToken(refreshToken);
token = refreshResponse.accessToken();
name = refreshResponse.userTable().getName();
} catch (HangarApiException ex) {
// no token + no valid refresh token -> no content
System.out.println("getCurrentUser failed: " + ex.getMessage());
return ResponseEntity.noContent().build();
}
} else {
// when we have a token, just use that
token = hangarAuthenticationToken.getCredentials().getToken();
name = hangarAuthenticationToken.getName();
}
return ResponseEntity.ok(usersApiService.getUser(hangarAuthenticationToken.getName(), HangarUser.class));
// get user
HangarUser user = usersApiService.getUser(name, HangarUser.class);
user.setAccessToken(token);
return ResponseEntity.ok(user);
}
// @el(userName: String)

View File

@ -15,6 +15,7 @@ public class HangarUser extends User implements Identified {
private final List<Integer> readPrompts;
private final String language;
private final String theme;
private String accessToken;
public HangarUser(OffsetDateTime createdAt, String name, String tagline, OffsetDateTime joinDate, List<GlobalRole> roles, long projectCount, boolean locked, long id, List<Integer> readPrompts, String language, String theme) {
super(createdAt, name, tagline, joinDate, roles, projectCount, locked);
@ -49,6 +50,14 @@ public class HangarUser extends User implements Identified {
return theme;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(final String accessToken) {
this.accessToken = accessToken;
}
public User toUser() {
return new User(
this.getCreatedAt(),

View File

@ -52,19 +52,18 @@ public class TokenService extends HangarComponent {
return getVerifier().verify(token);
}
public void issueRefreshAndAccessToken(UserTable userTable) {
public void issueRefreshToken(UserTable userTable) {
UserRefreshToken userRefreshToken = userRefreshTokenDAO.insert(new UserRefreshToken(userTable.getId(), UUID.randomUUID(), UUID.randomUUID()));
addCookie(SecurityConfig.REFRESH_COOKIE_NAME, userRefreshToken.getToken().toString(), config.security.refreshTokenExpiry().toSeconds(), true);
String accessToken = newToken0(userTable);
// let the access token cookie be around for longer, so we can more nicely detect expired tokens via the response code
addCookie(SecurityConfig.AUTH_NAME, accessToken, config.security.tokenExpiry().toSeconds() * 2, false);
}
private void addCookie(String name, String value, long maxAge, boolean httpOnly) {
response.addHeader(HttpHeaders.SET_COOKIE, ResponseCookie.from(name, value).path("/").secure(config.security.secure()).maxAge(maxAge).sameSite("Lax").httpOnly(httpOnly).build().toString());
}
public void refreshAccessToken(String refreshToken) {
public record RefreshResponse(String accessToken, UserTable userTable) {}
public RefreshResponse refreshAccessToken(String refreshToken) {
if (refreshToken == null) {
throw new HangarApiException(299, "No refresh token found");
}
@ -90,8 +89,7 @@ public class TokenService extends HangarComponent {
userRefreshToken = userRefreshTokenDAO.update(userRefreshToken);
addCookie(SecurityConfig.REFRESH_COOKIE_NAME, userRefreshToken.getToken().toString(), config.security.refreshTokenExpiry().toSeconds(), true);
// then issue a new access token
String accessToken = newToken0(userTable);
addCookie(SecurityConfig.AUTH_NAME, accessToken, config.security.tokenExpiry().toSeconds(), false);
return new RefreshResponse(newToken0(userTable), userTable);
}
public void invalidateToken(String refreshToken) {

View File

@ -2,13 +2,11 @@ package io.papermc.hangar.util;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.function.Function;
import java.util.stream.Collectors;
public class RequestUtil {
@ -47,4 +45,8 @@ public class RequestUtil {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
}
public static String appendParam(String url, String paramName, String paramValue) {
return UriComponentsBuilder.fromUriString(url).queryParam(paramName, paramValue).build().toUriString();
}
}

View File

@ -0,0 +1,17 @@
package io.papermc.hangar.util;
import org.junit.jupiter.api.Test;
import static io.papermc.hangar.util.RequestUtil.appendParam;
import static org.junit.jupiter.api.Assertions.*;
class RequestUtilTest {
@Test
void test_appendParam() {
assertEquals("http://localhost:8080?test=test", appendParam("http://localhost:8080", "test", "test"));
assertEquals("http://localhost:8080/?test=test", appendParam("http://localhost:8080/", "test", "test"));
assertEquals("http://localhost:8080/?dum=dum&test=test", appendParam("http://localhost:8080/?dum=dum", "test", "test"));
assertEquals("http://localhost:8080/?dum&test=test", appendParam("http://localhost:8080/?dum", "test", "test"));
}
}

View File

@ -1,23 +1,19 @@
import type { AxiosError, AxiosRequestConfig } from "axios";
import type { HangarApiException } from "hangar-api";
import qs from "qs";
import Cookies from "universal-cookie";
import { isEmpty } from "lodash-es";
import type { Ref } from "vue";
import { useAxios } from "~/composables/useAxios";
import { useCookies } from "~/composables/useCookies";
import { authLog, fetchLog } from "~/lib/composables/useLog";
import { useAuth } from "~/composables/useAuth";
import { useRequestEvent } from "#imports";
import { useAxios } from "~/composables/useAxios";
function request<T>(url: string, method: AxiosRequestConfig["method"], data: object, headers: Record<string, string> = {}, retry = false): Promise<T> {
function request<T>(url: string, method: AxiosRequestConfig["method"], data: object): Promise<T> {
const cookies = useCookies();
return new Promise<T>((resolve, reject) => {
return useAxios
return useAxios()
.request<T>({
method,
url: `/api/${url}`,
headers,
data: method?.toLowerCase() !== "get" ? data : {},
params: method?.toLowerCase() === "get" ? data : {},
paramsSerializer: (params) => {
@ -40,177 +36,24 @@ function request<T>(url: string, method: AxiosRequestConfig["method"], data: obj
}
resolve(data);
})
.catch(async (error: AxiosError) => {
.catch((error: AxiosError) => {
const { trace, ...err } = (error.response?.data as { trace: any }) || {};
authLog("failed", err);
// do we have an expired token?
if (error.response?.status === 403) {
if (retry) {
// we failed on a retry, let's invalidate
authLog("failed retry -> invalidate");
await useAuth.invalidate();
return reject(error);
}
// do we have a refresh token we could use?
const result = await useAuth.refreshToken();
if (result) {
// retry
authLog("Retrying request...");
if (typeof result === "string") {
headers = { ...headers, Authorization: `HangarAuth ${result}` };
authLog("using new token");
}
try {
const response = await request<T>(url, method, data, headers, true);
return resolve(response);
} catch (e) {
return reject(e);
}
} else {
authLog("Not retrying since refresh failed");
await useAuth.invalidate();
return reject(error);
}
}
authLog("failed", err, error);
reject(error);
});
});
}
export function useApi<T>(
url: string,
authed = true,
method: AxiosRequestConfig["method"] = "get",
data: object = {},
headers: Record<string, string> = {}
): Promise<T> {
export function useApi<T>(url: string, authed = true, method: AxiosRequestConfig["method"] = "get", data: object = {}): Promise<T> {
fetchLog("useApi", url);
return processAuthStuff(headers, authed, (headers) => request(`v1/${url}`, method, data, headers));
return request(`v1/${url}`, method, data);
}
export function useInternalApi<T = void>(
url: string,
authed = true,
method: AxiosRequestConfig["method"] = "get",
data: object = {},
headers: Record<string, string> = {}
): Promise<T> {
// TODO do something with authed
export function useInternalApi<T = void>(url: string, authed = true, method: AxiosRequestConfig["method"] = "get", data: object = {}): Promise<T> {
fetchLog("useInternalApi", url);
return processAuthStuff(headers, authed, (headers) => request(`internal/${url}`, method, data, headers));
}
export async function processAuthStuff<T>(headers: Record<string, string>, authRequired: boolean, handler: (headers: Record<string, string>) => Promise<T>) {
if (import.meta.env.SSR) {
// forward cookies I guess?
let token = useCookies().get("HangarAuth");
let refreshToken = useCookies().get("HangarAuth_REFRESH");
if (!token) {
const header = useRequestEvent().node.res?.getHeader("set-cookie") as string[];
if (header && header.join) {
const cookies = new Cookies(header.join("; "));
token = cookies.get("HangarAuth");
refreshToken = cookies.get("HangarAuth_REFRESH");
authLog("found token in set-cookie header");
}
}
if (token || refreshToken) {
authLog("forward token from cookie");
// make sure our token is still valid, else refresh
if (!useAuth.validateToken(token)) {
authLog("token no longer valid, lets refresh");
const result = await useAuth.refreshToken();
if (result) {
authLog("refreshed");
token = result;
} else {
authLog("could not refresh, invalidate");
await useAuth.invalidate();
// eslint-disable-next-line no-throw-literal
throw {
isAxiosError: true,
response: {
data: {
isHangarApiException: true,
httpError: {
statusCode: 401,
},
message: "You must be logged in",
} as HangarApiException,
},
};
}
}
headers = { ...headers, Authorization: `HangarAuth ${token}`, ...forwardHeader() };
} else {
authLog("can't forward token, no cookie");
if (authRequired) {
// eslint-disable-next-line no-throw-literal
throw {
isAxiosError: true,
response: {
data: {
isHangarApiException: true,
httpError: {
statusCode: 401,
},
message: "You must be logged in",
} as HangarApiException,
},
};
}
headers = { ...headers, ...forwardHeader() };
}
} else {
// validate and refresh
const token = useCookies().get("HangarAuth");
if (!useAuth.validateToken(token)) {
authLog("token no longer valid, lets refresh");
const result = await useAuth.refreshToken();
if (result) {
authLog("refreshed");
} else {
authLog("could not refresh, invalidate");
await useAuth.invalidate();
if (authRequired) {
// eslint-disable-next-line no-throw-literal
throw {
isAxiosError: true,
response: {
data: {
isHangarApiException: true,
httpError: {
statusCode: 401,
},
message: "You must be logged in",
} as HangarApiException,
},
};
}
}
}
}
return handler(headers);
}
function forwardHeader(): Record<string, string> {
const req = useRequestEvent().node.req;
const result: Record<string, string> = {};
if (!req) return result;
const forward = (header: string) => {
if (req.headers[header]) {
result[header] = req.headers[header];
}
};
forward("cf-connecting-ip");
forward("cf-ipcountry");
forward("x-forwarded-host");
forward("x-real-ip");
return result;
return request(`internal/${url}`, method, data);
}
export async function fetchIfNeeded<T>(func: () => Promise<T>, ref: Ref<T>) {

View File

@ -1,14 +1,13 @@
import { HangarUser } from "hangar-internal";
import { AxiosError, AxiosRequestHeaders } from "axios";
import Cookies from "universal-cookie";
import { AxiosError, AxiosInstance, AxiosRequestHeaders } from "axios";
import jwtDecode, { type JwtPayload } from "jwt-decode";
import { useAuthStore } from "~/store/auth";
import { useCookies } from "~/composables/useCookies";
import { useInternalApi } from "~/composables/useApi";
import { useAxios } from "~/composables/useAxios";
import { authLog } from "~/lib/composables/useLog";
import { useConfig } from "~/lib/composables/useConfig";
import { useRequestEvent } from "#imports";
import { useAxios } from "~/composables/useAxios";
class Auth {
loginUrl(redirectUrl: string): string {
@ -22,8 +21,8 @@ class Auth {
location.replace(`/logout?returnUrl=${useConfig().publicHost}?loggedOut`);
}
validateToken(token: string) {
if (!token) {
validateToken(token: unknown): token is string {
if (!token || typeof token !== "string") {
return false;
}
const decoded = jwtDecode<JwtPayload>(token);
@ -34,10 +33,10 @@ class Auth {
}
// TODO do we need to scope this to the user?
// TODO do we even need this anymore?
refreshPromise: Promise<boolean | string> | null = null;
refreshPromise: Promise<false | string> | null = null;
async refreshToken() {
async refreshToken(): Promise<false | string> {
// we use a promise as a lock here to make sure only one request is doing a refresh, avoids too many requests
authLog("refresh token");
if (this.refreshPromise) {
authLog("locked, lets wait");
@ -45,9 +44,8 @@ class Auth {
authLog("lock over", result);
return result;
}
// eslint-disable-next-line no-async-promise-executor
this.refreshPromise = new Promise<boolean | string>(async (resolve) => {
this.refreshPromise = new Promise<false | string>(async (resolve) => {
const refreshToken = useCookies().get("HangarAuth_REFRESH");
if (import.meta.env.SSR && !refreshToken) {
authLog("no cookie, no point in refreshing");
@ -63,28 +61,23 @@ class Auth {
headers.cookie = "HangarAuth_REFRESH=" + refreshToken;
authLog("pass refresh cookie", refreshToken);
}
const response = await useAxios.get("/refresh", { headers });
const response = await useAxios().get("/refresh", { headers });
if (response.status === 299) {
authLog("had no cookie");
resolve(false);
} else if (import.meta.env.SSR) {
if (response.headers["set-cookie"]) {
} else if (response.status === 204) {
// forward cookie header to renew refresh cookie
if (import.meta.env.SSR && response.headers["set-cookie"]) {
useRequestEvent().node.res?.setHeader("set-cookie", response.headers["set-cookie"]);
const token = new Cookies(response.headers["set-cookie"]?.join("; ")).get("HangarAuth");
if (token) {
authLog("got token");
resolve(token);
} else {
authLog("got no token in cookie header", response.headers["set-cookie"]);
resolve(false);
}
}
// validate and return token
const token = response.data;
if (useAuth.validateToken(token)) {
resolve(response.data);
} else {
authLog("got no cookie header back");
authLog("refreshed token is not valid?", token);
resolve(false);
}
} else {
authLog("done");
resolve(true);
}
this.refreshPromise = null;
} catch (e) {
@ -101,38 +94,45 @@ class Auth {
return this.refreshPromise;
}
async invalidate() {
async invalidate(axios: AxiosInstance) {
const store = useAuthStore();
store.$patch({
user: null,
authenticated: false,
token: null,
});
if (!store.invalidated) {
await useAxios.get("/invalidate").catch((e) => authLog("Invalidate failed", e.message));
}
if (!import.meta.env.SSR) {
useCookies().remove("HangarAuth_REFRESH", { path: "/" });
useCookies().remove("HangarAuth", { path: "/" });
authLog("Invalidated auth cookies");
await axios.get("/invalidate").catch((e) => authLog("Invalidate failed", e.message));
}
store.invalidated = true;
}
async updateUser(): Promise<void> {
const authStore = useAuthStore();
const axios = useAxios();
if (authStore.invalidated) {
authLog("no point in updating if we just invalidated");
return;
}
if (authStore.user) {
authLog("no point in updating if we already have a user");
return;
}
const user = await useInternalApi<HangarUser>("users/@me", true).catch((err) => {
authLog("no user", Object.assign({}, err));
return this.invalidate();
authLog("no user, with err", Object.assign({}, err));
return this.invalidate(axios);
});
if (user) {
authLog("patching " + user.name);
authStore.setUser(user);
authStore.$patch({ authenticated: true, invalidated: false });
authStore.user = user;
authStore.authenticated = true;
authStore.invalidated = false;
if (user.accessToken) {
authStore.token = user.accessToken;
}
authLog("user is now " + authStore.user?.name);
} else {
authLog("no user, no content");
}
}
}

View File

@ -1,11 +1,6 @@
import type { AxiosInstance } from "axios";
import axios from "axios";
import { axiosLog } from "~/lib/composables/useLog";
import { useConfig } from "~/lib/composables/useConfig";
import { AxiosInstance } from "axios";
import { useNuxtApp } from "#imports";
const config = useConfig();
const options = {
baseURL: import.meta.env.SSR ? config.proxyHost : config.publicHost,
};
axiosLog("axios options", options);
export const useAxios: AxiosInstance = axios.create(options);
export function useAxios() {
return useNuxtApp().$axios as AxiosInstance;
}

@ -1 +1 @@
Subproject commit 5ceb57e631896689b945290997fbe58ec39940bf
Subproject commit 54f6ba87a4f0664cf63f8132647ac558c324f8d4

View File

@ -1,47 +1,28 @@
import { RouteLocationNormalized, RouteLocationRaw } from "vue-router";
import { PermissionCheck, UserPermissions } from "hangar-api";
import { RouteLocationNamedRaw, RouteLocationNormalized } from "vue-router";
import { useI18n } from "vue-i18n";
import { NuxtApp } from "nuxt/app";
import { useAuth } from "~/composables/useAuth";
import { routePermLog } from "~/lib/composables/useLog";
import { PermissionCheck, UserPermissions } from "hangar-api";
import { defineNuxtRouteMiddleware, handleRequestError, hasPerms, navigateTo, toNamedPermission, useApi, useAuth } from "#imports";
import { useAuthStore } from "~/store/auth";
import { useApi } from "~/composables/useApi";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
import { hasPerms, toNamedPermission } from "~/composables/usePerm";
import { routePermLog } from "~/lib/composables/useLog";
import { NamedPermission, PermissionType } from "~/types/enums";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useSettingsStore } from "~/store/useSettingsStore";
import { defineNuxtPlugin, useRequestEvent, useRouter } from "#imports";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
export default defineNuxtPlugin(async (nuxtApp: NuxtApp) => {
useRouter().beforeEach(async (to, from, next) => {
if (to.fullPath.startsWith("/@vite")) {
// really don't need to do stuff for such meta routes
return;
}
await loadPerms(to);
const result = await handleRoutePerms(to);
if (result) {
next(result);
} else {
next();
}
});
await useAuth.updateUser();
if (!process.server) return;
const event = useRequestEvent();
const request = event.node.res;
const response = event.node.res;
if (request?.url?.includes("/@vite")) {
export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalized, from: RouteLocationNormalized) => {
if (to.fullPath.startsWith("/@vite")) {
// really don't need to do stuff for such meta routes
console.log("hit vite path???????????????????????", to.fullPath);
return;
}
await useSettingsStore().loadSettingsServer(request, response);
await useAuth.updateUser();
await loadRoutePerms(to);
const result = await handleRoutePerms(to);
if (result) {
return navigateTo(result, { redirectCode: result.params?.status });
}
});
async function loadPerms(to: RouteLocationNormalized) {
async function loadRoutePerms(to: RouteLocationNormalized) {
const authStore = useAuthStore();
if (to.params.user && to.params.project) {
if (authStore.authenticated) {
@ -88,7 +69,7 @@ type handlersType = {
[key: string]: (
authStore: ReturnType<typeof useAuthStore>,
to: RouteLocationNormalized
) => Promise<RouteLocationRaw | undefined> | RouteLocationRaw | undefined;
) => Promise<RouteLocationNamedRaw | undefined> | RouteLocationNamedRaw | undefined;
};
const handlers: handlersType = {
currentUserRequired,
@ -114,12 +95,13 @@ async function globalPermsRequired(authStore: ReturnType<typeof useAuthStore>, t
routePermLog("route globalPermsRequired", to.meta.globalPermsRequired);
const result = checkLogin(authStore, to, 403);
if (result) return result;
const i18n = useI18n();
const check = await useApi<PermissionCheck>("permissions/hasAll", true, "get", {
permissions: toNamedPermission(to.meta.globalPermsRequired as string[]),
}).catch((e) => {
try {
routePermLog("error!", e);
handleRequestError(e, useI18n());
handleRequestError(e, i18n as ReturnType<typeof useI18n>); // dont ask me why I need this cast...
} catch (e2) {
routePermLog("error while checking perm", e);
routePermLog("encountered additional error while error handling", e2);

View File

@ -0,0 +1,12 @@
import { RouteLocationNormalized } from "vue-router";
import { defineNuxtRouteMiddleware, useRequestEvent } from "#imports";
import { useSettingsStore } from "~/store/useSettingsStore";
export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalized, from: RouteLocationNormalized) => {
if (!process.server || to.fullPath.includes("/@vite")) return;
const event = useRequestEvent();
const request = event.node.res;
const response = event.node.res;
await useSettingsStore().loadSettingsServer(request, response);
});

View File

@ -0,0 +1,108 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { NuxtApp } from "nuxt/app";
import { defineNuxtPlugin, useAuth, useRequestEvent } from "#imports";
import { useAuthStore } from "~/store/auth";
import { authLog, axiosLog } from "~/lib/composables/useLog";
import { useConfig } from "~/lib/composables/useConfig";
export default defineNuxtPlugin((nuxtApp: NuxtApp) => {
const config = useConfig();
const options = {
baseURL: import.meta.env.SSR ? config.proxyHost : config.publicHost,
};
axiosLog("axios options", options);
const axiosInstance = axios.create(options);
axiosInstance.interceptors.request.use(
(config) => {
const authStore = useAuthStore();
const token = authStore.token;
// forward auth token
if (!config.headers) {
config.headers = {};
}
if (token) {
config.headers.Authorization = "HangarAuth " + token;
}
// forward other headers for ssr
forwardRequestHeaders(config, nuxtApp);
// axiosLog("calling with headers", config.headers);
return config;
},
(error) => {
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
(res) => {
// forward cookies and stuff to browser
forwardResponseHeaders(res, nuxtApp);
return res;
},
async (err) => {
const authStore = useAuthStore();
const originalConfig = err.config;
axiosLog("got error", err);
if (originalConfig?.url !== "/refresh" && originalConfig?.url !== "/invalidate" && err.response) {
// token expired
if (err.response.status === 401 && !originalConfig._retry) {
originalConfig._retry = true;
authLog("Request to", originalConfig.url, "failed with", err.response.status, "==> refreshing token");
const refreshedToken = await useAuth.refreshToken();
if (refreshedToken) {
authStore.token = refreshedToken;
return axiosInstance(originalConfig);
}
}
}
throw err;
}
);
return {
provide: {
axios: axiosInstance,
},
};
});
function forwardRequestHeaders(config: AxiosRequestConfig, nuxtApp: NuxtApp) {
if (!process.server) return;
const req = useRequestEvent(nuxtApp).node.req;
const forward = (header: string) => {
if (req.headers[header] && config.headers) {
config.headers[header] = req.headers[header];
}
};
forward("cf-connecting-ip");
forward("cf-ipcountry");
forward("x-forwarded-host");
forward("x-real-ip");
forward("cookie");
forward("sec-ch-prefers-color-scheme");
forward("sec-ch-ua-mobile");
forward("sec-ch-ua-platform");
forward("sec-ch-ua");
forward("user-agent");
}
function forwardResponseHeaders(axiosResponse: AxiosResponse, nuxtApp: NuxtApp) {
if (!process.server) return;
const res = useRequestEvent(nuxtApp).node.res;
const forward = (header: string) => {
if (axiosResponse.headers[header]) {
res.setHeader(header, axiosResponse.headers[header]);
}
};
forward("set-cookie");
forward("server");
}

View File

@ -4,6 +4,7 @@ import { ref } from "vue";
import { authLog } from "~/lib/composables/useLog";
export const useAuthStore = defineStore("auth", () => {
const token = ref<string | null>(null);
const authenticated = ref<boolean>(false);
const user = ref<HangarUser | null>(null);
const routePermissions = ref<string | null>(null);
@ -11,13 +12,9 @@ export const useAuthStore = defineStore("auth", () => {
authLog("create authStore");
function setUser(newUser: HangarUser) {
user.value = newUser;
}
function setRoutePerms(routePerms: string | null) {
routePermissions.value = routePerms;
}
return { authenticated, user, routePermissions, invalidated, setUser, setRoutePerms };
return { token, authenticated, user, routePermissions, invalidated, setRoutePerms };
});

View File

@ -42,6 +42,7 @@ declare module "hangar-internal" {
readPrompts: number[];
language: string;
theme: string;
accessToken?: string;
}
interface UserTable extends Table, Named {