feat: add qr code alternative sign in method (#1594)

Co-authored-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
This commit is contained in:
Elias Schneider
2026-07-28 01:30:13 +02:00
committed by GitHub
co-authored by ItalyPaleAle
parent 968f97fa61
commit e1fd1d320f
38 changed files with 2215 additions and 73 deletions
@@ -38,24 +38,13 @@
});
const isDesktop = new MediaQuery('(min-width: 1024px)');
let alternativeSignInButton = $state({
href: '/login/alternative',
let alternativeSignInButton = $derived({
href:
page.url.pathname === '/login'
? `/login/alternative${page.url.search}`
: `/login/alternative?redirect=${encodeURIComponent(page.url.pathname + page.url.search)}`,
label: m.alternative_sign_in_methods()
});
appConfigStore.subscribe((config) => {
if (config.emailOneTimeAccessAsUnauthenticatedEnabled) {
alternativeSignInButton.href = '/login/alternative';
alternativeSignInButton.label = m.alternative_sign_in_methods();
} else {
alternativeSignInButton.href = '/login/alternative/code';
alternativeSignInButton.label = m.sign_in_with_login_code();
}
if (page.url.pathname != '/login') {
alternativeSignInButton.href = `${alternativeSignInButton.href}?redirect=${encodeURIComponent(page.url.pathname + page.url.search)}`;
}
});
</script>
{#if backgroundImageExists === undefined}
@@ -32,9 +32,14 @@
}
};
QRCode.toCanvas(canvasEl, value, options).catch((error: Error) => {
console.error('Error generating QR Code:', error);
});
QRCode.toCanvas(canvasEl, value, options)
.then(() => {
canvasEl?.style.removeProperty('height');
canvasEl?.style.removeProperty('width');
})
.catch((error: Error) => {
console.error('Error generating QR Code:', error);
});
}
});
</script>
@@ -0,0 +1,35 @@
import type {
DeviceLoginDecision,
DeviceLoginExchangeResult,
DeviceLoginRequest,
DeviceLoginVerificationInfo
} from '$lib/types/device-login.type';
import APIService from './api-service';
export default class DeviceLoginService extends APIService {
createRequest = async (signal?: AbortSignal) => {
const response = await this.api.post('/device-login/requests', undefined, { signal });
return response.data as DeviceLoginRequest;
};
exchangeRequest = async (
requestId: string,
signal?: AbortSignal
): Promise<DeviceLoginExchangeResult> => {
const response = await this.api.post(
`/device-login/requests/${requestId}/exchange`,
undefined,
{ signal }
);
return response.status === 202 ? null : response.data;
};
inspectRequest = async (code: string) => {
const response = await this.api.post('/device-login/verification', { code });
return response.data as DeviceLoginVerificationInfo;
};
decideRequest = async (code: string, decision: DeviceLoginDecision) => {
await this.api.post('/device-login/verification/decision', { code, decision });
};
}
@@ -0,0 +1,21 @@
import type { User } from './user.type';
export type DeviceLoginRequest = {
id: string;
userCode: string;
verificationUri: string;
verificationUriComplete: string;
expiresAt: string;
interval: number;
};
export type DeviceLoginVerificationInfo = {
userCode: string;
device: string;
ipAddress?: string;
expiresAt: string;
};
export type DeviceLoginDecision = 'approve' | 'deny';
export type DeviceLoginExchangeResult = User | null;
@@ -3,6 +3,7 @@ import { m } from '$lib/paraglide/messages';
export const eventTypes: Record<string, string> = {
SIGN_IN: m.sign_in(),
TOKEN_SIGN_IN: m.token_sign_in(),
REMOTE_SIGN_IN: m.remote_sign_in(),
CLIENT_AUTHORIZATION: m.client_authorization(),
NEW_CLIENT_AUTHORIZATION: m.new_client_authorization(),
ACCOUNT_CREATED: m.account_created(),