got some lib exports done and WIP user login flow

This commit is contained in:
2026-01-25 16:53:56 -06:00
parent a9bf8317f4
commit 7b04aa3116
9 changed files with 838 additions and 355 deletions
+172
View File
@@ -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;
+2
View File
@@ -1 +1,3 @@
// place files you want to import through the `$lib` alias in this folder.
export * from "./axios";
export * from "./user";
+114
View File
@@ -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),
),
);
});
});
},
};