зеркало из https://github.com/mozilla/fxa.git
feat(gql-api): Add totp/recovery code mutations.
Because: * The gql-api needs the ability to issue new recovery codes and update the users two-factor auth settings. This commit: * Add's two-factor auth and recovery code mutations. Closes #5395
This commit is contained in:
Родитель
e18d2bd8dc
Коммит
17edcd473d
|
@ -2,7 +2,7 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
import { DataSource, DataSourceConfig } from 'apollo-datasource';
|
import { DataSource, DataSourceConfig } from 'apollo-datasource';
|
||||||
import { Client } from 'fxa-js-client';
|
import { Client, MetricContext } from 'fxa-js-client';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
import { fxAccountClientToken } from '../constants';
|
import { fxAccountClientToken } from '../constants';
|
||||||
|
@ -49,6 +49,16 @@ export class AuthServerSource extends DataSource {
|
||||||
return this.authClient.attachedClientDestroy(this.token, clientInfo);
|
return this.authClient.attachedClientDestroy(this.token, clientInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public createTotpToken(
|
||||||
|
options: MetricContext
|
||||||
|
): ReturnType<Client['createTotpToken']> {
|
||||||
|
return this.authClient.createTotpToken(this.token, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroyTotpToken(): Promise<any> {
|
||||||
|
return this.authClient.deleteTotpToken(this.token);
|
||||||
|
}
|
||||||
|
|
||||||
public totp(): Promise<any> {
|
public totp(): Promise<any> {
|
||||||
return this.authClient.checkTotpTokenExists(this.token);
|
return this.authClient.checkTotpTokenExists(this.token);
|
||||||
}
|
}
|
||||||
|
@ -73,4 +83,12 @@ export class AuthServerSource extends DataSource {
|
||||||
public recoveryEmailSecondaryResendCode(email: string): Promise<any> {
|
public recoveryEmailSecondaryResendCode(email: string): Promise<any> {
|
||||||
return this.authClient.recoveryEmailSecondaryResendCode(this.token, email);
|
return this.authClient.recoveryEmailSecondaryResendCode(this.token, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public replaceRecoveryCodes() {
|
||||||
|
return this.authClient.replaceRecoveryCodes(this.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public verifyTotp(code: string, options?: { service: string }) {
|
||||||
|
return this.authClient.verifyTotpCode(this.token, code, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,19 +24,96 @@ import { CatchGatewayError } from '../error';
|
||||||
import { Context } from '../server';
|
import { Context } from '../server';
|
||||||
import { Account as AccountType } from './types/account';
|
import { Account as AccountType } from './types/account';
|
||||||
import {
|
import {
|
||||||
|
AttachedClientDisconnectInput,
|
||||||
|
ChangeRecoveryCodesInput,
|
||||||
|
CreateTotpInput,
|
||||||
|
DeleteTotpInput,
|
||||||
EmailInput,
|
EmailInput,
|
||||||
UpdateAvatarInput,
|
UpdateAvatarInput,
|
||||||
UpdateDisplayNameInput,
|
UpdateDisplayNameInput,
|
||||||
|
VerifyTotpInput,
|
||||||
} from './types/input';
|
} from './types/input';
|
||||||
import {
|
import {
|
||||||
BasicPayload,
|
BasicPayload,
|
||||||
|
ChangeRecoveryCodesPayload,
|
||||||
|
CreateTotpPayload,
|
||||||
UpdateAvatarPayload,
|
UpdateAvatarPayload,
|
||||||
UpdateDisplayNamePayload,
|
UpdateDisplayNamePayload,
|
||||||
|
VerifyTotpPayload,
|
||||||
} from './types/payload';
|
} from './types/payload';
|
||||||
import { AttachedClientDisconnectInput } from './types/input/attached-client-disconnect';
|
|
||||||
|
|
||||||
@Resolver((of) => AccountType)
|
@Resolver((of) => AccountType)
|
||||||
export class AccountResolver {
|
export class AccountResolver {
|
||||||
|
@Mutation((returns) => CreateTotpPayload, {
|
||||||
|
description:
|
||||||
|
'Create a new randomly generated TOTP token for a user if they do not currently have one.',
|
||||||
|
})
|
||||||
|
@CatchGatewayError
|
||||||
|
public async createTotp(
|
||||||
|
@Ctx() context: Context,
|
||||||
|
@Arg('input', (type) => CreateTotpInput)
|
||||||
|
input: CreateTotpInput
|
||||||
|
): Promise<CreateTotpPayload> {
|
||||||
|
const result = await context.dataSources.authAPI.createTotpToken(
|
||||||
|
input.metricsContext
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
clientMutationId: input.clientMutationId,
|
||||||
|
...result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation((returns) => VerifyTotpPayload, {
|
||||||
|
description:
|
||||||
|
'Verifies the current session if the passed TOTP code is valid.',
|
||||||
|
})
|
||||||
|
@CatchGatewayError
|
||||||
|
public async verifyTotp(
|
||||||
|
@Ctx() context: Context,
|
||||||
|
@Arg('input', (type) => VerifyTotpInput)
|
||||||
|
input: VerifyTotpInput
|
||||||
|
): Promise<VerifyTotpPayload> {
|
||||||
|
const result = await context.dataSources.authAPI.verifyTotp(
|
||||||
|
input.code,
|
||||||
|
input.service ? { service: input.service } : undefined
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
clientMutationId: input.clientMutationId,
|
||||||
|
...result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation((returns) => BasicPayload, {
|
||||||
|
description: 'Deletes the current TOTP token for the user.',
|
||||||
|
})
|
||||||
|
@CatchGatewayError
|
||||||
|
public async deleteTotp(
|
||||||
|
@Ctx() context: Context,
|
||||||
|
@Arg('input', (type) => DeleteTotpInput)
|
||||||
|
input: DeleteTotpInput
|
||||||
|
): Promise<BasicPayload> {
|
||||||
|
await context.dataSources.authAPI.destroyTotpToken();
|
||||||
|
return {
|
||||||
|
clientMutationId: input.clientMutationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation((returns) => ChangeRecoveryCodesPayload, {
|
||||||
|
description: 'Return new recovery codes while removing old ones.',
|
||||||
|
})
|
||||||
|
@CatchGatewayError
|
||||||
|
public async changeRecoveryCodes(
|
||||||
|
@Ctx() context: Context,
|
||||||
|
@Arg('input', (type) => ChangeRecoveryCodesInput)
|
||||||
|
input: ChangeRecoveryCodesInput
|
||||||
|
): Promise<ChangeRecoveryCodesPayload> {
|
||||||
|
const result = await context.dataSources.authAPI.replaceRecoveryCodes();
|
||||||
|
return {
|
||||||
|
clientMutationId: input.clientMutationId,
|
||||||
|
recoveryCodes: result.recoveryCodes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation((returns) => UpdateDisplayNamePayload, {
|
@Mutation((returns) => UpdateDisplayNamePayload, {
|
||||||
description: 'Update the display name.',
|
description: 'Update the display name.',
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
import { Field, InputType } from 'type-graphql';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class ChangeRecoveryCodesInput {
|
||||||
|
@Field({
|
||||||
|
description: 'A unique identifier for the client performing the mutation.',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public clientMutationId?: string;
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
import { Field, InputType } from 'type-graphql';
|
||||||
|
|
||||||
|
import { MetricsContext } from './metrics-context';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class CreateTotpInput {
|
||||||
|
@Field({
|
||||||
|
description: 'A unique identifier for the client performing the mutation.',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public clientMutationId?: string;
|
||||||
|
|
||||||
|
@Field((type) => MetricsContext, { nullable: true })
|
||||||
|
public metricsContext!: MetricsContext;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
import { Field, InputType } from 'type-graphql';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class DeleteTotpInput {
|
||||||
|
@Field({
|
||||||
|
description: 'A unique identifier for the client performing the mutation.',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public clientMutationId?: string;
|
||||||
|
}
|
|
@ -4,3 +4,8 @@
|
||||||
export { EmailInput } from './email';
|
export { EmailInput } from './email';
|
||||||
export { UpdateAvatarInput } from './update-avatar';
|
export { UpdateAvatarInput } from './update-avatar';
|
||||||
export { UpdateDisplayNameInput } from './update-display-name';
|
export { UpdateDisplayNameInput } from './update-display-name';
|
||||||
|
export { CreateTotpInput } from './create-totp';
|
||||||
|
export { AttachedClientDisconnectInput } from './attached-client-disconnect';
|
||||||
|
export { ChangeRecoveryCodesInput } from './change-recovery-codes';
|
||||||
|
export { DeleteTotpInput } from './delete-totp';
|
||||||
|
export { VerifyTotpInput } from './verify-totp';
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
import { Field, InputType } from 'type-graphql';
|
||||||
|
|
||||||
|
@InputType({ description: 'Metrics context.' })
|
||||||
|
export class MetricsContext {
|
||||||
|
@Field({
|
||||||
|
description:
|
||||||
|
"The id of the client's device record, if it has registered one.",
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public deviceId?: string;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public entrypoint?: string;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public entrypointExperiment?: string;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public entrypointVariation?: string;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public flowId?: string;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public flowBeginTime?: number;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public productId?: string;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public planId?: string;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public utmCampaign?: number;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public utmContent?: number;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public utmMedium?: number;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public utmSource?: number;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public utmTerm?: number;
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
import { Field, InputType } from 'type-graphql';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class VerifyTotpInput {
|
||||||
|
@Field({
|
||||||
|
description: 'A unique identifier for the client performing the mutation.',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public clientMutationId?: string;
|
||||||
|
|
||||||
|
@Field({ description: 'The TOTP code to check' })
|
||||||
|
public code!: string;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public service?: string;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
import { Field, ObjectType } from 'type-graphql';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class ChangeRecoveryCodesPayload {
|
||||||
|
@Field({
|
||||||
|
description: 'A unique identifier for the client performing the mutation.',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public clientMutationId?: string;
|
||||||
|
|
||||||
|
@Field((returns) => [String])
|
||||||
|
public recoveryCodes!: string[];
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { Field, ObjectType } from 'type-graphql';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class CreateTotpPayload {
|
||||||
|
@Field({
|
||||||
|
description: 'A unique identifier for the client performing the mutation.',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public clientMutationId?: string;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
public qrCodeUrl!: string;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
public secret!: string;
|
||||||
|
|
||||||
|
@Field((returns) => [String])
|
||||||
|
public recoveryCodes!: string[];
|
||||||
|
}
|
|
@ -4,3 +4,6 @@
|
||||||
export { BasicPayload } from './basic';
|
export { BasicPayload } from './basic';
|
||||||
export { UpdateAvatarPayload } from './update-avatar';
|
export { UpdateAvatarPayload } from './update-avatar';
|
||||||
export { UpdateDisplayNamePayload } from './update-display-name';
|
export { UpdateDisplayNamePayload } from './update-display-name';
|
||||||
|
export { ChangeRecoveryCodesPayload } from './change-recovery-codes';
|
||||||
|
export { CreateTotpPayload } from './create-totp';
|
||||||
|
export { VerifyTotpPayload } from './verify-totp';
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
import { Field, ObjectType } from 'type-graphql';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class VerifyTotpPayload {
|
||||||
|
@Field({
|
||||||
|
description: 'A unique identifier for the client performing the mutation.',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public clientMutationId?: string;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
public success!: boolean;
|
||||||
|
|
||||||
|
@Field((returns) => [String], { nullable: true })
|
||||||
|
public recoveryCodes?: string[];
|
||||||
|
}
|
|
@ -170,6 +170,119 @@ describe('accountResolver', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createTotp', async () => {
|
||||||
|
it('succeeds', async () => {
|
||||||
|
context.dataSources.authAPI.createTotpToken.resolves({
|
||||||
|
qrCodeUrl: 'testurl',
|
||||||
|
recoveryCodes: ['test1', 'test2'],
|
||||||
|
secret: 'secretData',
|
||||||
|
});
|
||||||
|
const query = `mutation {
|
||||||
|
createTotp(input: {clientMutationId: "testid", metricsContext: {
|
||||||
|
deviceId: "device1",
|
||||||
|
flowBeginTime: 4238248
|
||||||
|
}}) {
|
||||||
|
clientMutationId
|
||||||
|
qrCodeUrl
|
||||||
|
secret
|
||||||
|
recoveryCodes
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
context.authUser = USER_1.uid;
|
||||||
|
const result = (await graphql(
|
||||||
|
schema,
|
||||||
|
query,
|
||||||
|
undefined,
|
||||||
|
context
|
||||||
|
)) as any;
|
||||||
|
assert.isDefined(result.data);
|
||||||
|
assert.isDefined(result.data.createTotp);
|
||||||
|
assert.deepEqual(result.data.createTotp, {
|
||||||
|
clientMutationId: 'testid',
|
||||||
|
qrCodeUrl: 'testurl',
|
||||||
|
recoveryCodes: ['test1', 'test2'],
|
||||||
|
secret: 'secretData',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('changeRecoveryCodes', async () => {
|
||||||
|
it('succeeds', async () => {
|
||||||
|
context.dataSources.authAPI.replaceRecoveryCodes.resolves({
|
||||||
|
recoveryCodes: ['test1', 'test2'],
|
||||||
|
});
|
||||||
|
const query = `mutation {
|
||||||
|
changeRecoveryCodes(input: {clientMutationId: "testid"}) {
|
||||||
|
clientMutationId
|
||||||
|
recoveryCodes
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
context.authUser = USER_1.uid;
|
||||||
|
const result = (await graphql(
|
||||||
|
schema,
|
||||||
|
query,
|
||||||
|
undefined,
|
||||||
|
context
|
||||||
|
)) as any;
|
||||||
|
assert.isDefined(result.data);
|
||||||
|
assert.isDefined(result.data.changeRecoveryCodes);
|
||||||
|
assert.deepEqual(result.data.changeRecoveryCodes, {
|
||||||
|
clientMutationId: 'testid',
|
||||||
|
recoveryCodes: ['test1', 'test2'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('verifyTotp', async () => {
|
||||||
|
it('succeeds', async () => {
|
||||||
|
context.dataSources.authAPI.verifyTotp.resolves({
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
const query = `mutation {
|
||||||
|
verifyTotp(input: {clientMutationId: "testid", code: "code1234"}) {
|
||||||
|
clientMutationId
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
context.authUser = USER_1.uid;
|
||||||
|
const result = (await graphql(
|
||||||
|
schema,
|
||||||
|
query,
|
||||||
|
undefined,
|
||||||
|
context
|
||||||
|
)) as any;
|
||||||
|
assert.isDefined(result.data);
|
||||||
|
assert.isDefined(result.data.verifyTotp);
|
||||||
|
assert.deepEqual(result.data.verifyTotp, {
|
||||||
|
clientMutationId: 'testid',
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteTotp', async () => {
|
||||||
|
it('succeeds', async () => {
|
||||||
|
context.dataSources.authAPI.destroyTotpToken.resolves(true);
|
||||||
|
const query = `mutation {
|
||||||
|
deleteTotp(input: {clientMutationId: "testid"}) {
|
||||||
|
clientMutationId
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
context.authUser = USER_1.uid;
|
||||||
|
const result = (await graphql(
|
||||||
|
schema,
|
||||||
|
query,
|
||||||
|
undefined,
|
||||||
|
context
|
||||||
|
)) as any;
|
||||||
|
assert.isDefined(result.data);
|
||||||
|
assert.isDefined(result.data.deleteTotp);
|
||||||
|
assert.deepEqual(result.data.deleteTotp, {
|
||||||
|
clientMutationId: 'testid',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('deleteSecondaryEmail', async () => {
|
describe('deleteSecondaryEmail', async () => {
|
||||||
it('succeeds', async () => {
|
it('succeeds', async () => {
|
||||||
context.dataSources.authAPI.recoveryEmailDestroy.resolves(true);
|
context.dataSources.authAPI.recoveryEmailDestroy.resolves(true);
|
||||||
|
|
|
@ -8,6 +8,22 @@ declare function FxAccountClient(
|
||||||
): FxAccountClient.Client;
|
): FxAccountClient.Client;
|
||||||
|
|
||||||
declare namespace FxAccountClient {
|
declare namespace FxAccountClient {
|
||||||
|
export type MetricContext = {
|
||||||
|
deviceId?: string;
|
||||||
|
entrypoint?: string;
|
||||||
|
entrypointExperiment?: string;
|
||||||
|
entrypointVariation?: string;
|
||||||
|
flowId?: string;
|
||||||
|
flowBeginTime?: number;
|
||||||
|
productId?: string;
|
||||||
|
planId?: string;
|
||||||
|
utmCampaign?: number;
|
||||||
|
utmContent?: number;
|
||||||
|
utmMedium?: number;
|
||||||
|
utmSource?: number;
|
||||||
|
utmTerm?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export interface Client {
|
export interface Client {
|
||||||
sessionStatus(
|
sessionStatus(
|
||||||
sessionToken: string
|
sessionToken: string
|
||||||
|
@ -36,6 +52,11 @@ declare namespace FxAccountClient {
|
||||||
token_type: string;
|
token_type: string;
|
||||||
expires_in: number;
|
expires_in: number;
|
||||||
}>;
|
}>;
|
||||||
|
createTotpToken(
|
||||||
|
sessionToken: string,
|
||||||
|
metricOptions: MetricContext
|
||||||
|
): Promise<{ qrCodeUrl: string; secret: string; recoveryCodes: string[] }>;
|
||||||
|
deleteTotpToken(sessionToken: string): Promise<any>;
|
||||||
recoveryEmailCreate(sessionToken: string, email: string): Promise<any>;
|
recoveryEmailCreate(sessionToken: string, email: string): Promise<any>;
|
||||||
recoveryEmailDestroy(sessionToken: string, email: string): Promise<any>;
|
recoveryEmailDestroy(sessionToken: string, email: string): Promise<any>;
|
||||||
recoveryEmailSetPrimaryEmail(
|
recoveryEmailSetPrimaryEmail(
|
||||||
|
@ -46,5 +67,13 @@ declare namespace FxAccountClient {
|
||||||
sessionToken: string,
|
sessionToken: string,
|
||||||
email: string
|
email: string
|
||||||
): Promise<any>;
|
): Promise<any>;
|
||||||
|
replaceRecoveryCodes(
|
||||||
|
sessionToken: string
|
||||||
|
): Promise<{ recoveryCodes: string[] }>;
|
||||||
|
verifyTotpCode(
|
||||||
|
sessionToken: string,
|
||||||
|
code: string,
|
||||||
|
options?: { service: string }
|
||||||
|
): Promise<{ success: boolean }>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче