Update signup flow to include check for and automatic migration of legacy (v2) accounts.

This commit is contained in:
Martin Kleinschrodt 2022-06-08 13:29:19 +02:00
parent c71c3d0147
commit bf2bef1e8e
7 changed files with 124 additions and 113 deletions

View File

@ -23,6 +23,8 @@ import { displayProvisioning } from "../lib/provisioning";
import { StartAuthRequestResponse } from "@padloc/core/src/api";
import { Confetti } from "./confetti";
import { singleton } from "../lib/singleton";
import { PBES2Container } from "@padloc/core/src/container";
import { importLegacyContainer } from "../lib/import";
@customElement("pl-login-signup")
export class LoginOrSignup extends StartForm {
@ -158,10 +160,12 @@ export class LoginOrSignup extends StartForm {
authenticatorIndex?: number;
pendingRequest?: StartAuthRequestResponse;
}): Promise<{
email: string;
token: string;
accountStatus: AccountStatus;
provisioning: AccountProvisioning;
deviceTrusted: boolean;
legacyData?: PBES2Container;
} | null> {
try {
if (!req) {
@ -238,6 +242,11 @@ export class LoginOrSignup extends StartForm {
return;
}
if (authRes.accountStatus === AccountStatus.Unregistered && authRes.legacyData) {
this._migrateLegacyAccount(authRes);
return;
}
router.go(authRes.accountStatus === AccountStatus.Active ? "login" : "signup", {
...this.router.params,
email,
@ -317,15 +326,6 @@ export class LoginOrSignup extends StartForm {
this.go("start", { email });
// if (!verify.hasAccount) {
// if (verify.hasLegacyAccount) {
// this._migrateAccount(email, password, verify.legacyToken!, verify.token);
// } else {
// this._accountDoesntExist(email);
// }
// return;
// }
return;
case ErrorCode.INVALID_CREDENTIALS:
this._loginError = $l("Wrong master password. Please try again!");
@ -489,6 +489,95 @@ export class LoginOrSignup extends StartForm {
}
}
protected async _migrateLegacyAccount(authResponse: {
email: string;
legacyData?: PBES2Container;
token: string;
}): Promise<boolean> {
const legacyData = authResponse.legacyData!;
this._submitEmailButton.start();
const choice = await alert(
$l(
"You don't have a Padloc 4 account yet but we've found " +
"an account from an older version. " +
"Would you like to migrate your account to Padloc 4 now?"
),
{
title: "Account Migration",
icon: "user",
options: [$l("Migrate"), $l("Learn More"), $l("Cancel")],
}
);
if (choice === 1) {
window.open("https://padloc.app/help/migrate-v3", "_system");
return this._migrateLegacyAccount(authResponse);
} else if (choice === 2) {
this._submitEmailButton.stop();
return false;
}
const password = await prompt($l("Please enter your master password!"), {
title: $l("Migrating Account"),
placeholder: $l("Enter Master Password"),
confirmLabel: $l("Submit"),
type: "password",
preventAutoClose: true,
validate: async (password: string) => {
try {
await legacyData.unlock(password);
} catch (e) {
throw $l("Wrong password! Please try again!");
}
return password;
},
});
const items = await importLegacyContainer(legacyData);
if (items && password) {
await this.app.signup({ email: authResponse.email, password, name: "", authToken: authResponse.token });
await this.app.addItems(items, this.app.mainVault!);
const deleteLegacy = await confirm(
$l(
"Your account and all associated data was migrated successfully! Do you want to delete your old account now?"
),
$l("Yes"),
$l("No"),
{ title: $l("Delete Legacy Account"), icon: "delete", preventAutoClose: true }
);
if (deleteLegacy) {
await this.app.api.deleteLegacyAccount();
}
await alert(
$l(
"All done! Please note that you won't be able to access your Padloc 4 account " +
"with older versions of the app, so please make sure you have the latest version installed " +
"on all your devices! (You can find download links for all platforms at " +
"https://padloc.app/downloads/). Enjoy using Padloc 4!"
),
{
title: $l("Migration Complete"),
type: "success",
}
);
const { email: _email, name: _name, authToken, deviceTrusted, ...params } = this.router.params;
this.go("signup/success", params);
this._submitEmailButton.success();
return true;
} else {
alert($l("Unfortunately we could not complete migration of your data."), {
type: "warning",
});
this._submitEmailButton.stop();
return false;
}
}
static styles = [
...StartForm.styles,
css`

View File

@ -1,13 +1,7 @@
import { translate as $l } from "@padloc/locale/src/translate";
import { GetLegacyDataParams } from "@padloc/core/src/api";
import { VaultItem } from "@padloc/core/src/item";
import { mixins, shared } from "../styles";
import { Routing } from "../mixins/routing";
import { StateMixin } from "../mixins/state";
import { animateElement, animateCascade } from "../lib/animation";
import { alert, confirm, prompt } from "../lib/dialog";
import { importLegacyContainer } from "../lib/import";
import { app } from "../globals";
import { Logo } from "./logo";
import "./icon";
import { css, LitElement } from "lit";
@ -151,101 +145,4 @@ export abstract class StartForm extends Routing(StateMixin(LitElement)) {
rumble() {
animateElement(this.renderRoot.querySelector("form")!, { animation: "rumble", duration: 200, clear: true });
}
protected async _migrateAccount(
email: string,
password: string,
legacyToken: string,
authToken: string,
name = ""
): Promise<boolean> {
const choice = await alert(
$l(
"You don't have a Padloc 3 account yet but we've found " +
"an account from an older version. " +
"Would you like to migrate your account to Padloc 3 now?"
),
{
title: "Account Migration",
icon: "user",
options: [$l("Migrate"), $l("Learn More"), $l("Cancel")],
}
);
if (choice === 1) {
window.open("https://padloc.app/help/migrate-v3", "_system");
return this._migrateAccount(email, password, legacyToken, authToken, name);
} else if (choice === 2) {
return false;
}
const legacyData = await app.api.getLegacyData(
new GetLegacyDataParams({
email,
verify: legacyToken,
})
);
let items: VaultItem[] | null = null;
try {
if (!password) {
throw "No password provided";
}
await legacyData.unlock(password);
items = await importLegacyContainer(legacyData);
} catch (e) {
password = await prompt($l("Please enter your master password!"), {
title: $l("Migrating Account"),
placeholder: $l("Enter Master Password"),
confirmLabel: $l("Submit"),
type: "password",
preventAutoClose: true,
validate: async (password: string) => {
try {
await legacyData.unlock(password);
items = await importLegacyContainer(legacyData);
} catch (e) {
throw $l("Wrong password! Please try again!");
}
return password;
},
});
}
if (items && password) {
await app.signup({ email, password, authToken, name });
await app.addItems(items, app.mainVault!);
const deleteLegacy = await confirm(
$l(
"Your account and all associated data was migrated successfully! Do you want to delete your old account now?"
),
$l("Yes"),
$l("No"),
{ title: $l("Delete Legacy Account"), icon: "delete", preventAutoClose: true }
);
if (deleteLegacy) {
await app.api.deleteLegacyAccount();
}
await alert(
$l(
"All done! Please note that you won't be able to access your Padloc 3 account " +
"with older versions of the app, so please make sure you have the latest version installed " +
"on all your devices! (You can find download links for all platforms at " +
"https://padloc.app/downloads/). Enjoy using Padloc 3!"
),
{
title: $l("Migration Complete"),
type: "success",
}
);
return true;
} else {
alert($l("Unfortunately we could not complete migration of your data."), {
type: "warning",
});
return false;
}
}
}

View File

@ -277,6 +277,7 @@ export class WebPlatform extends StubPlatform implements Platform {
async completeAuthRequest(req: StartAuthRequestResponse) {
if (req.requestStatus === AuthRequestStatus.Verified) {
return {
email: req.email,
token: req.token,
deviceTrusted: req.deviceTrusted,
accountStatus: req.accountStatus!,
@ -290,7 +291,7 @@ export class WebPlatform extends StubPlatform implements Platform {
throw new Err(ErrorCode.AUTHENTICATION_FAILED, $l("The request was canceled."));
}
const { accountStatus, deviceTrusted, provisioning } = await app.api.completeAuthRequest(
const { accountStatus, deviceTrusted, provisioning, legacyData } = await app.api.completeAuthRequest(
new CompleteAuthRequestParams({
id: req.id,
data,
@ -299,10 +300,12 @@ export class WebPlatform extends StubPlatform implements Platform {
);
return {
email: req.email,
token: req.token,
deviceTrusted,
accountStatus,
provisioning,
legacyData,
};
}

View File

@ -200,6 +200,9 @@ export class CompleteAuthRequestResponse extends Serializable {
@AsSerializable(AccountProvisioning)
provisioning!: AccountProvisioning;
@AsSerializable(PBES2Container)
legacyData?: PBES2Container;
constructor(props?: Partial<CompleteAuthRequestResponse>) {
super();
props && Object.assign(this, props);

View File

@ -8,6 +8,7 @@ import { KeyStoreEntryInfo } from "./key-store";
import { SessionInfo } from "./session";
import { SRPSession } from "./srp";
import { getIdFromEmail, uuid } from "./util";
import { PBES2Container } from "./container";
export enum AuthPurpose {
Signup = "signup",
@ -227,6 +228,9 @@ export class Auth extends Serializable implements Storable {
expires: string;
}[] = [];
@AsSerializable(PBES2Container)
legacyData?: PBES2Container;
constructor(public email: string = "") {
super();
}

View File

@ -7,6 +7,7 @@ import { Storage, MemoryStorage } from "./storage";
import { AccountStatus, AuthPurpose, AuthType } from "./auth";
import { AccountProvisioning } from "./provisioning";
import { StartAuthRequestResponse } from "./api";
import { PBES2Container } from "./container";
/**
* Object representing all information available for a given device.
@ -116,10 +117,12 @@ export interface Platform {
}): Promise<StartAuthRequestResponse>;
completeAuthRequest(req: StartAuthRequestResponse): Promise<{
email: string;
token: string;
accountStatus: AccountStatus;
deviceTrusted: boolean;
provisioning: AccountProvisioning;
legacyData?: PBES2Container;
}>;
readonly platformAuthType: AuthType | null;
@ -192,10 +195,12 @@ export class StubPlatform implements Platform {
}
async completeAuthRequest(_req: StartAuthRequestResponse): Promise<{
email: string;
token: string;
accountStatus: AccountStatus;
deviceTrusted: boolean;
provisioning: AccountProvisioning;
legacyData?: PBES2Container;
}> {
throw new Error("Method not implemented.");
}
@ -290,10 +295,12 @@ export function startAuthRequest(opts: {
}
export function completeAuthRequest(req: StartAuthRequestResponse): Promise<{
email: string;
token: string;
accountStatus: AccountStatus;
deviceTrusted: boolean;
provisioning: AccountProvisioning;
legacyData?: PBES2Container;
}> {
return platform.completeAuthRequest(req);
}

View File

@ -473,6 +473,7 @@ export class Controller extends API {
accountStatus: auth.accountStatus,
deviceTrusted,
provisioning: provisioning.account,
legacyData: auth.legacyData,
});
}
@ -1781,6 +1782,13 @@ export class Controller extends API {
if (!auth) {
auth = new Auth(email);
await auth.init();
// We didn't find anything for this user in the database.
// Let's see if there is any legacy (v2) data for this user.
const legacyData = await this.legacyServer?.getStore(email);
if (legacyData) {
auth.legacyData = legacyData;
}
}
let updateAuth = false;