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
+9
View File
@@ -0,0 +1,9 @@
import { URL } from 'url';
type CreateUrlParams = {
protocol?: string;
host?: string;
port?: string;
path?: string;
};
export declare function createUrl({ protocol, host, port, path }: CreateUrlParams): URL;
export {};
+17
View File
@@ -0,0 +1,17 @@
import { URL } from 'url';
export function createUrl({ protocol, host, port, path }) {
// wrap IPv6 host in brackets
const ipv6Host = host?.includes(':') ? `[${host}]` : host;
// use fallback values to create a valid URL (protocol: 'undefined:', host: '[::]')
// nock v13 issue: protocol and host are undefined (https://github.com/chimurai/http-proxy-middleware/issues/1035)
// nock v14+ seems to return protocol and host correctly
const base = `${protocol || 'undefined:'}//${ipv6Host || '[::]'}`;
const url = new URL(base);
if (port) {
url.port = port;
}
if (path) {
url.pathname = path;
}
return url;
}
+1
View File
@@ -0,0 +1 @@
export declare function getFunctionName(fn: Function): string;
+4
View File
@@ -0,0 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
export function getFunctionName(fn) {
return fn.name || '[anonymous Function]';
}
+20
View File
@@ -0,0 +1,20 @@
import type * as http from 'node:http';
import type { Options } from '../types.js';
/**
* Normalize bracketed IPv6 URL targets into unbracketed host options.
*
* RFC 2732 defines the URL syntax for literal IPv6 addresses as bracketed
* host references (for example `http://[::1]:8080/path` where host is
* `[::1]`).
*
* `httpxy` resolves bracketed hostnames (for example `[::1]`) via DNS,
* which can fail for IPv6 literals. This converts string/URL `target` and
* `forward` values into object form with `hostname: ::1` (brackets removed)
* so the address can be connected directly.
*
* Reference: RFC 2732, Section 2 (Literal IPv6 Address Format in URL's)
* https://www.ietf.org/rfc/rfc2732.txt
*
* The provided options object is mutated in place.
*/
export declare function normalizeIPv6LiteralTargets<TReq extends http.IncomingMessage = http.IncomingMessage, TRes extends http.ServerResponse = http.ServerResponse>(options: Options<TReq, TRes>): void;
+66
View File
@@ -0,0 +1,66 @@
import { Debug } from '../debug.js';
const debug = Debug.extend('ipv6');
/**
* Normalize bracketed IPv6 URL targets into unbracketed host options.
*
* RFC 2732 defines the URL syntax for literal IPv6 addresses as bracketed
* host references (for example `http://[::1]:8080/path` where host is
* `[::1]`).
*
* `httpxy` resolves bracketed hostnames (for example `[::1]`) via DNS,
* which can fail for IPv6 literals. This converts string/URL `target` and
* `forward` values into object form with `hostname: ::1` (brackets removed)
* so the address can be connected directly.
*
* Reference: RFC 2732, Section 2 (Literal IPv6 Address Format in URL's)
* https://www.ietf.org/rfc/rfc2732.txt
*
* The provided options object is mutated in place.
*/
export function normalizeIPv6LiteralTargets(options) {
options.target = normalizeIPv6ProxyTarget(options.target, 'target');
options.forward = normalizeIPv6ProxyTarget(options.forward, 'forward');
}
function normalizeIPv6ProxyTarget(target, optionName) {
const targetUrl = toTargetUrl(target);
if (targetUrl && isBracketedIPv6Hostname(targetUrl.hostname)) {
const normalizedHostname = normalizeIPv6DestinationHostname(stripBrackets(targetUrl.hostname));
debug('normalized IPv6 "%s" %s', optionName, target);
const auth = targetUrl.username || targetUrl.password
? `${targetUrl.username}:${targetUrl.password}`
: undefined;
return {
hostname: normalizedHostname,
auth,
pathname: targetUrl.pathname,
port: targetUrl.port,
protocol: targetUrl.protocol,
search: targetUrl.search,
};
}
return target;
}
function toTargetUrl(target) {
if (typeof target === 'string') {
return new URL(target);
}
if (target instanceof URL) {
return target;
}
return undefined;
}
function isBracketedIPv6Hostname(hostname) {
return hostname.startsWith('[') && hostname.endsWith(']');
}
function stripBrackets(hostname) {
return hostname.replace(/^\[|\]$/g, '');
}
function normalizeIPv6DestinationHostname(hostname) {
// The unspecified address (::) is not a routable destination for outbound client requests.
// Treat it as loopback so a target like http://[::]:port reaches local IPv6 listeners.
if (hostname === '::') {
debug('normalizing hostname unspecified IPv6 address (::) to loopback (::1)');
return '::1';
}
return hostname;
}
+7
View File
@@ -0,0 +1,7 @@
import type { Agent } from 'node:http';
export type Sockets = Pick<Agent, 'sockets'>;
/**
* Get port from target
* Using proxyRes.req.agent.sockets to determine the target port
*/
export declare function getPort(sockets?: Sockets): string | undefined;
+7
View File
@@ -0,0 +1,7 @@
/**
* Get port from target
* Using proxyRes.req.agent.sockets to determine the target port
*/
export function getPort(sockets) {
return Object.keys(sockets || {})?.[0]?.split(':')[1];
}
+1
View File
@@ -0,0 +1 @@
export declare function sanitize(input: string | undefined): string;
+3
View File
@@ -0,0 +1,3 @@
export function sanitize(input) {
return input?.replace(/[<>]/g, (i) => encodeURIComponent(i)) ?? '';
}