fix(snjs): refreshing sessions (#2106)
* fix(snjs): refreshing sessions * fix(snjs): bring back all tests * fix(snjs): passing session tokens values * fix(api): remove redundant specs * fix(snjs): add projecting sessions to storage values * fix(snjs): deps tree * fix(snjs): bring back subscription tests * fix(snjs): remove only tag for migration tests * fix(snjs): session specs
This commit is contained in:
@@ -34,23 +34,22 @@ import {
|
||||
API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS,
|
||||
} from '@standardnotes/services'
|
||||
import { FilesApiInterface } from '@standardnotes/files'
|
||||
|
||||
import { ServerSyncPushContextualPayload, SNFeatureRepo, FileContent } from '@standardnotes/models'
|
||||
import * as Responses from '@standardnotes/responses'
|
||||
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import { HttpResponseMeta } from '@standardnotes/api'
|
||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { ApiEndpointParam, ClientDisplayableError, CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
|
||||
import { HttpParams, HttpRequest, HttpVerb, SNHttpService } from './HttpService'
|
||||
import { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
||||
import { Paths } from './Paths'
|
||||
import { Session } from '../Session/Sessions/Session'
|
||||
import { TokenSession } from '../Session/Sessions/TokenSession'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { HttpResponseMeta } from '@standardnotes/api'
|
||||
import { UuidString } from '../../Types/UuidString'
|
||||
import merge from 'lodash/merge'
|
||||
import { SettingsServerInterface } from '../Settings/SettingsServerInterface'
|
||||
import { Strings } from '@Lib/Strings'
|
||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { ApiEndpointParam, ClientDisplayableError, CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
|
||||
/** Legacy api version field to be specified in params when calling v0 APIs. */
|
||||
const V0_API_VERSION = '20200115'
|
||||
@@ -66,7 +65,7 @@ export class SNApiService
|
||||
ItemsServerInterface,
|
||||
SettingsServerInterface
|
||||
{
|
||||
private session?: Session
|
||||
private session: Session | LegacySession | null
|
||||
public user?: Responses.User
|
||||
private registering = false
|
||||
private authenticating = false
|
||||
@@ -81,16 +80,20 @@ export class SNApiService
|
||||
private host: string,
|
||||
private inMemoryStore: KeyValueStoreInterface<string>,
|
||||
private crypto: PureCryptoInterface,
|
||||
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
|
||||
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
|
||||
this.session = null
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
;(this.httpService as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
this.invalidSessionObserver = undefined
|
||||
this.session = undefined
|
||||
this.session = null
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
@@ -145,14 +148,21 @@ export class SNApiService
|
||||
return this.filesHost
|
||||
}
|
||||
|
||||
public setSession(session: Session, persist = true): void {
|
||||
public setSession(session: Session | LegacySession, persist = true): void {
|
||||
this.session = session
|
||||
if (persist) {
|
||||
this.storageService.setValue(StorageKey.Session, session)
|
||||
let sessionProjection: Record<string, unknown>
|
||||
if (session instanceof Session) {
|
||||
sessionProjection = this.sessionStorageMapper.toProjection(session)
|
||||
} else {
|
||||
sessionProjection = this.legacySessionStorageMapper.toProjection(session)
|
||||
}
|
||||
|
||||
this.storageService.setValue(StorageKey.Session, sessionProjection)
|
||||
}
|
||||
}
|
||||
|
||||
public getSession(): Session | undefined {
|
||||
public getSession(): Session | LegacySession | null {
|
||||
return this.session
|
||||
}
|
||||
|
||||
@@ -252,7 +262,7 @@ export class SNApiService
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||
params,
|
||||
/** A session is optional here, if valid, endpoint bypasses 2FA and returns additional params */
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -289,7 +299,7 @@ export class SNApiService
|
||||
|
||||
signOut(): Promise<Responses.SignOutResponse> {
|
||||
const url = joinPaths(this.host, Paths.v1.signOut)
|
||||
return this.httpService.postAbsolute(url, undefined, this.session?.authorizationValue).catch((errorResponse) => {
|
||||
return this.httpService.postAbsolute(url, undefined, this.getSessionAccessToken()).catch((errorResponse) => {
|
||||
return errorResponse
|
||||
}) as Promise<Responses.SignOutResponse>
|
||||
}
|
||||
@@ -317,7 +327,7 @@ export class SNApiService
|
||||
...parameters.newKeyParams.getPortableValue(),
|
||||
})
|
||||
const response = await this.httpService
|
||||
.putAbsolute(url, params, this.session?.authorizationValue)
|
||||
.putAbsolute(url, params, this.getSessionAccessToken())
|
||||
.catch(async (errorResponse) => {
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
return this.refreshSessionThenRetryRequest({
|
||||
@@ -353,7 +363,7 @@ export class SNApiService
|
||||
[ApiEndpointParam.SyncDlLimit]: limit,
|
||||
})
|
||||
const response = await this.httpService
|
||||
.postAbsolute(url, params, this.session?.authorizationValue)
|
||||
.postAbsolute(url, params, this.getSessionAccessToken())
|
||||
.catch<Responses.HttpResponse>(async (errorResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -378,7 +388,7 @@ export class SNApiService
|
||||
return this.httpService
|
||||
.runHttp({
|
||||
...httpRequest,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
.catch((errorResponse) => {
|
||||
return errorResponse
|
||||
@@ -393,16 +403,54 @@ export class SNApiService
|
||||
}
|
||||
this.refreshingSession = true
|
||||
const url = joinPaths(this.host, Paths.v1.refreshSession)
|
||||
const session = this.session as TokenSession
|
||||
const session = this.session as Session
|
||||
const params = this.params({
|
||||
access_token: session.accessToken,
|
||||
refresh_token: session.refreshToken,
|
||||
access_token: session.accessToken.value,
|
||||
refresh_token: session.refreshToken.value,
|
||||
})
|
||||
const result = await this.httpService
|
||||
.postAbsolute(url, params)
|
||||
.then(async (response) => {
|
||||
const session = TokenSession.FromApiResponse(response as Responses.SessionRenewalResponse)
|
||||
await this.setSession(session)
|
||||
const sessionRenewalResponse = response as Responses.SessionRenewalResponse
|
||||
if (
|
||||
sessionRenewalResponse.error ||
|
||||
sessionRenewalResponse.data?.error ||
|
||||
!sessionRenewalResponse.data.session
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const accessTokenOrError = SessionToken.create(
|
||||
sessionRenewalResponse.data.session.access_token,
|
||||
sessionRenewalResponse.data.session.access_expiration,
|
||||
)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
|
||||
const refreshTokenOrError = SessionToken.create(
|
||||
sessionRenewalResponse.data.session.refresh_token,
|
||||
sessionRenewalResponse.data.session.refresh_expiration,
|
||||
)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const sessionOrError = Session.create(
|
||||
accessToken,
|
||||
refreshToken,
|
||||
sessionRenewalResponse.data.session.readonly_access,
|
||||
)
|
||||
if (sessionOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const session = sessionOrError.getValue()
|
||||
|
||||
this.session = session
|
||||
|
||||
this.setSession(session)
|
||||
this.processResponse(response)
|
||||
return response
|
||||
})
|
||||
@@ -411,6 +459,11 @@ export class SNApiService
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL)
|
||||
})
|
||||
this.refreshingSession = false
|
||||
|
||||
if (result === null) {
|
||||
return this.createErrorResponse(API_MESSAGE_INVALID_SESSION)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -421,7 +474,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, Paths.v1.sessions)
|
||||
const response = await this.httpService
|
||||
.getAbsolute(url, {}, this.session?.authorizationValue)
|
||||
.getAbsolute(url, {}, this.getSessionAccessToken())
|
||||
.catch(async (errorResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -444,7 +497,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, <string>Paths.v1.session(sessionId))
|
||||
const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService
|
||||
.deleteAbsolute(url, { uuid: sessionId }, this.session?.authorizationValue)
|
||||
.deleteAbsolute(url, { uuid: sessionId }, this.getSessionAccessToken())
|
||||
.catch((error: Responses.HttpResponse) => {
|
||||
const errorResponse = error as Responses.HttpResponse
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
@@ -467,7 +520,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, Paths.v1.itemRevisions(itemId))
|
||||
const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService
|
||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
||||
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||
.catch((errorResponse: Responses.HttpResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -492,7 +545,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, Paths.v1.itemRevision(itemId, entry.uuid))
|
||||
const response: Responses.SingleRevisionResponse | Responses.HttpResponse = await this.httpService
|
||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
||||
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||
.catch((errorResponse: Responses.HttpResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -510,7 +563,7 @@ export class SNApiService
|
||||
async getUserFeatures(userUuid: UuidString): Promise<Responses.HttpResponse | Responses.UserFeaturesResponse> {
|
||||
const url = joinPaths(this.host, Paths.v1.userFeatures(userUuid))
|
||||
const response = await this.httpService
|
||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
||||
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||
.catch((errorResponse: Responses.HttpResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -550,7 +603,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -568,7 +621,7 @@ export class SNApiService
|
||||
return this.tokenRefreshableRequest<Responses.UpdateSettingResponse>({
|
||||
verb: HttpVerb.Put,
|
||||
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
params,
|
||||
})
|
||||
@@ -578,7 +631,7 @@ export class SNApiService
|
||||
return await this.tokenRefreshableRequest<Responses.GetSettingResponse>({
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase() as SettingName)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
})
|
||||
}
|
||||
@@ -593,7 +646,7 @@ export class SNApiService
|
||||
this.host,
|
||||
Paths.v1.subscriptionSetting(userUuid, settingName.toLowerCase() as SubscriptionSettingName),
|
||||
),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
})
|
||||
}
|
||||
@@ -602,7 +655,7 @@ export class SNApiService
|
||||
return this.tokenRefreshableRequest<Responses.DeleteSettingResponse>({
|
||||
verb: HttpVerb.Delete,
|
||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
})
|
||||
}
|
||||
@@ -616,7 +669,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Delete,
|
||||
url,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_DELETE_REVISION,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
return response
|
||||
}
|
||||
@@ -635,7 +688,7 @@ export class SNApiService
|
||||
const response = await this.tokenRefreshableRequest({
|
||||
verb: HttpVerb.Get,
|
||||
url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
|
||||
})
|
||||
return response
|
||||
@@ -658,7 +711,7 @@ export class SNApiService
|
||||
const response: Responses.HttpResponse | Responses.PostSubscriptionTokensResponse = await this.request({
|
||||
verb: HttpVerb.Post,
|
||||
url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_ACCESS_PURCHASE,
|
||||
})
|
||||
return (response as Responses.PostSubscriptionTokensResponse).data?.token
|
||||
@@ -706,7 +759,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Post,
|
||||
url: joinPaths(this.host, Paths.v1.listedRegistration(this.user.uuid)),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_LISTED_REGISTRATION,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -725,7 +778,7 @@ export class SNApiService
|
||||
const response = await this.tokenRefreshableRequest<Responses.CreateValetTokenResponse>({
|
||||
verb: HttpVerb.Post,
|
||||
url: url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_CREATE_FILE_TOKEN,
|
||||
params,
|
||||
})
|
||||
@@ -860,7 +913,7 @@ export class SNApiService
|
||||
integrityPayloads,
|
||||
},
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INTEGRITY_CHECK_FAIL,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -869,7 +922,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.getSingleItem(itemUuid)),
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_SINGLE_ITEM_SYNC_FAIL,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -890,6 +943,18 @@ export class SNApiService
|
||||
}
|
||||
}
|
||||
|
||||
private getSessionAccessToken(): string | undefined {
|
||||
if (!this.session) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this.session instanceof Session) {
|
||||
return this.session.accessToken.value
|
||||
}
|
||||
|
||||
return this.session.accessToken
|
||||
}
|
||||
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return Promise.resolve({
|
||||
api: {
|
||||
|
||||
Reference in New Issue
Block a user