- Delete old Vite+Svelte frontend - Initialize new SvelteKit project with TypeScript - Configure Tailwind CSS v4 + DaisyUI - Implement JWT authentication with auto-refresh - Create login page with form validation (Zod) - Add protected route guards - Update Docker configuration for single-stage build - Add E2E tests with Playwright (6/11 passing) - Fix Svelte 5 reactivity with $state() runes Known issues: - 5 E2E tests failing (timing/async issues) - Token refresh implementation needs debugging - Validation error display timing
467 lines
19 KiB
JavaScript
467 lines
19 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const XMLHttpRequestEventTarget_js_1 = __importDefault(require("./XMLHttpRequestEventTarget.cjs"));
|
|
const PropertySymbol = __importStar(require("../PropertySymbol.cjs"));
|
|
const XMLHttpRequestReadyStateEnum_js_1 = __importDefault(require("./XMLHttpRequestReadyStateEnum.cjs"));
|
|
const Event_js_1 = __importDefault(require("../event/Event.cjs"));
|
|
const DOMException_js_1 = __importDefault(require("../exception/DOMException.cjs"));
|
|
const DOMExceptionNameEnum_js_1 = __importDefault(require("../exception/DOMExceptionNameEnum.cjs"));
|
|
const XMLHttpResponseTypeEnum_js_1 = __importDefault(require("./XMLHttpResponseTypeEnum.cjs"));
|
|
const ErrorEvent_js_1 = __importDefault(require("../event/events/ErrorEvent.cjs"));
|
|
const Headers_js_1 = __importDefault(require("../fetch/Headers.cjs"));
|
|
const Fetch_js_1 = __importDefault(require("../fetch/Fetch.cjs"));
|
|
const SyncFetch_js_1 = __importDefault(require("../fetch/SyncFetch.cjs"));
|
|
const ProgressEvent_js_1 = __importDefault(require("../event/events/ProgressEvent.cjs"));
|
|
const NodeTypeEnum_js_1 = __importDefault(require("../nodes/node/NodeTypeEnum.cjs"));
|
|
const XMLHttpRequestResponseDataParser_js_1 = __importDefault(require("./XMLHttpRequestResponseDataParser.cjs"));
|
|
const FetchRequestHeaderUtility_js_1 = __importDefault(require("../fetch/utilities/FetchRequestHeaderUtility.cjs"));
|
|
const WindowBrowserContext_js_1 = __importDefault(require("../window/WindowBrowserContext.cjs"));
|
|
/**
|
|
* XMLHttpRequest.
|
|
*
|
|
* Based on:
|
|
* https://github.com/mjwwit/node-XMLHttpRequest/blob/master/lib/XMLHttpRequest.js
|
|
*/
|
|
class XMLHttpRequest extends XMLHttpRequestEventTarget_js_1.default {
|
|
// Constants
|
|
static UNSENT = XMLHttpRequestReadyStateEnum_js_1.default.unsent;
|
|
static OPENED = XMLHttpRequestReadyStateEnum_js_1.default.opened;
|
|
static HEADERS_RECEIVED = XMLHttpRequestReadyStateEnum_js_1.default.headersRecieved;
|
|
static LOADING = XMLHttpRequestReadyStateEnum_js_1.default.loading;
|
|
static DONE = XMLHttpRequestReadyStateEnum_js_1.default.done;
|
|
// Public properties
|
|
upload = new this[PropertySymbol.window].XMLHttpRequestUpload();
|
|
withCredentials = false;
|
|
// Private properties
|
|
#async = true;
|
|
#abortController = null;
|
|
#aborted = false;
|
|
#request = null;
|
|
#response = null;
|
|
#responseType = '';
|
|
#responseBody = null;
|
|
#readyState = XMLHttpRequestReadyStateEnum_js_1.default.unsent;
|
|
#overriddenMimeType = null;
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
constructor() {
|
|
super();
|
|
if (!this[PropertySymbol.window]) {
|
|
throw new TypeError(`Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.`);
|
|
}
|
|
}
|
|
/**
|
|
* Returns the status.
|
|
*
|
|
* @returns Status.
|
|
*/
|
|
get status() {
|
|
return this.#response?.status || 0;
|
|
}
|
|
/**
|
|
* Returns the status text.
|
|
*
|
|
* @returns Status text.
|
|
*/
|
|
get statusText() {
|
|
return this.#response?.statusText || '';
|
|
}
|
|
/**
|
|
* Returns the response.
|
|
*
|
|
* @returns Response.
|
|
*/
|
|
get response() {
|
|
if (!this.#response) {
|
|
return '';
|
|
}
|
|
return this.#responseBody;
|
|
}
|
|
/**
|
|
* Get the response text.
|
|
*
|
|
* @throws {DOMException} If the response type is not text or empty.
|
|
* @returns The response text.
|
|
*/
|
|
get responseText() {
|
|
if (this.responseType !== XMLHttpResponseTypeEnum_js_1.default.text && this.responseType !== '') {
|
|
throw new this[PropertySymbol.window].DOMException(`Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '${this.responseType}').`, DOMExceptionNameEnum_js_1.default.invalidStateError);
|
|
}
|
|
return this.#responseBody ?? '';
|
|
}
|
|
/**
|
|
* Get the responseXML.
|
|
*
|
|
* @throws {DOMException} If the response type is not text or empty.
|
|
* @returns Response XML.
|
|
*/
|
|
get responseXML() {
|
|
if (this.responseType !== XMLHttpResponseTypeEnum_js_1.default.document && this.responseType !== '') {
|
|
throw new this[PropertySymbol.window].DOMException(`Failed to read the 'responseXML' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'document' (was '${this.responseType}').`, DOMExceptionNameEnum_js_1.default.invalidStateError);
|
|
}
|
|
return this.responseType === '' ? null : this.#responseBody;
|
|
}
|
|
/**
|
|
* Returns the response URL.
|
|
*
|
|
* @returns Response URL.
|
|
*/
|
|
get responseURL() {
|
|
return this.#response?.url || '';
|
|
}
|
|
/**
|
|
* Returns the ready state.
|
|
*
|
|
* @returns Ready state.
|
|
*/
|
|
get readyState() {
|
|
return this.#readyState;
|
|
}
|
|
/**
|
|
* Set response type.
|
|
*
|
|
* @param type Response type.
|
|
* @throws {DOMException} If the state is not unsent or opened.
|
|
* @throws {DOMException} If the request is synchronous.
|
|
*/
|
|
set responseType(type) {
|
|
// ResponseType can only be set when the state is unsent or opened.
|
|
if (this.readyState !== XMLHttpRequestReadyStateEnum_js_1.default.opened &&
|
|
this.readyState !== XMLHttpRequestReadyStateEnum_js_1.default.unsent) {
|
|
throw new this[PropertySymbol.window].DOMException(`Failed to set the 'responseType' property on 'XMLHttpRequest': The object's state must be OPENED or UNSENT.`, DOMExceptionNameEnum_js_1.default.invalidStateError);
|
|
}
|
|
// Sync requests can only have empty string or 'text' as response type.
|
|
if (!this.#async) {
|
|
throw new this[PropertySymbol.window].DOMException(`Failed to set the 'responseType' property on 'XMLHttpRequest': The response type cannot be changed for synchronous requests made from a document.`, DOMExceptionNameEnum_js_1.default.invalidStateError);
|
|
}
|
|
this.#responseType = type;
|
|
}
|
|
/**
|
|
* Get response Type.
|
|
*
|
|
* @returns Response type.
|
|
*/
|
|
get responseType() {
|
|
return this.#responseType;
|
|
}
|
|
/**
|
|
* Opens the connection.
|
|
*
|
|
* @param method Connection method (eg GET, POST).
|
|
* @param url URL for the connection.
|
|
* @param [async=true] Asynchronous connection.
|
|
* @param [user] Username for basic authentication (optional).
|
|
* @param [password] Password for basic authentication (optional).
|
|
*/
|
|
open(method, url, async = true, user, password) {
|
|
const window = this[PropertySymbol.window];
|
|
if (!async && !!this.responseType && this.responseType !== XMLHttpResponseTypeEnum_js_1.default.text) {
|
|
throw new window.DOMException(`Failed to execute 'open' on 'XMLHttpRequest': Synchronous requests from a document must not set a response type.`, DOMExceptionNameEnum_js_1.default.invalidAccessError);
|
|
}
|
|
const headers = new Headers_js_1.default();
|
|
if (user) {
|
|
const authBuffer = Buffer.from(`${user}:${password || ''}`);
|
|
headers.set('Authorization', 'Basic ' + authBuffer.toString('base64'));
|
|
}
|
|
this.#async = async;
|
|
this.#aborted = false;
|
|
this.#response = null;
|
|
this.#responseBody = null;
|
|
this.#abortController = new window.AbortController();
|
|
this.#request = new window.Request(url, {
|
|
method,
|
|
headers,
|
|
signal: this.#abortController.signal,
|
|
credentials: this.withCredentials ? 'include' : 'same-origin'
|
|
});
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.opened;
|
|
}
|
|
/**
|
|
* Sets a header for the request.
|
|
*
|
|
* @param name Header name.
|
|
* @param value Header value.
|
|
* @returns Header added.
|
|
*/
|
|
setRequestHeader(name, value) {
|
|
if (this.readyState !== XMLHttpRequestReadyStateEnum_js_1.default.opened) {
|
|
throw new this[PropertySymbol.window].DOMException(`Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.`, DOMExceptionNameEnum_js_1.default.invalidStateError);
|
|
}
|
|
// TODO: Use FetchRequestHeaderUtility.removeForbiddenHeaders() instead.
|
|
if (FetchRequestHeaderUtility_js_1.default.isHeaderForbidden(name)) {
|
|
return false;
|
|
}
|
|
this.#request.headers.set(name, value);
|
|
return true;
|
|
}
|
|
/**
|
|
* Gets a header from the server response.
|
|
*
|
|
* @param header header Name of header to get.
|
|
* @returns string Text of the header or null if it doesn't exist.
|
|
*/
|
|
getResponseHeader(header) {
|
|
return this.#response?.headers.get(header) ?? null;
|
|
}
|
|
/**
|
|
* Gets all the response headers.
|
|
*
|
|
* @returns A string with all response headers separated by CR+LF.
|
|
*/
|
|
getAllResponseHeaders() {
|
|
if (!this.#response) {
|
|
return '';
|
|
}
|
|
const result = [];
|
|
for (const [name, value] of this.#response?.headers) {
|
|
const lowerName = name.toLowerCase();
|
|
if (lowerName !== 'set-cookie' && lowerName !== 'set-cookie2') {
|
|
result.push(`${name}: ${value}`);
|
|
}
|
|
}
|
|
return result.join('\r\n');
|
|
}
|
|
/**
|
|
* Sends the request to the server.
|
|
*
|
|
* @param body Optional data to send as request body.
|
|
*/
|
|
send(body) {
|
|
const window = this[PropertySymbol.window];
|
|
if (this.readyState != XMLHttpRequestReadyStateEnum_js_1.default.opened) {
|
|
throw new this[PropertySymbol.window].DOMException(`Failed to execute 'send' on 'XMLHttpRequest': Connection must be opened before send() is called.`, DOMExceptionNameEnum_js_1.default.invalidStateError);
|
|
}
|
|
// When body is a Document, serialize it to a string.
|
|
if (typeof body === 'object' &&
|
|
body !== null &&
|
|
body[PropertySymbol.nodeType] === NodeTypeEnum_js_1.default.documentNode) {
|
|
body = new window.XMLSerializer().serializeToString(body);
|
|
}
|
|
if (this.#async) {
|
|
this.#sendAsync(body).catch((error) => {
|
|
throw error;
|
|
});
|
|
}
|
|
else {
|
|
this.#sendSync(body);
|
|
}
|
|
}
|
|
/**
|
|
* Aborts a request.
|
|
*/
|
|
abort() {
|
|
if (this.#aborted) {
|
|
return;
|
|
}
|
|
this.#aborted = true;
|
|
this.#abortController.abort();
|
|
}
|
|
/**
|
|
* Overrides the MIME type returned by the server.
|
|
* This method must be called before send().
|
|
*
|
|
* @param mimeType The MIME type to use instead of the one specified by the server.
|
|
* @throws {DOMException} If the request state is LOADING or DONE.
|
|
*/
|
|
overrideMimeType(mimeType) {
|
|
if (this.readyState === XMLHttpRequestReadyStateEnum_js_1.default.loading ||
|
|
this.readyState === XMLHttpRequestReadyStateEnum_js_1.default.done) {
|
|
throw new this[PropertySymbol.window].DOMException(`Failed to execute 'overrideMimeType' on 'XMLHttpRequest': MIME type cannot be overridden when the request state is LOADING or DONE.`, DOMExceptionNameEnum_js_1.default.invalidStateError);
|
|
}
|
|
this.#overriddenMimeType = mimeType;
|
|
}
|
|
/**
|
|
* Sends the request to the server asynchronously.
|
|
*
|
|
* @param body Optional data to send as request body.
|
|
*/
|
|
async #sendAsync(body) {
|
|
const window = this[PropertySymbol.window];
|
|
const browserFrame = new WindowBrowserContext_js_1.default(window).getBrowserFrame();
|
|
const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager];
|
|
const taskID = asyncTaskManager.startTask(() => this.abort());
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.loading;
|
|
this.dispatchEvent(new Event_js_1.default('readystatechange'));
|
|
this.dispatchEvent(new Event_js_1.default('loadstart'));
|
|
if (body) {
|
|
this.#request = new window.Request(this.#request.url, {
|
|
method: this.#request.method,
|
|
headers: this.#request.headers,
|
|
signal: this.#abortController.signal,
|
|
credentials: this.#request.credentials,
|
|
body
|
|
});
|
|
}
|
|
this.#abortController.signal.addEventListener('abort', () => {
|
|
this.#aborted = true;
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.unsent;
|
|
this.dispatchEvent(new Event_js_1.default('abort'));
|
|
this.dispatchEvent(new Event_js_1.default('loadend'));
|
|
this.dispatchEvent(new Event_js_1.default('readystatechange'));
|
|
asyncTaskManager.endTask(taskID);
|
|
});
|
|
const onError = (error) => {
|
|
if (error instanceof DOMException_js_1.default && error.name === DOMExceptionNameEnum_js_1.default.abortError) {
|
|
if (this.#aborted) {
|
|
return;
|
|
}
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.unsent;
|
|
this.dispatchEvent(new Event_js_1.default('abort'));
|
|
}
|
|
else {
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.done;
|
|
this.dispatchEvent(new ErrorEvent_js_1.default('error', { error, message: error.message }));
|
|
}
|
|
this.dispatchEvent(new Event_js_1.default('loadend'));
|
|
this.dispatchEvent(new Event_js_1.default('readystatechange'));
|
|
asyncTaskManager.endTask(taskID);
|
|
};
|
|
const fetch = new Fetch_js_1.default({
|
|
browserFrame: browserFrame,
|
|
window: window,
|
|
url: this.#request.url,
|
|
init: this.#request
|
|
});
|
|
try {
|
|
this.#response = await fetch.send();
|
|
}
|
|
catch (error) {
|
|
onError(error);
|
|
return;
|
|
}
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.headersRecieved;
|
|
this.dispatchEvent(new Event_js_1.default('readystatechange'));
|
|
const contentLength = this.#response.headers.get('Content-Length');
|
|
const contentLengthNumber = contentLength !== null && !isNaN(Number(contentLength)) ? Number(contentLength) : null;
|
|
let loaded = 0;
|
|
let data = Buffer.from([]);
|
|
if (this.#response.body) {
|
|
let eventError;
|
|
try {
|
|
for await (const chunk of this.#response.body) {
|
|
data = Buffer.concat([data, typeof chunk === 'string' ? Buffer.from(chunk) : chunk]);
|
|
loaded += chunk.length;
|
|
// We need to re-throw the error as we don't want it to be caught by the try/catch.
|
|
try {
|
|
this.dispatchEvent(new ProgressEvent_js_1.default('progress', {
|
|
lengthComputable: contentLengthNumber !== null,
|
|
loaded,
|
|
total: contentLengthNumber !== null ? contentLengthNumber : 0
|
|
}));
|
|
}
|
|
catch (error) {
|
|
eventError = error;
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
catch (error) {
|
|
if (error === eventError) {
|
|
throw error;
|
|
}
|
|
onError(error);
|
|
return;
|
|
}
|
|
}
|
|
this.#responseBody = XMLHttpRequestResponseDataParser_js_1.default.parse({
|
|
window: window,
|
|
responseType: this.#responseType,
|
|
data,
|
|
contentType: this.#overriddenMimeType ||
|
|
this.#response.headers.get('Content-Type') ||
|
|
this.#request.headers.get('Content-Type')
|
|
});
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.done;
|
|
asyncTaskManager.endTask(taskID);
|
|
this.dispatchEvent(new Event_js_1.default('readystatechange'));
|
|
this.dispatchEvent(new Event_js_1.default('load'));
|
|
this.dispatchEvent(new Event_js_1.default('loadend'));
|
|
}
|
|
/**
|
|
* Sends the request to the server synchronously.
|
|
*
|
|
* @param body Optional data to send as request body.
|
|
*/
|
|
#sendSync(body) {
|
|
const window = this[PropertySymbol.window];
|
|
const browserFrame = new WindowBrowserContext_js_1.default(window).getBrowserFrame();
|
|
if (body) {
|
|
this.#request = new window.Request(this.#request.url, {
|
|
method: this.#request.method,
|
|
headers: this.#request.headers,
|
|
signal: this.#abortController.signal,
|
|
credentials: this.#request.credentials,
|
|
body
|
|
});
|
|
}
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.loading;
|
|
const fetch = new SyncFetch_js_1.default({
|
|
browserFrame,
|
|
window: window,
|
|
url: this.#request.url,
|
|
init: this.#request
|
|
});
|
|
try {
|
|
this.#response = fetch.send();
|
|
}
|
|
catch (error) {
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.done;
|
|
this.dispatchEvent(new ErrorEvent_js_1.default('error', { error, message: error.message }));
|
|
this.dispatchEvent(new Event_js_1.default('loadend'));
|
|
this.dispatchEvent(new Event_js_1.default('readystatechange'));
|
|
return;
|
|
}
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.headersRecieved;
|
|
this.#responseBody = XMLHttpRequestResponseDataParser_js_1.default.parse({
|
|
window: window,
|
|
responseType: this.#responseType,
|
|
data: this.#response.body,
|
|
contentType: this.#overriddenMimeType ||
|
|
this.#response.headers.get('Content-Type') ||
|
|
this.#request.headers.get('Content-Type')
|
|
});
|
|
this.#readyState = XMLHttpRequestReadyStateEnum_js_1.default.done;
|
|
this.dispatchEvent(new Event_js_1.default('readystatechange'));
|
|
this.dispatchEvent(new Event_js_1.default('load'));
|
|
this.dispatchEvent(new Event_js_1.default('loadend'));
|
|
}
|
|
}
|
|
exports.default = XMLHttpRequest;
|
|
//# sourceMappingURL=XMLHttpRequest.cjs.map
|