got some lib exports done and WIP user login flow
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
if (event.url.pathname.startsWith('/custom')) {
|
||||
return new Response('custom response');
|
||||
}
|
||||
|
||||
const response = await resolve(event);
|
||||
return response;
|
||||
};
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
+53
-2
@@ -1,2 +1,53 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<script>
|
||||
function handleClick() {
|
||||
const url = 'https://login.microsoftonline.com/89e22de3-9f59-49a4-9399-00265468a818/oauth2/v2.0/authorize?client_id=1e233eae-f7b6-49cf-80ab-2cf5c86cab66&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fv1%2Fauth%2Fredirect&scope=openid+User.Read';
|
||||
const width = 420;
|
||||
const height = 800;
|
||||
const left = Math.round((window.screen.width - width) / 2);
|
||||
const top = Math.round((window.screen.height - height) / 2);
|
||||
const features = `width=${width},height=${height},left=${left},top=${top},resizable=no,menubar=no,toolbar=no,location=no,status=no,scrollbars=yes`;
|
||||
window.open(url, 'msAuth', features);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global(html, body, #svelte) {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f3f2f1;
|
||||
}
|
||||
.ms-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 18px;
|
||||
background: #2f2f2f;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
.ms-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg,#f25022 0 25%,#7fb900 25% 50%,#00a4ef 50% 75%,#ffb900 75% 100%);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<button class="ms-button" on:click={handleClick} aria-label="Sign in with Microsoft">
|
||||
<span class="ms-logo" aria-hidden="true"></span>
|
||||
Sign in with Microsoft
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user