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,111 @@
// @ts-expect-error moduleResolution:nodenext issue 54523
import { Chart as ChartJS, ChartConfiguration } from 'chart.js/auto';
import { ChartJSNodeCanvasBase, MimeType, Canvas } from './chartJSNodeCanvasBase';
const animationFrameProvider: AnimationFrameProvider = {
cancelAnimationFrame: (handle) => clearImmediate(handle as any),
requestAnimationFrame: (callback) => setImmediate(() => callback(Date.now())) as any,
};
type OnProgress = (chart: ChartJS, progress: number, initial: boolean) => void;
type OnComplete = (chart: ChartJS, initial: boolean) => void;
export class AnimatedChartJSNodeCanvas extends ChartJSNodeCanvasBase {
/**
* Render to a data url array.
* @see https://github.com/Automattic/node-canvas#canvastodataurl
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support), `raw` (unencoded ARGB32 data in native-endian byte order, top-to-bottom), `application/pdf` (for PDF canvases) and image/svg+xml (for SVG canvases). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas.
*/
public renderToDataURL(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): Promise<ReadonlyArray<string>> {
const frames: Array<string> = [];
return new Promise((resolve, _reject) => {
this.renderChart(configuration, (chart) => {
const canvas = chart.canvas as Canvas;
if (!canvas) {
throw new Error('Canvas is null');
}
const dataUrl = canvas.toDataURL(mimeType);
frames.push(dataUrl);
}, (chart) => {
resolve(frames);
chart.destroy();
});
});
}
/**
* Render to a buffer.
* @see https://github.com/Automattic/node-canvas#canvastobuffer
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support) or `raw` (unencoded ARGB32 data in native-endian byte order, top-to-bottom). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas.
*/
public renderToBuffer(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): Promise<ReadonlyArray<Buffer>> {
return new Promise((resolve, _reject) => {
const frames: Array<Buffer> = [];
this.renderChart(configuration, (chart) => {
const canvas = chart.canvas as Canvas;
if (!canvas) {
throw new Error('Canvas is null');
}
const buffer = canvas.toBuffer(mimeType);
frames.push(buffer);
}, (chart) => {
resolve(frames);
chart.destroy();
});
});
}
private renderChart(configuration: ChartConfiguration, onProgress: OnProgress, onComplete: OnComplete): ChartJS {
const canvas = this._createCanvas(this._width, this._height, this._type);
(canvas as any).style = (canvas as any).style || {};
const options = Object.assign({}, configuration.options);
options.responsive = false;
const animation = options.animation || {};
if (!animation.duration) {
animation.duration = 1000;
}
const baseOnProgress = animation.onProgress;
animation.onProgress = (event) => {
const currentStep: number = (event as any).currentStep; // type docs wrong?
const initial = !!(event as any).initial ? (event as any).initial : false; // added around 3.2.x
const progress = currentStep / event.numSteps;
if (baseOnProgress) {
//baseOnComplete(event.chart);
baseOnProgress.call(animation as any, event);
}
onProgress(event.chart, progress, initial);
};
const baseOnComplete = animation.onProgress;
animation.onComplete = (event) => {
const initial = !!(event as any).initial ? (event as any).initial : false; // added around 3.2.x
if (baseOnComplete) {
//baseOnComplete(event.chart);
baseOnComplete.call(animation as any, event);
}
onComplete(event.chart, initial);
};
const plugins = configuration.plugins || [];
const configuredChartConfig = { ...configuration, options, plugins };
global.window = global.window || {};
global.window.requestAnimationFrame = animationFrameProvider.requestAnimationFrame;
global.window.cancelAnimationFrame = animationFrameProvider.cancelAnimationFrame;
const context = canvas.getContext('2d');
(global as any).Image = this._image; // Some plugins use this API
const chart = new this._chartJs((context as any), configuredChartConfig);
delete (global as any).Image;
return chart;
}
}
@@ -0,0 +1,22 @@
// @ts-expect-error moduleResolution:nodenext issue 54523
import { Chart as ChartJS, Plugin as ChartJSPlugin } from 'chart.js/auto';
export class BackgroundColourPlugin implements ChartJSPlugin {
public readonly id: string = 'chartjs-plugin-chartjs-node-canvas-background-colour';
public constructor(
private readonly _width: number,
private readonly _height: number,
private readonly _fillStyle: string
) { }
public beforeDraw(chart: ChartJS): boolean | void {
const ctx = chart.ctx;
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = this._fillStyle;
ctx.fillRect(0, 0, this._width, this._height);
ctx.restore();
}
}
@@ -0,0 +1,140 @@
import { Readable } from 'stream';
// @ts-expect-error moduleResolution:nodenext issue 54523
import { Chart as ChartJS, ChartConfiguration, ChartComponentLike } from 'chart.js/auto';
import { ChartJSNodeCanvasBase, MimeType, Canvas } from './chartJSNodeCanvasBase';
export class ChartJSNodeCanvas extends ChartJSNodeCanvasBase {
/**
* Render to a data url.
* @see https://github.com/Automattic/node-canvas#canvastodataurl
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType The image format, `image/png` or `image/jpeg`.
*/
public renderToDataURL(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): Promise<string> {
const chart = this.renderChart(configuration);
return new Promise<string>((resolve, reject) => {
if (!chart.canvas) {
return reject(new Error('Canvas is null'));
}
const canvas = chart.canvas as Canvas;
canvas.toDataURL(mimeType, (error: Error | null, png: string) => {
chart.destroy();
if (error) {
return reject(error);
}
return resolve(png);
});
});
}
/**
* Render to a data url synchronously.
* @see https://github.com/Automattic/node-canvas#canvastodataurl
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType The image format, `image/png` or `image/jpeg`.
*/
public renderToDataURLSync(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): string {
const chart = this.renderChart(configuration);
if (!chart.canvas) {
throw new Error('Canvas is null');
}
const canvas = chart.canvas as Canvas;
const dataUrl = canvas.toDataURL(mimeType);
// Use this to destroy any chart instances that are created.
// This will clean up any references stored to the chart object within Chart.js, along with any associated event listeners attached by Chart.js.
// This must be called before the canvas is reused for a new chart.
chart.destroy();
return dataUrl;
}
/**
* Render to a buffer.
* @see https://github.com/Automattic/node-canvas#canvastobuffer
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support) or `raw` (unencoded ARGB32 data in native-endian byte order, top-to-bottom). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas.
*/
public renderToBuffer(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): Promise<Buffer> {
const chart = this.renderChart(configuration);
return new Promise<Buffer>((resolve, reject) => {
if (!chart.canvas) {
throw new Error('Canvas is null');
}
const canvas = chart.canvas as Canvas;
canvas.toBuffer((error: Error | null, buffer: Buffer) => {
chart.destroy();
if (error) {
return reject(error);
}
return resolve(buffer);
}, mimeType);
});
}
/**
* Render to a buffer synchronously.
* @see https://github.com/Automattic/node-canvas#canvastobuffer
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support), `raw` (unencoded ARGB32 data in native-endian byte order, top-to-bottom), `application/pdf` (for PDF canvases) and image/svg+xml (for SVG canvases). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas.
*/
public renderToBufferSync(configuration: ChartConfiguration, mimeType: MimeType | 'application/pdf' | 'image/svg+xml' = 'image/png'): Buffer {
const chart = this.renderChart(configuration);
if (!chart.canvas) {
throw new Error('Canvas is null');
}
const canvas = chart.canvas as Canvas;
const buffer = canvas.toBuffer(mimeType);
chart.destroy();
return buffer;
}
/**
* Render to a stream.
* @see https://github.com/Automattic/node-canvas#canvascreatepngstream
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support), `application/pdf` (for PDF canvases) and image/svg+xml (for SVG canvases). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas.
*/
public renderToStream(configuration: ChartConfiguration, mimeType: MimeType | 'application/pdf' = 'image/png'): Readable {
const chart = this.renderChart(configuration);
if (!chart.canvas) {
throw new Error('Canvas is null');
}
const canvas = chart.canvas as Canvas;
setImmediate(() => chart.destroy());
switch (mimeType) {
case 'image/png':
return canvas.createPNGStream();
case 'image/jpeg':
return canvas.createJPEGStream();
case 'application/pdf':
return canvas.createPDFStream();
default:
throw new Error(`Un-handled mimeType: ${mimeType}`);
}
}
private renderChart(configuration: ChartConfiguration): ChartJS {
const canvas = this._createCanvas(this._width, this._height, this._type);
(canvas as any).style = (canvas as any).style || {};
configuration.options = configuration.options || {};
configuration.options.responsive = false;
// Disable animation (otherwise charts will throw exceptions)
configuration.options.animation = false;
const context = canvas.getContext('2d');
(global as any).Image = this._image; // Some plugins use this API
const chart = new this._chartJs((context as any), configuration);
delete (global as any).Image;
return chart;
}
}
@@ -0,0 +1,169 @@
import { Readable } from 'stream';
// @ts-expect-error moduleResolution:nodenext issue 54523
import { Chart as ChartJS, ChartComponentLike } from 'chart.js/auto';
import { createCanvas, registerFont, Image } from 'canvas';
import { join as pathJoin } from 'path';
import { freshRequire } from './freshRequire';
import { BackgroundColourPlugin } from './backgroundColourPlugin';
export type ChartJSNodeCanvasPlugins = {
/**
* Global plugins, see https://www.chartjs.org/docs/latest/developers/plugins.html.
*/
readonly modern?: ReadonlyArray<string | ChartComponentLike>;
/**
* This will work for plugins that `require` ChartJS themselves.
*/
readonly requireChartJSLegacy?: ReadonlyArray<string>;
/**
* This should work for any plugin that expects a global Chart variable.
*/
readonly globalVariableLegacy?: ReadonlyArray<string>;
/**
* This will work with plugins that just return a plugin object and do no specific loading themselves.
*/
readonly requireLegacy?: ReadonlyArray<string>;
};
export type ChartCallback = (chartJS: typeof ChartJS) => void | Promise<void>;
export type CanvasType = 'pdf' | 'svg';
export type MimeType = 'image/png' | 'image/jpeg';
// https://github.com/Automattic/node-canvas#non-standard-apis
export type Canvas = HTMLCanvasElement & {
toBuffer(callback: (err: Error | null, result: Buffer) => void, mimeType?: string, config?: any): void;
toBuffer(mimeType?: string, config?: any): Buffer;
createPNGStream(config?: any): Readable;
createJPEGStream(config?: any): Readable;
createPDFStream(config?: any): Readable;
};
export interface ChartJSNodeCanvasOptions {
/**
* The width of the charts to render, in pixels.
*/
readonly width: number;
/**
* The height of the charts to render, in pixels.
*/
readonly height: number;
/**
* Optional callback which is called once with a new ChartJS global reference as the only parameter.
*/
readonly chartCallback?: ChartCallback;
/**
* Optional canvas type ('PDF' or 'SVG'), see the [canvas pdf doc](https://github.com/Automattic/node-canvas#pdf-output-support).
*/
readonly type?: CanvasType;
/**
* Optional plugins to register.
*/
readonly plugins?: ChartJSNodeCanvasPlugins;
/**
* Optional background color for the chart, otherwise it will be transparent. Note, this will apply to all charts. See the [fillStyle](https://www.w3schools.com/tags/canvas_fillstyle.asp) canvas API used for possible values.
*/
readonly backgroundColour?: string;
}
export abstract class ChartJSNodeCanvasBase {
protected readonly _width: number;
protected readonly _height: number;
protected readonly _chartJs: typeof ChartJS;
protected readonly _createCanvas: typeof createCanvas;
protected readonly _registerFont: typeof registerFont;
protected readonly _image: typeof Image;
protected readonly _type?: CanvasType;
/**
* Create a new instance of CanvasRenderService.
*
* @param options Configuration for this instance
*/
constructor(options: ChartJSNodeCanvasOptions) {
if (options === null || typeof (options) !== 'object') {
throw new Error('An options parameter object is required');
}
if (!options.width || typeof (options.width) !== 'number') {
throw new Error('A width option is required');
}
if (!options.height || typeof (options.height) !== 'number') {
throw new Error('A height option is required');
}
this._width = options.width;
this._height = options.height;
const canvas = freshRequire('canvas');
this._createCanvas = canvas.createCanvas;
this._registerFont = canvas.registerFont;
this._image = canvas.Image;
this._type = options.type && options.type.toLowerCase() as CanvasType;
this._chartJs = this.initialize(options);
}
/**
* Use to register the font with Canvas to use a font file that is not installed as a system font, this must be done before the Canvas is created.
*
* @param path The path to the font file.
* @param options The font options.
* @example
* registerFont('comicsans.ttf', { family: 'Comic Sans' });
*/
public registerFont(path: string, options: { readonly family: string, readonly weight?: string, readonly style?: string }): void {
this._registerFont(path, options);
}
protected initialize(options: ChartJSNodeCanvasOptions): typeof ChartJS {
const chartJs: typeof ChartJS = require('chart.js/auto');
if (options.plugins?.requireChartJSLegacy) {
for (const plugin of options.plugins.requireChartJSLegacy) {
require(plugin);
delete require.cache[require.resolve(plugin)];
}
}
if (options.plugins?.globalVariableLegacy) {
(global as any).Chart = chartJs;
for (const plugin of options.plugins.globalVariableLegacy) {
freshRequire(plugin);
}
delete (global as any).Chart;
}
if (options.plugins?.modern) {
for (const plugin of options.plugins.modern) {
if (typeof plugin === 'string') {
chartJs.register(freshRequire(plugin));
} else {
chartJs.register(plugin);
}
}
}
if (options.plugins?.requireLegacy) {
for (const plugin of options.plugins.requireLegacy) {
chartJs.register(freshRequire(plugin));
}
}
if (options.chartCallback) {
options.chartCallback(chartJs);
}
if (options.backgroundColour) {
chartJs.register(new BackgroundColourPlugin(options.width, options.height, options.backgroundColour));
}
const chartJsPath = pathJoin('node_modules','chart.js');
for (const key of Object.keys(require.cache)) {
if (key.includes(chartJsPath)) {
delete require.cache[key];
}
}
return chartJs;
}
}
+75
View File
@@ -0,0 +1,75 @@
import { ChartJSNodeCanvas, ChartCallback } from './';
// @ts-expect-error moduleResolution:nodenext issue 54523
import { ChartConfiguration } from 'chart.js/auto';
import path from 'path';
import { promises as fs } from 'fs';
async function main(): Promise<void> {
const width = 400;
const height = 400;
const configuration: ChartConfiguration = {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255,99,132,1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
},
plugins: [{
id: 'background-colour',
beforeDraw: (chart) => {
const ctx = chart.ctx;
ctx.save();
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
}]
};
const chartCallback: ChartCallback = (ChartJS) => {
ChartJS.defaults.responsive = true;
ChartJS.defaults.maintainAspectRatio = false;
};
console.log('here');
const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback });
const buffer = await chartJSNodeCanvas.renderToBuffer(configuration);
await fs.writeFile('./resources/example.png', buffer, 'base64');
const k = Object.keys(require.cache).find(key => key.includes(path.join('node_modules','chart.js')));
console.log('keys', k);
// const animatedChartJSNodeCanvas = new AnimatedChartJSNodeCanvas({ width, height, chartCallback });
// const buffers = await animatedChartJSNodeCanvas.renderToBuffer(configuration);
// const { Gif } = await import('make-a-gif');
// const gif = new Gif(width, height, 1);
// const totalDuration = 1000;
// const duration = totalDuration / buffers.length;
// await gif.setFrames(buffers.map(buffer => ({ src: new Uint8Array(buffer), duration })));
// // const data = await chartJSNodeCanvas.renderToDataURL(configuration);
// // console.log(data.length);
// const image = await gif.encode();
// await fs.writeFile('./resources/example.gif', image);
}
main();
+20
View File
@@ -0,0 +1,20 @@
// ES Module version of fresh-require
// export const freshImport = async (modulePath: string): Promise<any> => {
// // For ES modules, we need to construct the full URL
// const moduleUrl = new URL(modulePath, import.meta.url).href;
// // Clear module from import cache if possible
// // Note: ES modules caching works differently than CommonJS
// try {
// // Force a new module instance by appending a cache-busting query parameter
// const timestamp = Date.now();
// const urlWithCacheBuster = `${moduleUrl}?cache=${timestamp}`;
// // Dynamic import with cache buster
// const module = await import(/* @vite-ignore */ urlWithCacheBuster);
// return module;
// } catch (error) {
// console.error(`Error importing module ${modulePath}:`, error);
// throw error;
// }
// };
+11
View File
@@ -0,0 +1,11 @@
// https://github.com/hughsk/fresh-require
export const freshRequire: /*NodeJS.Require*/ (id: string) => any = (file) => {
const resolvedFile = require.resolve(file);
const temp = require.cache[resolvedFile];
delete require.cache[resolvedFile];
const modified = require(resolvedFile);
require.cache[resolvedFile] = temp;
return modified;
};
+4
View File
@@ -0,0 +1,4 @@
// syntax:
// declare module '<module>';
declare module 'resemblejs'; // Has a 'declare global' that causes typescript to add a '/// <reference types="resemblejs" />'
+3
View File
@@ -0,0 +1,3 @@
export * from './animatedChartJSNodeCanvas';
export * from './chartJSNodeCanvas';
export * from './chartJSNodeCanvasBase';