Skip to content

Commit

Permalink
feat(login): integrate basic login flows happy paths
Browse files Browse the repository at this point in the history
Fixes #17137
  • Loading branch information
jrainville committed Jan 29, 2025
1 parent 78e4617 commit 15b8543
Show file tree
Hide file tree
Showing 16 changed files with 327 additions and 82 deletions.
7 changes: 2 additions & 5 deletions src/app/boot/app_controller.nim
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,9 @@ proc connect(self: AppController) =
elif defined(production):
setLogLevel(chronicles.LogLevel.INFO)

# TODO remove these functions once we have only the new onboarding module
proc shouldStartWithOnboardingScreen(self: AppController): bool =
return self.accountsService.openedAccounts().len == 0
# TODO remove this function once we have only the new onboarding module
proc shouldUseTheNewOnboardingModule(self: AppController): bool =
# Only the onboarding for new users is implemented in the new module for now
return singletonInstance.featureFlags().getOnboardingV2Enabled() and self.shouldStartWithOnboardingScreen()
return singletonInstance.featureFlags().getOnboardingV2Enabled()

proc newAppController*(statusFoundation: StatusFoundation): AppController =
result = AppController()
Expand Down
61 changes: 57 additions & 4 deletions src/app/modules/onboarding/controller.nim
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import app_service/service/accounts/service as accounts_service
import app_service/service/accounts/dto/image_crop_rectangle
import app_service/service/devices/service as devices_service
import app_service/service/keycardV2/service as keycard_serviceV2
import app_service/common/utils
from app_service/service/keycardV2/dto import KeycardExportedKeysDto

logScope:
Expand Down Expand Up @@ -86,14 +87,29 @@ proc init*(self: Controller) =
self.delegate.onKeycardLoadMnemonicSuccess(args.keyUID)
self.connectionIds.add(handlerId)

handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_KEYS_FAILURE) do(e: Args):
handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_RESTORE_KEYS_FAILURE) do(e: Args):
let args = KeycardErrorArg(e)
self.delegate.onKeycardExportKeysFailure(args.error)
self.delegate.onKeycardExportRestoreKeysFailure(args.error)
self.connectionIds.add(handlerId)

handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_KEYS_SUCCESS) do(e: Args):
handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_RESTORE_KEYS_SUCCESS) do(e: Args):
let args = KeycardExportedKeysArg(e)
self.delegate.onKeycardExportKeysSuccess(args.exportedKeys)
self.delegate.onKeycardExportRestoreKeysSuccess(args.exportedKeys)
self.connectionIds.add(handlerId)

handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_LOGIN_KEYS_FAILURE) do(e: Args):
let args = KeycardErrorArg(e)
self.delegate.onKeycardExportLoginKeysFailure(args.error)
self.connectionIds.add(handlerId)

handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_LOGIN_KEYS_SUCCESS) do(e: Args):
let args = KeycardExportedKeysArg(e)
self.delegate.onKeycardExportLoginKeysSuccess(args.exportedKeys)
self.connectionIds.add(handlerId)

handlerId = self.events.onWithUUID(SIGNAL_LOGIN_ERROR) do(e: Args):
let args = LoginErrorArgs(e)
self.delegate.onAccountLoginError(args.error)
self.connectionIds.add(handlerId)

proc initialize*(self: Controller, pin: string) =
Expand Down Expand Up @@ -174,3 +190,40 @@ proc generateMnemonic*(self: Controller, length: int): string =

proc exportRecoverKeysFromKeycard*(self: Controller) =
self.keycardServiceV2.asyncExportRecoverKeys()

proc exportLoginKeysFromKeycard*(self: Controller) =
self.keycardServiceV2.asyncExportLoginKeys()

proc getOpenedAccounts*(self: Controller): seq[AccountDto] =
return self.accountsService.openedAccounts()

proc getAccountByKeyUid*(self: Controller, keyUid: string): AccountDto =
return self.accountsService.getAccountByKeyUid(keyUid)

proc login*(
self: Controller,
account: AccountDto,
password: string,
keycard: bool = false,
publicEncryptionKey: string = "",
privateWhisperKey: string = "",
mnemonic: string = "",
keycardReplacement: bool = false,
) =
var passwordHash, chatPrivateKey = ""

if not keycard:
passwordHash = hashPassword(password)
else:
passwordHash = publicEncryptionKey
chatPrivateKey = privateWhisperKey

