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
+12
View File
@@ -0,0 +1,12 @@
import { Status } from '../components/constants';
declare const regularItems: readonly ["Data", "Options", "Logs"];
export declare type TabsType = typeof regularItems[number] | 'Error';
export declare function useDetailsTabs(currentStatus: Status): {
tabs: {
title: TabsType;
isActive: boolean;
selectTab: () => void;
}[];
selectedTab: TabsType;
};
export {};
+26
View File
@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.useDetailsTabs = void 0;
const react_1 = require("react");
const constants_1 = require("../components/constants");
const regularItems = ['Data', 'Options', 'Logs'];
function useDetailsTabs(currentStatus) {
const [tabs, updateTabs] = react_1.useState([]);
const [selectedTabIdx, setSelectedTabIdx] = react_1.useState(0);
const selectedTab = tabs[selectedTabIdx];
react_1.useEffect(() => {
updateTabs(currentStatus === constants_1.STATUSES.failed
? ['Error', ...regularItems]
: [...regularItems]);
}, [currentStatus]);
return {
tabs: tabs.map((title, index) => ({
title,
isActive: title === selectedTab,
selectTab: () => setSelectedTabIdx(index),
})),
selectedTab,
};
}
exports.useDetailsTabs = useDetailsTabs;
//# sourceMappingURL=useDetailsTabs.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"useDetailsTabs.js","sourceRoot":"","sources":["../../../src/ui/hooks/useDetailsTabs.tsx"],"names":[],"mappings":";;;AAAA,iCAA2C;AAC3C,uDAA0D;AAE1D,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAU,CAAA;AAIzD,SAAgB,cAAc,CAAC,aAAqB;IAClD,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,gBAAQ,CAAa,EAAE,CAAC,CAAA;IACnD,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,gBAAQ,CAAC,CAAC,CAAC,CAAA;IACvD,MAAM,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,CAAA;IAExC,iBAAS,CAAC,GAAG,EAAE;QACb,UAAU,CACR,aAAa,KAAK,oBAAQ,CAAC,MAAM;YAC/B,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,YAAY,CAAC;YAC5B,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CACtB,CAAA;IACH,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAA;IAEnB,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;YAChC,KAAK;YACL,QAAQ,EAAE,KAAK,KAAK,WAAW;YAC/B,SAAS,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC;SAC1C,CAAC,CAAC;QACH,WAAW;KACZ,CAAA;AACH,CAAC;AArBD,wCAqBC"}
+1
View File
@@ -0,0 +1 @@
export declare function useInterval(callback: () => void, delay: number | null, deps?: any[]): void;
+25
View File
@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.useInterval = void 0;
const react_1 = require("react");
function useInterval(callback, delay, deps = []) {
const savedCallback = react_1.useRef(callback);
// Remember the latest callback if it changes.
react_1.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
react_1.useEffect(() => {
// Don't schedule if no delay is specified.
if (delay === null) {
return;
}
savedCallback.current();
const id = setInterval(() => savedCallback.current(), delay);
return () => {
clearInterval(id);
};
}, [delay, ...deps]);
}
exports.useInterval = useInterval;
//# sourceMappingURL=useInterval.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"useInterval.js","sourceRoot":"","sources":["../../../src/ui/hooks/useInterval.ts"],"names":[],"mappings":";;;AAAA,iCAAyC;AAEzC,SAAgB,WAAW,CACzB,QAAoB,EACpB,KAAoB,EACpB,OAAc,EAAE;IAEhB,MAAM,aAAa,GAAG,cAAM,CAAC,QAAQ,CAAC,CAAA;IAEtC,8CAA8C;IAC9C,iBAAS,CAAC,GAAG,EAAE;QACb,aAAa,CAAC,OAAO,GAAG,QAAQ,CAAA;IAClC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEd,uBAAuB;IACvB,iBAAS,CAAC,GAAG,EAAE;QACb,2CAA2C;QAC3C,IAAI,KAAK,KAAK,IAAI,EAAE;YAClB,OAAM;SACP;QAED,aAAa,CAAC,OAAO,EAAE,CAAA;QAEvB,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAA;QAE5D,OAAO,GAAG,EAAE;YACV,aAAa,CAAC,EAAE,CAAC,CAAA;QACnB,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;AACtB,CAAC;AA3BD,kCA2BC"}
+1
View File
@@ -0,0 +1 @@
export declare function useScrollTopOnNav(): void;
+13
View File
@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.useScrollTopOnNav = void 0;
const react_1 = require("react");
const react_router_dom_1 = require("react-router-dom");
function useScrollTopOnNav() {
const { key } = react_router_dom_1.useLocation();
react_1.useEffect(() => {
window.scrollTo(0, 0);
}, [key]);
}
exports.useScrollTopOnNav = useScrollTopOnNav;
//# sourceMappingURL=useScrollTopOnNav.js.map
@@ -0,0 +1 @@
{"version":3,"file":"useScrollTopOnNav.js","sourceRoot":"","sources":["../../../src/ui/hooks/useScrollTopOnNav.ts"],"names":[],"mappings":";;;AAAA,iCAAiC;AACjC,uDAA8C;AAE9C,SAAgB,iBAAiB;IAC/B,MAAM,EAAE,GAAG,EAAE,GAAG,8BAAW,EAAE,CAAA;IAC7B,iBAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACvB,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;AACX,CAAC;AALD,8CAKC"}
@@ -0,0 +1,2 @@
import { SelectedStatuses } from '../../@types/app';
export declare function useSelectedStatuses(): SelectedStatuses;
+22
View File
@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.useSelectedStatuses = void 0;
const react_1 = require("react");
const react_router_dom_1 = require("react-router-dom");
const constants_1 = require("../components/constants");
function useSelectedStatuses() {
const { search, pathname } = react_router_dom_1.useLocation();
const match = react_router_dom_1.useRouteMatch({ path: '/queue/:name' });
const [selectedStatuses, setSelectedStatuses] = react_1.useState({});
react_1.useEffect(() => {
const query = new URLSearchParams(search);
const status = query.get('status') || constants_1.STATUS_LIST[0];
const queue = match ? decodeURIComponent(match === null || match === void 0 ? void 0 : match.params.name) : '';
if (queue) {
setSelectedStatuses({ ...selectedStatuses, [queue]: status });
}
}, [search, pathname]);
return selectedStatuses;
}
exports.useSelectedStatuses = useSelectedStatuses;
//# sourceMappingURL=useSelectedStatuses.js.map
@@ -0,0 +1 @@
{"version":3,"file":"useSelectedStatuses.js","sourceRoot":"","sources":["../../../src/ui/hooks/useSelectedStatuses.ts"],"names":[],"mappings":";;;AAAA,iCAA2C;AAC3C,uDAA6D;AAE7D,uDAA6D;AAE7D,SAAgB,mBAAmB;IACjC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,8BAAW,EAAE,CAAA;IAC1C,MAAM,KAAK,GAAG,gCAAa,CAAmB,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAA;IAEvE,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAAG,gBAAQ,CAAmB,EAAE,CAAC,CAAA;IAE9E,iBAAS,CAAC,GAAG,EAAE;QACb,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,CAAA;QACzC,MAAM,MAAM,GAAI,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAY,IAAI,uBAAW,CAAC,CAAC,CAAC,CAAA;QAChE,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QACjE,IAAI,KAAK,EAAE;YACT,mBAAmB,CAAC,EAAE,GAAG,gBAAgB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;SAC9D;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;IAEtB,OAAO,gBAAgB,CAAA;AACzB,CAAC;AAhBD,kDAgBC"}
+14
View File
@@ -0,0 +1,14 @@
import * as api from '../../@types/api';
import { QueueActions, SelectedStatuses } from '../../@types/app';
import { Api } from '../services/Api';
declare type State = {
data: null | api.GetQueues;
loading: boolean;
};
export interface Store {
state: State;
actions: QueueActions;
selectedStatuses: SelectedStatuses;
}
export declare const useStore: (api: Api) => Store;
export {};
+46
View File
@@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.useStore = void 0;
const react_1 = require("react");
const useInterval_1 = require("./useInterval");
const useSelectedStatuses_1 = require("./useSelectedStatuses");
const interval = 5000;
const useStore = (api) => {
const [state, setState] = react_1.useState({
data: null,
loading: true,
});
const selectedStatuses = useSelectedStatuses_1.useSelectedStatuses();
const update = () => api
.getQueues({ status: selectedStatuses })
.then((data) => {
setState({ data, loading: false });
})
// eslint-disable-next-line no-console
.catch((error) => console.error('Failed to poll', error));
useInterval_1.useInterval(update, interval, [selectedStatuses]);
const promoteJob = (queueName) => (job) => () => api.promoteJob(queueName, job.id).then(update);
const retryJob = (queueName) => (job) => () => api.retryJob(queueName, job.id).then(update);
const cleanJob = (queueName) => (job) => () => api.cleanJob(queueName, job.id).then(update);
const retryAll = (queueName) => () => api.retryAll(queueName).then(update);
const cleanAllDelayed = (queueName) => () => api.cleanAllDelayed(queueName).then(update);
const cleanAllFailed = (queueName) => () => api.cleanAllFailed(queueName).then(update);
const cleanAllCompleted = (queueName) => () => api.cleanAllCompleted(queueName).then(update);
const getJobLogs = (queueName) => (job) => () => api.getJobLogs(queueName, job.id);
return {
state,
actions: {
promoteJob,
retryJob,
retryAll,
cleanJob,
cleanAllDelayed,
cleanAllFailed,
cleanAllCompleted,
getJobLogs,
},
selectedStatuses,
};
};
exports.useStore = useStore;
//# sourceMappingURL=useStore.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"useStore.js","sourceRoot":"","sources":["../../../src/ui/hooks/useStore.ts"],"names":[],"mappings":";;;AAAA,iCAAgC;AAIhC,+CAA2C;AAC3C,+DAA2D;AAE3D,MAAM,QAAQ,GAAG,IAAI,CAAA;AAad,MAAM,QAAQ,GAAG,CAAC,GAAQ,EAAS,EAAE;IAC1C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,gBAAQ,CAAQ;QACxC,IAAI,EAAE,IAAI;QACV,OAAO,EAAE,IAAI;KACd,CAAC,CAAA;IAEF,MAAM,gBAAgB,GAAG,yCAAmB,EAAE,CAAA;IAE9C,MAAM,MAAM,GAAG,GAAG,EAAE,CAClB,GAAG;SACA,SAAS,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;SACvC,IAAI,CAAC,CAAC,IAAmB,EAAE,EAAE;QAC5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;IACpC,CAAC,CAAC;QACF,sCAAsC;SACrC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAA;IAE7D,yBAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAA;IAEjD,MAAM,UAAU,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,EAAE,CAC9D,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAEhD,MAAM,QAAQ,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,EAAE,CAC5D,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAE9C,MAAM,QAAQ,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,EAAE,CAC5D,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAE9C,MAAM,QAAQ,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC,GAAG,EAAE,CAC3C,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAEtC,MAAM,eAAe,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC,GAAG,EAAE,CAClD,GAAG,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAE7C,MAAM,cAAc,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC,GAAG,EAAE,CACjD,GAAG,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAE5C,MAAM,iBAAiB,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC,GAAG,EAAE,CACpD,GAAG,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAE/C,MAAM,UAAU,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,EAAE,CAC9D,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC,CAAA;IAEnC,OAAO;QACL,KAAK;QACL,OAAO,EAAE;YACP,UAAU;YACV,QAAQ;YACR,QAAQ;YACR,QAAQ;YACR,eAAe;YACf,cAAc;YACd,iBAAiB;YACjB,UAAU;SACX;QACD,gBAAgB;KACjB,CAAA;AACH,CAAC,CAAA;AAzDY,QAAA,QAAQ,YAyDpB"}