got some lib exports done and WIP user login flow
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import { getRequestEvent } from "$app/server";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from "axios";
|
||||
|
||||
type GetAccessToken = () => string | null | Promise<string | null>;
|
||||
type SetAccessToken = (token: string | null) => void;
|
||||
type RefreshHandler = () => Promise<string>;
|
||||
type LogoutHandler = () => void;
|
||||
|
||||
interface Handlers {
|
||||
getAccessToken?: GetAccessToken;
|
||||
setAccessToken?: SetAccessToken;
|
||||
refreshHandler?: RefreshHandler;
|
||||
onRefreshFailed?: LogoutHandler;
|
||||
}
|
||||
|
||||
const event = getRequestEvent();
|
||||
const api: AxiosInstance = axios.create({
|
||||
baseURL: process.env.API_URL || "",
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
let handlers: Handlers = {
|
||||
getAccessToken() {
|
||||
if (!event) return null;
|
||||
const token = event.cookies.get("authToken");
|
||||
return token || null;
|
||||
},
|
||||
setAccessToken(token: string | null) {
|
||||
if (!event) return;
|
||||
if (token) {
|
||||
event.cookies.set("authToken", token, { httpOnly: true, path: "/" });
|
||||
}
|
||||
},
|
||||
refreshHandler: async () => {
|
||||
if (!event) throw new Error("No request event available");
|
||||
const refreshToken = event.cookies.get("refreshToken");
|
||||
if (!refreshToken) throw new Error("No refresh token available");
|
||||
|
||||
const response = await api.post(
|
||||
"/auth/refresh",
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
"x-refresh-token": `${refreshToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
const newAuthToken = response.data.data.authToken;
|
||||
const newRefreshToken = response.data.data.refreshToken;
|
||||
|
||||
// Update cookies
|
||||
event.cookies.set("authToken", newAuthToken, { httpOnly: true, path: "/" });
|
||||
event.cookies.set("refreshToken", newRefreshToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return newAuthToken;
|
||||
},
|
||||
onRefreshFailed: () => {
|
||||
if (!event) return;
|
||||
event.cookies.delete("authToken", { path: "/" });
|
||||
event.cookies.delete("refreshToken", { path: "/" });
|
||||
|
||||
return redirect(303, "/");
|
||||
},
|
||||
};
|
||||
let isRefreshing = false;
|
||||
let refreshPromise: Promise<string> | null = null;
|
||||
const queue: {
|
||||
resolve: (value?: unknown) => void;
|
||||
reject: (err: unknown) => void;
|
||||
config: AxiosRequestConfig;
|
||||
}[] = [];
|
||||
|
||||
export function registerAuthHandlers(h: Handlers) {
|
||||
handlers = { ...handlers, ...h };
|
||||
}
|
||||
|
||||
async function getToken(): Promise<string | null> {
|
||||
if (!handlers.getAccessToken) return null;
|
||||
return handlers.getAccessToken();
|
||||
}
|
||||
|
||||
function setToken(token: string | null) {
|
||||
if (!handlers.setAccessToken) return;
|
||||
handlers.setAccessToken(token);
|
||||
}
|
||||
|
||||
function enqueueRequest(p: {
|
||||
resolve: (value?: unknown) => void;
|
||||
reject: (err: unknown) => void;
|
||||
config: AxiosRequestConfig;
|
||||
}) {
|
||||
queue.push(p);
|
||||
}
|
||||
|
||||
function processQueue(error: unknown, token: string | null = null) {
|
||||
while (queue.length) {
|
||||
const p = queue.shift();
|
||||
if (!p) continue;
|
||||
if (error) p.reject(error);
|
||||
else {
|
||||
if (token && p.config.headers) {
|
||||
(p.config.headers as Record<string, string>)["Authorization"] =
|
||||
`Bearer ${token}`;
|
||||
}
|
||||
p.resolve(api.request(p.config));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
api.interceptors.request.use(
|
||||
async (config) => {
|
||||
const token = await getToken();
|
||||
if (token && config.headers) {
|
||||
(config.headers as Record<string, string>)["Authorization"] =
|
||||
`Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(err) => Promise.reject(err),
|
||||
);
|
||||
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
async (
|
||||
error: AxiosError & { config?: AxiosRequestConfig & { _retry?: boolean } },
|
||||
) => {
|
||||
const originalConfig = error.config;
|
||||
if (!originalConfig) return Promise.reject(error);
|
||||
|
||||
const status = error.response?.status;
|
||||
if (status === 401 && !originalConfig._retry) {
|
||||
originalConfig._retry = true;
|
||||
|
||||
if (!handlers.refreshHandler) {
|
||||
handlers.onRefreshFailed?.();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
refreshPromise = handlers.refreshHandler!()
|
||||
.then((newToken) => {
|
||||
setToken(newToken);
|
||||
processQueue(null, newToken);
|
||||
return newToken;
|
||||
})
|
||||
.catch((err) => {
|
||||
processQueue(err);
|
||||
handlers.onRefreshFailed?.();
|
||||
throw err;
|
||||
})
|
||||
.finally(() => {
|
||||
isRefreshing = false;
|
||||
refreshPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
enqueueRequest({ resolve, reject, config: originalConfig });
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export { api };
|
||||
export default api;
|
||||
@@ -1 +1,3 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
export * from "./axios";
|
||||
export * from "./user";
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { getRequestEvent } from "$app/server";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
export const user = {
|
||||
isLoggedIn(): boolean {
|
||||
const event = getRequestEvent();
|
||||
const authToken = event.cookies.get("authToken");
|
||||
return !!authToken;
|
||||
},
|
||||
|
||||
logout() {
|
||||
const event = getRequestEvent();
|
||||
if (!event) return;
|
||||
|
||||
// Clear authentication cookies
|
||||
event.cookies.delete("authToken", { path: "/" });
|
||||
event.cookies.delete("refreshToken", { path: "/" });
|
||||
|
||||
return redirect(303, "/");
|
||||
},
|
||||
|
||||
async awaitAuthCallback(): Promise<{
|
||||
authToken: string;
|
||||
refreshToken: string;
|
||||
}> {
|
||||
const event = getRequestEvent();
|
||||
if (!event) return Promise.reject(new Error("No request event"));
|
||||
|
||||
const state =
|
||||
event.url.searchParams.get("state") ??
|
||||
event.cookies.get("authState") ??
|
||||
"";
|
||||
if (!state)
|
||||
return Promise.reject(new Error("Missing state to correlate socket"));
|
||||
|
||||
const base = process.env.API_URL || "";
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const socket = io(base, { auth: { state }, transports: ["websocket"] });
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
try {
|
||||
socket.disconnect();
|
||||
} catch {}
|
||||
reject(new Error("Timed out waiting for auth callback"));
|
||||
},
|
||||
2 * 60 * 1000,
|
||||
); // 2 minutes
|
||||
|
||||
const handlePayload = (payload: any) => {
|
||||
try {
|
||||
const { authToken, refreshToken } = payload ?? {};
|
||||
if (authToken && refreshToken) {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
event.cookies.set("authToken", authToken, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
});
|
||||
event.cookies.set("refreshToken", refreshToken, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
socket.disconnect();
|
||||
} catch {}
|
||||
resolve({ authToken, refreshToken });
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
};
|
||||
|
||||
socket.on("connect", () => {});
|
||||
socket.on("auth-callback", handlePayload);
|
||||
socket.on("message", handlePayload);
|
||||
socket.on("connect_error", (err: any) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
socket.disconnect();
|
||||
} catch {}
|
||||
reject(err instanceof Error ? err : new Error("Socket connect_error"));
|
||||
});
|
||||
socket.on("error", (err: any) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
socket.disconnect();
|
||||
} catch {}
|
||||
reject(err instanceof Error ? err : new Error("Socket error"));
|
||||
});
|
||||
socket.on("disconnect", (reason: any) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
reject(
|
||||
new Error(
|
||||
"Socket disconnected before auth was received: " + String(reason),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user