chore: auth verification (#2867) [skip e2e]
This commit is contained in:
@@ -72,6 +72,7 @@ export class AuthApiService implements AuthApiServiceInterface {
|
|||||||
password: string
|
password: string
|
||||||
codeVerifier: string
|
codeVerifier: string
|
||||||
recoveryCodes: string
|
recoveryCodes: string
|
||||||
|
hvmToken?: string
|
||||||
}): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>> {
|
}): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>> {
|
||||||
if (this.operationsInProgress.get(AuthApiOperations.SignInWithRecoveryCodes)) {
|
if (this.operationsInProgress.get(AuthApiOperations.SignInWithRecoveryCodes)) {
|
||||||
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
||||||
@@ -86,6 +87,7 @@ export class AuthApiService implements AuthApiServiceInterface {
|
|||||||
password: dto.password,
|
password: dto.password,
|
||||||
recovery_codes: dto.recoveryCodes,
|
recovery_codes: dto.recoveryCodes,
|
||||||
username: dto.username,
|
username: dto.username,
|
||||||
|
hvm_token: dto.hvmToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ export interface AuthApiServiceInterface {
|
|||||||
password: string
|
password: string
|
||||||
codeVerifier: string
|
codeVerifier: string
|
||||||
recoveryCodes: string
|
recoveryCodes: string
|
||||||
|
hvmToken?: string
|
||||||
}): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>>
|
}): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export class UserApiService implements UserApiServiceInterface {
|
|||||||
async register(registerDTO: {
|
async register(registerDTO: {
|
||||||
email: string
|
email: string
|
||||||
serverPassword: string
|
serverPassword: string
|
||||||
|
hvmToken?: string
|
||||||
keyParams: RootKeyParamsInterface
|
keyParams: RootKeyParamsInterface
|
||||||
ephemeral: boolean
|
ephemeral: boolean
|
||||||
}): Promise<HttpResponse<UserRegistrationResponseBody>> {
|
}): Promise<HttpResponse<UserRegistrationResponseBody>> {
|
||||||
@@ -76,6 +77,7 @@ export class UserApiService implements UserApiServiceInterface {
|
|||||||
[ApiEndpointParam.ApiVersion]: this.apiVersion,
|
[ApiEndpointParam.ApiVersion]: this.apiVersion,
|
||||||
password: registerDTO.serverPassword,
|
password: registerDTO.serverPassword,
|
||||||
email: registerDTO.email,
|
email: registerDTO.email,
|
||||||
|
hvm_token: registerDTO.hvmToken,
|
||||||
ephemeral: registerDTO.ephemeral,
|
ephemeral: registerDTO.ephemeral,
|
||||||
...registerDTO.keyParams.getPortableValue(),
|
...registerDTO.keyParams.getPortableValue(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface UserApiServiceInterface {
|
|||||||
register(registerDTO: {
|
register(registerDTO: {
|
||||||
email: string
|
email: string
|
||||||
serverPassword: string
|
serverPassword: string
|
||||||
|
hvmToken?: string
|
||||||
keyParams: RootKeyParamsInterface
|
keyParams: RootKeyParamsInterface
|
||||||
ephemeral: boolean
|
ephemeral: boolean
|
||||||
}): Promise<HttpResponse<UserRegistrationResponseBody>>
|
}): Promise<HttpResponse<UserRegistrationResponseBody>>
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export interface SignInWithRecoveryCodesRequestParams {
|
|||||||
password: string
|
password: string
|
||||||
code_verifier: string
|
code_verifier: string
|
||||||
recovery_codes: string
|
recovery_codes: string
|
||||||
|
hvm_token?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ export type UserRegistrationRequestParams = AnyKeyParamsContent & {
|
|||||||
[additionalParam: string]: unknown
|
[additionalParam: string]: unknown
|
||||||
password: string
|
password: string
|
||||||
email: string
|
email: string
|
||||||
|
hvm_token?: string
|
||||||
ephemeral: boolean
|
ephemeral: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ platform :android do
|
|||||||
|
|
||||||
desc 'Deploy production app'
|
desc 'Deploy production app'
|
||||||
lane :prod do
|
lane :prod do
|
||||||
version = 3_002_000 + ENV['BUILD_NUMBER'].to_i
|
version = 3_004_000 + ENV['BUILD_NUMBER'].to_i
|
||||||
deploy_android 'prod', version
|
deploy_android 'prod', version
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type MetaEndpointResponse = {
|
||||||
|
captchaUIUrl: string | null
|
||||||
|
}
|
||||||
@@ -26,6 +26,30 @@ export function isErrorResponse<T>(response: HttpResponse<T>): response is HttpE
|
|||||||
return (response.data as HttpErrorResponseBody)?.error != undefined || response.status >= 400
|
return (response.data as HttpErrorResponseBody)?.error != undefined || response.status >= 400
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCaptchaHeader<T>(response: HttpResponse<T>) {
|
||||||
|
const captchaHeader = response.headers?.get('x-captcha-required')
|
||||||
|
if (captchaHeader) {
|
||||||
|
return captchaHeader
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getErrorMessageFromErrorResponseBody(data: HttpErrorResponseBody, defaultMessage?: string): string {
|
||||||
|
let errorMessage = defaultMessage || 'Unknown error'
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
typeof data === 'object' &&
|
||||||
|
'error' in data &&
|
||||||
|
data.error &&
|
||||||
|
typeof data.error === 'object' &&
|
||||||
|
'message' in data.error
|
||||||
|
) {
|
||||||
|
errorMessage = data.error.message as string
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorMessage
|
||||||
|
}
|
||||||
|
|
||||||
export function getErrorFromErrorResponse(response: HttpErrorResponse): HttpError {
|
export function getErrorFromErrorResponse(response: HttpErrorResponse): HttpError {
|
||||||
const embeddedError = response.data.error
|
const embeddedError = response.data.error
|
||||||
if (embeddedError) {
|
if (embeddedError) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export * from './Auth/SignInData'
|
|||||||
export * from './Auth/SignInResponse'
|
export * from './Auth/SignInResponse'
|
||||||
export * from './Auth/SignOutResponse'
|
export * from './Auth/SignOutResponse'
|
||||||
export * from './Auth/User'
|
export * from './Auth/User'
|
||||||
|
export * from './Auth/MetaEndpointResponse'
|
||||||
|
|
||||||
/** Temps are awaiting final publish state on server repo */
|
/** Temps are awaiting final publish state on server repo */
|
||||||
export * from './Temp/SharedVaultMoveType'
|
export * from './Temp/SharedVaultMoveType'
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ export interface AuthClientInterface {
|
|||||||
password: string
|
password: string
|
||||||
codeVerifier: string
|
codeVerifier: string
|
||||||
recoveryCodes: string
|
recoveryCodes: string
|
||||||
|
hvmToken?: string
|
||||||
}): Promise<
|
}): Promise<
|
||||||
| {
|
| {
|
||||||
|
success: true
|
||||||
keyParams: AnyKeyParamsContent
|
keyParams: AnyKeyParamsContent
|
||||||
session: SessionBody
|
session: SessionBody
|
||||||
user: {
|
user: {
|
||||||
@@ -23,6 +25,9 @@ export interface AuthClientInterface {
|
|||||||
protocolVersion: string
|
protocolVersion: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| false
|
| {
|
||||||
|
success: false
|
||||||
|
captchaURL: string
|
||||||
|
}
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AuthApiServiceInterface } from '@standardnotes/api'
|
import { AuthApiServiceInterface } from '@standardnotes/api'
|
||||||
import { AnyKeyParamsContent } from '@standardnotes/common'
|
import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||||
import { isErrorResponse, SessionBody } from '@standardnotes/responses'
|
import { isErrorResponse, getCaptchaHeader } from '@standardnotes/responses'
|
||||||
|
|
||||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||||
import { AbstractService } from '../Service/AbstractService'
|
import { AbstractService } from '../Service/AbstractService'
|
||||||
@@ -45,37 +45,39 @@ export class AuthManager extends AbstractService implements AuthClientInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signInWithRecoveryCodes(dto: {
|
async signInWithRecoveryCodes(
|
||||||
username: string
|
dto: Parameters<AuthClientInterface['signInWithRecoveryCodes']>[0],
|
||||||
password: string
|
): ReturnType<AuthClientInterface['signInWithRecoveryCodes']> {
|
||||||
codeVerifier: string
|
|
||||||
recoveryCodes: string
|
|
||||||
}): Promise<
|
|
||||||
| {
|
|
||||||
keyParams: AnyKeyParamsContent
|
|
||||||
session: SessionBody
|
|
||||||
user: {
|
|
||||||
uuid: string
|
|
||||||
email: string
|
|
||||||
protocolVersion: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| false
|
|
||||||
> {
|
|
||||||
try {
|
try {
|
||||||
const result = await this.authApiService.signInWithRecoveryCodes(dto)
|
const result = await this.authApiService.signInWithRecoveryCodes(dto)
|
||||||
|
|
||||||
|
const captchaURL = getCaptchaHeader(result)
|
||||||
|
|
||||||
|
if (captchaURL) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
captchaURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isErrorResponse(result)) {
|
if (isErrorResponse(result)) {
|
||||||
return false
|
return {
|
||||||
|
success: false,
|
||||||
|
captchaURL: '',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
success: true,
|
||||||
keyParams: result.data.key_params as AnyKeyParamsContent,
|
keyParams: result.data.key_params as AnyKeyParamsContent,
|
||||||
session: result.data.session,
|
session: result.data.session,
|
||||||
user: result.data.user,
|
user: result.data.user,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false
|
return {
|
||||||
|
success: false,
|
||||||
|
captchaURL: '',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,14 @@ export interface SessionsClientInterface {
|
|||||||
revokeAllOtherSessions(): Promise<void>
|
revokeAllOtherSessions(): Promise<void>
|
||||||
|
|
||||||
isCurrentSessionReadOnly(): boolean | undefined
|
isCurrentSessionReadOnly(): boolean | undefined
|
||||||
register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>
|
register(email: string, password: string, hvmToken: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>
|
||||||
signIn(
|
signIn(
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
strict: boolean,
|
strict: boolean,
|
||||||
ephemeral: boolean,
|
ephemeral: boolean,
|
||||||
minAllowedVersion?: ProtocolVersion,
|
minAllowedVersion?: ProtocolVersion,
|
||||||
|
hvmToken?: string,
|
||||||
): Promise<SessionManagerResponse>
|
): Promise<SessionManagerResponse>
|
||||||
bypassChecksAndSignInWithRootKey(
|
bypassChecksAndSignInWithRootKey(
|
||||||
email: string,
|
email: string,
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export class UserService
|
|||||||
public async register(
|
public async register(
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
|
hvmToken: string,
|
||||||
ephemeral = false,
|
ephemeral = false,
|
||||||
mergeLocal = true,
|
mergeLocal = true,
|
||||||
): Promise<UserRegistrationResponseBody> {
|
): Promise<UserRegistrationResponseBody> {
|
||||||
@@ -157,7 +158,7 @@ export class UserService
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.lockSyncing()
|
this.lockSyncing()
|
||||||
const response = await this.sessions.register(email, password, ephemeral)
|
const response = await this.sessions.register(email, password, hvmToken, ephemeral)
|
||||||
|
|
||||||
await this.notifyEventSync(AccountEvent.SignedInOrRegistered, {
|
await this.notifyEventSync(AccountEvent.SignedInOrRegistered, {
|
||||||
payload: {
|
payload: {
|
||||||
@@ -190,6 +191,7 @@ export class UserService
|
|||||||
ephemeral = false,
|
ephemeral = false,
|
||||||
mergeLocal = true,
|
mergeLocal = true,
|
||||||
awaitSync = false,
|
awaitSync = false,
|
||||||
|
hvmToken?: string,
|
||||||
): Promise<HttpResponse<SignInResponse>> {
|
): Promise<HttpResponse<SignInResponse>> {
|
||||||
if (this.encryption.hasAccount()) {
|
if (this.encryption.hasAccount()) {
|
||||||
throw Error('Tried to sign in when an account already exists.')
|
throw Error('Tried to sign in when an account already exists.')
|
||||||
@@ -205,7 +207,7 @@ export class UserService
|
|||||||
/** Prevent a timed sync from occuring while signing in. */
|
/** Prevent a timed sync from occuring while signing in. */
|
||||||
this.lockSyncing()
|
this.lockSyncing()
|
||||||
|
|
||||||
const { response } = await this.sessions.signIn(email, password, strict, ephemeral)
|
const { response } = await this.sessions.signIn(email, password, strict, ephemeral, undefined, hvmToken)
|
||||||
|
|
||||||
if (!isErrorResponse(response)) {
|
if (!isErrorResponse(response)) {
|
||||||
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
|
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface UserServiceInterface extends AbstractService<AccountEvent, Acco
|
|||||||
register(
|
register(
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
|
hvmToken: string,
|
||||||
ephemeral: boolean,
|
ephemeral: boolean,
|
||||||
mergeLocal: boolean,
|
mergeLocal: boolean,
|
||||||
): Promise<UserRegistrationResponseBody>
|
): Promise<UserRegistrationResponseBody>
|
||||||
@@ -31,6 +32,7 @@ export interface UserServiceInterface extends AbstractService<AccountEvent, Acco
|
|||||||
ephemeral: boolean,
|
ephemeral: boolean,
|
||||||
mergeLocal: boolean,
|
mergeLocal: boolean,
|
||||||
awaitSync: boolean,
|
awaitSync: boolean,
|
||||||
|
hvmToken?: string,
|
||||||
): Promise<HttpResponse<SignInResponse>>
|
): Promise<HttpResponse<SignInResponse>>
|
||||||
deleteAccount(): Promise<{
|
deleteAccount(): Promise<{
|
||||||
error: boolean
|
error: boolean
|
||||||
|
|||||||
@@ -203,7 +203,6 @@ export * from './User/SignedInOrRegisteredEventPayload'
|
|||||||
export * from './User/SignedOutEventPayload'
|
export * from './User/SignedOutEventPayload'
|
||||||
export * from './User/UserService'
|
export * from './User/UserService'
|
||||||
export * from './User/UserServiceInterface'
|
export * from './User/UserServiceInterface'
|
||||||
export * from './User/UserServiceInterface'
|
|
||||||
export * from './UserEvent/NotificationService'
|
export * from './UserEvent/NotificationService'
|
||||||
export * from './UserEvent/NotificationServiceEvent'
|
export * from './UserEvent/NotificationServiceEvent'
|
||||||
export * from './Vault/UseCase/AuthorizeVaultDeletion'
|
export * from './Vault/UseCase/AuthorizeVaultDeletion'
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ import {
|
|||||||
SignInResponse,
|
SignInResponse,
|
||||||
ClientDisplayableError,
|
ClientDisplayableError,
|
||||||
SessionListEntry,
|
SessionListEntry,
|
||||||
|
MetaEndpointResponse,
|
||||||
} from '@standardnotes/responses'
|
} from '@standardnotes/responses'
|
||||||
import {
|
import {
|
||||||
SyncService,
|
SyncService,
|
||||||
@@ -117,7 +118,7 @@ import {
|
|||||||
LoggerInterface,
|
LoggerInterface,
|
||||||
canBlockDeinit,
|
canBlockDeinit,
|
||||||
} from '@standardnotes/utils'
|
} from '@standardnotes/utils'
|
||||||
import { UuidString, ApplicationEventPayload } from '../Types'
|
import { UuidString } from '../Types'
|
||||||
import { applicationEventForSyncEvent } from '@Lib/Application/Event'
|
import { applicationEventForSyncEvent } from '@Lib/Application/Event'
|
||||||
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
|
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
|
||||||
import { ComputePrivateUsername } from '@standardnotes/encryption'
|
import { ComputePrivateUsername } from '@standardnotes/encryption'
|
||||||
@@ -275,12 +276,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const syncEventCallback = async (eventName: SyncEvent) => {
|
const syncEventCallback = async (eventName: SyncEvent, data?: unknown) => {
|
||||||
const appEvent = applicationEventForSyncEvent(eventName)
|
const appEvent = applicationEventForSyncEvent(eventName)
|
||||||
if (appEvent) {
|
if (appEvent) {
|
||||||
await encryptionService.onSyncEvent(eventName)
|
await encryptionService.onSyncEvent(eventName)
|
||||||
|
|
||||||
await this.notifyEvent(appEvent)
|
await this.notifyEvent(appEvent, data)
|
||||||
|
|
||||||
if (appEvent === ApplicationEvent.CompletedFullSync) {
|
if (appEvent === ApplicationEvent.CompletedFullSync) {
|
||||||
if (!this.handledFullSyncStage) {
|
if (!this.handledFullSyncStage) {
|
||||||
@@ -535,7 +536,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
return this.addEventObserver(filteredCallback, event)
|
return this.addEventObserver(filteredCallback, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async notifyEvent(event: ApplicationEvent, data?: ApplicationEventPayload) {
|
private async notifyEvent(event: ApplicationEvent, data?: unknown) {
|
||||||
if (event === ApplicationEvent.Started) {
|
if (event === ApplicationEvent.Started) {
|
||||||
this.onStart()
|
this.onStart()
|
||||||
} else if (event === ApplicationEvent.Launched) {
|
} else if (event === ApplicationEvent.Launched) {
|
||||||
@@ -768,10 +769,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
public async register(
|
public async register(
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
|
hvmToken: string,
|
||||||
ephemeral = false,
|
ephemeral = false,
|
||||||
mergeLocal = true,
|
mergeLocal = true,
|
||||||
): Promise<UserRegistrationResponseBody> {
|
): Promise<UserRegistrationResponseBody> {
|
||||||
return this.user.register(email, password, ephemeral, mergeLocal)
|
return this.user.register(email, password, hvmToken, ephemeral, mergeLocal)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -785,8 +787,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
ephemeral = false,
|
ephemeral = false,
|
||||||
mergeLocal = true,
|
mergeLocal = true,
|
||||||
awaitSync = false,
|
awaitSync = false,
|
||||||
|
hvmToken?: string,
|
||||||
): Promise<HttpResponse<SignInResponse>> {
|
): Promise<HttpResponse<SignInResponse>> {
|
||||||
return this.user.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync)
|
return this.user.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync, hvmToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getCaptchaUrl(): Promise<HttpResponse<MetaEndpointResponse>> {
|
||||||
|
return this.legacyApi.getCaptchaUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
public async changeEmail(
|
public async changeEmail(
|
||||||
|
|||||||
@@ -166,7 +166,9 @@ describe('SignInWithRecoveryCodes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should fail if the sign in with recovery code fails', async () => {
|
it('should fail if the sign in with recovery code fails', async () => {
|
||||||
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue(false)
|
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue({
|
||||||
|
success: false,
|
||||||
|
})
|
||||||
|
|
||||||
const useCase = createUseCase()
|
const useCase = createUseCase()
|
||||||
const result = await useCase.execute({
|
const result = await useCase.execute({
|
||||||
|
|||||||
@@ -68,9 +68,18 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<void> {
|
|||||||
recoveryCodes: dto.recoveryCodes,
|
recoveryCodes: dto.recoveryCodes,
|
||||||
username: dto.username,
|
username: dto.username,
|
||||||
password: rootKey.serverPassword as string,
|
password: rootKey.serverPassword as string,
|
||||||
|
hvmToken: dto.hvmToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (signInResult === false) {
|
if (signInResult.success === false) {
|
||||||
|
if (signInResult.captchaURL) {
|
||||||
|
return Result.fail(
|
||||||
|
JSON.stringify({
|
||||||
|
captchaURL: signInResult.captchaURL,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return Result.fail('Could not sign in with recovery code')
|
return Result.fail('Could not sign in with recovery code')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export interface SignInWithRecoveryCodesDTO {
|
|||||||
recoveryCodes: string
|
recoveryCodes: string
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
|
hvmToken?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import {
|
|||||||
isErrorResponse,
|
isErrorResponse,
|
||||||
MoveFileResponse,
|
MoveFileResponse,
|
||||||
ValetTokenOperation,
|
ValetTokenOperation,
|
||||||
|
MetaEndpointResponse,
|
||||||
} from '@standardnotes/responses'
|
} from '@standardnotes/responses'
|
||||||
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||||
import { HttpServiceInterface } from '@standardnotes/api'
|
import { HttpServiceInterface } from '@standardnotes/api'
|
||||||
@@ -290,6 +291,7 @@ export class LegacyApiService
|
|||||||
email: string
|
email: string
|
||||||
serverPassword: string
|
serverPassword: string
|
||||||
ephemeral: boolean
|
ephemeral: boolean
|
||||||
|
hvmToken?: string
|
||||||
}): Promise<HttpResponse<SignInResponse>> {
|
}): Promise<HttpResponse<SignInResponse>> {
|
||||||
if (this.authenticating) {
|
if (this.authenticating) {
|
||||||
return this.createErrorResponse(API_MESSAGE_LOGIN_IN_PROGRESS, HttpStatusCode.BadRequest)
|
return this.createErrorResponse(API_MESSAGE_LOGIN_IN_PROGRESS, HttpStatusCode.BadRequest)
|
||||||
@@ -301,6 +303,7 @@ export class LegacyApiService
|
|||||||
password: dto.serverPassword,
|
password: dto.serverPassword,
|
||||||
ephemeral: dto.ephemeral,
|
ephemeral: dto.ephemeral,
|
||||||
code_verifier: this.inMemoryStore.getValue(StorageKey.CodeVerifier) as string,
|
code_verifier: this.inMemoryStore.getValue(StorageKey.CodeVerifier) as string,
|
||||||
|
hvm_token: dto.hvmToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await this.request<SignInResponse>({
|
const response = await this.request<SignInResponse>({
|
||||||
@@ -958,4 +961,9 @@ export class LegacyApiService
|
|||||||
|
|
||||||
return this.session.accessToken
|
return this.session.accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCaptchaUrl() {
|
||||||
|
const response = this.httpService.get<MetaEndpointResponse>(Paths.v1.meta)
|
||||||
|
return response
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export const Paths = {
|
|||||||
...SettingsPaths,
|
...SettingsPaths,
|
||||||
...SubscriptionPaths,
|
...SubscriptionPaths,
|
||||||
...UserPaths,
|
...UserPaths,
|
||||||
|
meta: '/v1/meta',
|
||||||
},
|
},
|
||||||
v2: {
|
v2: {
|
||||||
...UserPathsV2,
|
...UserPathsV2,
|
||||||
|
|||||||
@@ -404,7 +404,12 @@ export class SessionManager
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody> {
|
async register(
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
hvmToken: string,
|
||||||
|
ephemeral: boolean,
|
||||||
|
): Promise<UserRegistrationResponseBody> {
|
||||||
if (password.length < MINIMUM_PASSWORD_LENGTH) {
|
if (password.length < MINIMUM_PASSWORD_LENGTH) {
|
||||||
throw new ApiCallError(
|
throw new ApiCallError(
|
||||||
ErrorMessage.InsufficientPasswordMessage.replace('%LENGTH%', MINIMUM_PASSWORD_LENGTH.toString()),
|
ErrorMessage.InsufficientPasswordMessage.replace('%LENGTH%', MINIMUM_PASSWORD_LENGTH.toString()),
|
||||||
@@ -429,6 +434,7 @@ export class SessionManager
|
|||||||
const registerResponse = await this.userApiService.register({
|
const registerResponse = await this.userApiService.register({
|
||||||
email,
|
email,
|
||||||
serverPassword,
|
serverPassword,
|
||||||
|
hvmToken,
|
||||||
keyParams,
|
keyParams,
|
||||||
ephemeral,
|
ephemeral,
|
||||||
})
|
})
|
||||||
@@ -503,8 +509,9 @@ export class SessionManager
|
|||||||
strict = false,
|
strict = false,
|
||||||
ephemeral = false,
|
ephemeral = false,
|
||||||
minAllowedVersion?: Common.ProtocolVersion,
|
minAllowedVersion?: Common.ProtocolVersion,
|
||||||
|
hvmToken?: string,
|
||||||
): Promise<SessionManagerResponse> {
|
): Promise<SessionManagerResponse> {
|
||||||
const result = await this.performSignIn(email, password, strict, ephemeral, minAllowedVersion)
|
const result = await this.performSignIn(email, password, strict, ephemeral, minAllowedVersion, hvmToken)
|
||||||
if (
|
if (
|
||||||
isErrorResponse(result.response) &&
|
isErrorResponse(result.response) &&
|
||||||
getErrorFromErrorResponse(result.response).tag !== ErrorTag.ClientValidationError &&
|
getErrorFromErrorResponse(result.response).tag !== ErrorTag.ClientValidationError &&
|
||||||
@@ -515,7 +522,7 @@ export class SessionManager
|
|||||||
/**
|
/**
|
||||||
* Try signing in with trimmed + lowercase version of email
|
* Try signing in with trimmed + lowercase version of email
|
||||||
*/
|
*/
|
||||||
return this.performSignIn(cleanedEmail, password, strict, ephemeral, minAllowedVersion)
|
return this.performSignIn(cleanedEmail, password, strict, ephemeral, minAllowedVersion, hvmToken)
|
||||||
} else {
|
} else {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -530,6 +537,7 @@ export class SessionManager
|
|||||||
strict = false,
|
strict = false,
|
||||||
ephemeral = false,
|
ephemeral = false,
|
||||||
minAllowedVersion?: Common.ProtocolVersion,
|
minAllowedVersion?: Common.ProtocolVersion,
|
||||||
|
hvmToken?: string,
|
||||||
): Promise<SessionManagerResponse> {
|
): Promise<SessionManagerResponse> {
|
||||||
const paramsResult = await this.retrieveKeyParams({
|
const paramsResult = await this.retrieveKeyParams({
|
||||||
email,
|
email,
|
||||||
@@ -593,7 +601,7 @@ export class SessionManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rootKey = await this.encryptionService.computeRootKey(password, keyParams)
|
const rootKey = await this.encryptionService.computeRootKey(password, keyParams)
|
||||||
const signInResponse = await this.bypassChecksAndSignInWithRootKey(email, rootKey, ephemeral)
|
const signInResponse = await this.bypassChecksAndSignInWithRootKey(email, rootKey, ephemeral, hvmToken)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response: signInResponse,
|
response: signInResponse,
|
||||||
@@ -604,6 +612,7 @@ export class SessionManager
|
|||||||
email: string,
|
email: string,
|
||||||
rootKey: SNRootKey,
|
rootKey: SNRootKey,
|
||||||
ephemeral = false,
|
ephemeral = false,
|
||||||
|
hvmToken?: string,
|
||||||
): Promise<HttpResponse<SignInResponse>> {
|
): Promise<HttpResponse<SignInResponse>> {
|
||||||
const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable()
|
const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable()
|
||||||
|
|
||||||
@@ -619,6 +628,7 @@ export class SessionManager
|
|||||||
email,
|
email,
|
||||||
serverPassword: rootKey.serverPassword as string,
|
serverPassword: rootKey.serverPassword as string,
|
||||||
ephemeral,
|
ephemeral,
|
||||||
|
hvmToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!signInResponse.data || isErrorResponse(signInResponse)) {
|
if (!signInResponse.data || isErrorResponse(signInResponse)) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/web",
|
"name": "@standardnotes/web",
|
||||||
"version": "3.192.10",
|
"version": "3.192.12",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "dist/app.js",
|
"main": "dist/app.js",
|
||||||
"author": "Standard Notes",
|
"author": "Standard Notes",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
|
|||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import IconButton from '@/Components/Button/IconButton'
|
import IconButton from '@/Components/Button/IconButton'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||||
|
import { isErrorResponse } from '@standardnotes/snjs'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setMenuPane: (pane: AccountMenuPane) => void
|
setMenuPane: (pane: AccountMenuPane) => void
|
||||||
@@ -33,6 +35,39 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
|||||||
const [shouldMergeLocal, setShouldMergeLocal] = useState(true)
|
const [shouldMergeLocal, setShouldMergeLocal] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const [hvmToken, setHVMToken] = useState('')
|
||||||
|
const [captchaURL, setCaptchaURL] = useState('')
|
||||||
|
|
||||||
|
const register = useCallback(() => {
|
||||||
|
setIsRegistering(true)
|
||||||
|
application
|
||||||
|
.register(email, password, hvmToken, isEphemeral, shouldMergeLocal)
|
||||||
|
.then(() => {
|
||||||
|
application.accountMenuController.closeAccountMenu()
|
||||||
|
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
setError(err.message)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsRegistering(false)
|
||||||
|
})
|
||||||
|
}, [application, email, hvmToken, isEphemeral, password, shouldMergeLocal])
|
||||||
|
|
||||||
|
const captchaIframe = useCaptcha(captchaURL, (token) => {
|
||||||
|
setHVMToken(token)
|
||||||
|
setCaptchaURL('')
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hvmToken) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
register()
|
||||||
|
}, [hvmToken, register])
|
||||||
|
|
||||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,6 +86,28 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
|||||||
setShouldMergeLocal(!shouldMergeLocal)
|
setShouldMergeLocal(!shouldMergeLocal)
|
||||||
}, [shouldMergeLocal])
|
}, [shouldMergeLocal])
|
||||||
|
|
||||||
|
const checkIfCaptchaRequiredAndRegister = useCallback(() => {
|
||||||
|
application
|
||||||
|
.getCaptchaUrl()
|
||||||
|
.then((response) => {
|
||||||
|
if (isErrorResponse(response)) {
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
const { captchaUIUrl } = response.data
|
||||||
|
if (captchaUIUrl) {
|
||||||
|
setCaptchaURL(captchaUIUrl)
|
||||||
|
} else {
|
||||||
|
setCaptchaURL('')
|
||||||
|
register()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
setCaptchaURL('')
|
||||||
|
register()
|
||||||
|
})
|
||||||
|
}, [application, register])
|
||||||
|
|
||||||
const handleConfirmFormSubmit: FormEventHandler = useCallback(
|
const handleConfirmFormSubmit: FormEventHandler = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -60,28 +117,16 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password === confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
setIsRegistering(true)
|
|
||||||
application
|
|
||||||
.register(email, password, isEphemeral, shouldMergeLocal)
|
|
||||||
.then(() => {
|
|
||||||
application.accountMenuController.closeAccountMenu()
|
|
||||||
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
setError(err.message)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsRegistering(false)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setError(STRING_NON_MATCHING_PASSWORDS)
|
setError(STRING_NON_MATCHING_PASSWORDS)
|
||||||
setConfirmPassword('')
|
setConfirmPassword('')
|
||||||
passwordInputRef.current?.focus()
|
passwordInputRef.current?.focus()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkIfCaptchaRequiredAndRegister()
|
||||||
},
|
},
|
||||||
[application, confirmPassword, email, isEphemeral, password, shouldMergeLocal],
|
[checkIfCaptchaRequiredAndRegister, confirmPassword, password],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||||
@@ -100,35 +145,26 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
|||||||
setMenuPane(AccountMenuPane.Register)
|
setMenuPane(AccountMenuPane.Register)
|
||||||
}, [setMenuPane])
|
}, [setMenuPane])
|
||||||
|
|
||||||
return (
|
const confirmPasswordForm = (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3 mt-1 flex items-center px-3">
|
|
||||||
<IconButton
|
|
||||||
icon="arrow-left"
|
|
||||||
title="Go back"
|
|
||||||
className="mr-2 flex p-0 text-neutral"
|
|
||||||
onClick={handleGoBack}
|
|
||||||
focusable={true}
|
|
||||||
disabled={isRegistering}
|
|
||||||
/>
|
|
||||||
<div className="text-base font-bold">Confirm password</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-3 px-3 text-sm">
|
<div className="mb-3 px-3 text-sm">
|
||||||
Because your notes are encrypted using your password,{' '}
|
Because your notes are encrypted using your password,{' '}
|
||||||
<span className="text-danger">Standard Notes does not have a password reset option</span>. If you forget your
|
<span className="text-danger">Standard Notes does not have a password reset option</span>. If you forget your
|
||||||
password, you will permanently lose access to your data.
|
password, you will permanently lose access to your data.
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleConfirmFormSubmit} className="mb-1 px-3">
|
<form onSubmit={handleConfirmFormSubmit} className="mb-1 px-3">
|
||||||
<DecoratedPasswordInput
|
{!isRegistering && (
|
||||||
className={{ container: 'mb-2' }}
|
<DecoratedPasswordInput
|
||||||
disabled={isRegistering}
|
className={{ container: 'mb-2' }}
|
||||||
left={[<Icon type="password" className="text-neutral" />]}
|
disabled={isRegistering}
|
||||||
onChange={handlePasswordChange}
|
left={[<Icon type="password" className="text-neutral" />]}
|
||||||
onKeyDown={handleKeyDown}
|
onChange={handlePasswordChange}
|
||||||
placeholder="Confirm password"
|
onKeyDown={handleKeyDown}
|
||||||
ref={passwordInputRef}
|
placeholder="Confirm password"
|
||||||
value={confirmPassword}
|
ref={passwordInputRef}
|
||||||
/>
|
value={confirmPassword}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{error ? <div className="my-2 text-danger">{error}</div> : null}
|
{error ? <div className="my-2 text-danger">{error}</div> : null}
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
@@ -157,6 +193,23 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
|||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-3 mt-1 flex items-center px-3">
|
||||||
|
<IconButton
|
||||||
|
icon="arrow-left"
|
||||||
|
title="Go back"
|
||||||
|
className="mr-2 flex p-0 text-neutral"
|
||||||
|
onClick={handleGoBack}
|
||||||
|
focusable={true}
|
||||||
|
disabled={isRegistering}
|
||||||
|
/>
|
||||||
|
<div className="text-base font-bold">{captchaURL ? 'Human verification' : 'Confirm password'}</div>
|
||||||
|
</div>
|
||||||
|
{captchaURL ? <div className="p-[10px]">{captchaIframe}</div> : confirmPasswordForm}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(ConfirmPassword)
|
export default observer(ConfirmPassword)
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import Icon from '@/Components/Icon/Icon'
|
|||||||
import IconButton from '@/Components/Button/IconButton'
|
import IconButton from '@/Components/Button/IconButton'
|
||||||
import AdvancedOptions from './AdvancedOptions'
|
import AdvancedOptions from './AdvancedOptions'
|
||||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||||
import { getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/snjs'
|
import { getErrorFromErrorResponse, isErrorResponse, getCaptchaHeader } from '@standardnotes/snjs'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setMenuPane: (pane: AccountMenuPane) => void
|
setMenuPane: (pane: AccountMenuPane) => void
|
||||||
@@ -34,6 +35,15 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
|||||||
|
|
||||||
const [isRecoverySignIn, setIsRecoverySignIn] = useState(false)
|
const [isRecoverySignIn, setIsRecoverySignIn] = useState(false)
|
||||||
|
|
||||||
|
const [captchaURL, setCaptchaURL] = useState('')
|
||||||
|
const [showCaptcha, setShowCaptcha] = useState(false)
|
||||||
|
const [hvmToken, setHVMToken] = useState('')
|
||||||
|
const captchaIframe = useCaptcha(captchaURL, (token) => {
|
||||||
|
setHVMToken(token)
|
||||||
|
setShowCaptcha(false)
|
||||||
|
setCaptchaURL('')
|
||||||
|
})
|
||||||
|
|
||||||
const emailInputRef = useRef<HTMLInputElement>(null)
|
const emailInputRef = useRef<HTMLInputElement>(null)
|
||||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
@@ -95,8 +105,12 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
|||||||
passwordInputRef?.current?.blur()
|
passwordInputRef?.current?.blur()
|
||||||
|
|
||||||
application
|
application
|
||||||
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal)
|
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal, false, hvmToken)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
const captchaURL = getCaptchaHeader(response)
|
||||||
|
if (captchaURL) {
|
||||||
|
setCaptchaURL(captchaURL)
|
||||||
|
}
|
||||||
if (isErrorResponse(response)) {
|
if (isErrorResponse(response)) {
|
||||||
throw new Error(getErrorFromErrorResponse(response).message)
|
throw new Error(getErrorFromErrorResponse(response).message)
|
||||||
}
|
}
|
||||||
@@ -106,12 +120,13 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
setError(err.message ?? err.toString())
|
setError(err.message ?? err.toString())
|
||||||
setPassword('')
|
setPassword('')
|
||||||
|
setHVMToken('')
|
||||||
passwordInputRef?.current?.blur()
|
passwordInputRef?.current?.blur()
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsSigningIn(false)
|
setIsSigningIn(false)
|
||||||
})
|
})
|
||||||
}, [application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal])
|
}, [application, email, hvmToken, isEphemeral, isStrictSignin, password, shouldMergeLocal])
|
||||||
|
|
||||||
const recoverySignIn = useCallback(() => {
|
const recoverySignIn = useCallback(() => {
|
||||||
setIsSigningIn(true)
|
setIsSigningIn(true)
|
||||||
@@ -123,10 +138,21 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
|||||||
recoveryCodes,
|
recoveryCodes,
|
||||||
username: email,
|
username: email,
|
||||||
password: password,
|
password: password,
|
||||||
|
hvmToken,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.isFailed()) {
|
if (result.isFailed()) {
|
||||||
throw new Error(result.getError())
|
const error = result.getError()
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(error)
|
||||||
|
if (parsed.captchaURL) {
|
||||||
|
setCaptchaURL(parsed.captchaURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setCaptchaURL('')
|
||||||
|
}
|
||||||
|
throw new Error(error)
|
||||||
}
|
}
|
||||||
application.accountMenuController.closeAccountMenu()
|
application.accountMenuController.closeAccountMenu()
|
||||||
})
|
})
|
||||||
@@ -134,12 +160,13 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
setError(err.message ?? err.toString())
|
setError(err.message ?? err.toString())
|
||||||
setPassword('')
|
setPassword('')
|
||||||
|
setHVMToken('')
|
||||||
passwordInputRef?.current?.blur()
|
passwordInputRef?.current?.blur()
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsSigningIn(false)
|
setIsSigningIn(false)
|
||||||
})
|
})
|
||||||
}, [application, email, password, recoveryCodes])
|
}, [application.accountMenuController, application.signInWithRecoveryCodes, email, hvmToken, password, recoveryCodes])
|
||||||
|
|
||||||
const onPrivateUsernameChange = useCallback(
|
const onPrivateUsernameChange = useCallback(
|
||||||
(newisPrivateUsername: boolean, privateUsernameIdentifier?: string) => {
|
(newisPrivateUsername: boolean, privateUsernameIdentifier?: string) => {
|
||||||
@@ -151,28 +178,37 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
|||||||
[setEmail],
|
[setEmail],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const performSignIn = useCallback(() => {
|
||||||
|
if (!email || email.length === 0) {
|
||||||
|
emailInputRef?.current?.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password || password.length === 0) {
|
||||||
|
passwordInputRef?.current?.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRecoverySignIn) {
|
||||||
|
recoverySignIn()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signIn()
|
||||||
|
}, [email, isRecoverySignIn, password, recoverySignIn, signIn])
|
||||||
|
|
||||||
const handleSignInFormSubmit = useCallback(
|
const handleSignInFormSubmit = useCallback(
|
||||||
(e: React.SyntheticEvent) => {
|
(e: React.SyntheticEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (!email || email.length === 0) {
|
if (captchaURL) {
|
||||||
emailInputRef?.current?.focus()
|
setShowCaptcha(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password || password.length === 0) {
|
performSignIn()
|
||||||
passwordInputRef?.current?.focus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRecoverySignIn) {
|
|
||||||
recoverySignIn()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
signIn()
|
|
||||||
},
|
},
|
||||||
[email, password, isRecoverySignIn, signIn, recoverySignIn],
|
[captchaURL, performSignIn],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||||
@@ -184,19 +220,16 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
|||||||
[handleSignInFormSubmit],
|
[handleSignInFormSubmit],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
|
if (!hvmToken) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
performSignIn()
|
||||||
|
}, [hvmToken, performSignIn])
|
||||||
|
|
||||||
|
const signInForm = (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3 mt-1 flex items-center px-3">
|
|
||||||
<IconButton
|
|
||||||
icon="arrow-left"
|
|
||||||
title="Go back"
|
|
||||||
className="mr-2 flex p-0 text-neutral"
|
|
||||||
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
|
|
||||||
focusable={true}
|
|
||||||
disabled={isSigningIn}
|
|
||||||
/>
|
|
||||||
<div className="text-base font-bold">Sign in</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-1 px-3">
|
<div className="mb-1 px-3">
|
||||||
<DecoratedInput
|
<DecoratedInput
|
||||||
className={{ container: `mb-2 ${error ? 'border-danger' : null}` }}
|
className={{ container: `mb-2 ${error ? 'border-danger' : null}` }}
|
||||||
@@ -257,6 +290,23 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-3 mt-1 flex items-center px-3">
|
||||||
|
<IconButton
|
||||||
|
icon="arrow-left"
|
||||||
|
title="Go back"
|
||||||
|
className="mr-2 flex p-0 text-neutral"
|
||||||
|
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
|
||||||
|
focusable={true}
|
||||||
|
disabled={isSigningIn}
|
||||||
|
/>
|
||||||
|
<div className="text-base font-bold">{showCaptcha ? 'Human verification' : 'Sign in'}</div>
|
||||||
|
</div>
|
||||||
|
{showCaptcha ? <div className="p-[10px]">{captchaIframe}</div> : signInForm}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(SignInPane)
|
export default observer(SignInPane)
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
|
import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
|
||||||
import { getPlatformString } from '@/Utils'
|
import { getPlatformString } from '@/Utils'
|
||||||
import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs'
|
import {
|
||||||
|
ApplicationEvent,
|
||||||
|
Challenge,
|
||||||
|
getErrorMessageFromErrorResponseBody,
|
||||||
|
HttpErrorResponseBody,
|
||||||
|
removeFromArray,
|
||||||
|
WebAppEvent,
|
||||||
|
} from '@standardnotes/snjs'
|
||||||
import { alertDialog, isIOS, RouteType } from '@standardnotes/ui-services'
|
import { alertDialog, isIOS, RouteType } from '@standardnotes/ui-services'
|
||||||
import { WebApplication } from '@/Application/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import Footer from '@/Components/Footer/Footer'
|
import Footer from '@/Components/Footer/Footer'
|
||||||
@@ -117,7 +124,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
onAppLaunch()
|
onAppLaunch()
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeAppObserver = application.addEventObserver(async (eventName) => {
|
const removeAppObserver = application.addEventObserver(async (eventName, data?: unknown) => {
|
||||||
if (eventName === ApplicationEvent.Started) {
|
if (eventName === ApplicationEvent.Started) {
|
||||||
onAppStart()
|
onAppStart()
|
||||||
} else if (eventName === ApplicationEvent.Launched) {
|
} else if (eventName === ApplicationEvent.Launched) {
|
||||||
@@ -147,6 +154,14 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
type: ToastType.Error,
|
type: ToastType.Error,
|
||||||
message: 'Too many requests. Please try again later.',
|
message: 'Too many requests. Please try again later.',
|
||||||
})
|
})
|
||||||
|
} else if (eventName === ApplicationEvent.FailedSync) {
|
||||||
|
addToast({
|
||||||
|
type: ToastType.Error,
|
||||||
|
message: getErrorMessageFromErrorResponseBody(
|
||||||
|
data as HttpErrorResponseBody,
|
||||||
|
'Sync error. Please try again later.',
|
||||||
|
),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import Button from '@/Components/Button/Button'
|
|||||||
import { WebApplication } from '@/Application/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import { PurchaseFlowPane } from '@/Controllers/PurchaseFlow/PurchaseFlowPane'
|
import { PurchaseFlowPane } from '@/Controllers/PurchaseFlow/PurchaseFlowPane'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { ChangeEventHandler, FunctionComponent, useEffect, useRef, useState } from 'react'
|
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
|
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
|
||||||
import { isEmailValid } from '@/Utils'
|
import { isEmailValid } from '@/Utils'
|
||||||
import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from '@standardnotes/icons'
|
import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from '@standardnotes/icons'
|
||||||
|
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||||
|
import { AccountMenuPane } from '../../AccountMenu/AccountMenuPane'
|
||||||
|
import { isErrorResponse } from '@standardnotes/snjs'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -20,6 +23,61 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
|||||||
const [isEmailInvalid, setIsEmailInvalid] = useState(false)
|
const [isEmailInvalid, setIsEmailInvalid] = useState(false)
|
||||||
const [isPasswordNotMatching, setIsPasswordNotMatching] = useState(false)
|
const [isPasswordNotMatching, setIsPasswordNotMatching] = useState(false)
|
||||||
|
|
||||||
|
const [hvmToken, setHVMToken] = useState('')
|
||||||
|
const [captchaURL, setCaptchaURL] = useState('')
|
||||||
|
|
||||||
|
const register = useCallback(() => {
|
||||||
|
setIsCreatingAccount(true)
|
||||||
|
application
|
||||||
|
.register(email, password, hvmToken)
|
||||||
|
.then(() => {
|
||||||
|
application.accountMenuController.closeAccountMenu()
|
||||||
|
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
application.alerts.alert(err as string).catch(console.error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsCreatingAccount(false)
|
||||||
|
})
|
||||||
|
}, [application, email, hvmToken, password])
|
||||||
|
|
||||||
|
const captchaIframe = useCaptcha(captchaURL, (token) => {
|
||||||
|
setHVMToken(token)
|
||||||
|
setCaptchaURL('')
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hvmToken) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
register()
|
||||||
|
}, [hvmToken, register])
|
||||||
|
|
||||||
|
const checkIfCaptchaRequiredAndRegister = useCallback(() => {
|
||||||
|
application
|
||||||
|
.getCaptchaUrl()
|
||||||
|
.then((response) => {
|
||||||
|
if (isErrorResponse(response)) {
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
const { captchaUIUrl } = response.data
|
||||||
|
if (captchaUIUrl) {
|
||||||
|
setCaptchaURL(captchaUIUrl)
|
||||||
|
} else {
|
||||||
|
setCaptchaURL('')
|
||||||
|
register()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
setCaptchaURL('')
|
||||||
|
register()
|
||||||
|
})
|
||||||
|
}, [application, register])
|
||||||
|
|
||||||
const emailInputRef = useRef<HTMLInputElement>(null)
|
const emailInputRef = useRef<HTMLInputElement>(null)
|
||||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||||
const confirmPasswordInputRef = useRef<HTMLInputElement>(null)
|
const confirmPasswordInputRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -81,21 +139,52 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsCreatingAccount(true)
|
checkIfCaptchaRequiredAndRegister()
|
||||||
|
|
||||||
try {
|
|
||||||
await application.register(email, password)
|
|
||||||
|
|
||||||
application.purchaseFlowController.closePurchaseFlow()
|
|
||||||
void application.purchaseFlowController.openPurchaseFlow()
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
application.alerts.alert(err as string).catch(console.error)
|
|
||||||
} finally {
|
|
||||||
setIsCreatingAccount(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CreateAccountForm = (
|
||||||
|
<form onSubmit={handleCreateAccount}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<FloatingLabelInput
|
||||||
|
className={`min-w-auto md:min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
|
||||||
|
id="purchase-sign-in-email"
|
||||||
|
type="email"
|
||||||
|
label="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
ref={emailInputRef}
|
||||||
|
disabled={isCreatingAccount}
|
||||||
|
isInvalid={isEmailInvalid}
|
||||||
|
/>
|
||||||
|
{isEmailInvalid ? <div className="mb-4 text-danger">Please provide a valid email.</div> : null}
|
||||||
|
<FloatingLabelInput
|
||||||
|
className="min-w-auto mb-4 md:min-w-90"
|
||||||
|
id="purchase-create-account-password"
|
||||||
|
type="password"
|
||||||
|
label="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
ref={passwordInputRef}
|
||||||
|
disabled={isCreatingAccount}
|
||||||
|
/>
|
||||||
|
<FloatingLabelInput
|
||||||
|
className={`min-w-auto md:min-w-90 ${isPasswordNotMatching ? 'mb-2' : 'mb-4'}`}
|
||||||
|
id="create-account-confirm"
|
||||||
|
type="password"
|
||||||
|
label="Repeat password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={handleConfirmPasswordChange}
|
||||||
|
ref={confirmPasswordInputRef}
|
||||||
|
disabled={isCreatingAccount}
|
||||||
|
isInvalid={isPasswordNotMatching}
|
||||||
|
/>
|
||||||
|
{isPasswordNotMatching ? (
|
||||||
|
<div className="mb-4 text-danger">Passwords don't match. Please try again.</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CircleIcon className="absolute -left-28 top-[40%] h-8 w-8" />
|
<CircleIcon className="absolute -left-28 top-[40%] h-8 w-8" />
|
||||||
@@ -109,46 +198,7 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
|||||||
<div className="mr-0 lg:mr-12">
|
<div className="mr-0 lg:mr-12">
|
||||||
<h1 className="mb-2 mt-0 text-2xl font-bold">Create your free account</h1>
|
<h1 className="mb-2 mt-0 text-2xl font-bold">Create your free account</h1>
|
||||||
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
|
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
|
||||||
<form onSubmit={handleCreateAccount}>
|
{captchaURL ? captchaIframe : CreateAccountForm}
|
||||||
<div className="flex flex-col">
|
|
||||||
<FloatingLabelInput
|
|
||||||
className={`min-w-auto md:min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
|
|
||||||
id="purchase-sign-in-email"
|
|
||||||
type="email"
|
|
||||||
label="Email"
|
|
||||||
value={email}
|
|
||||||
onChange={handleEmailChange}
|
|
||||||
ref={emailInputRef}
|
|
||||||
disabled={isCreatingAccount}
|
|
||||||
isInvalid={isEmailInvalid}
|
|
||||||
/>
|
|
||||||
{isEmailInvalid ? <div className="mb-4 text-danger">Please provide a valid email.</div> : null}
|
|
||||||
<FloatingLabelInput
|
|
||||||
className="min-w-auto mb-4 md:min-w-90"
|
|
||||||
id="purchase-create-account-password"
|
|
||||||
type="password"
|
|
||||||
label="Password"
|
|
||||||
value={password}
|
|
||||||
onChange={handlePasswordChange}
|
|
||||||
ref={passwordInputRef}
|
|
||||||
disabled={isCreatingAccount}
|
|
||||||
/>
|
|
||||||
<FloatingLabelInput
|
|
||||||
className={`min-w-auto md:min-w-90 ${isPasswordNotMatching ? 'mb-2' : 'mb-4'}`}
|
|
||||||
id="create-account-confirm"
|
|
||||||
type="password"
|
|
||||||
label="Repeat password"
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={handleConfirmPasswordChange}
|
|
||||||
ref={confirmPasswordInputRef}
|
|
||||||
disabled={isCreatingAccount}
|
|
||||||
isInvalid={isPasswordNotMatching}
|
|
||||||
/>
|
|
||||||
{isPasswordNotMatching ? (
|
|
||||||
<div className="mb-4 text-danger">Passwords don't match. Please try again.</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div className="flex flex-col-reverse items-start justify-between md:flex-row md:items-center">
|
<div className="flex flex-col-reverse items-start justify-between md:flex-row md:items-center">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { ChangeEventHandler, FunctionComponent, useEffect, useRef, useState } fr
|
|||||||
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
|
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
|
||||||
import { isEmailValid } from '@/Utils'
|
import { isEmailValid } from '@/Utils'
|
||||||
import { BlueDotIcon, CircleIcon, DiamondIcon } from '@standardnotes/icons'
|
import { BlueDotIcon, CircleIcon, DiamondIcon } from '@standardnotes/icons'
|
||||||
import { isErrorResponse } from '@standardnotes/snjs'
|
import { isErrorResponse, getCaptchaHeader } from '@standardnotes/snjs'
|
||||||
|
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -21,6 +22,15 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
|||||||
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
||||||
const [otherErrorMessage, setOtherErrorMessage] = useState('')
|
const [otherErrorMessage, setOtherErrorMessage] = useState('')
|
||||||
|
|
||||||
|
const [captchaURL, setCaptchaURL] = useState('')
|
||||||
|
const [showCaptcha, setShowCaptcha] = useState(false)
|
||||||
|
const [hvmToken, setHVMToken] = useState('')
|
||||||
|
const captchaIframe = useCaptcha(captchaURL, (token) => {
|
||||||
|
setHVMToken(token)
|
||||||
|
setShowCaptcha(false)
|
||||||
|
setCaptchaURL('')
|
||||||
|
})
|
||||||
|
|
||||||
const emailInputRef = useRef<HTMLInputElement>(null)
|
const emailInputRef = useRef<HTMLInputElement>(null)
|
||||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
@@ -65,10 +75,22 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (captchaURL) {
|
||||||
|
setShowCaptcha(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setIsSigningIn(true)
|
setIsSigningIn(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await application.signIn(email, password)
|
const response = await application.signIn(email, password, undefined, undefined, undefined, undefined, hvmToken)
|
||||||
|
const captchaURL = getCaptchaHeader(response)
|
||||||
|
if (captchaURL) {
|
||||||
|
setCaptchaURL(captchaURL)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
setCaptchaURL('')
|
||||||
|
}
|
||||||
if (isErrorResponse(response)) {
|
if (isErrorResponse(response)) {
|
||||||
throw new Error(response.data.error?.message)
|
throw new Error(response.data.error?.message)
|
||||||
} else {
|
} else {
|
||||||
@@ -78,7 +100,6 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
if ((err as Error).toString().includes('Invalid email or password')) {
|
if ((err as Error).toString().includes('Invalid email or password')) {
|
||||||
setIsSigningIn(false)
|
|
||||||
setIsEmailInvalid(true)
|
setIsEmailInvalid(true)
|
||||||
setIsPasswordInvalid(true)
|
setIsPasswordInvalid(true)
|
||||||
setOtherErrorMessage('Invalid email or password.')
|
setOtherErrorMessage('Invalid email or password.')
|
||||||
@@ -86,9 +107,51 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
|||||||
} else {
|
} else {
|
||||||
application.alerts.alert(err as string).catch(console.error)
|
application.alerts.alert(err as string).catch(console.error)
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSigningIn(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signInForm = (
|
||||||
|
<form onSubmit={handleSignIn}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<FloatingLabelInput
|
||||||
|
className={`min-w-auto sm:min-w-90 ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
||||||
|
id="purchase-sign-in-email"
|
||||||
|
type="email"
|
||||||
|
label="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
ref={emailInputRef}
|
||||||
|
disabled={isSigningIn}
|
||||||
|
isInvalid={isEmailInvalid}
|
||||||
|
/>
|
||||||
|
{isEmailInvalid && !otherErrorMessage ? (
|
||||||
|
<div className="mb-4 text-danger">Please provide a valid email.</div>
|
||||||
|
) : null}
|
||||||
|
<FloatingLabelInput
|
||||||
|
className={`min-w-auto sm:min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
||||||
|
id="purchase-sign-in-password"
|
||||||
|
type="password"
|
||||||
|
label="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
ref={passwordInputRef}
|
||||||
|
disabled={isSigningIn}
|
||||||
|
isInvalid={isPasswordInvalid}
|
||||||
|
/>
|
||||||
|
{otherErrorMessage ? <div className="mb-4 text-danger">{otherErrorMessage}</div> : null}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} mb-5 py-2.5`}
|
||||||
|
primary
|
||||||
|
label={isSigningIn ? 'Signing in...' : 'Sign in'}
|
||||||
|
onClick={handleSignIn}
|
||||||
|
disabled={isSigningIn}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CircleIcon className="absolute -left-56 top-[35%] h-8 w-8" />
|
<CircleIcon className="absolute -left-56 top-[35%] h-8 w-8" />
|
||||||
@@ -102,43 +165,7 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="mb-2 mt-0 text-2xl font-bold">Sign in</h1>
|
<h1 className="mb-2 mt-0 text-2xl font-bold">Sign in</h1>
|
||||||
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
|
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
|
||||||
<form onSubmit={handleSignIn}>
|
{showCaptcha ? captchaIframe : signInForm}
|
||||||
<div className="flex flex-col">
|
|
||||||
<FloatingLabelInput
|
|
||||||
className={`min-w-auto sm:min-w-90 ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
|
||||||
id="purchase-sign-in-email"
|
|
||||||
type="email"
|
|
||||||
label="Email"
|
|
||||||
value={email}
|
|
||||||
onChange={handleEmailChange}
|
|
||||||
ref={emailInputRef}
|
|
||||||
disabled={isSigningIn}
|
|
||||||
isInvalid={isEmailInvalid}
|
|
||||||
/>
|
|
||||||
{isEmailInvalid && !otherErrorMessage ? (
|
|
||||||
<div className="mb-4 text-danger">Please provide a valid email.</div>
|
|
||||||
) : null}
|
|
||||||
<FloatingLabelInput
|
|
||||||
className={`min-w-auto sm:min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
|
||||||
id="purchase-sign-in-password"
|
|
||||||
type="password"
|
|
||||||
label="Password"
|
|
||||||
value={password}
|
|
||||||
onChange={handlePasswordChange}
|
|
||||||
ref={passwordInputRef}
|
|
||||||
disabled={isSigningIn}
|
|
||||||
isInvalid={isPasswordInvalid}
|
|
||||||
/>
|
|
||||||
{otherErrorMessage ? <div className="mb-4 text-danger">{otherErrorMessage}</div> : null}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} mb-5 py-2.5`}
|
|
||||||
primary
|
|
||||||
label={isSigningIn ? 'Signing in...' : 'Sign in'}
|
|
||||||
onClick={handleSignIn}
|
|
||||||
disabled={isSigningIn}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
<div className="text-sm font-medium text-passive-1">
|
<div className="text-sm font-medium text-passive-1">
|
||||||
Don’t have an account yet?{' '}
|
Don’t have an account yet?{' '}
|
||||||
<a
|
<a
|
||||||
|
|||||||
31
packages/web/src/javascripts/Hooks/useCaptcha.tsx
Normal file
31
packages/web/src/javascripts/Hooks/useCaptcha.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export const useCaptcha = (captchaURL: string, callback: (token: string) => void) => {
|
||||||
|
useEffect(() => {
|
||||||
|
function handleCaptchaEvent(event: any) {
|
||||||
|
if (!captchaURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.origin !== new URL(captchaURL).origin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event?.data?.type?.includes('captcha')) {
|
||||||
|
callback(event.data.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', handleCaptchaEvent)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleCaptchaEvent)
|
||||||
|
}
|
||||||
|
}, [callback])
|
||||||
|
|
||||||
|
if (!captchaURL) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <iframe src={captchaURL} height={480}></iframe>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user