Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/models/src/Domain/Syncable/Note/Note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpe
public readonly preview_html: string
public readonly spellcheck?: boolean
public readonly noteType?: NoteType
public readonly authorizedForListed: boolean

/** The package_info.identifier of the editor (not its uuid), such as org.standardnotes.advanced-markdown */
public readonly editorIdentifier?: FeatureIdentifier | string
Expand All @@ -31,6 +32,7 @@ export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpe
this.spellcheck = this.payload.content.spellcheck
this.noteType = this.payload.content.noteType
this.editorIdentifier = this.payload.content.editorIdentifier
this.authorizedForListed = this.payload.content.authorizedForListed || false

if (!this.noteType) {
const prefersPlain = this.getAppDomainValueWithDefault(AppDataField.LegacyPrefersPlainEditor, false)
Expand Down
1 change: 1 addition & 0 deletions packages/models/src/Domain/Syncable/Note/NoteContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface NoteContentSpecialized {
spellcheck?: boolean
noteType?: NoteType
editorIdentifier?: FeatureIdentifier | string
authorizedForListed?: boolean
}

export type NoteContent = NoteContentSpecialized & ItemContent
4 changes: 4 additions & 0 deletions packages/models/src/Domain/Syncable/Note/NoteMutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export class NoteMutator extends DecryptedItemMutator<NoteContent> {
this.mutableContent.editorIdentifier = identifier
}

set authorizedForListed(authorizedForListed: boolean) {
this.mutableContent.authorizedForListed = authorizedForListed
}

toggleSpellcheck(): void {
if (this.mutableContent.spellcheck == undefined) {
this.mutableContent.spellcheck = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ export enum ChallengeReason {
UnprotectFile,
UnprotectNote,
DeleteAccount,
AuthorizeNoteForListed,
}
35 changes: 10 additions & 25 deletions packages/snjs/lib/Application/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import { ClientDisplayableError } from '@standardnotes/responses'

import { SnjsVersion } from './../Version'
import { SNLog } from '../Log'
import { Challenge, ChallengeResponse } from '../Services'
import { Challenge, ChallengeResponse, ListedClientInterface } from '../Services'
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
import { ApplicationOptionsDefaults } from './Options/Defaults'

Expand All @@ -86,9 +86,7 @@ type ApplicationObserver = {

type ObserverRemover = () => void

export class SNApplication
implements ApplicationInterface, AppGroupManagedApplication, InternalServices.ListedClientInterface
{
export class SNApplication implements ApplicationInterface, AppGroupManagedApplication {
onDeinit!: ExternalServices.DeinitCallback

/**
Expand Down Expand Up @@ -273,6 +271,10 @@ export class SNApplication
return this.componentManagerService
}

public get listed(): ListedClientInterface {
return this.listedService
}

public computePrivateUsername(username: string): Promise<string | undefined> {
return ComputePrivateUsername(this.options.crypto, username)
}
Expand Down Expand Up @@ -682,25 +684,6 @@ export class SNApplication
return this.protectionService.authorizeSearchingProtectedNotesText()
}

public canRegisterNewListedAccount(): boolean {
return this.listedService.canRegisterNewListedAccount()
}

public async requestNewListedAccount(): Promise<Responses.ListedAccount | undefined> {
return this.listedService.requestNewListedAccount()
}

public async getListedAccounts(): Promise<Responses.ListedAccount[]> {
return this.listedService.getListedAccounts()
}

public getListedAccountInfo(
account: Responses.ListedAccount,
inContextOfItem?: UuidString,
): Promise<Responses.ListedAccountInfo | undefined> {
return this.listedService.getListedAccountInfo(account, inContextOfItem)
}

public async createEncryptedBackupFileForAutomatedDesktopBackups(): Promise<BackupFile | undefined> {
return this.protocolService.createEncryptedBackupFile()
}
Expand Down Expand Up @@ -1096,11 +1079,11 @@ export class SNApplication
this.createComponentManager()
this.createMigrationService()
this.createMfaService()
this.createListedService()
this.createActionsManager()
this.createFileService()
this.createIntegrityService()
this.createMutatorService()
this.createListedService()
this.createActionsManager()
this.createStatusService()

if (isDesktopDevice(this.deviceInterface)) {
Expand Down Expand Up @@ -1175,6 +1158,8 @@ export class SNApplication
this.itemManager,
this.settingsService,
this.deprecatedHttpService,
this.protectionService,
this.mutator,
this.internalEventBus,
)
this.services.push(this.listedService)
Expand Down
1 change: 1 addition & 0 deletions packages/snjs/lib/Services/Api/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export const ChallengeStrings = {
SelectProtectedNote: 'Authentication is required to select a protected note',
DisableMfa: 'Authentication is required to disable two-factor authentication',
DeleteAccount: 'Authentication is required to delete your account',
ListedAuthorization: 'Authentication is required to approve this note for Listed',
}

export const ErrorAlertStrings = {
Expand Down
2 changes: 2 additions & 0 deletions packages/snjs/lib/Services/Challenge/Challenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export class Challenge implements ChallengeInterface {
return ChallengeStrings.DisableMfa
case ChallengeReason.DeleteAccount:
return ChallengeStrings.DeleteAccount
case ChallengeReason.AuthorizeNoteForListed:
return ChallengeStrings.ListedAuthorization
case ChallengeReason.Custom:
return ''
default:
Expand Down
3 changes: 3 additions & 0 deletions packages/snjs/lib/Services/Listed/ListedClientInterface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SNNote } from '@standardnotes/models'
import { Uuid } from '@standardnotes/common'
import { ListedAccount, ListedAccountInfo } from '@standardnotes/responses'

Expand All @@ -6,4 +7,6 @@ export interface ListedClientInterface {
requestNewListedAccount: () => Promise<ListedAccount | undefined>
getListedAccounts(): Promise<ListedAccount[]>
getListedAccountInfo(account: ListedAccount, inContextOfItem?: Uuid): Promise<ListedAccountInfo | undefined>
isNoteAuthorizedForListed(note: SNNote): boolean
authorizeNoteForListed(note: SNNote): Promise<boolean>
}
33 changes: 29 additions & 4 deletions packages/snjs/lib/Services/Listed/ListedService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import { SNSettingsService } from '../Settings/SNSettingsService'
import { ListedClientInterface } from './ListedClientInterface'
import { SNApiService } from '../Api/ApiService'
import { ListedAccount, ListedAccountInfo, ListedAccountInfoResponse } from '@standardnotes/responses'
import { SNActionsExtension } from '@standardnotes/models'
import { AbstractService, InternalEventBusInterface } from '@standardnotes/services'
import { NoteMutator, SNActionsExtension, SNNote } from '@standardnotes/models'
import { AbstractService, InternalEventBusInterface, MutatorClientInterface } from '@standardnotes/services'
import { SNProtectionService } from '../Protection'

export class ListedService extends AbstractService implements ListedClientInterface {
constructor(
private apiService: SNApiService,
private itemManager: ItemManager,
private settingsService: SNSettingsService,
private httpSerivce: SNHttpService,
private protectionService: SNProtectionService,
private mutatorService: MutatorClientInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
Expand All @@ -27,13 +30,32 @@ export class ListedService extends AbstractService implements ListedClientInterf
;(this.settingsService as unknown) = undefined
;(this.apiService as unknown) = undefined
;(this.httpSerivce as unknown) = undefined
;(this.protectionService as unknown) = undefined
;(this.mutatorService as unknown) = undefined
super.deinit()
}

public canRegisterNewListedAccount(): boolean {
return this.apiService.user != undefined
}

public isNoteAuthorizedForListed(note: SNNote): boolean {
return note.authorizedForListed
}

public async authorizeNoteForListed(note: SNNote): Promise<boolean> {
const result = await this.protectionService.authorizeListedPublishing()
if (result === false) {
return false
}

await this.mutatorService.changeAndSaveItem<NoteMutator>(note, (mutator) => {
mutator.authorizedForListed = true
})

return true
}

/**
* Account creation is asyncronous on the backend due to message-based nature of architecture.
* In order to get the newly created account, we poll the server to check for new accounts.
Expand Down Expand Up @@ -73,8 +95,11 @@ export class ListedService extends AbstractService implements ListedClientInterf
if (inContextOfItem) {
url += `&item_uuid=${inContextOfItem}`
}
const response = (await this.httpSerivce.getAbsolute(url)) as ListedAccountInfoResponse
if (response.error || !response.data || isString(response.data)) {

const response = (await this.httpSerivce.getAbsolute(url).catch((error) => {
console.error(error)
})) as ListedAccountInfoResponse
if (!response || response.error || !response.data || isString(response.data)) {
return undefined
}

Expand Down
11 changes: 8 additions & 3 deletions packages/snjs/lib/Services/Protection/ProtectionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,18 @@ export class SNProtectionService extends AbstractService<ProtectionEvent> implem
return this.authorizeAction(ChallengeReason.RevokeSession)
}

async authorizeListedPublishing(): Promise<boolean> {
return this.authorizeAction(ChallengeReason.AuthorizeNoteForListed, { forcePrompt: true })
}

async authorizeAction(
reason: ChallengeReason,
{ fallBackToAccountPassword = true, requireAccountPassword = false } = {},
{ fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {},
): Promise<boolean> {
return this.validateOrRenewSession(reason, {
requireAccountPassword,
fallBackToAccountPassword,
forcePrompt,
})
}

Expand Down Expand Up @@ -295,9 +300,9 @@ export class SNProtectionService extends AbstractService<ProtectionEvent> implem

private async validateOrRenewSession(
reason: ChallengeReason,
{ fallBackToAccountPassword = true, requireAccountPassword = false } = {},
{ fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {},
): Promise<boolean> {
if (this.getSessionExpiryDate() > new Date()) {
if (this.getSessionExpiryDate() > new Date() && !forcePrompt) {
return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,27 @@ type ListedActionsMenuProps = {
const ListedActionsMenu = ({ application, note }: ListedActionsMenuProps) => {
const [menuGroups, setMenuGroups] = useState<ListedMenuGroup[]>([])
const [isFetchingAccounts, setIsFetchingAccounts] = useState(true)
const [isAuthorized, setIsAuthorized] = useState(false)

useEffect(() => {
const authorize = async () => {
if (!application.listed.isNoteAuthorizedForListed(note)) {
await application.listed.authorizeNoteForListed(note)
}

setIsAuthorized(application.listed.isNoteAuthorizedForListed(note))
}

void authorize()
}, [application, note])

const reloadMenuGroup = useCallback(
async (group: ListedMenuGroup) => {
const updatedAccountInfo = await application.getListedAccountInfo(group.account, note.uuid)
if (!isAuthorized) {
return
}

const updatedAccountInfo = await application.listed.getListedAccountInfo(group.account, note.uuid)

if (!updatedAccountInfo) {
return
Expand All @@ -39,7 +56,7 @@ const ListedActionsMenu = ({ application, note }: ListedActionsMenuProps) => {

setMenuGroups(updatedGroups)
},
[application, menuGroups, note],
[application, menuGroups, note, isAuthorized],
)

useEffect(() => {
Expand All @@ -49,19 +66,21 @@ const ListedActionsMenu = ({ application, note }: ListedActionsMenuProps) => {
return
}

if (!isAuthorized) {
return
}

try {
const listedAccountEntries = await application.getListedAccounts()
const listedAccountEntries = await application.listed.getListedAccounts()

if (!listedAccountEntries.length) {
throw new Error('No Listed accounts found')
}

const menuGroups: ListedMenuGroup[] = []

await Promise.all(
listedAccountEntries.map(async (account) => {
const accountInfo = await application.getListedAccountInfo(account, note.uuid)

const accountInfo = await application.listed.getListedAccountInfo(account, note.uuid)
if (accountInfo) {
menuGroups.push({
name: accountInfo.display_name,
Expand Down Expand Up @@ -91,7 +110,11 @@ const ListedActionsMenu = ({ application, note }: ListedActionsMenuProps) => {
}

void fetchListedAccounts()
}, [application, note.uuid])
}, [application, note.uuid, isAuthorized])

if (!isAuthorized) {
return null
}

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ const ListedActionsOption: FunctionComponent<Props> = ({ application, note }) =>

const [isOpen, setIsOpen] = useState(false)

const toggleMenu = useCallback(() => {
setIsOpen((isOpen) => !isOpen)
}, [])
const toggleMenu = useCallback(async () => {
if (!application.listed.isNoteAuthorizedForListed(note)) {
await application.listed.authorizeNoteForListed(note)
}

if (application.listed.isNoteAuthorizedForListed(note)) {
setIsOpen((isOpen) => !isOpen)
}
}, [application, note])

return (
<div ref={menuContainerRef}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Listed = ({ application }: Props) => {
const [requestingAccount, setRequestingAccount] = useState<boolean>()

const reloadAccounts = useCallback(async () => {
setAccounts(await application.getListedAccounts())
setAccounts(await application.listed.getListedAccounts())
}, [application])

useEffect(() => {
Expand All @@ -30,7 +30,7 @@ const Listed = ({ application }: Props) => {
setRequestingAccount(true)

const requestAccount = async () => {
const account = await application.requestNewListedAccount()
const account = await application.listed.requestNewListedAccount()
if (account) {
const openSettings = await application.alertService.confirm(
'Your new Listed blog has been successfully created!' +
Expand All @@ -43,7 +43,7 @@ const Listed = ({ application }: Props) => {
)
reloadAccounts().catch(console.error)
if (openSettings) {
const info = await application.getListedAccountInfo(account)
const info = await application.listed.getListedAccountInfo(account)
if (info) {
application.deviceInterface.openUrl(info?.settings_url)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const ListedAccountItem: FunctionComponent<Props> = ({ account, showSeparator, a
useEffect(() => {
const loadAccount = async () => {
setIsLoading(true)
const info = await application.getListedAccountInfo(account)
const info = await application.listed.getListedAccountInfo(account)
setAccountInfo(info)
setIsLoading(false)
}
Expand Down