# if keycard and keycardReplacement:
# self.delegate.applyKeycardReplacementAfterLogin()

self.accountsService.login(
account,
passwordHash,
chatPrivateKey,
mnemonic,
)
16 changes: 14 additions & 2 deletions src/app/modules/onboarding/io_interface.nim
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ method loadMnemonic*(self: AccessInterface, dataJson: string) {.base.} =
method finishOnboardingFlow*(self: AccessInterface, flowInt: int, dataJson: string): string {.base.} =
raise newException(ValueError, "No implementation available")

method loginRequested*(self: AccessInterface, keyUid: string, loginFlow: int, dataJson: string) {.base.} =
raise newException(ValueError, "No implementation available")

method onLocalPairingStatusUpdate*(self: AccessInterface, status: LocalPairingStatus) {.base.} =
raise newException(ValueError, "No implementation available")

Expand All @@ -63,10 +66,19 @@ method onKeycardLoadMnemonicFailure*(self: AccessInterface, error: string) {.bas
method onKeycardLoadMnemonicSuccess*(self: AccessInterface, keyUID: string) {.base.} =
raise newException(ValueError, "No implementation available")

method onKeycardExportKeysFailure*(self: AccessInterface, error: string) {.base.} =
method onKeycardExportRestoreKeysFailure*(self: AccessInterface, error: string) {.base.} =
raise newException(ValueError, "No implementation available")

method onKeycardExportRestoreKeysSuccess*(self: AccessInterface, exportedKeys: KeycardExportedKeysDto) {.base.} =
raise newException(ValueError, "No implementation available")

method onKeycardExportLoginKeysFailure*(self: AccessInterface, error: string) {.base.} =
raise newException(ValueError, "No implementation available")

method onKeycardExportLoginKeysSuccess*(self: AccessInterface, exportedKeys: KeycardExportedKeysDto) {.base.} =
raise newException(ValueError, "No implementation available")

method onKeycardExportKeysSuccess*(self: AccessInterface, exportedKeys: KeycardExportedKeysDto) {.base.} =
method onAccountLoginError*(self: AccessInterface, error: string) {.base.} =
raise newException(ValueError, "No implementation available")

method exportRecoverKeys*(self: AccessInterface) {.base.} =
Expand Down
119 changes: 96 additions & 23 deletions src/app/modules/onboarding/module.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import NimQml, chronicles, json
import NimQml, chronicles, json, strutils
import logging

import io_interface
Expand All @@ -14,12 +14,14 @@ from app_service/service/settings/dto/settings import SettingsDto
from app_service/service/accounts/dto/accounts import AccountDto
from app_service/service/keycardV2/dto import KeycardEventDto, KeycardExportedKeysDto, KeycardState

import ../startup/models/login_account_item as login_acc_item

export io_interface

logScope:
topics = "onboarding-module"

type SecondaryFlow* {.pure} = enum
type OnboardingFlow* {.pure} = enum
Unknown = 0,
CreateProfileWithPassword,
CreateProfileWithSeedphrase,
Expand All @@ -28,13 +30,17 @@ type SecondaryFlow* {.pure} = enum
LoginWithSeedphrase,
LoginWithSyncing,
LoginWithKeycard,
ActualLogin, # TODO get the real name and value for this when it's implemented on the front-end

type LoginMethod* {.pure} = enum
Unknown = 0,
Password,
Keycard,

type ProgressState* {.pure.} = enum
Idle,
InProgress,
Success,
Failed
Failed,

type
Module*[T: io_interface.DelegateInterface] = ref object of io_interface.AccessInterface
Expand All @@ -43,7 +49,8 @@ type
viewVariant: QVariant
controller: Controller
localPairingStatus: LocalPairingStatus
currentFlow: SecondaryFlow
loginFlow: LoginMethod
onboardingFlow: OnboardingFlow
exportedKeys: KeycardExportedKeysDto

proc newModule*[T](
Expand All @@ -58,6 +65,8 @@ proc newModule*[T](
result.delegate = delegate
result.view = view.newView(result)
result.viewVariant = newQVariant(result.view)
result.onboardingFlow = OnboardingFlow.Unknown
result.loginFlow = LoginMethod.Unknown
result.controller = controller.newController(
result,
events,
Expand Down Expand Up @@ -87,6 +96,20 @@ method onAppLoaded*[T](self: Module[T]) =
method load*[T](self: Module[T]) =
singletonInstance.engine.setRootContextProperty("onboardingModule", self.viewVariant)
self.controller.init()

let openedAccounts = self.controller.getOpenedAccounts()
if openedAccounts.len > 0:
var items: seq[login_acc_item.Item]
for i in 0..<openedAccounts.len:
let acc = openedAccounts[i]
var thumbnailImage: string
var largeImage: string
acc.extractImages(thumbnailImage, largeImage)
items.add(login_acc_item.initItem(order = i, acc.name, icon = "", thumbnailImage, largeImage, acc.keyUid, acc.colorHash,
acc.colorId, acc.keycardPairing))

self.view.setLoginAccountsModelItems(items)

self.delegate.onboardingDidLoad()

method initialize*[T](self: Module[T], pin: string) =
Expand Down Expand Up @@ -118,26 +141,26 @@ method loadMnemonic*[T](self: Module[T], mnemonic: string) =

method finishOnboardingFlow*[T](self: Module[T], flowInt: int, dataJson: string): string =
try:
self.currentFlow = SecondaryFlow(flowInt)
self.onboardingFlow = OnboardingFlow(flowInt)

let data = parseJson(dataJson)
let password = data["password"].str
let seedPhrase = data["seedphrase"].str

var err = ""

case self.currentFlow:
case self.onboardingFlow:
# CREATE PROFILE FLOWS
of SecondaryFlow.CreateProfileWithPassword:
of OnboardingFlow.CreateProfileWithPassword:
err = self.controller.createAccountAndLogin(password)
of SecondaryFlow.CreateProfileWithSeedphrase:
of OnboardingFlow.CreateProfileWithSeedphrase:
err = self.controller.restoreAccountAndLogin(
password,
seedPhrase,
recoverAccount = false,
keycardInstanceUID = "",
)
of SecondaryFlow.CreateProfileWithKeycardNewSeedphrase:
of OnboardingFlow.CreateProfileWithKeycardNewSeedphrase:
# New user with a seedphrase we showed them
let keycardEvent = self.view.getKeycardEvent()
err = self.controller.restoreAccountAndLogin(
Expand All @@ -146,7 +169,7 @@ method finishOnboardingFlow*[T](self: Module[T], flowInt: int, dataJson: string)
recoverAccount = false,
keycardInstanceUID = keycardEvent.keycardInfo.instanceUID,
)
of SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase:
of OnboardingFlow.CreateProfileWithKeycardExistingSeedphrase:
# New user who entered their own seed phrase
let keycardEvent = self.view.getKeycardEvent()
err = self.controller.restoreAccountAndLogin(
Expand All @@ -157,53 +180,78 @@ method finishOnboardingFlow*[T](self: Module[T], flowInt: int, dataJson: string)
)

# LOGIN FLOWS
of SecondaryFlow.LoginWithSeedphrase:
of OnboardingFlow.LoginWithSeedphrase:
err = self.controller.restoreAccountAndLogin(
password,
seedPhrase,
recoverAccount = true,
keycardInstanceUID = "",
)
of SecondaryFlow.LoginWithSyncing:
of OnboardingFlow.LoginWithSyncing:
# The pairing was already done directly through inputConnectionStringForBootstrapping, we can login
self.controller.loginLocalPairingAccount(
self.localPairingStatus.account,
self.localPairingStatus.password,
self.localPairingStatus.chatKey,
)
of SecondaryFlow.LoginWithKeycard:
of OnboardingFlow.LoginWithKeycard:
err = self.controller.restoreKeycardAccountAndLogin(
self.view.getKeycardEvent().keycardInfo.keyUID,
self.view.getKeycardEvent().keycardInfo.instanceUID,
self.exportedKeys,
recoverAccount = true
)
else:
raise newException(ValueError, "Unknown flow: " & $self.currentFlow)
raise newException(ValueError, "Unknown flow: " & $self.onboardingFlow)

return err
except Exception as e:
error "Error finishing Onboarding Flow", msg = e.msg
return e.msg

method loginRequested*[T](self: Module[T], keyUid: string, loginFlow: int, dataJson: string) =
try:
self.loginFlow = LoginMethod(loginFlow)

let data = parseJson(dataJson)
let account = self.controller.getAccountByKeyUid(keyUid)

case self.loginFlow:
of LoginMethod.Password:
self.controller.login(account, data["password"].str)
of LoginMethod.Keycard:
self.authorize(data["pin"].str)
# We will continue the flow when the card is authorized in onKeycardStateUpdated
else:
raise newException(ValueError, "Unknown flow: " & $self.onboardingFlow)

except Exception as e:
error "Error finishing Login Flow", msg = e.msg
self.view.accountLoginFailed(e.msg, wrongPassword = false)

proc finishAppLoading2[T](self: Module[T]) =
self.delegate.appReady()

# TODO get the flow to send the right metric
var eventType = "user-logged-in"
if self.currentFlow != SecondaryFlow.ActualLogin:
if self.loginFlow == LoginMethod.Unknown:
eventType = "onboarding-completed"
singletonInstance.globalEvents.addCentralizedMetricIfEnabled(eventType,
$(%*{"flowType": repr(self.currentFlow)}))
$(%*{"flowType": repr(self.onboardingFlow)}))

self.controller.stopKeycardService()

self.delegate.finishAppLoading()


method onAccountLoginError*[T](self: Module[T], error: string) =
# SQLITE_NOTADB: "file is not a database"
var wrongPassword = false
if error.contains("file is not a database"):
wrongPassword = true
self.view.accountLoginFailed(error, wrongPassword)

method onNodeLogin*[T](self: Module[T], error: string, account: AccountDto, settings: SettingsDto) =
if error.len != 0:
# TODO: Handle error
echo "ERROR from onNodeLogin: ", error
self.onAccountLoginError(error)
return

self.controller.setLoggedInAccount(account)
Expand All @@ -221,6 +269,11 @@ method onLocalPairingStatusUpdate*[T](self: Module[T], status: LocalPairingStatu
method onKeycardStateUpdated*[T](self: Module[T], keycardEvent: KeycardEventDto) =
self.view.setKeycardEvent(keycardEvent)

if keycardEvent.state == KeycardState.Authorized and self.loginFlow == LoginMethod.Keycard:
# After authorizing, we export the keys
self.controller.exportLoginKeysFromKeycard()
# We will login once we have the keys in onKeycardExportLoginKeysSuccess

if keycardEvent.state == KeycardState.NotEmpty and self.view.getPinSettingState() == ProgressState.InProgress.int:
# We just finished setting the pin
self.view.setPinSettingState(ProgressState.Success.int)
Expand All @@ -235,19 +288,39 @@ method onKeycardSetPinFailure*[T](self: Module[T], error: string) =
method onKeycardAuthorizeFailure*[T](self: Module[T], error: string) =
self.view.setAuthorizationState(ProgressState.Failed.int)

if self.loginFlow == LoginMethod.Keycard:
# We were trying to login and the authorization failed
var wrongPassword = false
if error.contains("wrong pin"):
wrongPassword = true
self.view.accountLoginFailed(error, wrongPassword)

method onKeycardLoadMnemonicFailure*[T](self: Module[T], error: string) =
self.view.setAddKeyPairState(ProgressState.Failed.int)

method onKeycardLoadMnemonicSuccess*[T](self: Module[T], keyUID: string) =
self.view.setAddKeyPairState(ProgressState.Success.int)

method onKeycardExportKeysFailure*[T](self: Module[T], error: string) =
method onKeycardExportRestoreKeysFailure*[T](self: Module[T], error: string) =
self.view.setRestoreKeysExportState(ProgressState.Failed.int)

method onKeycardExportKeysSuccess*[T](self: Module[T], exportedKeys: KeycardExportedKeysDto) =
method onKeycardExportRestoreKeysSuccess*[T](self: Module[T], exportedKeys: KeycardExportedKeysDto) =
self.exportedKeys = exportedKeys
self.view.setRestoreKeysExportState(ProgressState.Success.int)

method onKeycardExportLoginKeysFailure*[T](self: Module[T], error: string) =
self.view.accountLoginFailed(error, wrongPassword = false)

method onKeycardExportLoginKeysSuccess*[T](self: Module[T], exportedKeys: KeycardExportedKeysDto) =
# We got the keys, now we can login. If everything goes well, we will finish the app loading
self.controller.login(
self.controller.getAccountByKeyUid(self.view.getKeycardEvent.keycardInfo.keyUID),
password = "",
keycard = true,
publicEncryptionKey = exportedKeys.encryptionKey.publicKey,
privateWhisperKey = exportedKeys.whisperKey.privateKey,
)

method exportRecoverKeys*[T](self: Module[T]) =
self.view.setRestoreKeysExportState(ProgressState.InProgress.int)
self.controller.exportRecoverKeysFromKeycard()
Expand Down
Loading

0 comments on commit 15b8543

Please sign in to comment.