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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"selfReferences": true
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/home-server": "^1.22.1",
|
||||
"keytar": "^7.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "*",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type OfflineSubscriptionEntitlements = {
|
||||
featuresUrl: string
|
||||
extensionKey: string
|
||||
subscriptionId: number
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export enum InternalFeature {
|
||||
Vaults = 'vaults',
|
||||
HomeServer = 'home-server',
|
||||
}
|
||||
|
||||
@@ -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.'
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": "*"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user