diff --git a/addons/dexie-cloud/src/DexieCloudAPI.ts b/addons/dexie-cloud/src/DexieCloudAPI.ts index 8902f64c1..82684213f 100644 --- a/addons/dexie-cloud/src/DexieCloudAPI.ts +++ b/addons/dexie-cloud/src/DexieCloudAPI.ts @@ -12,6 +12,15 @@ import { BehaviorSubject, Observable } from 'rxjs'; /** The API of db.cloud, where `db` is an instance of Dexie with dexie-cloud-addon active. */ + +export interface LoginHints { + email?: string; + userId?: string; + grant_type?: 'demo' | 'otp'; + otpId?: string; + otp?: string; +} + export interface DexieCloudAPI { // Version of dexie-cloud-addon version: string; @@ -67,11 +76,7 @@ export interface DexieCloudAPI { * @param userId Optional userId to authenticate * @param grant_type requested grant type */ - login(hint?: { - email?: string; - userId?: string; - grant_type?: 'demo' | 'otp'; - }): Promise; + login(hint?: LoginHints): Promise; logout(options?: {force?: boolean}): Promise; diff --git a/addons/dexie-cloud/src/authentication/authenticate.ts b/addons/dexie-cloud/src/authentication/authenticate.ts index 56abe8f3c..720732cd7 100644 --- a/addons/dexie-cloud/src/authentication/authenticate.ts +++ b/addons/dexie-cloud/src/authentication/authenticate.ts @@ -16,10 +16,11 @@ import { import { TokenErrorResponseError } from './TokenErrorResponseError'; import { alertUser, interactWithUser } from './interactWithUser'; import { InvalidLicenseError } from '../InvalidLicenseError'; +import { LoginHints } from '../DexieCloudAPI'; export type FetchTokenCallback = (tokenParams: { public_key: string; - hints?: { userId?: string; email?: string; grant_type?: string }; + hints?: LoginHints; }) => Promise; export async function loadAccessToken( @@ -63,7 +64,7 @@ export async function authenticate( context: UserLogin, fetchToken: FetchTokenCallback, userInteraction: BehaviorSubject, - hints?: { userId?: string; email?: string; grant_type?: string } + hints?: LoginHints ): Promise { if ( context.accessToken && @@ -145,7 +146,7 @@ async function userAuthenticate( context: UserLogin, fetchToken: FetchTokenCallback, userInteraction: BehaviorSubject, - hints?: { userId?: string; email?: string; grant_type?: string } + hints?: LoginHints ) { if (!crypto.subtle) { if (typeof location !== 'undefined' && location.protocol === 'http:') { diff --git a/addons/dexie-cloud/src/authentication/login.ts b/addons/dexie-cloud/src/authentication/login.ts index 811fcc9d4..762e984f5 100644 --- a/addons/dexie-cloud/src/authentication/login.ts +++ b/addons/dexie-cloud/src/authentication/login.ts @@ -1,4 +1,5 @@ import { DexieCloudDB } from '../db/DexieCloudDB'; +import { LoginHints } from '../DexieCloudAPI'; import { triggerSync } from '../sync/triggerSync'; import { authenticate, loadAccessToken } from './authenticate'; import { AuthPersistedContext } from './AuthPersistedContext'; @@ -9,7 +10,7 @@ import { UNAUTHORIZED_USER } from './UNAUTHORIZED_USER'; export async function login( db: DexieCloudDB, - hints?: { email?: string; userId?: string; grant_type?: string } + hints?: LoginHints ) { const currentUser = await db.getCurrentUser(); const origUserId = currentUser.userId; diff --git a/addons/dexie-cloud/src/authentication/otpFetchTokenCallback.ts b/addons/dexie-cloud/src/authentication/otpFetchTokenCallback.ts index 23a442a11..569d0d88e 100644 --- a/addons/dexie-cloud/src/authentication/otpFetchTokenCallback.ts +++ b/addons/dexie-cloud/src/authentication/otpFetchTokenCallback.ts @@ -1,4 +1,7 @@ import { + DemoTokenRequest, + OTPTokenRequest1, + OTPTokenRequest2, TokenErrorResponse, TokenFinalResponse, TokenRequest, @@ -25,8 +28,20 @@ export function otpFetchTokenCallback(db: DexieCloudDB): FetchTokenCallback { demo_user, grant_type: 'demo', scopes: ['ACCESS_DB'], + public_key + } satisfies DemoTokenRequest; + } else if (hints?.otpId && hints.otp) { + // User provided OTP ID and OTP code. This means that the OTP email + // has already gone out and the user may have clicked a magic link + // in the email with otp and otpId in query and the app has picked + // up those values and passed them to db.cloud.login(). + tokenRequest = { + grant_type: 'otp', + otp_id: hints.otpId, + otp: hints.otp, + scopes: ['ACCESS_DB'], public_key, - }; + } satisfies OTPTokenRequest2; } else { const email = await promptForEmail( userInteraction, @@ -37,8 +52,7 @@ export function otpFetchTokenCallback(db: DexieCloudDB): FetchTokenCallback { email, grant_type: 'otp', scopes: ['ACCESS_DB'], - public_key, - }; + } satisfies OTPTokenRequest1; } const res1 = await fetch(`${url}/token`, { body: JSON.stringify(tokenRequest), @@ -60,29 +74,32 @@ export function otpFetchTokenCallback(db: DexieCloudDB): FetchTokenCallback { // Demo user request can get a "tokens" response right away // Error can also be returned right away. return response; - } else if (tokenRequest.grant_type === 'otp') { + } else if (tokenRequest.grant_type === 'otp' && 'email' in tokenRequest) { if (response.type !== 'otp-sent') throw new Error(`Unexpected response from ${url}/token`); const otp = await promptForOTP(userInteraction, tokenRequest.email); - tokenRequest.otp = otp || ''; - tokenRequest.otp_id = response.otp_id; + const tokenRequest2 = { + ...tokenRequest, + otp: otp || '', + otp_id: response.otp_id, + } satisfies OTPTokenRequest2; let res2 = await fetch(`${url}/token`, { - body: JSON.stringify(tokenRequest), + body: JSON.stringify(tokenRequest2), method: 'post', headers: { 'Content-Type': 'application/json' }, mode: 'cors', }); while (res2.status === 401) { const errorText = await res2.text(); - tokenRequest.otp = await promptForOTP(userInteraction, tokenRequest.email, { + tokenRequest2.otp = await promptForOTP(userInteraction, tokenRequest.email, { type: 'error', messageCode: 'INVALID_OTP', message: errorText, messageParams: {} }); res2 = await fetch(`${url}/token`, { - body: JSON.stringify(tokenRequest), + body: JSON.stringify(tokenRequest2), method: 'post', headers: { 'Content-Type': 'application/json' }, mode: 'cors', diff --git a/libs/dexie-cloud-common/package.json b/libs/dexie-cloud-common/package.json index fd1ee1318..7d45ea572 100644 --- a/libs/dexie-cloud-common/package.json +++ b/libs/dexie-cloud-common/package.json @@ -1,6 +1,6 @@ { "name": "dexie-cloud-common", - "version": "1.0.31", + "version": "1.0.32", "description": "Library for shared code between dexie-cloud-addon, dexie-cloud (CLI) and dexie-cloud-server", "type": "module", "module": "dist/index.js", diff --git a/libs/dexie-cloud-common/src/types.ts b/libs/dexie-cloud-common/src/types.ts index 59bafe210..2fe58beda 100644 --- a/libs/dexie-cloud-common/src/types.ts +++ b/libs/dexie-cloud-common/src/types.ts @@ -1,16 +1,23 @@ export type TokenRequest = - | OTPTokenRequest + | OTPTokenRequest1 + | OTPTokenRequest2 | ClientCredentialsTokenRequest | RefreshTokenRequest | DemoTokenRequest; -export interface OTPTokenRequest { +export type OTPTokenRequest = OTPTokenRequest1 | OTPTokenRequest2; +export interface OTPTokenRequest1 { grant_type: 'otp'; - public_key?: string; // If a refresh token is requested. Clients own the keypair and sign refresh_token requests using it. email: string; scopes: string[]; // TODO use CLIENT_SCOPE type. - otp_id?: string; - otp?: string; +} + +export interface OTPTokenRequest2 { + grant_type: 'otp'; + public_key?: string; + scopes: string[]; // TODO use CLIENT_SCOPE type. + otp_id: string; + otp: string; } export interface ClientCredentialsTokenRequest {