got some lib exports done and WIP user login flow
This commit is contained in:
+30
-28
@@ -23,38 +23,40 @@
|
|||||||
"publish": "electron-forge publish"
|
"publish": "electron-forge publish"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.8.1",
|
"@electron-forge/cli": "^7.11.1",
|
||||||
"@electron-forge/maker-deb": "^7.8.1",
|
"@electron-forge/maker-deb": "^7.11.1",
|
||||||
"@electron-forge/maker-dmg": "^7.8.1",
|
"@electron-forge/maker-dmg": "^7.11.1",
|
||||||
"@electron-forge/maker-rpm": "^7.8.1",
|
"@electron-forge/maker-rpm": "^7.11.1",
|
||||||
"@electron-forge/maker-squirrel": "^7.8.1",
|
"@electron-forge/maker-squirrel": "^7.11.1",
|
||||||
"@electron-forge/maker-zip": "^7.8.1",
|
"@electron-forge/maker-zip": "^7.11.1",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
"@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
|
||||||
"@electron-forge/plugin-fuses": "^7.8.1",
|
"@electron-forge/plugin-fuses": "^7.11.1",
|
||||||
"@electron-forge/plugin-vite": "^7.8.1",
|
"@electron-forge/plugin-vite": "^7.11.1",
|
||||||
"@electron/fuses": "^1.8.0",
|
"@electron/fuses": "^1.8.0",
|
||||||
"@playwright/test": "^1.49.1",
|
"@playwright/test": "^1.58.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.8",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.50.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/svelte": "^5.2.4",
|
"@testing-library/svelte": "^5.3.1",
|
||||||
"@types/electron-squirrel-startup": "^1.0.2",
|
"@types/electron-squirrel-startup": "^1.0.2",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22.19.7",
|
||||||
"electron": "^36.2.1",
|
"electron": "^36.9.5",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.1.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.48.2",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.3.5",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^6.2.6",
|
"vite": "^6.4.1",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-squirrel-startup": "^1.0.1"
|
"axios": "^1.13.3",
|
||||||
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
"socket.io-client": "^4.8.3"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
@@ -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.
|
// 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>
|
<script>
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
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>
|
||||||
|
|||||||
-1
Submodule sveltronkit deleted from 6ccd3cf00b
+18
-17
@@ -1,20 +1,21 @@
|
|||||||
{
|
{
|
||||||
"extends": "./.svelte-kit/tsconfig.json",
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "bundler"
|
"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
|
// 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
|
// 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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user