feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
@@ -0,0 +1,6 @@
import type { Plugin } from '../../types.js';
/**
* Subscribe to {@link https://github.com/unjs/httpxy#events `httpxy` error events} to prevent server from crashing.
* Errors are logged with {@link https://www.npmjs.com/package/debug debug} library.
*/
export declare const debugProxyErrorsPlugin: Plugin;
@@ -0,0 +1,77 @@
import { styleText } from 'node:util';
import { Debug } from '../../debug.js';
import { definePlugin } from '../define-plugin.js';
const debug = Debug.extend('debug-proxy-errors-plugin');
const BODY_PARSER_ERROR_MESSAGE = `[HPM] Connection reset (ECONNRESET) detected with non-empty "req.body" [ERR_HPM.GH40].
This usually means that the POST request body (req.body) was already parsed before reaching the proxy.
When bodyParser runs first, it consumes the request stream, leaving the proxy unable to forward the body data to the target server.
How to fix this issue:
- Option 1: Place the proxy middleware before the bodyParser middleware.
- Option 2: Use 'fixRequestBody()' helper to fix this issue.
For more details, see: https://github.com/chimurai/http-proxy-middleware/issues/40\n`;
function hasParsedBody(req) {
return Boolean(req && req.method === 'POST' && 'body' in req && req.body);
}
/**
* Subscribe to {@link https://github.com/unjs/httpxy#events `httpxy` error events} to prevent server from crashing.
* Errors are logged with {@link https://www.npmjs.com/package/debug debug} library.
*/
export const debugProxyErrorsPlugin = definePlugin((proxyServer, options) => {
/**
* The old `http-proxy` doesn't handle any errors by default (https://github.com/http-party/node-http-proxy#listening-for-proxy-events)
* > We do not do any error handling of messages passed between client and proxy, and messages passed between proxy and target, so it is recommended that you listen on errors and handle them.
* Subscribing to error event to prevent server from crashing
*/
proxyServer.on('error', (error, req, res, target) => {
debug(`httpxy error event: \n%O`, error);
// detect request body (when bodyParser used) and log an error message to help debugging
if (error.code === 'ECONNRESET' && hasParsedBody(req)) {
console.error(styleText('red', BODY_PARSER_ERROR_MESSAGE));
}
});
proxyServer.on('proxyReq', (proxyReq, req, socket) => {
socket.on('error', (error) => {
debug('Socket error in proxyReq event: \n%O', error);
});
});
/**
* Fix SSE close events
* @link https://github.com/chimurai/http-proxy-middleware/issues/678
* @link https://github.com/http-party/node-http-proxy/issues/1520#issue-877626125
*/
proxyServer.on('proxyRes', (proxyRes, req, res) => {
res.on('close', () => {
if (!res.writableEnded) {
debug('Destroying proxyRes in proxyRes close event');
proxyRes.destroy();
}
});
});
/**
* Fix crash when target server restarts
* https://github.com/chimurai/http-proxy-middleware/issues/476#issuecomment-746329030
* https://github.com/webpack/webpack-dev-server/issues/1642#issuecomment-790602225
*/
proxyServer.on('proxyReqWs', (proxyReq, req, socket) => {
socket.on('error', (error) => {
debug('Socket error in proxyReqWs event: \n%O', error);
});
});
proxyServer.on('open', (proxySocket) => {
proxySocket.on('error', (error) => {
debug('Socket error in open event: \n%O', error);
});
});
proxyServer.on('close', (req, socket, head) => {
socket.on('error', (error) => {
debug('Socket error in close event: \n%O', error);
});
});
// https://github.com/webpack/webpack-dev-server/issues/1642#issuecomment-1103136590
proxyServer.on('econnreset', (error, req, res, target) => {
debug(`httpxy econnreset event: \n%O`, error);
});
});
@@ -0,0 +1,2 @@
import type { Plugin } from '../../types.js';
export declare const errorResponsePlugin: Plugin;
@@ -0,0 +1,28 @@
import { getStatusCode } from '../../status-code.js';
import { sanitize } from '../../utils/sanitize.js';
import { definePlugin } from '../define-plugin.js';
function isResponseLike(obj) {
return obj && typeof obj.writeHead === 'function';
}
function isSocketLike(obj) {
return obj && typeof obj.write === 'function' && !('writeHead' in obj);
}
export const errorResponsePlugin = definePlugin((proxyServer, options) => {
proxyServer.on('error', (err, req, res, target) => {
// Re-throw error. Not recoverable since req & res are empty.
if (!req || !res) {
throw err; // "Error: Must provide a proper URL as target"
}
if (isResponseLike(res)) {
if (!res.headersSent) {
const statusCode = getStatusCode(err.code);
res.writeHead(statusCode);
}
const host = req.headers && req.headers.host;
res.end(`Error occurred while trying to proxy: ${sanitize(host)}${sanitize(req.url)}`);
}
else if (isSocketLike(res)) {
res.destroy();
}
});
});
+4
View File
@@ -0,0 +1,4 @@
export * from './debug-proxy-errors-plugin.js';
export * from './error-response-plugin.js';
export * from './logger-plugin.js';
export * from './proxy-events.js';
+4
View File
@@ -0,0 +1,4 @@
export * from './debug-proxy-errors-plugin.js';
export * from './error-response-plugin.js';
export * from './logger-plugin.js';
export * from './proxy-events.js';
@@ -0,0 +1,2 @@
import type { Plugin } from '../../types.js';
export declare const loggerPlugin: Plugin;
@@ -0,0 +1,57 @@
import { URL } from 'node:url';
import { getLogger } from '../../logger.js';
import { createUrl } from '../../utils/create-url.js';
import { getPort } from '../../utils/logger-plugin.js';
import { definePlugin } from '../define-plugin.js';
export const loggerPlugin = definePlugin((proxyServer, options) => {
const logger = getLogger(options);
proxyServer.on('error', (err, req, res, target) => {
const hostname = req?.headers?.host;
const requestHref = `${hostname}${req?.url}`;
const targetHref = `${target?.href}`; // target is undefined when websocket errors
const errorMessage = '[HPM] Error occurred while proxying request %s to %s [%s] (%s)';
const errReference = 'https://nodejs.org/api/errors.html#errors_common_system_errors'; // link to Node Common Systems Errors page
logger.error(errorMessage, requestHref, targetHref, err.code || err, errReference);
});
/**
* Log request and response
* @example
* ```shell
* [HPM] GET /users/ -> http://jsonplaceholder.typicode.com/users/ [304]
* ```
*/
proxyServer.on('proxyRes', (proxyRes, req, res) => {
// BrowserSync uses req.originalUrl
// Next.js doesn't have req.baseUrl
const originalUrl = req.originalUrl ?? `${req.baseUrl || ''}${req.url}`;
// construct targetUrl
let target;
try {
const port = getPort(proxyRes.req?.agent?.sockets);
const { protocol, host, path } = proxyRes.req;
target = createUrl({ protocol, host, port, path });
}
catch (err) {
// should not error. keeping fallback just in case
console.error('[HPM] Unexpected error while creating target URL', err);
// fallback to old implementation (less correct - without port)
target = new URL(options.target);
target.pathname = proxyRes.req.path;
}
const targetUrl = target.toString();
const exchange = `[HPM] ${req.method} ${originalUrl} -> ${targetUrl} [${proxyRes.statusCode}]`;
logger.info(exchange);
});
/**
* When client opens WebSocket connection
*/
proxyServer.on('open', (socket) => {
logger.info('[HPM] Client connected: %o', socket.address());
});
/**
* When client closes WebSocket connection
*/
proxyServer.on('close', (req, proxySocket, proxyHead) => {
logger.info('[HPM] Client disconnected: %o', proxySocket.address());
});
});
@@ -0,0 +1,22 @@
import type { Plugin } from '../../types.js';
/**
* Implements option.on object to subscribe to `httpxy` events.
*
* @example
* ```js
* createProxyMiddleware({
* on: {
* error: (error, req, res, target) => {},
* proxyReq: (proxyReq, req, res, options) => {},
* proxyReqWs: (proxyReq, req, socket, options) => {},
* proxyRes: (proxyRes, req, res) => {},
* open: (proxySocket) => {},
* close: (proxyRes, proxySocket, proxyHead) => {},
* start: (req, res, target) => {},
* end: (req, res, proxyRes) => {},
* econnreset: (error, req, res, target) => {},
* }
* });
* ```
*/
export declare const proxyEventsPlugin: Plugin;
@@ -0,0 +1,42 @@
import { Debug } from '../../debug.js';
import { getFunctionName } from '../../utils/function.js';
import { definePlugin } from '../define-plugin.js';
const debug = Debug.extend('proxy-events-plugin');
/**
* Implements option.on object to subscribe to `httpxy` events.
*
* @example
* ```js
* createProxyMiddleware({
* on: {
* error: (error, req, res, target) => {},
* proxyReq: (proxyReq, req, res, options) => {},
* proxyReqWs: (proxyReq, req, socket, options) => {},
* proxyRes: (proxyRes, req, res) => {},
* open: (proxySocket) => {},
* close: (proxyRes, proxySocket, proxyHead) => {},
* start: (req, res, target) => {},
* end: (req, res, proxyRes) => {},
* econnreset: (error, req, res, target) => {},
* }
* });
* ```
*/
export const proxyEventsPlugin = definePlugin((proxyServer, options) => {
if (!options.on) {
return;
}
// hoist variable here for better typing
let eventName;
// for in provide better typing than Object.entries()
for (eventName in options.on) {
if (Object.prototype.hasOwnProperty.call(options.on, eventName)) {
const handler = options.on[eventName];
if (!handler) {
continue;
}
debug(`register event handler: "${eventName}" -> "${getFunctionName(handler)}"`);
proxyServer.on(eventName, handler);
}
}
});