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
+439 -307
View File
File diff suppressed because it is too large Load Diff
+30 -28
View File
@@ -23,38 +23,40 @@
"publish": "electron-forge publish"
},
"devDependencies": {
"@electron-forge/cli": "^7.8.1",
"@electron-forge/maker-deb": "^7.8.1",
"@electron-forge/maker-dmg": "^7.8.1",
"@electron-forge/maker-rpm": "^7.8.1",
"@electron-forge/maker-squirrel": "^7.8.1",
"@electron-forge/maker-zip": "^7.8.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
"@electron-forge/plugin-fuses": "^7.8.1",
"@electron-forge/plugin-vite": "^7.8.1",
"@electron-forge/cli": "^7.11.1",
"@electron-forge/maker-deb": "^7.11.1",
"@electron-forge/maker-dmg": "^7.11.1",
"@electron-forge/maker-rpm": "^7.11.1",
"@electron-forge/maker-squirrel": "^7.11.1",
"@electron-forge/maker-zip": "^7.11.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
"@electron-forge/plugin-fuses": "^7.11.1",
"@electron-forge/plugin-vite": "^7.11.1",
"@electron/fuses": "^1.8.0",
"@playwright/test": "^1.49.1",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.4",
"@playwright/test": "^1.58.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1",
"@types/electron-squirrel-startup": "^1.0.2",
"@types/node": "^22",
"electron": "^36.2.1",
"jsdom": "^26.0.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.2.6",
"vitest": "^3.0.0"
"@types/node": "^22.19.7",
"electron": "^36.9.5",
"jsdom": "^26.1.0",
"svelte": "^5.48.2",
"svelte-check": "^4.3.5",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^6.4.1",
"vitest": "^3.2.4"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.1"
"axios": "^1.13.3",
"electron-squirrel-startup": "^1.0.1",
"socket.io-client": "^4.8.3"
},
"pnpm": {
"onlyBuiltDependencies": [
+10
View File
@@ -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;
};
+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),
),
);
});
});
},
};
+53 -2
View File
@@ -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>
Submodule sveltronkit deleted from 6ccd3cf00b
+18 -17
View File
@@ -1,20 +1,21 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler",
"verbatimModuleSyntax": false
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}