feat: New one-click Home Server, now in Labs. Launch your own self-hosted server instance with just 1 click from the Preferences window. (#2645)

This commit is contained in:
Karol Sójko
2023-11-30 18:31:45 +01:00
committed by GitHub
parent a928bcf359
commit 17039cbb80
321 changed files with 3758 additions and 205 deletions

View File

@@ -1,25 +1,16 @@
import path from 'path'
import os from 'os'
import {
HomeServerManagerInterface,
HomeServerEnvironmentConfiguration,
Result,
} from '@web/Application/Device/DesktopSnjsExports'
import { HomeServer, HomeServerInterface } from '@standardnotes/home-server'
import { WebContents } from 'electron'
import { MessageToWebApp } from '../../Shared/IpcMessages'
import { FilesManagerInterface } from '../File/FilesManagerInterface'
import { HomeServerConfigurationFile } from './HomeServerConfigurationFile'
import { isWindows } from '../Types/Platforms'
const os = require('os')
interface TempHomeServerInterface {
start(configuration?: unknown): Promise<Result<string>>
activatePremiumFeatures(username: string): Promise<Result<string>>
stop(): Promise<Result<string>>
isRunning(): Promise<boolean>
}
export class HomeServerManager implements HomeServerManagerInterface {
private readonly HOME_SERVER_CONFIGURATION_FILE_NAME = 'config.json'
@@ -31,7 +22,7 @@ export class HomeServerManager implements HomeServerManagerInterface {
private readonly LOGS_BUFFER_SIZE = 1000
private homeServer?: TempHomeServerInterface
private homeServer?: HomeServerInterface
constructor(
private webContents: WebContents,
@@ -55,12 +46,15 @@ export class HomeServerManager implements HomeServerManagerInterface {
return this.lastErrorMessage
}
async activatePremiumFeatures(username: string): Promise<string | undefined> {
async activatePremiumFeatures(username: string, subscriptionId: number): Promise<string | undefined> {
if (!this.homeServer) {
return
}
const result = await this.homeServer.activatePremiumFeatures(username)
const result = await this.homeServer.activatePremiumFeatures({
username,
subscriptionId,
})
if (result.isFailed()) {
return result.getError()
@@ -137,11 +131,7 @@ export class HomeServerManager implements HomeServerManagerInterface {
}
async startHomeServer(): Promise<string | undefined> {
await this.lazyLoadHomeServerOnApplicablePlatforms()
if (!this.homeServer) {
return
}
this.homeServer = new HomeServer()
try {
this.lastErrorMessage = undefined
@@ -232,6 +222,9 @@ export class HomeServerManager implements HomeServerManagerInterface {
const interfaces = os.networkInterfaces()
for (const interfaceName in interfaces) {
const addresses = interfaces[interfaceName]
if (!addresses) {
continue
}
for (const address of addresses) {
if (address.family === 'IPv4' && !address.internal) {
return address.address
@@ -274,14 +267,4 @@ export class HomeServerManager implements HomeServerManagerInterface {
return configuration
}
private async lazyLoadHomeServerOnApplicablePlatforms(): Promise<void> {
if (isWindows()) {
return
}
if (this.homeServer) {
return
}
}
}

View File

@@ -283,8 +283,8 @@ export class RemoteBridge implements CrossProcessBridge {
return this.homeServerManager.setHomeServerDataLocation(location)
}
async activatePremiumFeatures(username: string): Promise<string | undefined> {
return this.homeServerManager.activatePremiumFeatures(username)
async activatePremiumFeatures(username: string, subscriptionId: number): Promise<string | undefined> {
return this.homeServerManager.activatePremiumFeatures(username, subscriptionId)
}
async isHomeServerRunning(): Promise<boolean> {

View File

@@ -37,8 +37,8 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
return this.remoteBridge.isHomeServerRunning()
}
async activatePremiumFeatures(username: string): Promise<string | undefined> {
return this.remoteBridge.activatePremiumFeatures(username)
async activatePremiumFeatures(username: string, subscriptionId: number): Promise<string | undefined> {
return this.remoteBridge.activatePremiumFeatures(username, subscriptionId)
}
async setHomeServerConfiguration(configurationJSONString: string): Promise<void> {

View File

@@ -10,6 +10,7 @@
"selfReferences": true
},
"dependencies": {
"@standardnotes/home-server": "^1.22.1",
"keytar": "^7.9.0"
}
}

View File

@@ -28,7 +28,8 @@
"release:mac": "node scripts/build.mjs mac",
"start": "electron ./app --enable-logging --icon _icon/icon.png",
"ava": "rimraf test/data/tmp && ava --serial",
"rebuild:keytar": "yarn app/node_modules/keytar build "
"rebuild:keytar": "yarn app/node_modules/keytar build ",
"rebuild:home-server": "electron-rebuild -f -w @standardnotes/home-server -m ./app"
},
"installConfig": {
"hoistingLimits": "workspaces"
@@ -64,6 +65,7 @@
"babel-loader": "^9.1.0",
"copy-webpack-plugin": "^11.0.0",
"electron-builder": "23.6.0",
"electron-rebuild": "^3.2.9",
"eslint": "*",
"eslint-config-prettier": "^8.9.0",
"eslint-plugin-import": "^2.26.0",

View File

@@ -39,7 +39,7 @@
"@standardnotes/snjs": "workspace:*",
"@standardnotes/web": "workspace:*",
"@tsconfig/react-native": "^3.0.2",
"@types/react": "^18.2.28",
"@types/react": "^18.2.39",
"@types/react-native": "^0.72.3",
"@typescript-eslint/eslint-plugin": "*",
"@typescript-eslint/parser": "*",

View File

@@ -1,8 +1,11 @@
import { DecryptedItemInterface } from '@standardnotes/models'
import { FeatureStatus } from './FeatureStatus'
import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse'
import { NativeFeatureIdentifier } from '@standardnotes/features'
import { RoleName, Uuid } from '@standardnotes/domain-core'
import { ClientDisplayableError } from '@standardnotes/responses'
import { FeatureStatus } from './FeatureStatus'
import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse'
import { OfflineSubscriptionEntitlements } from './OfflineSubscriptionEntitlements'
export interface FeaturesClientInterface {
getFeatureStatus(
@@ -12,6 +15,7 @@ export interface FeaturesClientInterface {
hasMinimumRole(role: string): boolean
hasRole(roleName: RoleName): boolean
hasFirstPartyOfflineSubscription(): boolean
parseOfflineEntitlementsCode(code: string): OfflineSubscriptionEntitlements | ClientDisplayableError
setOfflineFeaturesCode(code: string): Promise<SetOfflineFeaturesFunctionResponse>
hasOfflineRepo(): boolean
deleteOfflineFeatureRepo(): Promise<void>

View File

@@ -1,4 +1,5 @@
export type OfflineSubscriptionEntitlements = {
featuresUrl: string
extensionKey: string
subscriptionId: number
}

View File

@@ -4,7 +4,7 @@ export interface HomeServerManagerInterface {
getHomeServerConfiguration(): Promise<string | undefined>
setHomeServerDataLocation(location: string): Promise<void>
stopHomeServer(): Promise<string | undefined>
activatePremiumFeatures(username: string): Promise<string | undefined>
activatePremiumFeatures(username: string, subscriptionId: number): Promise<string | undefined>
getHomeServerLogs(): Promise<string[]>
isHomeServerRunning(): Promise<boolean>
getHomeServerUrl(): Promise<string | undefined>

View File

@@ -82,8 +82,8 @@ export class HomeServerService
return this.desktopDevice.isHomeServerRunning()
}
async activatePremiumFeatures(username: string): Promise<Result<string>> {
const result = await this.desktopDevice.activatePremiumFeatures(username)
async activatePremiumFeatures(username: string, subscriptionId: number): Promise<Result<string>> {
const result = await this.desktopDevice.activatePremiumFeatures(username, subscriptionId)
if (result !== undefined) {
return Result.fail(result)

View File

@@ -4,7 +4,7 @@ import { HomeServerEnvironmentConfiguration } from './HomeServerEnvironmentConfi
import { HomeServerStatus } from './HomeServerStatus'
export interface HomeServerServiceInterface {
activatePremiumFeatures(username: string): Promise<Result<string>>
activatePremiumFeatures(username: string, subscriptionId: number): Promise<Result<string>>
isHomeServerRunning(): Promise<boolean>
isHomeServerEnabled(): Promise<boolean>
getHomeServerDataLocation(): Promise<string | undefined>

View File

@@ -1,4 +1,3 @@
export enum InternalFeature {
Vaults = 'vaults',
HomeServer = 'home-server',
}

View File

@@ -48,7 +48,7 @@ export const API_MESSAGE_UNTRUSTED_EXTENSIONS_WARNING = `The extension you are a
export const API_MESSAGE_FAILED_DOWNLOADING_EXTENSION = `Error downloading package details. Please check the
extension link and try again.`
export const API_MESSAGE_FAILED_OFFLINE_ACTIVATION =
'An unknown issue occurred during offline activation. Please try again.'
'An unknown issue occurred during offline activation. Please download your activation code again and try once more.'
export const INVALID_EXTENSION_URL = 'Invalid extension URL.'

View File

@@ -50,8 +50,6 @@ import { MigrateFeatureRepoToOfflineEntitlementsUseCase } from './UseCase/Migrat
import { GetFeatureStatusUseCase } from './UseCase/GetFeatureStatus'
import { SettingsClientInterface } from '../Settings/SettingsClientInterface'
type GetOfflineSubscriptionDetailsResponse = OfflineSubscriptionEntitlements | ClientDisplayableError
export class FeaturesService
extends AbstractService<FeaturesEvent>
implements FeaturesClientInterface, InternalEventHandlerInterface
@@ -231,9 +229,7 @@ export class FeaturesService
public async setOfflineFeaturesCode(code: string): Promise<SetOfflineFeaturesFunctionResponse> {
try {
const activationCodeWithoutSpaces = code.replace(/\s/g, '')
const decodedData = this.crypto.base64Decode(activationCodeWithoutSpaces)
const result = this.parseOfflineEntitlementsCode(decodedData)
const result = this.parseOfflineEntitlementsCode(code)
if (result instanceof ClientDisplayableError) {
return result
@@ -275,12 +271,16 @@ export class FeaturesService
}
}
private parseOfflineEntitlementsCode(code: string): GetOfflineSubscriptionDetailsResponse | ClientDisplayableError {
parseOfflineEntitlementsCode(code: string): OfflineSubscriptionEntitlements | ClientDisplayableError {
try {
const { featuresUrl, extensionKey } = JSON.parse(code)
const activationCodeWithoutSpaces = code.replace(/\s/g, '')
const decodedData = this.crypto.base64Decode(activationCodeWithoutSpaces)
const { featuresUrl, extensionKey, subscriptionId } = JSON.parse(decodedData)
return {
featuresUrl,
extensionKey,
subscriptionId,
}
} catch (error) {
return new ClientDisplayableError(API_MESSAGE_FAILED_OFFLINE_ACTIVATION)

View File

@@ -13,12 +13,12 @@
"@standardnotes/icons": "workspace:*",
"nanoid": "^4.0.0",
"nanostores": "^0.9.3",
"react": "link:../web/node_modules/react",
"react-dom": "link:../web/node_modules/react-dom"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "link:../web/node_modules/@types/react",
"@types/react-dom": "link:../web/node_modules/@types/react-dom",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.17",
"eslint": "*",
"prettier": "*",
"typescript": "*"

View File

@@ -57,8 +57,8 @@
"@standardnotes/toast": "workspace:*",
"@standardnotes/ui-services": "workspace:^",
"@types/jest": "^29.2.4",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.17",
"@types/wicg-file-system-access": "^2020.9.5",
"@zip.js/zip.js": "^2.6.60",
"autoprefixer": "^10.4.13",

View File

@@ -4,7 +4,6 @@ import { WebApplicationInterface } from '@standardnotes/ui-services'
export class DevMode {
constructor(private application: WebApplicationInterface) {
InternalFeatureService.get().enableFeature(InternalFeature.Vaults)
InternalFeatureService.get().enableFeature(InternalFeature.HomeServer)
}
/** Valid only when running a mock event publisher on port 3124 */

View File

@@ -4,7 +4,6 @@ import { PackageProvider } from '../Panes/Plugins/PackageProvider'
import { securityPrefsHasBubble } from '../Panes/Security/securityPrefsHasBubble'
import { PreferencePaneId, StatusServiceEvent } from '@standardnotes/services'
import { isDesktopApplication } from '@/Utils'
import { featureTrunkHomeServerEnabled } from '@/FeatureTrunk'
import { PreferencesMenuItem } from './PreferencesMenuItem'
import { SelectableMenuItem } from './SelectableMenuItem'
import { PREFERENCES_MENU_ITEMS, READY_PREFERENCES_MENU_ITEMS } from './MenuItems'
@@ -30,7 +29,7 @@ export class PreferencesSessionController {
menuItems.push({ id: 'vaults', label: 'Vaults', icon: 'safe-square', order: 5 })
}
if (featureTrunkHomeServerEnabled() && isDesktopApplication()) {
if (isDesktopApplication()) {
menuItems.push({ id: 'home-server', label: 'Home Server', icon: 'server', order: 5 })
}

View File

@@ -50,7 +50,17 @@ const OfflineSubscription: FunctionComponent<Props> = ({ application, onSuccess
return
}
const serverActivationResult = await homeServer.activatePremiumFeatures(signedInUser.email)
const parsedOfflineFeaturesCodeResult = application.features.parseOfflineEntitlementsCode(activationCode)
if (parsedOfflineFeaturesCodeResult instanceof ClientDisplayableError) {
await application.alerts.alert(parsedOfflineFeaturesCodeResult.text)
return
}
const serverActivationResult = await homeServer.activatePremiumFeatures(
signedInUser.email,
parsedOfflineFeaturesCodeResult.subscriptionId,
)
if (serverActivationResult.isFailed()) {
await application.alerts.alert(serverActivationResult.getError())

View File

@@ -1,7 +1,4 @@
import { isWindowsPlatform } from '@standardnotes/ui-services'
import { useApplication } from '@/Components/ApplicationProvider'
import { Pill, Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
@@ -9,30 +6,6 @@ import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import HomeServerSettings from './HomeServerSettings'
const HomeServer = () => {
const application = useApplication()
if (isWindowsPlatform(application.platform)) {
return (
<PreferencesPane>
<PreferencesGroup>
<PreferencesSegment>
<div className="flex items-center justify-between">
<div className="flex items-start">
<Title>Home Server</Title>
<Pill style={'success'}>Labs</Pill>
</div>
</div>
<div className="flex items-center justify-between">
<div className="mr-10 flex flex-col">
<Subtitle>Windows support for home server is coming soon.</Subtitle>
</div>
</div>
</PreferencesSegment>
</PreferencesGroup>
</PreferencesPane>
)
}
return (
<PreferencesPane>
<PreferencesGroup>

View File

@@ -374,8 +374,8 @@ const HomeServerSettings = () => {
<h1 className="sk-h3 m-0 text-sm font-semibold">Activate Premium Features</h1>
</div>
<p className="col-start-1 col-end-3 m-0 mt-1 text-sm">
Enter your purchased offline subscription code to activate all the features offered by the home
server.
Enter your purchased offline subscription code to activate all the features offered by your home
server, likes files support and Super notes.
</p>
<Button
primary

View File

@@ -44,7 +44,7 @@ const StatusIndicator = ({ status, className, homeServerService }: Props) => {
const isUsingHomeServer = await application.isUsingHomeServer()
if (isUsingHomeServer) {
setSignInStatusMessage(`You are currently signed into your home server under ${signedInUser.email}`)
setSignInStatusClassName('bg-success')
setSignInStatusClassName('bg-success text-success-contrast')
setSignInStatusIcon('check')
} else {
setSignInStatusMessage(
@@ -52,14 +52,14 @@ const StatusIndicator = ({ status, className, homeServerService }: Props) => {
signedInUser.email
}, then sign in or register using ${await homeServerService.getHomeServerUrl()}.`,
)
setSignInStatusClassName('bg-warning')
setSignInStatusClassName('bg-warning text-warning-contrast')
setSignInStatusIcon('warning')
}
} else {
setSignInStatusMessage(
`You are not currently signed into your home server. To use your home server, sign in or register using ${await homeServerService.getHomeServerUrl()}`,
)
setSignInStatusClassName('bg-warning')
setSignInStatusClassName('bg-warning text-warning-contrast')
setSignInStatusIcon('warning')
}
}

View File

@@ -11,7 +11,3 @@ export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean {
export function featureTrunkVaultsEnabled(): boolean {
return InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)
}
export function featureTrunkHomeServerEnabled(): boolean {
return InternalFeatureService.get().isFeatureEnabled(InternalFeature.HomeServer)
}