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
+18
View File
@@ -0,0 +1,18 @@
import { Job } from 'bull';
import { Job as JobMq } from 'bullmq';
import { JobCleanStatus, JobCounts, JobStatus, QueueAdapter, QueueAdapterOptions } from '../@types/app';
import * as Redis from 'ioredis';
export declare abstract class BaseAdapter implements QueueAdapter {
readonly readOnlyMode: boolean;
private formatters;
protected constructor(options?: Partial<QueueAdapterOptions>);
setFormatter(field: 'data' | 'returnValue', formatter: (data: any) => any): void;
format(field: 'data' | 'returnValue', data: any): any;
abstract clean(queueStatus: JobCleanStatus, graceTimeMs: number): Promise<void>;
abstract getJob(id: string): Promise<Job | JobMq | undefined | null>;
abstract getJobCounts(...jobStatuses: JobStatus[]): Promise<JobCounts>;
abstract getJobs(jobStatuses: JobStatus[], start?: number, end?: number): Promise<(Job | JobMq)[]>;
abstract getJobLogs(id: string): Promise<string[]>;
abstract getName(): string;
abstract getClient(): Promise<Redis.Redis>;
}
+20
View File
@@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseAdapter = void 0;
class BaseAdapter {
constructor(options = {}) {
this.formatters = {};
this.readOnlyMode = options.readOnlyMode === true;
}
setFormatter(field, formatter) {
this.formatters[field] = formatter;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
format(field, data) {
return typeof this.formatters[field] === 'function'
? this.formatters[field](data)
: data;
}
}
exports.BaseAdapter = BaseAdapter;
//# sourceMappingURL=base.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"base.js","sourceRoot":"","sources":["../../src/queueAdapters/base.ts"],"names":[],"mappings":";;;AAWA,MAAsB,WAAW;IAI/B,YAAsB,UAAwC,EAAE;QAFxD,eAAU,GAAuC,EAAE,CAAA;QAGzD,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,KAAK,IAAI,CAAA;IACnD,CAAC;IAEM,YAAY,CACjB,KAA6B,EAC7B,SAA6B;QAE7B,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,SAAS,CAAA;IACpC,CAAC;IAED,6EAA6E;IACtE,MAAM,CAAC,KAA6B,EAAE,IAAS;QACpD,OAAO,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,UAAU;YACjD,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC;YAC9B,CAAC,CAAC,IAAI,CAAA;IACV,CAAC;CAsBF;AA1CD,kCA0CC"}
+14
View File
@@ -0,0 +1,14 @@
import { Job, Queue } from 'bull';
import { JobCleanStatus, JobCounts, JobStatus, QueueAdapterOptions } from '../@types/app';
import { BaseAdapter } from './base';
export declare class BullAdapter extends BaseAdapter {
queue: Queue;
constructor(queue: Queue, options?: Partial<QueueAdapterOptions>);
getClient(): Promise<Queue['client']>;
getName(): string;
clean(jobStatus: JobCleanStatus, graceTimeMs: number): Promise<any>;
getJob(id: string): Promise<Job | undefined | null>;
getJobs(jobStatuses: JobStatus[], start?: number, end?: number): Promise<Job[]>;
getJobCounts(..._jobStatuses: JobStatus[]): Promise<JobCounts>;
getJobLogs(id: string): Promise<string[]>;
}
+34
View File
@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BullAdapter = void 0;
const base_1 = require("./base");
class BullAdapter extends base_1.BaseAdapter {
constructor(queue, options = {}) {
super(options);
this.queue = queue;
}
getClient() {
return Promise.resolve(this.queue.client);
}
getName() {
return this.queue.name;
}
clean(jobStatus, graceTimeMs) {
return this.queue.clean(graceTimeMs, jobStatus);
}
getJob(id) {
return this.queue.getJob(id);
}
getJobs(jobStatuses, start, end) {
return this.queue.getJobs(jobStatuses, start, end);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getJobCounts(..._jobStatuses) {
return this.queue.getJobCounts();
}
getJobLogs(id) {
return this.queue.getJobLogs(id).then(({ logs }) => logs);
}
}
exports.BullAdapter = BullAdapter;
//# sourceMappingURL=bull.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"bull.js","sourceRoot":"","sources":["../../src/queueAdapters/bull.ts"],"names":[],"mappings":";;;AAOA,iCAAoC;AAEpC,MAAa,WAAY,SAAQ,kBAAW;IAC1C,YAAmB,KAAY,EAAE,UAAwC,EAAE;QACzE,KAAK,CAAC,OAAO,CAAC,CAAA;QADG,UAAK,GAAL,KAAK,CAAO;IAE/B,CAAC;IAEM,SAAS;QACd,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAC3C,CAAC;IAEM,OAAO;QACZ,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAA;IACxB,CAAC;IAEM,KAAK,CAAC,SAAyB,EAAE,WAAmB;QACzD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,SAAgB,CAAC,CAAA;IACxD,CAAC;IAEM,MAAM,CAAC,EAAU;QACtB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAC9B,CAAC;IAEM,OAAO,CACZ,WAAwB,EACxB,KAAc,EACd,GAAY;QAEZ,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAkB,EAAE,KAAK,EAAE,GAAG,CAAC,CAAA;IAC3D,CAAC;IAED,6DAA6D;IACtD,YAAY,CAAC,GAAG,YAAyB;QAC9C,OAAQ,IAAI,CAAC,KAAK,CAAC,YAAY,EAAoC,CAAA;IACrE,CAAC;IAEM,UAAU,CAAC,EAAU;QAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAA;IAC3D,CAAC;CACF;AArCD,kCAqCC"}
+15
View File
@@ -0,0 +1,15 @@
import { Job, Queue } from 'bullmq';
import { JobCleanStatus, JobCounts, JobStatus, QueueAdapterOptions } from '../@types/app';
import { BaseAdapter } from './base';
export declare class BullMQAdapter extends BaseAdapter {
private queue;
private readonly LIMIT;
constructor(queue: Queue, options?: Partial<QueueAdapterOptions>);
getClient(): Queue['client'];
getName(): string;
clean(jobStatus: JobCleanStatus, graceTimeMs: number): Promise<void>;
getJob(id: string): Promise<Job | undefined>;
getJobs(jobStatuses: JobStatus[], start?: number, end?: number): Promise<Job[]>;
getJobCounts(...jobStatuses: JobStatus[]): Promise<JobCounts>;
getJobLogs(id: string): Promise<string[]>;
}
+34
View File
@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BullMQAdapter = void 0;
const base_1 = require("./base");
class BullMQAdapter extends base_1.BaseAdapter {
constructor(queue, options = {}) {
super(options);
this.queue = queue;
this.LIMIT = 1000;
}
getClient() {
return this.queue.client;
}
getName() {
return this.queue.name;
}
clean(jobStatus, graceTimeMs) {
return this.queue.clean(graceTimeMs, this.LIMIT, jobStatus);
}
getJob(id) {
return this.queue.getJob(id);
}
getJobs(jobStatuses, start, end) {
return this.queue.getJobs(jobStatuses, start, end);
}
getJobCounts(...jobStatuses) {
return this.queue.getJobCounts(...jobStatuses);
}
getJobLogs(id) {
return this.queue.getJobLogs(id).then(({ logs }) => logs);
}
}
exports.BullMQAdapter = BullMQAdapter;
//# sourceMappingURL=bullMQ.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"bullMQ.js","sourceRoot":"","sources":["../../src/queueAdapters/bullMQ.ts"],"names":[],"mappings":";;;AAOA,iCAAoC;AAEpC,MAAa,aAAc,SAAQ,kBAAW;IAG5C,YACU,KAAY,EACpB,UAAwC,EAAE;QAE1C,KAAK,CAAC,OAAO,CAAC,CAAA;QAHN,UAAK,GAAL,KAAK,CAAO;QAHL,UAAK,GAAG,IAAI,CAAA;IAO7B,CAAC;IAEM,SAAS;QACd,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAA;IAC1B,CAAC;IAEM,OAAO;QACZ,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAA;IACxB,CAAC;IAEM,KAAK,CAAC,SAAyB,EAAE,WAAmB;QACzD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IAC7D,CAAC;IAEM,MAAM,CAAC,EAAU;QACtB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAC9B,CAAC;IAEM,OAAO,CACZ,WAAwB,EACxB,KAAc,EACd,GAAY;QAEZ,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,CAAC,CAAA;IACpD,CAAC;IAEM,YAAY,CAAC,GAAG,WAAwB;QAC7C,OAAQ,IAAI,CAAC,KAAK,CAAC,YAAY,CAC7B,GAAG,WAAW,CACmB,CAAA;IACrC,CAAC;IAEM,UAAU,CAAC,EAAU;QAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAA;IAC3D,CAAC;CACF;AA3CD,sCA2CC"}