mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 18:31:52 +00:00
Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
894fa21002 | ||
|
|
d611c69b0c | ||
|
|
d430986403 | ||
|
|
c8aafbdbc9 | ||
|
|
2e3e1401f5 | ||
|
|
deb0b1e561 | ||
|
|
daf701a76c | ||
|
|
43035e7271 | ||
|
|
e37db33d62 | ||
|
|
adab9e7fca | ||
|
|
a21bd91460 | ||
|
|
acc2722586 | ||
|
|
d8d6541b11 | ||
|
|
1a7d40afa9 | ||
|
|
b152a93dd8 | ||
|
|
ae90b70348 | ||
|
|
21f982e9a6 | ||
|
|
4100d4ca97 | ||
|
|
f5ae93c999 | ||
|
|
2f5ad4d82b | ||
|
|
6e2a332bcd | ||
|
|
4a2e496e8a | ||
|
|
e69a936fb8 | ||
|
|
d9894d4082 | ||
|
|
6b3fa54551 | ||
|
|
9f47a34b11 | ||
|
|
531dec936d | ||
|
|
1d7684f4d4 | ||
|
|
cfacbcabd3 | ||
|
|
4fcdf5f832 | ||
|
|
2189c5643d | ||
|
|
aada5755de | ||
|
|
60d31163c5 | ||
|
|
fd6a1897c8 | ||
|
|
62e0f742ba | ||
|
|
c42ff81a38 | ||
|
|
cc49a73954 | ||
|
|
29a4506a40 | ||
|
|
efa60bf4ab | ||
|
|
1c2fd192df | ||
|
|
0a9349bbc1 | ||
|
|
653961da64 | ||
|
|
6585d91816 | ||
|
|
3e3097945f | ||
|
|
c0f5f09767 | ||
|
|
1c8889a60c | ||
|
|
218bae3b46 | ||
|
|
92c297648c | ||
|
|
68eccdb63c | ||
|
|
ee1c66d0c2 | ||
|
|
c52f75c9e3 | ||
|
|
b46c75e391 | ||
|
|
7fb8f88c8d | ||
|
|
c83baec363 | ||
|
|
882cfde5f3 | ||
|
|
53720130b3 | ||
|
|
7c321bbf6b | ||
|
|
bd060670e4 | ||
|
|
7ff538a526 | ||
|
|
3c74f3d46e | ||
|
|
54ae7f82d6 | ||
|
|
39867478d0 | ||
|
|
8e2642a741 | ||
|
|
a4f7d53a6b | ||
|
|
397236c68e | ||
|
|
8ce43d808e | ||
|
|
e1200c2991 | ||
|
|
0937c81e46 | ||
|
|
02ab63da81 | ||
|
|
5cf7d1ff88 | ||
|
|
a20970fa17 | ||
|
|
30dd62285b | ||
|
|
3065323e79 | ||
|
|
e1a6a3d9ed | ||
|
|
c0e08df221 | ||
|
|
108213f920 | ||
|
|
a8e229821f | ||
|
|
a6b7d657a0 | ||
|
|
77b8d0b2a0 | ||
|
|
9503f754ad | ||
|
|
540868220d | ||
|
|
dd8037fda1 | ||
|
|
6f09738b49 | ||
|
|
808fff4187 | ||
|
|
a9735fd777 | ||
|
|
327c79350e | ||
|
|
44def3be85 | ||
|
|
18df87b8f5 | ||
|
|
97a85d6e01 | ||
|
|
3d4930acb4 | ||
|
|
58468dd53f | ||
|
|
50a2be243a | ||
|
|
0d7b087665 | ||
|
|
0e87cce8de | ||
|
|
537f2d91b8 | ||
|
|
79604182c8 | ||
|
|
68faa17ab6 | ||
|
|
13a6d7f0c7 |
@@ -11,7 +11,7 @@
|
||||
|
||||
EmbassyOS is a mass-market, graphical operating system designed to facilitate the discovery, installation, configuration, private self-hosting, and reliable operation of open-source software services and applications. It aims to eliminate trust and custodianship from personal computing.
|
||||
|
||||

|
||||

|
||||
|
||||
## ⚠️ Caution
|
||||
Some technologies supported by this software, such as [Lightning](https://lightning.network/), are considered in active development and might experience issues. Do not commit any funds you are not willing to loose. Be #reckless at your own risk.
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/v0/specs SpecsR GET
|
||||
/v0/metrics MetricsR GET
|
||||
|
||||
/v0/logs LogsR GET
|
||||
/v0/sshKeys SshKeysR GET POST
|
||||
/v0/sshKeys/#Text SshKeyByFingerprintR DELETE
|
||||
/v0/password PasswordR PATCH
|
||||
@@ -38,6 +39,9 @@
|
||||
/v0/apps/#AppId/backup/stop StopBackupR POST
|
||||
/v0/apps/#AppId/backup/restore RestoreBackupR POST
|
||||
/v0/apps/#AppId/autoconfig/#AppId AutoconfigureR POST
|
||||
/v0/apps/#AppId/actions ActionR POST
|
||||
|
||||
/v0/network/lan/reset ResetLanR POST
|
||||
|
||||
/v0/disks DisksR GET
|
||||
/v0/disks/eject EjectR POST
|
||||
|
||||
@@ -33,6 +33,6 @@ database:
|
||||
database: "start9_agent.sqlite3"
|
||||
poolsize: "_env:YESOD_SQLITE_POOLSIZE:10"
|
||||
|
||||
app-mgr-version-spec: "=0.2.8"
|
||||
app-mgr-version-spec: "=0.2.9"
|
||||
|
||||
#analytics: UA-YOURCODE
|
||||
|
||||
1
agent/migrations/0.2.8::0.2.9
Normal file
1
agent/migrations/0.2.8::0.2.9
Normal file
@@ -0,0 +1 @@
|
||||
SELECT TRUE;
|
||||
@@ -1,5 +1,5 @@
|
||||
name: ambassador-agent
|
||||
version: 0.2.8
|
||||
version: 0.2.9
|
||||
|
||||
default-extensions:
|
||||
- NoImplicitPrelude
|
||||
@@ -65,6 +65,7 @@ dependencies:
|
||||
- http-types
|
||||
- interpolate
|
||||
- iso8601-time
|
||||
- json-rpc
|
||||
- lens
|
||||
- lens-aeson
|
||||
- lifted-async
|
||||
|
||||
@@ -54,21 +54,21 @@ import Yesod.Persist.Core
|
||||
import Constants
|
||||
import qualified Daemon.AppNotifications as AppNotifications
|
||||
import Daemon.RefreshProcDev
|
||||
import qualified Daemon.SslRenew as SSLRenew
|
||||
import Daemon.TorHealth
|
||||
import Daemon.ZeroConf
|
||||
import Foundation
|
||||
import Lib.Algebra.State.RegistryUrl
|
||||
import Lib.Background
|
||||
import Lib.Database
|
||||
import Lib.External.Metrics.ProcDev
|
||||
import Lib.SelfUpdate
|
||||
import Lib.Sound
|
||||
import Lib.SystemPaths
|
||||
import Lib.Tor ( newTorManager )
|
||||
import Lib.WebServer
|
||||
import Model
|
||||
import Settings
|
||||
import Lib.Background
|
||||
import qualified Daemon.SslRenew as SSLRenew
|
||||
import Lib.Tor (newTorManager)
|
||||
import Daemon.TorHealth
|
||||
|
||||
appMain :: IO ()
|
||||
appMain = do
|
||||
@@ -118,6 +118,7 @@ makeFoundation appSettings = do
|
||||
def <- getDefaultProcDevMetrics
|
||||
appProcDevMomentCache <- newIORef (now, mempty, def)
|
||||
appLastTorRestart <- newIORef now
|
||||
appLanThread <- forkIO (sleep 10) >>= newMVar
|
||||
|
||||
-- We need a log function to create a connection pool. We need a connection
|
||||
-- pool to create our foundation. And we need our foundation to get a
|
||||
|
||||
@@ -18,6 +18,9 @@ import Lib.ProductKey
|
||||
import Lib.SystemPaths
|
||||
|
||||
import Settings
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Control.Carrier.Lift
|
||||
import Lib.Error
|
||||
|
||||
start9AgentServicePrefix :: IsString a => a
|
||||
start9AgentServicePrefix = "start9-"
|
||||
@@ -53,4 +56,10 @@ publishAgentToAvahi = do
|
||||
"_http._tcp"
|
||||
agentPort
|
||||
lift Avahi.reload
|
||||
lift $ threadDelay 10_000_000
|
||||
tid <- asks appLanThread >>= liftIO . takeMVar
|
||||
liftIO $ killThread tid
|
||||
tid' <- liftIO $ forkIO (runM . void . runExceptT @S9Error $ AppMgr2.runAppMgrCliC AppMgr2.lanEnable)
|
||||
asks appLanThread >>= liftIO . flip putMVar tid'
|
||||
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ data AgentCtx = AgentCtx
|
||||
, appBackgroundJobs :: TVar JobCache
|
||||
, appIconTags :: TVar (HM.HashMap AppId (Digest MD5))
|
||||
, appLastTorRestart :: IORef UTCTime
|
||||
, appLanThread :: MVar ThreadId
|
||||
}
|
||||
|
||||
setWebProcessThreadId :: ThreadId -> AgentCtx -> IO ()
|
||||
|
||||
@@ -7,78 +7,82 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
module Handler.Apps where
|
||||
|
||||
import Startlude hiding ( modify
|
||||
, execState
|
||||
import Startlude hiding ( Reader
|
||||
, asks
|
||||
, Reader
|
||||
, runReader
|
||||
, catchError
|
||||
, forkFinally
|
||||
, empty
|
||||
, execState
|
||||
, forkFinally
|
||||
, modify
|
||||
, runReader
|
||||
)
|
||||
|
||||
import Control.Carrier.Reader
|
||||
import Control.Carrier.Error.Church
|
||||
import Control.Carrier.Lift
|
||||
import Control.Carrier.Reader
|
||||
import qualified Control.Concurrent.Async.Lifted
|
||||
as LAsync
|
||||
import qualified Control.Concurrent.Lifted as Lifted
|
||||
import qualified Control.Exception.Lifted as Lifted
|
||||
import Control.Concurrent.STM.TVar
|
||||
import Control.Effect.Empty hiding ( guard )
|
||||
import Control.Effect.Labelled ( HasLabelled
|
||||
, Labelled
|
||||
, runLabelled
|
||||
)
|
||||
import qualified Control.Exception.Lifted as Lifted
|
||||
import Control.Lens hiding ( (??) )
|
||||
import Control.Monad.Logger
|
||||
import Control.Monad.Trans.Control ( MonadBaseControl )
|
||||
import Crypto.Hash
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Lens
|
||||
import Data.Aeson.Types ( parseMaybe )
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import Data.IORef
|
||||
import qualified Data.HashMap.Lazy as HML
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.IORef
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import Data.Singletons
|
||||
import Data.Singletons.Prelude.Bool ( SBool(..)
|
||||
, If
|
||||
import Data.Singletons.Prelude.Bool ( If
|
||||
, SBool(..)
|
||||
)
|
||||
import Data.Singletons.Prelude.List ( Elem )
|
||||
|
||||
import qualified Data.Text as Text
|
||||
import Database.Persist
|
||||
import Database.Persist.Sql ( ConnectionPool )
|
||||
import Database.Persist.Sqlite ( runSqlPool )
|
||||
import Exinst
|
||||
import Network.HTTP.Types
|
||||
import qualified Network.JSONRPC as JSONRPC
|
||||
import Yesod.Core.Content
|
||||
import Yesod.Core.Json
|
||||
import Yesod.Core.Handler hiding ( cached )
|
||||
import Yesod.Core.Json
|
||||
import Yesod.Core.Types ( JSONResponse(..) )
|
||||
import Yesod.Persist.Core
|
||||
|
||||
import Foundation
|
||||
import Handler.Backups
|
||||
import Handler.Icons
|
||||
import Handler.Network
|
||||
import Handler.Types.Apps
|
||||
import Handler.Util
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Lib.Algebra.State.RegistryUrl
|
||||
import Lib.Background
|
||||
import Lib.Error
|
||||
import qualified Lib.External.AppManifest as AppManifest
|
||||
import qualified Lib.External.AppMgr as AppMgr
|
||||
import qualified Lib.External.Registry as Reg
|
||||
import qualified Lib.External.AppManifest as AppManifest
|
||||
import Lib.IconCache
|
||||
import qualified Lib.Notifications as Notifications
|
||||
import Lib.SystemPaths
|
||||
import Lib.TyFam.ConditionalData
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.Emver
|
||||
import Lib.Types.NetAddress
|
||||
import Lib.Types.ServerApp
|
||||
import Model
|
||||
import Settings
|
||||
import Crypto.Hash
|
||||
|
||||
pureLog :: Show a => a -> Handler a
|
||||
pureLog = liftA2 (*>) ($logInfo . show) pure
|
||||
@@ -244,18 +248,30 @@ getInstalledAppsLogic = do
|
||||
, appInstalledPreviewStatus = AppStatusTmp Installing
|
||||
, appInstalledPreviewVersionInstalled = storeAppVersionInfoVersion
|
||||
, appInstalledPreviewTorAddress = Nothing
|
||||
, appInstalledPreviewUi = False
|
||||
, appInstalledPreviewLanAddress = Nothing
|
||||
, appInstalledPreviewTorUi = False
|
||||
, appInstalledPreviewLanUi = False
|
||||
}
|
||||
installedPreviews = flip
|
||||
HML.mapWithKey
|
||||
remapped
|
||||
\appId (s, v, AppMgr2.InfoRes {..}) -> AppInstalledPreview
|
||||
{ appInstalledPreviewBase = AppBase appId infoResTitle (iconUrl appId v)
|
||||
, appInstalledPreviewStatus = s
|
||||
, appInstalledPreviewVersionInstalled = v
|
||||
, appInstalledPreviewTorAddress = infoResTorAddress
|
||||
, appInstalledPreviewUi = AppManifest.uiAvailable infoResManifest
|
||||
}
|
||||
\appId (s, v, AppMgr2.InfoRes {..}) ->
|
||||
let
|
||||
mLanAddress = do -- Maybe
|
||||
addrBase <- infoResTorAddress
|
||||
let
|
||||
lanConfs = mapMaybe AppManifest.portMapEntryLan
|
||||
$ AppManifest.appManifestPortMapping infoResManifest
|
||||
guard (not . null $ lanConfs)
|
||||
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
|
||||
in AppInstalledPreview { appInstalledPreviewBase = AppBase appId infoResTitle (iconUrl appId v)
|
||||
, appInstalledPreviewStatus = s
|
||||
, appInstalledPreviewVersionInstalled = v
|
||||
, appInstalledPreviewTorAddress = infoResTorAddress
|
||||
, appInstalledPreviewLanAddress = mLanAddress
|
||||
, appInstalledPreviewTorUi = AppManifest.torUiAvailable infoResManifest
|
||||
, appInstalledPreviewLanUi = AppManifest.lanUiAvailable infoResManifest
|
||||
}
|
||||
|
||||
pure $ HML.elems $ HML.union installingPreviews installedPreviews
|
||||
|
||||
@@ -286,9 +302,14 @@ getInstalledAppByIdLogic appId = do
|
||||
, appInstalledFullInstructions = Nothing
|
||||
, appInstalledFullLastBackup = backupTime
|
||||
, appInstalledFullTorAddress = Nothing
|
||||
, appInstalledFullLanAddress = Nothing
|
||||
, appInstalledFullTorUi = False
|
||||
, appInstalledFullLanUi = False
|
||||
, appInstalledFullConfiguredRequirements = []
|
||||
, appInstalledFullUninstallAlert = Nothing
|
||||
, appInstalledFullRestoreAlert = Nothing
|
||||
, appInstalledFullStartAlert = Nothing
|
||||
, appInstalledFullActions = []
|
||||
}
|
||||
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|]
|
||||
let remapped = remapAppMgrInfo jobCache serverApps
|
||||
@@ -316,18 +337,30 @@ getInstalledAppByIdLogic appId = do
|
||||
(HM.lookup depId installCache $> AppStatusTmp Installing)
|
||||
<|> (view _1 <$> HM.lookup depId remapped)
|
||||
pure $ dependencyInfoToDependencyRequirement (AsInstalled STrue) (base, depStatus, depInfo)
|
||||
manifest <- lift $ LAsync.wait manifest'
|
||||
manifest <- (lift $ LAsync.wait manifest') >>= \case
|
||||
Nothing -> throwError $ NotFoundE "manifest" (show appId)
|
||||
Just x -> pure x
|
||||
instructions <- lift $ LAsync.wait instructions'
|
||||
backupTime <- lift $ LAsync.wait backupTime'
|
||||
let lanAddress = do
|
||||
addrBase <- infoResTorAddress
|
||||
let lanConfs = mapMaybe AppManifest.portMapEntryLan $ AppManifest.appManifestPortMapping manifest
|
||||
guard (not . null $ lanConfs)
|
||||
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
|
||||
pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version)
|
||||
, appInstalledFullStatus = status
|
||||
, appInstalledFullVersionInstalled = version
|
||||
, appInstalledFullInstructions = instructions
|
||||
, appInstalledFullLastBackup = backupTime
|
||||
, appInstalledFullTorAddress = infoResTorAddress
|
||||
, appInstalledFullLanAddress = lanAddress
|
||||
, appInstalledFullTorUi = AppManifest.torUiAvailable manifest
|
||||
, appInstalledFullLanUi = AppManifest.lanUiAvailable manifest
|
||||
, appInstalledFullConfiguredRequirements = HM.elems requirements
|
||||
, appInstalledFullUninstallAlert = manifest >>= AppManifest.appManifestUninstallAlert
|
||||
, appInstalledFullRestoreAlert = manifest >>= AppManifest.appManifestRestoreAlert
|
||||
, appInstalledFullUninstallAlert = AppManifest.appManifestUninstallAlert manifest
|
||||
, appInstalledFullRestoreAlert = AppManifest.appManifestRestoreAlert manifest
|
||||
, appInstalledFullStartAlert = AppManifest.appManifestStartAlert manifest
|
||||
, appInstalledFullActions = AppManifest.appManifestActions manifest
|
||||
}
|
||||
runMaybeT (installing <|> installed) `orThrowM` NotFoundE "appId" (show appId)
|
||||
|
||||
@@ -361,7 +394,9 @@ postUninstallAppLogic appId dryrun = do
|
||||
breakageIds <- HM.keys . AppMgr2.unBreakageMap <$> AppMgr2.remove flags appId
|
||||
bs <- pure (traverse (hydrate $ (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) <$> serverApps) breakageIds)
|
||||
`orThrowM` InternalE "Reported app breakage for app that isn't installed, contact support"
|
||||
when (not $ coerce dryrun) $ clearIcon appId
|
||||
when (not $ coerce dryrun) $ do
|
||||
clearIcon appId
|
||||
postResetLanLogic
|
||||
pure $ WithBreakages bs ()
|
||||
|
||||
type InstallResponse :: Bool -> Type
|
||||
@@ -465,6 +500,7 @@ postInstallNewAppLogic appId appVersion dryrun = do
|
||||
(void $ Notifications.emit k infoResVersion (Notifications.RestartFailed e))
|
||||
pool
|
||||
)
|
||||
postResetLanLogic
|
||||
|
||||
|
||||
postStartServerAppR :: AppId -> Handler ()
|
||||
@@ -769,3 +805,21 @@ dependencyInfoToDependencyRequirement asInstalled (base, status, AppMgr2.Depende
|
||||
let appDependencyRequirementReasonOptional = dependencyInfoReasonOptional
|
||||
appDependencyRequirementDefault = dependencyInfoRequired
|
||||
in AppDependencyRequirement { .. }
|
||||
|
||||
postActionR :: AppId -> Handler (JSONResponse JSONRPC.Response)
|
||||
postActionR appId = do
|
||||
req <- requireCheckJsonBody
|
||||
fmap JSONResponse . intoHandler $ postActionLogic appId req
|
||||
|
||||
postActionLogic :: (Has (Error S9Error) sig m, Has AppMgr2.AppMgr sig m)
|
||||
=> AppId
|
||||
-> JSONRPC.Request
|
||||
-> m JSONRPC.Response
|
||||
postActionLogic appId (JSONRPC.Request { getReqMethod, getReqId }) = do
|
||||
hm <- AppMgr2.action appId getReqMethod
|
||||
case (HM.lookup "result" hm, HM.lookup "error" hm >>= parseMaybe parseJSON) of
|
||||
(Just v , _ ) -> pure (JSONRPC.Response JSONRPC.V2 v getReqId)
|
||||
(_ , Just e ) -> pure (JSONRPC.ResponseError JSONRPC.V2 e getReqId)
|
||||
(Nothing, Nothing) -> throwError
|
||||
$ AppMgrParseE "action" (decodeUtf8 . LBS.toStrict $ encode (Object hm)) "Invalid JSONRPC Response"
|
||||
postActionLogic _ r = throwError $ InvalidRequestE (toJSON r) "Invalid JSONRPC Request"
|
||||
|
||||
32
agent/src/Handler/Network.hs
Normal file
32
agent/src/Handler/Network.hs
Normal file
@@ -0,0 +1,32 @@
|
||||
module Handler.Network where
|
||||
|
||||
import Startlude hiding ( Reader
|
||||
, asks
|
||||
, runReader
|
||||
)
|
||||
|
||||
import Control.Carrier.Lift ( runM )
|
||||
import Control.Effect.Error
|
||||
import Control.Carrier.Reader
|
||||
import Lib.Error
|
||||
import Yesod.Core ( getYesod )
|
||||
|
||||
import Foundation
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Lib.Types.Core
|
||||
|
||||
postResetLanR :: Handler ()
|
||||
postResetLanR = do
|
||||
ctx <- getYesod
|
||||
runM . handleS9ErrC . runReader ctx $ postResetLanLogic
|
||||
|
||||
postResetLanLogic :: (MonadIO m, Has (Reader AgentCtx) sig m, Has (Error S9Error) sig m) => m ()
|
||||
postResetLanLogic = do
|
||||
threadVar <- asks appLanThread
|
||||
mtid <- liftIO . tryTakeMVar $ threadVar
|
||||
case mtid of
|
||||
Nothing -> throwError $ TemporarilyForbiddenE (AppId "LAN") "reset" "being reset"
|
||||
Just tid -> liftIO $ do
|
||||
killThread tid
|
||||
newTid <- forkIO (void . runM . runExceptT @S9Error . AppMgr2.runAppMgrCliC $ AppMgr2.lanEnable)
|
||||
putMVar threadVar newTid
|
||||
@@ -28,6 +28,9 @@ import Lib.SystemPaths hiding ( (</>) )
|
||||
import Lib.Tor
|
||||
import Settings
|
||||
import Control.Carrier.Lift ( runM )
|
||||
import System.Process
|
||||
import qualified UnliftIO
|
||||
import System.FileLock
|
||||
|
||||
getVersionR :: Handler AppVersionRes
|
||||
getVersionR = pure . AppVersionRes $ agentVersion
|
||||
@@ -35,8 +38,7 @@ getVersionR = pure . AppVersionRes $ agentVersion
|
||||
getVersionLatestR :: Handler VersionLatestRes
|
||||
getVersionLatestR = handleS9ErrT $ do
|
||||
s <- getsYesod appSettings
|
||||
v <- interp s $ Reg.getLatestAgentVersion
|
||||
pure $ VersionLatestRes v
|
||||
uncurry VersionLatestRes <$> interp s Reg.getLatestAgentVersion
|
||||
where interp s = ExceptT . liftIO . runError . injectFilesystemBaseFromContext s . runRegistryUrlIOC
|
||||
|
||||
|
||||
@@ -48,6 +50,8 @@ getSpecsR = handleS9ErrT $ do
|
||||
specsDisk <- fmap show . metricDiskSize <$> getDfMetrics
|
||||
specsNetworkId <- lift . runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostname
|
||||
specsTorAddress <- lift . runM . injectFilesystemBaseFromContext settings $ getAgentHiddenServiceUrl
|
||||
specsLanAddress <-
|
||||
fmap (<> ".local") . lift . runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostname
|
||||
|
||||
let specsAgentVersion = agentVersion
|
||||
returnJsonEncoding SpecsRes { .. }
|
||||
@@ -69,3 +73,9 @@ patchServerR = do
|
||||
getGitR :: Handler Text
|
||||
getGitR = pure $embedGitRevision
|
||||
|
||||
getLogsR :: Handler (JSONResponse [Text])
|
||||
getLogsR = do
|
||||
let debugLock = "/root/agent/tmp/debug.lock"
|
||||
UnliftIO.bracket (liftIO $ lockFile debugLock Exclusive) (liftIO . unlockFile) $ const $ do
|
||||
liftIO $ callCommand "journalctl -u agent --since \"1 hour ago\" > /root/agent/tmp/debug.log"
|
||||
liftIO $ JSONResponse . lines <$> readFile "/root/agent/tmp/debug.log"
|
||||
|
||||
@@ -9,6 +9,7 @@ import Data.Aeson
|
||||
import Data.Aeson.Flatten
|
||||
import Data.Singletons
|
||||
|
||||
import qualified Lib.External.AppManifest as Manifest
|
||||
import Lib.TyFam.ConditionalData
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.Emver
|
||||
@@ -45,7 +46,9 @@ data AppInstalledPreview = AppInstalledPreview
|
||||
, appInstalledPreviewStatus :: AppStatus
|
||||
, appInstalledPreviewVersionInstalled :: Version
|
||||
, appInstalledPreviewTorAddress :: Maybe TorAddress
|
||||
, appInstalledPreviewUi :: Bool
|
||||
, appInstalledPreviewLanAddress :: Maybe LanAddress
|
||||
, appInstalledPreviewTorUi :: Bool
|
||||
, appInstalledPreviewLanUi :: Bool
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
instance ToJSON AppInstalledPreview where
|
||||
@@ -53,7 +56,9 @@ instance ToJSON AppInstalledPreview where
|
||||
[ "status" .= appInstalledPreviewStatus
|
||||
, "versionInstalled" .= appInstalledPreviewVersionInstalled
|
||||
, "torAddress" .= (unTorAddress <$> appInstalledPreviewTorAddress)
|
||||
, "ui" .= appInstalledPreviewUi
|
||||
, "lanAddress" .= (unLanAddress <$> appInstalledPreviewLanAddress)
|
||||
, "torUi" .= appInstalledPreviewTorUi
|
||||
, "lanUi" .= appInstalledPreviewLanUi
|
||||
]
|
||||
|
||||
data InstallNewAppReq = InstallNewAppReq
|
||||
@@ -129,11 +134,16 @@ data AppInstalledFull = AppInstalledFull
|
||||
, appInstalledFullStatus :: AppStatus
|
||||
, appInstalledFullVersionInstalled :: Version
|
||||
, appInstalledFullTorAddress :: Maybe TorAddress
|
||||
, appInstalledFullLanAddress :: Maybe LanAddress
|
||||
, appInstalledFullTorUi :: Bool
|
||||
, appInstalledFullLanUi :: Bool
|
||||
, appInstalledFullInstructions :: Maybe Text
|
||||
, appInstalledFullLastBackup :: Maybe UTCTime
|
||||
, appInstalledFullConfiguredRequirements :: [Stripped AppDependencyRequirement]
|
||||
, appInstalledFullUninstallAlert :: Maybe Text
|
||||
, appInstalledFullRestoreAlert :: Maybe Text
|
||||
, appInstalledFullStartAlert :: Maybe Text
|
||||
, appInstalledFullActions :: [Manifest.Action]
|
||||
}
|
||||
instance ToJSON AppInstalledFull where
|
||||
toJSON AppInstalledFull {..} = object
|
||||
@@ -141,6 +151,9 @@ instance ToJSON AppInstalledFull where
|
||||
, "lastBackup" .= appInstalledFullLastBackup
|
||||
, "configuredRequirements" .= appInstalledFullConfiguredRequirements
|
||||
, "torAddress" .= (unTorAddress <$> appInstalledFullTorAddress)
|
||||
, "lanAddress" .= (unLanAddress <$> appInstalledFullLanAddress)
|
||||
, "torUi" .= appInstalledFullTorUi
|
||||
, "lanUi" .= appInstalledFullLanUi
|
||||
, "id" .= appBaseId appInstalledFullBase
|
||||
, "title" .= appBaseTitle appInstalledFullBase
|
||||
, "iconURL" .= appBaseIconUrl appInstalledFullBase
|
||||
@@ -148,6 +161,8 @@ instance ToJSON AppInstalledFull where
|
||||
, "status" .= appInstalledFullStatus
|
||||
, "uninstallAlert" .= appInstalledFullUninstallAlert
|
||||
, "restoreAlert" .= appInstalledFullRestoreAlert
|
||||
, "startAlert" .= appInstalledFullStartAlert
|
||||
, "actions" .= appInstalledFullActions
|
||||
]
|
||||
|
||||
data AppVersionInfo = AppVersionInfo
|
||||
|
||||
@@ -15,11 +15,13 @@ import Lib.Types.Emver
|
||||
import Model
|
||||
|
||||
data VersionLatestRes = VersionLatestRes
|
||||
{ versionLatestVersion :: Version
|
||||
{ versionLatestVersion :: Version
|
||||
, versionLatestReleaseNotes :: Maybe Text
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
instance ToJSON VersionLatestRes where
|
||||
toJSON VersionLatestRes {..} = object $ ["versionLatest" .= versionLatestVersion]
|
||||
toJSON VersionLatestRes {..} =
|
||||
object $ ["versionLatest" .= versionLatestVersion, "releaseNotes" .= versionLatestReleaseNotes]
|
||||
instance ToTypedContent VersionLatestRes where
|
||||
toTypedContent = toTypedContent . toJSON
|
||||
instance ToContent VersionLatestRes where
|
||||
@@ -31,14 +33,15 @@ data ServerRes = ServerRes
|
||||
, serverStatus :: Maybe AppStatus
|
||||
, serverStatusAt :: UTCTime
|
||||
, serverVersionInstalled :: Version
|
||||
, serverNotifications :: [ Entity Notification ]
|
||||
, serverNotifications :: [Entity Notification]
|
||||
, serverWifi :: WifiList
|
||||
, serverSsh :: [ SshKeyFingerprint ]
|
||||
, serverSsh :: [SshKeyFingerprint]
|
||||
, serverAlternativeRegistryUrl :: Maybe Text
|
||||
, serverSpecs :: SpecsRes
|
||||
, serverWelcomeAck :: Bool
|
||||
, serverAutoCheckUpdates :: Bool
|
||||
} deriving (Eq, Show)
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
type JsonEncoding a = Encoding
|
||||
jsonEncode :: (Monad m, ToJSON a) => a -> m (JsonEncoding a)
|
||||
|
||||
@@ -16,6 +16,7 @@ data SpecsRes = SpecsRes
|
||||
, specsNetworkId :: Text
|
||||
, specsAgentVersion :: Version
|
||||
, specsTorAddress :: Text
|
||||
, specsLanAddress :: Text
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -23,6 +24,7 @@ instance ToJSON SpecsRes where
|
||||
toJSON SpecsRes {..} = object
|
||||
[ "EmbassyOS Version" .= specsAgentVersion
|
||||
, "Tor Address" .= specsTorAddress
|
||||
, "LAN Address" .= specsLanAddress
|
||||
, "Network ID" .= specsNetworkId
|
||||
, "CPU" .= specsCPU
|
||||
, "Memory" .= specsMem
|
||||
@@ -33,6 +35,7 @@ instance ToJSON SpecsRes where
|
||||
. fold
|
||||
$ [ "EmbassyOS Version" .= specsAgentVersion
|
||||
, "Tor Address" .= specsTorAddress
|
||||
, "LAN Address" .= specsLanAddress
|
||||
, "Network ID" .= specsNetworkId
|
||||
, "CPU" .= specsCPU
|
||||
, "Memory" .= specsMem
|
||||
|
||||
@@ -93,6 +93,7 @@ getSpecs settings = do
|
||||
specsDisk <- fmap show . metricDiskSize <$> getDfMetrics
|
||||
specsNetworkId <- runM $ injectFilesystemBaseFromContext settings getStart9AgentHostname
|
||||
specsTorAddress <- runM $ injectFilesystemBaseFromContext settings getAgentHiddenServiceUrl
|
||||
specsLanAddress <- fmap (<> ".local") . runM $ injectFilesystemBaseFromContext settings getStart9AgentHostname
|
||||
|
||||
let specsAgentVersion = agentVersion
|
||||
pure $ SpecsRes { .. }
|
||||
|
||||
@@ -29,7 +29,7 @@ import qualified Data.String as String
|
||||
import Lib.Algebra.Domain.AppMgr.Types
|
||||
import Lib.Algebra.Domain.AppMgr.TH
|
||||
import Lib.Error
|
||||
import Lib.External.AppManifest
|
||||
import qualified Lib.External.AppManifest as Manifest
|
||||
import Lib.TyFam.ConditionalData
|
||||
import Lib.Types.Core ( AppId(..)
|
||||
, AppContainerStatus(..)
|
||||
@@ -50,6 +50,7 @@ import Control.Monad.Trans.Control ( defaultLiftBaseWith
|
||||
, MonadBaseControl(..)
|
||||
)
|
||||
import qualified Data.ByteString.Char8 as C8
|
||||
import System.Process
|
||||
|
||||
|
||||
type InfoRes :: Either OnlyInfoFlag [IncludeInfoFlag] -> Type
|
||||
@@ -66,7 +67,7 @@ data InfoRes a = InfoRes
|
||||
(Either_ (DefaultEqSym1 'OnlyDependencies) (ElemSym1 'IncludeDependencies) a)
|
||||
(HM.HashMap AppId DependencyInfo)
|
||||
, infoResManifest
|
||||
:: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) AppManifest
|
||||
:: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) Manifest.AppManifest
|
||||
, infoResStatus :: Include (Either_ (DefaultEqSym1 'OnlyStatus) (ElemSym1 'IncludeStatus) a) AppContainerStatus
|
||||
}
|
||||
instance SingI (a :: Either OnlyInfoFlag [IncludeInfoFlag]) => FromJSON (InfoRes a) where
|
||||
@@ -270,6 +271,8 @@ data AppMgr (m :: Type -> Type) k where
|
||||
-- Tor ::_
|
||||
Update ::DryRun -> AppId -> Maybe VersionRange -> AppMgr m BreakageMap
|
||||
-- Verify ::_
|
||||
LanEnable ::AppMgr m ()
|
||||
Action ::AppId -> Text -> AppMgr m (HM.HashMap Text Value)
|
||||
makeSmartConstructors ''AppMgr
|
||||
|
||||
newtype AppMgrCliC m a = AppMgrCliC { runAppMgrCliC :: m a }
|
||||
@@ -421,6 +424,16 @@ instance (Has (Error S9Error) sig m, Algebra sig m, MonadIO m) => Algebra (AppMg
|
||||
ExitFailure 6 ->
|
||||
throwError $ NotFoundE "appId@version" ([i|#{appId}#{maybe "" (('@':) . show) version}|])
|
||||
ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n
|
||||
(L LanEnable ) -> liftIO $ callProcess "appmgr" ["lan", "enable"] $> ctx
|
||||
(L (Action appId action)) -> do
|
||||
let args = ["actions", show appId, toS action]
|
||||
(ec, out) <- readProcessInheritStderr "appmgr" args ""
|
||||
case ec of
|
||||
ExitSuccess -> case eitherDecodeStrict out of
|
||||
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
|
||||
Right x -> pure $ ctx $> x
|
||||
ExitFailure 6 -> throwError $ NotFoundE "appId" (show appId)
|
||||
ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n
|
||||
R other -> AppMgrCliC $ alg (runAppMgrCliC . hdl) other ctx
|
||||
where
|
||||
versionSpec :: (IsString a, Semigroup a, ConvertText String a) => Maybe VersionRange -> a -> a
|
||||
|
||||
74
agent/src/Lib/External/AppManifest.hs
vendored
74
agent/src/Lib/External/AppManifest.hs
vendored
@@ -9,12 +9,12 @@ import Data.Aeson
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Yaml as Yaml
|
||||
|
||||
import Control.Monad.Fail ( MonadFail(fail) )
|
||||
import Lib.Error
|
||||
import Lib.SystemPaths
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.Emver
|
||||
import Lib.Types.Emver.Orphans ( )
|
||||
import Control.Monad.Fail ( MonadFail(fail) )
|
||||
|
||||
data ImageType = ImageTypeTar
|
||||
deriving (Eq, Show)
|
||||
@@ -47,6 +47,33 @@ instance FromJSON AssetMapping where
|
||||
assetMappingOverwrite <- o .: "overwrite"
|
||||
pure $ AssetMapping { .. }
|
||||
|
||||
data Action = Action
|
||||
{ actionId :: Text
|
||||
, actionName :: Text
|
||||
, actionDescription :: Text
|
||||
, actionWarning :: Maybe Text
|
||||
, actionAllowedStatuses :: [AppContainerStatus]
|
||||
}
|
||||
deriving Show
|
||||
instance FromJSON Action where
|
||||
parseJSON = withObject "AppAction" $ \o -> do
|
||||
actionId <- o .: "id"
|
||||
actionName <- o .: "name"
|
||||
actionDescription <- o .: "description"
|
||||
actionWarning <- o .:? "warning"
|
||||
actionAllowedStatuses <- o .: "allowed-statuses"
|
||||
pure Action { .. }
|
||||
instance ToJSON Action where
|
||||
toJSON Action {..} =
|
||||
object
|
||||
$ [ "id" .= actionId
|
||||
, "name" .= actionName
|
||||
, "description" .= actionDescription
|
||||
, "allowedStatuses" .= actionAllowedStatuses
|
||||
]
|
||||
<> maybeToList (("warning" .=) <$> actionWarning)
|
||||
|
||||
|
||||
data AppManifest where
|
||||
AppManifest ::{ appManifestId :: AppId
|
||||
, appManifestVersion :: Version
|
||||
@@ -54,7 +81,7 @@ data AppManifest where
|
||||
, appManifestDescShort :: Text
|
||||
, appManifestDescLong :: Text
|
||||
, appManifestReleaseNotes :: Text
|
||||
, appManifestPortMapping :: HM.HashMap Word16 Word16
|
||||
, appManifestPortMapping :: [PortMapEntry]
|
||||
, appManifestImageType :: ImageType
|
||||
, appManifestMount :: FilePath
|
||||
, appManifestAssets :: [AssetMapping]
|
||||
@@ -62,10 +89,20 @@ data AppManifest where
|
||||
, appManifestDependencies :: HM.HashMap AppId VersionRange
|
||||
, appManifestUninstallAlert :: Maybe Text
|
||||
, appManifestRestoreAlert :: Maybe Text
|
||||
, appManifestStartAlert :: Maybe Text
|
||||
, appManifestActions :: [Action]
|
||||
} -> AppManifest
|
||||
deriving instance Show AppManifest
|
||||
|
||||
uiAvailable :: AppManifest -> Bool
|
||||
uiAvailable AppManifest {..} = isJust $ HM.lookup 80 appManifestPortMapping
|
||||
torUiAvailable :: AppManifest -> Bool
|
||||
torUiAvailable AppManifest {..} = any (== 80) $ portMapEntryTor <$> appManifestPortMapping
|
||||
|
||||
lanUiAvailable :: AppManifest -> Bool
|
||||
lanUiAvailable AppManifest {..} = any id $ fmap portMapEntryLan appManifestPortMapping <&> \case
|
||||
Just Standard -> True
|
||||
Just (Custom 443) -> True
|
||||
Just (Custom 80 ) -> True
|
||||
_ -> False
|
||||
|
||||
instance FromJSON AppManifest where
|
||||
parseJSON = withObject "App Manifest " $ \o -> do
|
||||
@@ -75,7 +112,7 @@ instance FromJSON AppManifest where
|
||||
appManifestDescShort <- o .: "description" >>= (.: "short")
|
||||
appManifestDescLong <- o .: "description" >>= (.: "long")
|
||||
appManifestReleaseNotes <- o .: "release-notes"
|
||||
appManifestPortMapping <- o .: "ports" >>= fmap HM.fromList . traverse parsePortMapping
|
||||
appManifestPortMapping <- o .: "ports"
|
||||
appManifestImageType <- o .: "image" >>= (.: "type")
|
||||
appManifestMount <- o .: "mount"
|
||||
appManifestAssets <- o .: "assets" >>= traverse parseJSON
|
||||
@@ -83,13 +120,34 @@ instance FromJSON AppManifest where
|
||||
appManifestDependencies <- o .:? "dependencies" .!= HM.empty >>= traverse parseDepInfo
|
||||
appManifestUninstallAlert <- o .:? "uninstall-alert"
|
||||
appManifestRestoreAlert <- o .:? "restore-alert"
|
||||
appManifestStartAlert <- o .:? "start-alert"
|
||||
appManifestActions <- o .: "actions"
|
||||
pure $ AppManifest { .. }
|
||||
where
|
||||
parsePortMapping = withObject "Port Mapping" $ \o -> liftA2 (,) (o .: "tor") (o .: "internal")
|
||||
parseDepInfo = withObject "Dep Info" $ (.: "version")
|
||||
where parseDepInfo = withObject "Dep Info" $ (.: "version")
|
||||
|
||||
getAppManifest :: (MonadIO m, HasFilesystemBase sig m) => AppId -> S9ErrT m (Maybe AppManifest)
|
||||
getAppManifest appId = do
|
||||
base <- ask @"filesystemBase"
|
||||
ExceptT $ first (ManifestParseE appId) <$> liftIO
|
||||
(Yaml.decodeFileEither . toS $ (appMgrAppPath appId <> "manifest.yaml") `relativeTo` base)
|
||||
|
||||
data LanConfiguration = Standard | Custom Word16 deriving (Eq, Show)
|
||||
instance FromJSON LanConfiguration where
|
||||
parseJSON = liftA2 (<|>) standard custom
|
||||
where
|
||||
standard =
|
||||
withText "Standard Lan" \t -> if t == "standard" then pure Standard else fail "Not Standard Lan Conf"
|
||||
custom = withObject "Custom Lan" $ \o -> do
|
||||
Custom <$> (o .: "custom" >>= (.: "port"))
|
||||
data PortMapEntry = PortMapEntry
|
||||
{ portMapEntryInternal :: Word16
|
||||
, portMapEntryTor :: Word16
|
||||
, portMapEntryLan :: Maybe LanConfiguration
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
instance FromJSON PortMapEntry where
|
||||
parseJSON = withObject "Port Map Entry" $ \o -> do
|
||||
portMapEntryInternal <- o .: "internal"
|
||||
portMapEntryTor <- o .: "tor"
|
||||
portMapEntryLan <- o .:? "lan"
|
||||
pure PortMapEntry { .. }
|
||||
|
||||
7
agent/src/Lib/External/Registry.hs
vendored
7
agent/src/Lib/External/Registry.hs
vendored
@@ -150,12 +150,13 @@ getAppVersionForSpec appId spec = do
|
||||
v <- o .: "version"
|
||||
pure v
|
||||
|
||||
getLatestAgentVersion :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) => m Version
|
||||
getLatestAgentVersion :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) => m (Version, Maybe Text)
|
||||
getLatestAgentVersion = do
|
||||
val <- registryRequest agentVersionPath
|
||||
parseOrThrow agentVersionPath val $ withObject "version response" $ \o -> do
|
||||
v <- o .: "version"
|
||||
pure v
|
||||
v <- o .: "version"
|
||||
rn <- o .:? "release-notes"
|
||||
pure (v, rn)
|
||||
where agentVersionPath = "sys/version/agent"
|
||||
|
||||
getLatestAgentVersionForSpec :: (Has RegistryUrl sig m, Has (Lift IO) sig m, Has (Error S9Error) sig m)
|
||||
|
||||
@@ -11,9 +11,9 @@ import Startlude hiding ( check
|
||||
import qualified Startlude.ByteStream as ByteStream
|
||||
import qualified Startlude.ByteStream.Char8 as ByteStream
|
||||
|
||||
import Control.Carrier.Lift ( runM )
|
||||
import qualified Control.Effect.Reader.Labelled
|
||||
as Fused
|
||||
import Control.Carrier.Lift ( runM )
|
||||
import Control.Monad.Trans.Reader ( mapReaderT )
|
||||
import Control.Monad.Trans.Resource
|
||||
import Data.Attoparsec.Text
|
||||
@@ -21,51 +21,52 @@ import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Char8 as B8
|
||||
import qualified Data.Conduit as Conduit
|
||||
import qualified Data.Conduit.Combinators as Conduit
|
||||
import qualified Data.Conduit.Tar as Conduit
|
||||
import Data.Conduit.Shell hiding ( arch
|
||||
, hostname
|
||||
, patch
|
||||
, stream
|
||||
, hostname
|
||||
)
|
||||
import qualified Data.Conduit.Tar as Conduit
|
||||
import Data.FileEmbed
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.IORef
|
||||
import Data.String.Interpolate.IsString
|
||||
import qualified Data.Yaml as Yaml
|
||||
import Exinst
|
||||
import System.FilePath ( splitPath
|
||||
import qualified Streaming.Conduit as Conduit
|
||||
import qualified Streaming.Prelude as Stream
|
||||
import qualified Streaming.Zip as Stream
|
||||
import System.Directory
|
||||
import System.FilePath ( (</>)
|
||||
, joinPath
|
||||
, (</>)
|
||||
, splitPath
|
||||
)
|
||||
import System.FilePath.Posix ( takeDirectory )
|
||||
import System.Directory
|
||||
import System.IO.Error
|
||||
import System.Posix.Files
|
||||
import System.Process ( callCommand )
|
||||
import qualified Streaming.Prelude as Stream
|
||||
import qualified Streaming.Conduit as Conduit
|
||||
import qualified Streaming.Zip as Stream
|
||||
|
||||
import Constants
|
||||
import Control.Effect.Error hiding ( run )
|
||||
import Daemon.ZeroConf ( getStart9AgentHostname )
|
||||
import qualified Data.Text as T
|
||||
import Foundation
|
||||
import Handler.Network
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Lib.ClientManifest
|
||||
import Lib.Error
|
||||
import qualified Lib.External.AppMgr as AppMgr
|
||||
import Lib.External.Registry
|
||||
import Lib.Sound
|
||||
import Lib.Ssl
|
||||
import Lib.Tor
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.NetAddress
|
||||
import Lib.Types.Emver
|
||||
import Lib.SystemCtl
|
||||
import Lib.SystemPaths hiding ( (</>) )
|
||||
import Lib.Tor
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.Emver
|
||||
import Lib.Types.NetAddress
|
||||
import Settings
|
||||
import Util.File
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Daemon.ZeroConf ( getStart9AgentHostname )
|
||||
import qualified Data.Text as T
|
||||
import Control.Effect.Error hiding ( run )
|
||||
|
||||
|
||||
data Synchronizer = Synchronizer
|
||||
@@ -96,15 +97,16 @@ parseKernelVersion = do
|
||||
pure $ KernelVersion (Version (major', minor', patch', 0)) arch
|
||||
|
||||
synchronizer :: Synchronizer
|
||||
synchronizer = sync_0_2_8
|
||||
synchronizer = sync_0_2_9
|
||||
{-# INLINE synchronizer #-}
|
||||
|
||||
sync_0_2_8 :: Synchronizer
|
||||
sync_0_2_8 = Synchronizer
|
||||
"0.2.8"
|
||||
sync_0_2_9 :: Synchronizer
|
||||
sync_0_2_9 = Synchronizer
|
||||
"0.2.9"
|
||||
[ syncCreateAgentTmp
|
||||
, syncCreateSshDir
|
||||
, syncRemoveAvahiSystemdDependency
|
||||
, syncInstallLibAvahi
|
||||
, syncInstallAppMgr
|
||||
, syncFullUpgrade
|
||||
, sync32BitKernel
|
||||
@@ -113,6 +115,7 @@ sync_0_2_8 = Synchronizer
|
||||
, syncInstallDuplicity
|
||||
, syncInstallExfatFuse
|
||||
, syncInstallExfatUtils
|
||||
, syncUpgradeTor
|
||||
, syncInstallAmbassadorUI
|
||||
, syncOpenHttpPorts
|
||||
, syncUpgradeLifeline
|
||||
@@ -240,6 +243,19 @@ syncInstallExfatUtils = SyncOp "Install exfat-utils" check migrate False
|
||||
shell "apt-get update"
|
||||
shell "apt-get install -y exfat-utils"
|
||||
|
||||
syncInstallLibAvahi :: SyncOp
|
||||
syncInstallLibAvahi = SyncOp "Install libavahi-client" check migrate False
|
||||
where
|
||||
check =
|
||||
liftIO
|
||||
$ (run (shell [i|dpkg -l|] $| shell [i|grep libavahi-client3|] $| conduit await) $> False)
|
||||
`catch` \(e :: ProcessException) -> case e of
|
||||
ProcessException _ (ExitFailure 1) -> pure True
|
||||
_ -> throwIO e
|
||||
migrate = liftIO . run $ do
|
||||
shell "apt-get update"
|
||||
shell "apt-get install -y libavahi-client3"
|
||||
|
||||
syncWriteConf :: Text -> ByteString -> SystemPath -> SyncOp
|
||||
syncWriteConf name contents' confLocation = SyncOp [i|Write #{name} Conf|] check migrate False
|
||||
where
|
||||
@@ -423,6 +439,7 @@ syncInstallAppMgr = SyncOp "Install AppMgr" check migrate False
|
||||
avs <- asks $ appMgrVersionSpec . appSettings
|
||||
av <- AppMgr.installNewAppMgr avs
|
||||
unless (av <|| avs) $ throwE $ AppMgrVersionE av avs
|
||||
postResetLanLogic -- to accommodate 0.2.x -> 0.2.9 where previous appmgr didn't correctly set up lan
|
||||
|
||||
syncUpgradeLifeline :: SyncOp
|
||||
syncUpgradeLifeline = SyncOp "Upgrade Lifeline" check migrate False
|
||||
@@ -480,7 +497,7 @@ replaceDerivativeCerts :: (HasFilesystemBase sig m, Fused.Has (Error S9Error) si
|
||||
replaceDerivativeCerts = do
|
||||
sid <- getStart9AgentHostname
|
||||
let hostname = sid <> ".local"
|
||||
tor <- getAgentHiddenServiceUrl
|
||||
torAddr <- getAgentHiddenServiceUrl
|
||||
|
||||
caKeyPath <- toS <$> getAbsoluteLocationFor rootCaKeyPath
|
||||
caConfPath <- toS <$> getAbsoluteLocationFor rootCaOpenSslConfPath
|
||||
@@ -531,7 +548,7 @@ replaceDerivativeCerts = do
|
||||
, duration = 365
|
||||
}
|
||||
hostname
|
||||
tor
|
||||
torAddr
|
||||
liftIO $ do
|
||||
putStrLn @Text "openssl logs"
|
||||
putStrLn @Text "exit code: "
|
||||
@@ -563,6 +580,21 @@ syncRestarterService = SyncOp "Install Restarter Service" check migrate True
|
||||
liftIO $ callCommand "systemctl enable restarter.service"
|
||||
liftIO $ callCommand "systemctl enable restarter.timer"
|
||||
|
||||
syncUpgradeTor :: SyncOp
|
||||
syncUpgradeTor = SyncOp "Install Tor 0.3.5.12-1" check migrate False
|
||||
where
|
||||
check =
|
||||
liftIO
|
||||
$ ( run (shell [i|dpkg -l|] $| shell [i|grep tor|] $| shell [i|grep 0.3.5.12-1|] $| conduit await)
|
||||
$> False
|
||||
)
|
||||
`catch` \(e :: ProcessException) -> case e of
|
||||
ProcessException _ (ExitFailure 1) -> pure True
|
||||
_ -> throwIO e
|
||||
migrate = liftIO . run $ do
|
||||
shell "apt-get update"
|
||||
shell "apt-get install -y tor=0.3.5.12-1"
|
||||
|
||||
failUpdate :: S9Error -> ExceptT Void (ReaderT AgentCtx IO) ()
|
||||
failUpdate e = do
|
||||
ref <- asks appIsUpdateFailed
|
||||
|
||||
@@ -7,9 +7,7 @@ import Network.HTTP.Client
|
||||
import Network.Connection
|
||||
|
||||
import Lib.SystemPaths
|
||||
import Network.HTTP.Client.TLS ( mkManagerSettings
|
||||
, newTlsManagerWith
|
||||
)
|
||||
import Network.HTTP.Client.TLS ( mkManagerSettings )
|
||||
import Data.Default
|
||||
|
||||
getAgentHiddenServiceUrl :: (HasFilesystemBase sig m, MonadIO m) => m Text
|
||||
|
||||
@@ -7,6 +7,10 @@ newtype TorAddress = TorAddress { unTorAddress :: Text } deriving (Eq)
|
||||
instance Show TorAddress where
|
||||
show = toS . unTorAddress
|
||||
|
||||
newtype LanAddress = LanAddress { unLanAddress :: Text } deriving (Eq)
|
||||
instance Show LanAddress where
|
||||
show = toS . unLanAddress
|
||||
|
||||
newtype LanIp = LanIp { unLanIp :: Text } deriving (Eq)
|
||||
instance Show LanIp where
|
||||
show = toS . unLanIp
|
||||
|
||||
@@ -45,6 +45,7 @@ import Handler.Backups
|
||||
import Handler.Hosts
|
||||
import Handler.Icons
|
||||
import Handler.Login
|
||||
import Handler.Network
|
||||
import Handler.Notifications
|
||||
import Handler.PasswordUpdate
|
||||
import Handler.PowerOff
|
||||
|
||||
67
agent/test/Lib/External/AppManifestSpec.hs
vendored
67
agent/test/Lib/External/AppManifestSpec.hs
vendored
@@ -66,12 +66,65 @@ assets:
|
||||
hidden-service-version: v3
|
||||
|]
|
||||
|
||||
mastodon330Manifest :: ByteString
|
||||
mastodon330Manifest = [i|
|
||||
---
|
||||
id: mastodon
|
||||
version: 3.3.0.1
|
||||
title: Mastodon
|
||||
description:
|
||||
short: "A free, open-source social network server."
|
||||
long: "Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub)!"
|
||||
release-notes: Added an acation to reset the admin password
|
||||
install-alert: "After starting mastodon for the first time, it can take a long time (several minutes) to be ready.\nPlease be patient. On future starts of the service, it will be faster, but still takes longer than other services.\nMake sure to sign up for a user before giving out your link. The first user to sign up is set as the admin user.\n"
|
||||
uninstall-alert: ~
|
||||
restore-alert: ~
|
||||
start-alert: "It may take several minutes after startup for this service to be ready for use.\n"
|
||||
has-instructions: true
|
||||
os-version-required: ">=0.2.8"
|
||||
os-version-recommended: ">=0.2.8"
|
||||
ports:
|
||||
- internal: 80
|
||||
tor: 80
|
||||
lan: standard
|
||||
- internal: 443
|
||||
tor: 443
|
||||
lan:
|
||||
custom:
|
||||
port: 443
|
||||
- internal: 3000
|
||||
tor: 3000
|
||||
lan: ~
|
||||
- internal: 4000
|
||||
tor: 4000
|
||||
lan: ~
|
||||
image:
|
||||
type: tar
|
||||
shm-size-mb: ~
|
||||
mount: /root/persistence
|
||||
public: ~
|
||||
shared: ~
|
||||
assets: []
|
||||
hidden-service-version: v3
|
||||
dependencies: {}
|
||||
actions:
|
||||
- id: reset-admin-password
|
||||
name: Reset Admin Password
|
||||
description: This action will reset your admin password to a random value
|
||||
allowed-statuses:
|
||||
- RUNNING
|
||||
command:
|
||||
- docker_entrypoint.sh
|
||||
- reset_admin_password.sh
|
||||
|]
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = do
|
||||
describe "parsing app manifest ports" $ do
|
||||
it "should yield true for cups 0.2.3" $ do
|
||||
res <- decodeThrow @IO @(AppManifest 0) cups023Manifest
|
||||
uiAvailable res `shouldBe` True
|
||||
it "should yield false for cups 0.2.3 Mod" $ do
|
||||
res <- decodeThrow @IO @(AppManifest 0) cups023ManifestModNoUI
|
||||
uiAvailable res `shouldBe` False
|
||||
describe "parsing app manifest ports" $ do
|
||||
it "should parse mastodon 3.3.0" $ do
|
||||
res <- decodeThrow @IO @AppManifest mastodon330Manifest
|
||||
print res
|
||||
lanUiAvailable res `shouldBe` True
|
||||
torUiAvailable res `shouldBe` True
|
||||
|
||||
|
||||
1070
appmgr/Cargo.lock
generated
1070
appmgr/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
authors = ["Aiden McClelland <me@drbonez.dev>"]
|
||||
edition = "2018"
|
||||
name = "appmgr"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
|
||||
[lib]
|
||||
name = "appmgrlib"
|
||||
@@ -13,23 +13,27 @@ name = "appmgr"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
avahi = ["avahi-sys"]
|
||||
default = ["avahi"]
|
||||
portable = []
|
||||
production = []
|
||||
|
||||
[dependencies]
|
||||
argonautica = "0.2.0"
|
||||
async-trait = "0.1.42"
|
||||
avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", branch = "feature/dynamic-linking", features = ["dynamic"], optional = true }
|
||||
base32 = "0.4.0"
|
||||
clap = "2.33"
|
||||
ctrlc = "3.1.7"
|
||||
ed25519-dalek = "1.0.1"
|
||||
emver = { version = "0.1.0", features = ["serde"] }
|
||||
failure = "0.1.8"
|
||||
file-lock = "1.1"
|
||||
futures = "0.3.8"
|
||||
git-version = "0.3.4"
|
||||
http = "0.2.3"
|
||||
itertools = "0.9.0"
|
||||
lazy_static = "1.4"
|
||||
libc = "0.2.86"
|
||||
linear-map = { version = "1.2", features = ["serde_impl"] }
|
||||
log = "0.4.11"
|
||||
nix = "0.19.1"
|
||||
@@ -41,6 +45,8 @@ rand = "0.7.3"
|
||||
regex = "1.4.2"
|
||||
reqwest = { version = "0.10.9", features = ["stream", "json"] }
|
||||
rpassword = "5.0.0"
|
||||
rust-argon2 = "0.8.3"
|
||||
scopeguard = "1.1" # because avahi-sys fucks your shit up
|
||||
serde = { version = "1.0.118", features = ["derive", "rc"] }
|
||||
serde_cbor = "0.11.1"
|
||||
serde_json = "1.0.59"
|
||||
@@ -49,3 +55,4 @@ simple-logging = "2.0"
|
||||
tokio = { version = "0.3.5", features = ["full"] }
|
||||
tokio-compat-02 = "0.1.2"
|
||||
tokio-tar = { version = "0.3.0", git = "https://github.com/dr-bonez/tokio-tar.git", rev = "1ba710f3" }
|
||||
yajrc = { version = "0.1.0", git = "https://github.com/dr-bonez/yajrc", rev = "c2952a4a21c50f7be6f8003afa37ee77deb66d56" }
|
||||
|
||||
@@ -6,4 +6,4 @@ shopt -s expand_aliases
|
||||
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
|
||||
|
||||
cd ..
|
||||
rust-arm-builder sh -c "(cd appmgr && cargo build --release)"
|
||||
rust-arm-builder sh -c "(cd appmgr && cargo build)"
|
||||
|
||||
116
appmgr/src/actions.rs
Normal file
116
appmgr/src/actions.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::process::Stdio;
|
||||
|
||||
use linear_map::set::LinearSet;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, Error as IoError};
|
||||
use yajrc::RpcError;
|
||||
|
||||
use crate::apps::DockerStatus;
|
||||
|
||||
pub const STATUS_NOT_ALLOWED: i32 = -2;
|
||||
pub const INVALID_COMMAND: i32 = -3;
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Action {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub warning: Option<String>,
|
||||
pub allowed_statuses: LinearSet<DockerStatus>,
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
||||
async fn tee<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
|
||||
mut r: R,
|
||||
mut w: W,
|
||||
) -> Result<Vec<u8>, IoError> {
|
||||
let mut res = Vec::new();
|
||||
let mut buf = vec![0; 2048];
|
||||
let mut bytes;
|
||||
while {
|
||||
bytes = r.read(&mut buf).await?;
|
||||
bytes != 0
|
||||
} {
|
||||
res.extend_from_slice(&buf[..bytes]);
|
||||
w.write_all(&buf[..bytes]).await?;
|
||||
}
|
||||
w.flush().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub async fn perform(&self, app_id: &str) -> Result<String, RpcError> {
|
||||
let man = crate::apps::manifest(app_id)
|
||||
.await
|
||||
.map_err(failure::Error::from)
|
||||
.map_err(failure::Error::compat)?;
|
||||
let status = crate::apps::status(app_id, true)
|
||||
.await
|
||||
.map_err(failure::Error::from)
|
||||
.map_err(failure::Error::compat)?
|
||||
.status;
|
||||
if !self.allowed_statuses.contains(&status) {
|
||||
return Err(RpcError {
|
||||
code: STATUS_NOT_ALLOWED,
|
||||
message: format!(
|
||||
"{} is in status {:?} which is not allowed by {}",
|
||||
app_id, status, self.id
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
let mut cmd = if status == DockerStatus::Running {
|
||||
let mut cmd = tokio::process::Command::new("docker");
|
||||
cmd.arg("exec").arg(&app_id).args(&self.command);
|
||||
cmd
|
||||
} else {
|
||||
let mut cmd = tokio::process::Command::new("docker");
|
||||
let entrypoint = self.command.get(0).ok_or_else(|| RpcError {
|
||||
code: INVALID_COMMAND,
|
||||
message: "Command Cannot Be Empty".to_owned(),
|
||||
data: None,
|
||||
})?;
|
||||
cmd.arg("run")
|
||||
.arg("--rm")
|
||||
.arg("--name")
|
||||
.arg(format!("{}_{}", app_id, self.id))
|
||||
.arg("--mount")
|
||||
.arg(format!(
|
||||
"type=bind,src={}/{},dst={}",
|
||||
crate::VOLUMES,
|
||||
app_id,
|
||||
man.mount.display()
|
||||
))
|
||||
.arg("--entrypoint")
|
||||
.arg(entrypoint)
|
||||
.arg(format!("start9/{}", app_id))
|
||||
.args(&self.command[1..]);
|
||||
// TODO: 0.3.0: net, tor, shm
|
||||
cmd
|
||||
};
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
let mut child = cmd.spawn()?;
|
||||
|
||||
let (stdout, stderr) = futures::try_join!(
|
||||
tee(child.stdout.take().unwrap(), tokio::io::sink()),
|
||||
tee(child.stderr.take().unwrap(), tokio::io::sink())
|
||||
)?;
|
||||
|
||||
let status = child.wait().await?;
|
||||
if status.success() {
|
||||
String::from_utf8(stdout).map_err(From::from)
|
||||
} else {
|
||||
Err(RpcError {
|
||||
code: status
|
||||
.code()
|
||||
.unwrap_or_else(|| status.signal().unwrap_or(0) + 128),
|
||||
message: String::from_utf8(stderr)?,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::path::Path;
|
||||
|
||||
use argonautica::{Hasher, Verifier};
|
||||
use argon2::Config;
|
||||
use emver::Version;
|
||||
use futures::try_join;
|
||||
use futures::TryStreamExt;
|
||||
use rand::Rng;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::util::from_yaml_async_reader;
|
||||
@@ -46,10 +47,7 @@ pub async fn create_backup<P: AsRef<Path>>(
|
||||
let mut hash = String::new();
|
||||
f.read_to_string(&mut hash).await?;
|
||||
crate::ensure_code!(
|
||||
Verifier::new()
|
||||
.with_password(password)
|
||||
.with_hash(hash)
|
||||
.verify()
|
||||
argon2::verify_encoded(&hash, password.as_bytes())
|
||||
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
|
||||
crate::error::INVALID_BACKUP_PASSWORD,
|
||||
"Invalid Backup Decryption Password"
|
||||
@@ -58,10 +56,8 @@ pub async fn create_backup<P: AsRef<Path>>(
|
||||
{
|
||||
// save password
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
let mut hasher = Hasher::default();
|
||||
hasher.opt_out_of_secret_key(true);
|
||||
let hash = hasher.with_password(password).hash().no_code()?;
|
||||
let salt = rand::thread_rng().gen::<[u8; 32]>();
|
||||
let hash = argon2::hash_encoded(password.as_bytes(), &salt, &Config::default()).unwrap(); // this is safe because apparently the API was poorly designed
|
||||
let mut f = tokio::fs::File::create(pw_path).await?;
|
||||
f.write_all(hash.as_bytes()).await?;
|
||||
f.flush().await?;
|
||||
@@ -160,10 +156,7 @@ pub async fn restore_backup<P: AsRef<Path>>(
|
||||
let mut hash = String::new();
|
||||
f.read_to_string(&mut hash).await?;
|
||||
crate::ensure_code!(
|
||||
Verifier::new()
|
||||
.with_password(password)
|
||||
.with_hash(hash)
|
||||
.verify()
|
||||
argon2::verify_encoded(&hash, password.as_bytes())
|
||||
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
|
||||
crate::error::INVALID_BACKUP_PASSWORD,
|
||||
"Invalid Backup Decryption Password"
|
||||
|
||||
10
appmgr/src/cert-local.csr.conf.template
Normal file
10
appmgr/src/cert-local.csr.conf.template
Normal file
@@ -0,0 +1,10 @@
|
||||
[req]
|
||||
default_bits = 4096
|
||||
default_md = sha256
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
CN = {hostname}.local
|
||||
O = Start9 Labs
|
||||
OU = Embassy
|
||||
@@ -1864,6 +1864,9 @@ mod test {
|
||||
hidden_service_version: crate::tor::HiddenServiceVersion::V3,
|
||||
dependencies: deps,
|
||||
extra: LinearMap::new(),
|
||||
install_alert: None,
|
||||
restore_alert: None,
|
||||
uninstall_alert: None,
|
||||
})
|
||||
.unwrap();
|
||||
let config = spec
|
||||
|
||||
@@ -256,6 +256,19 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
|
||||
"Package Name Does Not Match Expected"
|
||||
);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Creating metadata directory: {}/apps/{}",
|
||||
crate::PERSISTENCE_DIR,
|
||||
manifest.id
|
||||
);
|
||||
let app_dir = PersistencePath::from_ref("apps").join(&manifest.id);
|
||||
let app_dir_path = app_dir.path();
|
||||
if app_dir_path.exists() {
|
||||
tokio::fs::remove_dir_all(&app_dir_path).await?;
|
||||
}
|
||||
tokio::fs::create_dir_all(&app_dir_path).await?;
|
||||
|
||||
let (ip, tor_addr, tor_key) = crate::tor::set_svc(
|
||||
&manifest.id,
|
||||
crate::tor::NewService {
|
||||
@@ -270,12 +283,6 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
|
||||
log::info!("Creating volume {}/{}.", crate::VOLUMES, manifest.id);
|
||||
tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id)).await?;
|
||||
|
||||
let app_dir = PersistencePath::from_ref("apps").join(&manifest.id);
|
||||
let app_dir_path = app_dir.path();
|
||||
if app_dir_path.exists() {
|
||||
tokio::fs::remove_dir_all(&app_dir_path).await?;
|
||||
}
|
||||
tokio::fs::create_dir_all(&app_dir_path).await?;
|
||||
let _lock = app_dir.lock(true).await?;
|
||||
log::info!("Saving manifest.");
|
||||
let mut manifest_out = app_dir.join("manifest.yaml").write(None).await?;
|
||||
|
||||
93
appmgr/src/lan.rs
Normal file
93
appmgr/src/lan.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use crate::Error;
|
||||
use avahi_sys;
|
||||
use futures::future::pending;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct AppId {
|
||||
pub un_app_id: String,
|
||||
}
|
||||
|
||||
pub async fn enable_lan() -> Result<(), Error> {
|
||||
unsafe {
|
||||
let app_list = crate::apps::list_info().await?;
|
||||
|
||||
let simple_poll = avahi_sys::avahi_simple_poll_new();
|
||||
let poll = avahi_sys::avahi_simple_poll_get(simple_poll);
|
||||
let mut stack_err = 0;
|
||||
let err_c: *mut i32 = &mut stack_err;
|
||||
let avahi_client = avahi_sys::avahi_client_new(
|
||||
poll,
|
||||
avahi_sys::AvahiClientFlags::AVAHI_CLIENT_NO_FAIL,
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
err_c,
|
||||
);
|
||||
let group =
|
||||
avahi_sys::avahi_entry_group_new(avahi_client, Some(noop), std::ptr::null_mut());
|
||||
let hostname_raw = avahi_sys::avahi_client_get_host_name_fqdn(avahi_client);
|
||||
let hostname_bytes = std::ffi::CStr::from_ptr(hostname_raw).to_bytes_with_nul();
|
||||
const HOSTNAME_LEN: usize = 1 + 15 + 1 + 5; // leading byte, main address, dot, "local"
|
||||
debug_assert_eq!(hostname_bytes.len(), HOSTNAME_LEN);
|
||||
let mut hostname_buf = [0; HOSTNAME_LEN + 1];
|
||||
hostname_buf[1..].copy_from_slice(hostname_bytes);
|
||||
// assume fixed length prefix on hostname due to local address
|
||||
hostname_buf[0] = 15; // set the prefix length to 15 for the main address
|
||||
hostname_buf[16] = 5; // set the prefix length to 5 for "local"
|
||||
|
||||
for (app_id, app_info) in app_list {
|
||||
let man = crate::apps::manifest(&app_id).await?;
|
||||
if man
|
||||
.ports
|
||||
.iter()
|
||||
.filter(|p| p.lan.is_some())
|
||||
.next()
|
||||
.is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let tor_address = if let Some(addr) = app_info.tor_address {
|
||||
addr
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let lan_address = tor_address
|
||||
.strip_suffix(".onion")
|
||||
.ok_or_else(|| failure::format_err!("Invalid Tor Address: {:?}", tor_address))?
|
||||
.to_owned()
|
||||
+ ".local";
|
||||
let lan_address_ptr = std::ffi::CString::new(lan_address)
|
||||
.expect("Could not cast lan address to c string");
|
||||
let _ = avahi_sys::avahi_entry_group_add_record(
|
||||
group,
|
||||
avahi_sys::AVAHI_IF_UNSPEC,
|
||||
avahi_sys::AVAHI_PROTO_UNSPEC,
|
||||
avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST
|
||||
| avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_ALLOW_MULTIPLE,
|
||||
lan_address_ptr.as_ptr(),
|
||||
avahi_sys::AVAHI_DNS_CLASS_IN as u16,
|
||||
avahi_sys::AVAHI_DNS_TYPE_CNAME as u16,
|
||||
avahi_sys::AVAHI_DEFAULT_TTL,
|
||||
hostname_buf.as_ptr().cast(),
|
||||
hostname_buf.len(),
|
||||
);
|
||||
log::info!("Published {:?}", lan_address_ptr);
|
||||
}
|
||||
avahi_sys::avahi_entry_group_commit(group);
|
||||
ctrlc::set_handler(move || {
|
||||
// please the borrow checker with the below semantics
|
||||
// avahi_sys::avahi_entry_group_free(group);
|
||||
// avahi_sys::avahi_client_free(avahi_client);
|
||||
// drop(Box::from_raw(err_c));
|
||||
std::process::exit(0);
|
||||
})
|
||||
.expect("Error setting signal handler");
|
||||
}
|
||||
pending().await
|
||||
}
|
||||
|
||||
unsafe extern "C" fn noop(
|
||||
_group: *mut avahi_sys::AvahiEntryGroup,
|
||||
_state: avahi_sys::AvahiEntryGroupState,
|
||||
_userdata: *mut core::ffi::c_void,
|
||||
) {
|
||||
}
|
||||
@@ -20,6 +20,7 @@ lazy_static::lazy_static! {
|
||||
pub static ref QUIET: tokio::sync::RwLock<bool> = tokio::sync::RwLock::new(!std::env::var("APPMGR_QUIET").map(|a| a == "0").unwrap_or(true));
|
||||
}
|
||||
|
||||
pub mod actions;
|
||||
pub mod apps;
|
||||
pub mod backup;
|
||||
pub mod config;
|
||||
@@ -30,6 +31,8 @@ pub mod error;
|
||||
pub mod index;
|
||||
pub mod inspect;
|
||||
pub mod install;
|
||||
#[cfg(feature = "avahi")]
|
||||
pub mod lan;
|
||||
pub mod logs;
|
||||
pub mod manifest;
|
||||
pub mod pack;
|
||||
|
||||
@@ -162,6 +162,17 @@ async fn inner_main() -> Result<(), Error> {
|
||||
),
|
||||
);
|
||||
|
||||
#[cfg(feature = "avahi")]
|
||||
#[allow(unused_mut)]
|
||||
let mut app = app.subcommand(
|
||||
SubCommand::with_name("lan")
|
||||
.about("Configures LAN services")
|
||||
.subcommand(
|
||||
SubCommand::with_name("enable")
|
||||
.about("Publishes the LAN addresses for all services"),
|
||||
),
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "portable"))]
|
||||
let mut app = app
|
||||
.subcommand(
|
||||
@@ -399,7 +410,6 @@ async fn inner_main() -> Result<(), Error> {
|
||||
.about("Removes an installed app")
|
||||
.arg(
|
||||
Arg::with_name("purge")
|
||||
.short("p")
|
||||
.long("purge")
|
||||
.help("Deletes all application data"),
|
||||
)
|
||||
@@ -820,6 +830,16 @@ async fn inner_main() -> Result<(), Error> {
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("repair-app-status").about("Restarts crashed apps"), // TODO: remove
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("actions")
|
||||
.about("Perform an action for a service")
|
||||
.arg(
|
||||
Arg::with_name("SERVICE")
|
||||
.help("ID of the service to perform an action on")
|
||||
.required(true),
|
||||
)
|
||||
.arg(Arg::with_name("ACTION").help("ID of the action to perform")),
|
||||
);
|
||||
|
||||
let matches = app.clone().get_matches();
|
||||
@@ -1178,6 +1198,15 @@ async fn inner_main() -> Result<(), Error> {
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "avahi")]
|
||||
#[cfg(not(feature = "portable"))]
|
||||
("lan", Some(sub_m)) => match sub_m.subcommand() {
|
||||
("enable", _) => crate::lan::enable_lan().await?,
|
||||
_ => {
|
||||
println!("{}", sub_m.usage());
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
#[cfg(not(feature = "portable"))]
|
||||
("info", Some(sub_m)) => {
|
||||
let name = sub_m.value_of("ID").unwrap();
|
||||
@@ -1547,6 +1576,34 @@ async fn inner_main() -> Result<(), Error> {
|
||||
("repair-app-status", _) => {
|
||||
control::repair_app_status().await?;
|
||||
}
|
||||
#[cfg(not(feature = "portable"))]
|
||||
("actions", Some(sub_m)) => {
|
||||
use yajrc::{GenericRpcMethod, RpcResponse};
|
||||
|
||||
let man = apps::manifest(sub_m.value_of("SERVICE").unwrap()).await?;
|
||||
let action_id = sub_m.value_of("ACTION").unwrap();
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&RpcResponse::<GenericRpcMethod>::from_result(
|
||||
man.actions
|
||||
.iter()
|
||||
.filter(|a| &a.id == &action_id)
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
failure::format_err!(
|
||||
"action {} does not exist for {}",
|
||||
action_id,
|
||||
man.id
|
||||
)
|
||||
})
|
||||
.with_code(error::NOT_FOUND)?
|
||||
.perform(&man.id)
|
||||
.await
|
||||
.map(serde_json::Value::String)
|
||||
))
|
||||
.with_code(error::SERDE_ERROR)?
|
||||
)
|
||||
}
|
||||
("pack", Some(sub_m)) => {
|
||||
pack(
|
||||
sub_m.value_of("PATH").unwrap(),
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use linear_map::LinearMap;
|
||||
|
||||
use crate::actions::Action;
|
||||
use crate::dependencies::Dependencies;
|
||||
use crate::tor::HiddenServiceVersion;
|
||||
use crate::tor::PortMapping;
|
||||
@@ -43,6 +44,8 @@ pub struct ManifestV0 {
|
||||
#[serde(default)]
|
||||
pub restore_alert: Option<String>,
|
||||
#[serde(default)]
|
||||
pub start_alert: Option<String>,
|
||||
#[serde(default)]
|
||||
pub has_instructions: bool,
|
||||
#[serde(default = "emver::VersionRange::any")]
|
||||
pub os_version_required: emver::VersionRange,
|
||||
@@ -63,6 +66,8 @@ pub struct ManifestV0 {
|
||||
pub hidden_service_version: HiddenServiceVersion,
|
||||
#[serde(default)]
|
||||
pub dependencies: Dependencies,
|
||||
#[serde(default)]
|
||||
pub actions: Vec<Action>,
|
||||
#[serde(flatten)]
|
||||
pub extra: LinearMap<String, serde_yaml::Value>,
|
||||
}
|
||||
|
||||
18
appmgr/src/nginx-standard.conf.template
Normal file
18
appmgr/src/nginx-standard.conf.template
Normal file
@@ -0,0 +1,18 @@
|
||||
server {{
|
||||
listen 443 ssl;
|
||||
server_name {hostname}.local;
|
||||
ssl_certificate /root/appmgr/apps/{app_id}/cert-local.fullchain.crt.pem;
|
||||
ssl_certificate_key /root/appmgr/apps/{app_id}/cert-local.key.pem;
|
||||
location / {{
|
||||
proxy_pass http://{app_ip}:{internal_port}/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}}
|
||||
}}
|
||||
server {{
|
||||
listen 80;
|
||||
server_name {hostname}.local;
|
||||
return 301 https://$host$request_uri;
|
||||
}}
|
||||
8
appmgr/src/nginx.conf.template
Normal file
8
appmgr/src/nginx.conf.template
Normal file
@@ -0,0 +1,8 @@
|
||||
server {{
|
||||
listen {port};
|
||||
server_name {hostname}.local;
|
||||
location / {{
|
||||
proxy_pass http://{app_ip}:{internal_port}/;
|
||||
proxy_set_header Host $host;
|
||||
}}
|
||||
}}
|
||||
@@ -217,6 +217,13 @@ pub async fn verify(path: &str) -> Result<(), failure::Error> {
|
||||
if let Some(shared) = &manifest.shared {
|
||||
validate_path(shared)?;
|
||||
}
|
||||
for action in &manifest.actions {
|
||||
ensure!(
|
||||
!action.command.is_empty(),
|
||||
"Command Cannot Be Empty: {}",
|
||||
action.id
|
||||
);
|
||||
}
|
||||
log::info!("Opening config spec from archive.");
|
||||
let config_spec = entries
|
||||
.next()
|
||||
|
||||
@@ -8,17 +8,62 @@ use failure::ResultExt as _;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::util::{PersistencePath, YamlUpdateHandle};
|
||||
use crate::util::{Invoke, PersistencePath, YamlUpdateHandle};
|
||||
use crate::{Error, ResultExt as _};
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LanOptions {
|
||||
Standard,
|
||||
Custom { port: u16 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize)]
|
||||
pub struct PortMapping {
|
||||
pub internal: u16,
|
||||
pub tor: u16,
|
||||
pub lan: Option<LanOptions>, // only for http interfaces
|
||||
}
|
||||
impl<'de> serde::de::Deserialize<'de> for PortMapping {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct PortMappingIF {
|
||||
pub internal: u16,
|
||||
pub tor: u16,
|
||||
#[serde(default, deserialize_with = "deserialize_some")]
|
||||
pub lan: Option<Option<LanOptions>>,
|
||||
}
|
||||
|
||||
fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
T: serde::de::Deserialize<'de>,
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
serde::de::Deserialize::deserialize(deserializer).map(Some)
|
||||
}
|
||||
|
||||
let input_format: PortMappingIF = serde::de::Deserialize::deserialize(deserializer)?;
|
||||
Ok(PortMapping {
|
||||
internal: input_format.internal,
|
||||
tor: input_format.tor,
|
||||
lan: if let Some(lan) = input_format.lan {
|
||||
lan
|
||||
} else if input_format.tor == 80 {
|
||||
Some(LanOptions::Standard)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc";
|
||||
pub const HIDDEN_SERVICE_DIR_ROOT: &'static str = "/var/lib/tor";
|
||||
pub const ETC_HOSTNAME: &'static str = "/etc/hostname";
|
||||
pub const ETC_NGINX_SERVICES_CONF: &'static str = "/etc/nginx/sites-available/start9-services.conf";
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -179,6 +224,167 @@ pub async fn write_services(hidden_services: &ServicesMap) -> Result<(), Error>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Error> {
|
||||
let mut f = tokio::fs::File::create(ETC_NGINX_SERVICES_CONF).await?;
|
||||
for (app_id, service) in &hidden_services.map {
|
||||
let hostname = tokio::fs::read_to_string(
|
||||
Path::new(HIDDEN_SERVICE_DIR_ROOT)
|
||||
.join(format!("app-{}", app_id))
|
||||
.join("hostname"),
|
||||
)
|
||||
.await
|
||||
.with_context(|e| format!("{}/app-{}/hostname: {}", HIDDEN_SERVICE_DIR_ROOT, app_id, e))
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||
let hostname_str = hostname
|
||||
.trim()
|
||||
.strip_suffix(".onion")
|
||||
.ok_or_else(|| failure::format_err!("invalid tor hostname"))
|
||||
.no_code()?;
|
||||
for mapping in &service.ports {
|
||||
match &mapping.lan {
|
||||
Some(LanOptions::Standard) => {
|
||||
log::info!("Writing LAN certificates for {}", app_id);
|
||||
let base_path = PersistencePath::from_ref("apps").join(&app_id);
|
||||
let key_path = base_path.join("cert-local.key.pem").path();
|
||||
if tokio::fs::metadata(&key_path).await.is_err() {
|
||||
tokio::process::Command::new("openssl")
|
||||
.arg("ecparam")
|
||||
.arg("-genkey")
|
||||
.arg("-name")
|
||||
.arg("prime256v1")
|
||||
.arg("-noout")
|
||||
.arg("-out")
|
||||
.arg(&key_path)
|
||||
.invoke("OpenSSL GenKey")
|
||||
.await?;
|
||||
}
|
||||
let conf_path = base_path.join("cert-local.csr.conf").path();
|
||||
if tokio::fs::metadata(&conf_path).await.is_err() {
|
||||
tokio::fs::write(
|
||||
&conf_path,
|
||||
format!(
|
||||
include_str!("cert-local.csr.conf.template"),
|
||||
hostname = hostname_str
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let req_path = base_path.join("cert-local.csr").path();
|
||||
if tokio::fs::metadata(&req_path).await.is_err() {
|
||||
tokio::process::Command::new("openssl")
|
||||
.arg("req")
|
||||
.arg("-config")
|
||||
.arg(&conf_path)
|
||||
.arg("-key")
|
||||
.arg(&key_path)
|
||||
.arg("-new")
|
||||
.arg("-addext")
|
||||
.arg(format!(
|
||||
"subjectAltName=DNS:{hostname}.local",
|
||||
hostname = hostname_str
|
||||
))
|
||||
.arg("-out")
|
||||
.arg(&req_path)
|
||||
.invoke("OpenSSL Req")
|
||||
.await?;
|
||||
}
|
||||
let cert_path = base_path.join("cert-local.crt.pem").path();
|
||||
if tokio::fs::metadata(&cert_path).await.is_err() {
|
||||
tokio::process::Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-batch")
|
||||
.arg("-config")
|
||||
.arg("/root/agent/ca/intermediate/openssl.conf")
|
||||
.arg("-rand_serial")
|
||||
.arg("-keyfile")
|
||||
.arg("/root/agent/ca/intermediate/private/embassy-int-ca.key.pem")
|
||||
.arg("-cert")
|
||||
.arg("/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem")
|
||||
.arg("-extensions")
|
||||
.arg("server_cert")
|
||||
.arg("-days")
|
||||
.arg("365")
|
||||
.arg("-notext")
|
||||
.arg("-in")
|
||||
.arg(&req_path)
|
||||
.arg("-out")
|
||||
.arg(&cert_path)
|
||||
.invoke("OpenSSL CA")
|
||||
.await?;
|
||||
}
|
||||
let fullchain_path = base_path.join("cert-local.fullchain.crt.pem");
|
||||
if !fullchain_path.exists().await {
|
||||
log::info!("Writing fullchain to: {}", fullchain_path.path().display());
|
||||
let mut fullchain_file = fullchain_path.write(None).await?;
|
||||
tokio::io::copy(
|
||||
&mut tokio::fs::File::open(&cert_path).await?,
|
||||
&mut *fullchain_file,
|
||||
)
|
||||
.await?;
|
||||
tokio::io::copy(
|
||||
&mut tokio::fs::File::open(
|
||||
"/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem",
|
||||
)
|
||||
.await
|
||||
.with_context(|e| {
|
||||
format!(
|
||||
"{}: /root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem",
|
||||
e
|
||||
)
|
||||
})
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?,
|
||||
&mut *fullchain_file,
|
||||
)
|
||||
.await?;
|
||||
tokio::io::copy(
|
||||
&mut tokio::fs::File::open(
|
||||
"/root/agent/ca/certs/embassy-root-ca.cert.pem",
|
||||
)
|
||||
.await
|
||||
.with_context(|e| {
|
||||
format!("{}: /root/agent/ca/certs/embassy-root-ca.cert.pem", e)
|
||||
})
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?,
|
||||
&mut *fullchain_file,
|
||||
)
|
||||
.await?;
|
||||
fullchain_file.commit().await?;
|
||||
log::info!("{} written successfully", fullchain_path.path().display());
|
||||
}
|
||||
f.write_all(
|
||||
format!(
|
||||
include_str!("nginx-standard.conf.template"),
|
||||
hostname = hostname_str,
|
||||
app_ip = service.ip,
|
||||
internal_port = mapping.internal,
|
||||
app_id = app_id,
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
f.sync_all().await?;
|
||||
}
|
||||
Some(LanOptions::Custom { port }) => {
|
||||
f.write_all(
|
||||
format!(
|
||||
include_str!("nginx.conf.template"),
|
||||
hostname = hostname_str,
|
||||
app_ip = service.ip,
|
||||
port = port,
|
||||
internal_port = mapping.internal,
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_tor_address(name: &str, timeout: Option<Duration>) -> Result<String, Error> {
|
||||
log::info!("Retrieving Tor hidden service address for {}.", name);
|
||||
let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT)
|
||||
@@ -287,8 +493,8 @@ pub async fn set_svc(
|
||||
Err(e)
|
||||
}
|
||||
})?;
|
||||
#[cfg(target_os = "linux")]
|
||||
nix::unistd::sync();
|
||||
hidden_services.commit().await?;
|
||||
log::info!("Reloading Tor.");
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["tor", "reload"])
|
||||
@@ -302,19 +508,32 @@ pub async fn set_svc(
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
Ok((
|
||||
ip,
|
||||
if is_listening {
|
||||
Some(read_tor_address(name, Some(Duration::from_secs(30))).await?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if is_listening {
|
||||
Some(read_tor_key(name, ver, Some(Duration::from_secs(30))).await?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
))
|
||||
let addr = if is_listening {
|
||||
Some(read_tor_address(name, Some(Duration::from_secs(30))).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let key = if is_listening {
|
||||
Some(read_tor_key(name, ver, Some(Duration::from_secs(30))).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
write_lan_services(&hidden_services).await?;
|
||||
log::info!("Reloading Nginx.");
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["nginx", "reload"])
|
||||
.status()?;
|
||||
crate::ensure_code!(
|
||||
svc_exit.success(),
|
||||
crate::error::GENERAL_ERROR,
|
||||
"Failed to Reload Nginx: {}",
|
||||
svc_exit
|
||||
.code()
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
hidden_services.commit().await?;
|
||||
Ok((ip, addr, key))
|
||||
}
|
||||
|
||||
pub async fn rm_svc(name: &str) -> Result<(), Error> {
|
||||
@@ -333,7 +552,6 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> {
|
||||
}
|
||||
log::info!("Removing Tor hidden service {} from {}.", name, ETC_TOR_RC);
|
||||
write_services(&hidden_services).await?;
|
||||
hidden_services.commit().await?;
|
||||
log::info!("Reloading Tor.");
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["tor", "reload"])
|
||||
@@ -344,6 +562,21 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> {
|
||||
"Failed to Reload Tor: {}",
|
||||
svc_exit.code().unwrap_or(0)
|
||||
);
|
||||
write_lan_services(&hidden_services).await?;
|
||||
log::info!("Reloading Nginx.");
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["nginx", "reload"])
|
||||
.status()?;
|
||||
crate::ensure_code!(
|
||||
svc_exit.success(),
|
||||
crate::error::GENERAL_ERROR,
|
||||
"Failed to Reload Nginx: {}",
|
||||
svc_exit
|
||||
.code()
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
hidden_services.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ impl PersistenceFile {
|
||||
if let Some(mut file) = self.file.take() {
|
||||
file.flush().await?;
|
||||
file.shutdown().await?;
|
||||
file.sync_all().await?;
|
||||
drop(file);
|
||||
}
|
||||
if let Some(path) = self.needs_commit.take() {
|
||||
@@ -156,10 +157,6 @@ impl PersistenceFile {
|
||||
.await
|
||||
.with_context(|e| format!("{}.lock: {}", path.path().display(), e))
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||
tokio::fs::remove_file(format!("{}.lock", path.path().display()))
|
||||
.await
|
||||
.with_context(|e| format!("{}.lock: {}", path.path().display(), e))
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -24,8 +24,9 @@ mod v0_2_5;
|
||||
mod v0_2_6;
|
||||
mod v0_2_7;
|
||||
mod v0_2_8;
|
||||
mod v0_2_9;
|
||||
|
||||
pub use v0_2_8::Version as Current;
|
||||
pub use v0_2_9::Version as Current;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
@@ -46,6 +47,7 @@ enum Version {
|
||||
V0_2_6(Wrapper<v0_2_6::Version>),
|
||||
V0_2_7(Wrapper<v0_2_7::Version>),
|
||||
V0_2_8(Wrapper<v0_2_8::Version>),
|
||||
V0_2_9(Wrapper<v0_2_9::Version>),
|
||||
Other(emver::Version),
|
||||
}
|
||||
|
||||
@@ -156,6 +158,7 @@ pub async fn init() -> Result<(), failure::Error> {
|
||||
Version::V0_2_6(v) => v.0.migrate_to(&Current::new()).await?,
|
||||
Version::V0_2_7(v) => v.0.migrate_to(&Current::new()).await?,
|
||||
Version::V0_2_8(v) => v.0.migrate_to(&Current::new()).await?,
|
||||
Version::V0_2_9(v) => v.0.migrate_to(&Current::new()).await?,
|
||||
Version::Other(_) => (),
|
||||
// TODO find some way to automate this?
|
||||
}
|
||||
@@ -172,7 +175,8 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
|
||||
.collect();
|
||||
let url = format!("{}/appmgr?spec={}", &*crate::SYS_REGISTRY_URL, req_str);
|
||||
log::info!("Fetching new version from {}", url);
|
||||
let response = reqwest::get(&url).compat()
|
||||
let response = reqwest::get(&url)
|
||||
.compat()
|
||||
.await
|
||||
.with_code(crate::error::NETWORK_ERROR)?
|
||||
.error_for_status()
|
||||
@@ -244,6 +248,7 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
|
||||
Version::V0_2_6(v) => Current::new().migrate_to(&v.0).await?,
|
||||
Version::V0_2_7(v) => Current::new().migrate_to(&v.0).await?,
|
||||
Version::V0_2_8(v) => Current::new().migrate_to(&v.0).await?,
|
||||
Version::V0_2_9(v) => Current::new().migrate_to(&v.0).await?,
|
||||
Version::Other(_) => (),
|
||||
// TODO find some way to automate this?
|
||||
};
|
||||
|
||||
75
appmgr/src/version/v0_2_9.rs
Normal file
75
appmgr/src/version/v0_2_9.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
const V0_2_9: emver::Version = emver::Version::new(0, 2, 9, 0);
|
||||
|
||||
pub struct Version;
|
||||
#[async_trait]
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_2_8::Version;
|
||||
fn new() -> Self {
|
||||
Version
|
||||
}
|
||||
fn semver(&self) -> &'static emver::Version {
|
||||
&V0_2_9
|
||||
}
|
||||
async fn up(&self) -> Result<(), Error> {
|
||||
crate::tor::write_lan_services(
|
||||
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::os::unix::symlink(
|
||||
crate::tor::ETC_NGINX_SERVICES_CONF,
|
||||
"/etc/nginx/sites-enabled/start9-services.conf",
|
||||
)
|
||||
.await
|
||||
.or_else(|e| {
|
||||
if e.kind() == std::io::ErrorKind::AlreadyExists {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})?;
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["nginx", "reload"])
|
||||
.status()?;
|
||||
crate::ensure_code!(
|
||||
svc_exit.success(),
|
||||
crate::error::GENERAL_ERROR,
|
||||
"Failed to Reload Nginx: {}",
|
||||
svc_exit
|
||||
.code()
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
async fn down(&self) -> Result<(), Error> {
|
||||
tokio::fs::remove_file("/etc/nginx/sites-enabled/start9-services.conf")
|
||||
.await
|
||||
.or_else(|e| match e {
|
||||
e if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
tokio::fs::remove_file(crate::tor::ETC_NGINX_SERVICES_CONF)
|
||||
.await
|
||||
.or_else(|e| match e {
|
||||
e if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["nginx", "reload"])
|
||||
.status()?;
|
||||
crate::ensure_code!(
|
||||
svc_exit.success(),
|
||||
crate::error::GENERAL_ERROR,
|
||||
"Failed to Reload Nginx: {}",
|
||||
svc_exit
|
||||
.code()
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ set -e
|
||||
|
||||
echo "turn off mocks"
|
||||
echo "$( jq '.useMocks = false' use-mocks.json )" > use-mocks.json
|
||||
echo "$( jq '.skipStartupAlerts = false' use-mocks.json )" > use-mocks.json
|
||||
|
||||
echo "FILTER: rm -rf www"
|
||||
rm -rf www
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
manifest-version: 0
|
||||
app-id: start9-ambassador
|
||||
app-version: 0.2.8
|
||||
app-version: 0.2.9
|
||||
uri-rewrites:
|
||||
- =/api -> http://{{start9-ambassador}}:5959/authenticate
|
||||
- /api/ -> http://{{start9-ambassador}}:5959/
|
||||
|
||||
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "embassy-ui",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.9",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -7903,9 +7903,9 @@
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-1.2.5.tgz",
|
||||
"integrity": "sha512-2AlqgYnVPOc9WDyWu7S5DJaEZsfk6dNh/neatQ3IHUW4QLutM/VPSH9lG7bif+XjFWc9K9XR3QvR+fXuECmfdA=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-2.0.0.tgz",
|
||||
"integrity": "sha512-NqRSh2+LlN2NInpqTQnS614Y/3NkVMFFU6sJlRFEpxJ/LHuK/qJECH7/fXZjk4VZstPW/Pevjil/VtSONsLc7Q=="
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "embassy-ui",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.9",
|
||||
"description": "GUI for EmbassyOS",
|
||||
"author": "Start9 Labs",
|
||||
"homepage": "https://github.com/Start9Labs/embassy-ui",
|
||||
@@ -36,7 +36,7 @@
|
||||
"json-pointer": "^0.6.1",
|
||||
"jsonpointerx": "^1.0.30",
|
||||
"jsontokens": "^3.0.0",
|
||||
"marked": "^1.2.0",
|
||||
"marked": "^2.0.0",
|
||||
"rxjs": "^6.6.3",
|
||||
"uuid": "^8.3.1",
|
||||
"zone.js": "^0.11.2"
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
<ion-icon name="arrow-forward"></ion-icon>
|
||||
<ion-icon name="arrow-up"></ion-icon>
|
||||
<ion-icon name="bookmark-outline"></ion-icon>
|
||||
<ion-icon name="cart-outline"></ion-icon>
|
||||
<ion-icon name="chevron-down"></ion-icon>
|
||||
<ion-icon name="chevron-up"></ion-icon>
|
||||
<ion-icon name="close"></ion-icon>
|
||||
@@ -86,20 +85,22 @@
|
||||
<ion-icon name="eye-off-outline"></ion-icon>
|
||||
<ion-icon name="eye-outline"></ion-icon>
|
||||
<ion-icon name="file-tray-stacked-outline"></ion-icon>
|
||||
<ion-icon name="flash-outline"></ion-icon>
|
||||
<ion-icon name="grid-outline"></ion-icon>
|
||||
<ion-icon name="help-circle-outline"></ion-icon>
|
||||
<ion-icon name="home-outline"></ion-icon>
|
||||
<ion-icon name="information-circle-outline"></ion-icon>
|
||||
<ion-icon name="list-outline"></ion-icon>
|
||||
<ion-icon name="newspaper-outline"></ion-icon>
|
||||
<ion-icon name="notifications-outline"></ion-icon>
|
||||
<ion-icon name="notifications-outline"></ion-icon>
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
<ion-icon name="power"></ion-icon>
|
||||
<ion-icon name="pulse"></ion-icon>
|
||||
<ion-icon name="qr-code-outline"></ion-icon>
|
||||
<ion-icon name="globe-outline"></ion-icon>
|
||||
<ion-icon name="reload-outline"></ion-icon>
|
||||
<ion-icon name="refresh-outline"></ion-icon>
|
||||
<ion-icon name="save-outline"></ion-icon>
|
||||
<ion-icon name="storefront-outline"></ion-icon>
|
||||
<ion-icon name="terminal-outline"></ion-icon>
|
||||
<ion-icon name="trash-outline"></ion-icon>
|
||||
<ion-icon name="warning-outline"></ion-icon>
|
||||
|
||||
@@ -42,7 +42,7 @@ export class AppComponent {
|
||||
{
|
||||
title: 'Marketplace',
|
||||
url: '/services/marketplace',
|
||||
icon: 'cart-outline',
|
||||
icon: 'storefront-outline',
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
|
||||
<ion-slide *ngFor="let slide of params.slideDefinitions">
|
||||
<dependencies #components *ngIf="slide.selector === 'dependencies'" [params]="slide.params"></dependencies>
|
||||
<developer-notes #components *ngIf="slide.selector === 'developer-notes'" [params]="slide.params"></developer-notes>
|
||||
<notes #components *ngIf="slide.selector === 'notes'" [params]="slide.params"></notes>
|
||||
<dependents #components *ngIf="slide.selector === 'dependents'" [params]="slide.params" [finished]="finished"></dependents>
|
||||
<complete #components *ngIf="slide.selector === 'complete'" [params]="slide.params" [finished]="finished"></complete>
|
||||
</ion-slide>
|
||||
@@ -43,8 +43,8 @@
|
||||
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
|
||||
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button slot="end" *ngIf="currentSlideDef.nextButton as nextButton" (click)="finished({})" [disabled]="$anythingLoading$ | async" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="nextButton.length > 16">{{nextButton}}</ion-text></ion-button>
|
||||
<ion-button slot="end" *ngIf="currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" [disabled]="$anythingLoading$ | async" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button>
|
||||
<ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.nextButton as nextButton" (click)="finished({})" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="nextButton.length > 16">{{nextButton}}</ion-text></ion-button>
|
||||
<ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="$error$ | async">
|
||||
<ion-button slot="start" (click)="finished({ final: true })" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { DependenciesComponentModule } from './dependencies/dependencies.component.module'
|
||||
import { DependentsComponentModule } from './dependents/dependents.component.module'
|
||||
import { CompleteComponentModule } from './complete/complete.component.module'
|
||||
import { DeveloperNotesComponentModule } from './developer-notes/developer-notes.component.module'
|
||||
import { NotesComponentModule } from './notes/notes.component.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -21,7 +21,7 @@ import { DeveloperNotesComponentModule } from './developer-notes/developer-notes
|
||||
DependenciesComponentModule,
|
||||
DependentsComponentModule,
|
||||
CompleteComponentModule,
|
||||
DeveloperNotesComponentModule,
|
||||
NotesComponentModule,
|
||||
],
|
||||
exports: [InstallWizardComponent],
|
||||
})
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
font-size: small;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #393b40;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (min-width:500px) {
|
||||
@@ -57,6 +58,7 @@
|
||||
font-size: medium;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #393b40;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Component, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
|
||||
import { Component, Input, NgZone, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
|
||||
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
||||
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
|
||||
import { CompleteComponent } from './complete/complete.component'
|
||||
import { DependenciesComponent } from './dependencies/dependencies.component'
|
||||
import { DependentsComponent } from './dependents/dependents.component'
|
||||
import { DeveloperNotesComponent } from './developer-notes/developer-notes.component'
|
||||
import { NotesComponent } from './notes/notes.component'
|
||||
import { Colorable, Loadable } from './loadable'
|
||||
import { WizardAction } from './wizard-types'
|
||||
|
||||
@@ -50,7 +50,7 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
||||
$currentColor$: BehaviorSubject<string> = new BehaviorSubject('medium')
|
||||
$error$ = new BehaviorSubject(undefined)
|
||||
|
||||
constructor (private readonly modalController: ModalController) { super() }
|
||||
constructor (private readonly modalController: ModalController, private readonly zone: NgZone) { super() }
|
||||
ngOnInit () { }
|
||||
|
||||
ngAfterViewInit () {
|
||||
@@ -80,15 +80,15 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
||||
|
||||
private async slide () {
|
||||
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.finished({ final: true }) }
|
||||
this.slideIndex += 1
|
||||
this.currentSlide.load()
|
||||
await this.slideContainer.lockSwipes(false)
|
||||
await Promise.all([
|
||||
this.contentContainer.scrollToTop(),
|
||||
this.slideContainer.slideNext(500),
|
||||
])
|
||||
await this.slideContainer.lockSwipes(true)
|
||||
this.slideContainer.update()
|
||||
this.zone.run(async () => {
|
||||
this.slideComponents[this.slideIndex + 1].load()
|
||||
await pauseFor(50)
|
||||
this.slideIndex += 1
|
||||
await this.slideContainer.lockSwipes(false)
|
||||
await this.contentContainer.scrollToTop()
|
||||
await this.slideContainer.slideNext(500)
|
||||
await this.slideContainer.lockSwipes(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +114,8 @@ export type SlideDefinition = SlideCommon & (
|
||||
selector: 'complete',
|
||||
params: CompleteComponent['params']
|
||||
} | {
|
||||
selector: 'developer-notes',
|
||||
params: DeveloperNotesComponent['params']
|
||||
selector: 'notes',
|
||||
params: NotesComponent['params']
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
<div style="margin-top: 25px;">
|
||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
||||
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
|
||||
Warning
|
||||
{{params.title}}
|
||||
</ion-label>
|
||||
</div>
|
||||
<div class="long-message">
|
||||
{{params.developerNotes}}
|
||||
</div>
|
||||
<div class="long-message" [innerHTML]="params.notes | markdown"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,13 +1,13 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { DeveloperNotesComponent } from './developer-notes.component'
|
||||
import { NotesComponent } from './notes.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DeveloperNotesComponent,
|
||||
NotesComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -15,6 +15,6 @@ import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [DeveloperNotesComponent],
|
||||
exports: [NotesComponent],
|
||||
})
|
||||
export class DeveloperNotesComponentModule { }
|
||||
export class NotesComponentModule { }
|
||||
@@ -4,24 +4,24 @@ import { Colorable, Loadable } from '../loadable'
|
||||
import { WizardAction } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'developer-notes',
|
||||
templateUrl: './developer-notes.component.html',
|
||||
selector: 'notes',
|
||||
templateUrl: './notes.component.html',
|
||||
styleUrls: ['../install-wizard.component.scss'],
|
||||
})
|
||||
export class DeveloperNotesComponent implements OnInit, Loadable, Colorable {
|
||||
export class NotesComponent implements OnInit, Loadable, Colorable {
|
||||
@Input() params: {
|
||||
action: WizardAction
|
||||
developerNotes: string
|
||||
notes: string
|
||||
title: string
|
||||
titleColor: string
|
||||
}
|
||||
|
||||
$loading$ = new BehaviorSubject(false)
|
||||
$color$ = new BehaviorSubject('warning')
|
||||
$color$ = new BehaviorSubject('light')
|
||||
$cancel$ = new Subject<void>()
|
||||
|
||||
load () { }
|
||||
|
||||
constructor () { }
|
||||
ngOnInit () {
|
||||
console.log('Developer Notes', this.params)
|
||||
}
|
||||
ngOnInit () { this.$color$.next(this.params.titleColor) }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
||||
import { exists } from 'src/app/util/misc.util'
|
||||
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
|
||||
import { ApiService } from '../../services/api/api.service'
|
||||
@@ -7,7 +8,11 @@ import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class WizardBaker {
|
||||
constructor (private readonly apiService: ApiService, private readonly appModel: AppModel) { }
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly updateService: OsUpdateService,
|
||||
private readonly appModel: AppModel
|
||||
) { }
|
||||
|
||||
install (values: {
|
||||
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
|
||||
@@ -23,8 +28,8 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, developerNotes: installAlert,
|
||||
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
|
||||
}} : undefined,
|
||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Install', params: {
|
||||
action, title, version, serviceRequirements,
|
||||
@@ -52,8 +57,8 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, developerNotes: installAlert,
|
||||
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
|
||||
}} : undefined,
|
||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update', params: {
|
||||
action, title, version, serviceRequirements,
|
||||
@@ -70,6 +75,26 @@ export class WizardBaker {
|
||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||
}
|
||||
|
||||
updateOS (values: {
|
||||
version: string, releaseNotes: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { version, releaseNotes } = values
|
||||
|
||||
const action = 'update'
|
||||
const title = 'EmbassyOS'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update OS', params: {
|
||||
action, notes: releaseNotes || 'No release notes for this version', title: 'Release Notes', titleColor: 'dark',
|
||||
}},
|
||||
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
|
||||
action, verb: 'beginning update for', title, executeAction: () => this.updateService.updateEmbassyOS(version),
|
||||
}},
|
||||
]
|
||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||
}
|
||||
|
||||
downgrade (values: {
|
||||
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
@@ -84,8 +109,8 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, developerNotes: installAlert,
|
||||
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
|
||||
}} : undefined,
|
||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade', params: {
|
||||
action, title, version, serviceRequirements,
|
||||
@@ -115,8 +140,8 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Continue', params: {
|
||||
action, developerNotes: uninstallAlert || defaultUninstallationWarning(title) },
|
||||
{ selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Continue', params: {
|
||||
action, notes: uninstallAlert || defaultUninstallationWarning(title), title: 'Warning', titleColor: 'warning' },
|
||||
},
|
||||
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Uninstall', params: {
|
||||
action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ion-item button lines="none" *ngIf="updateAvailable$ | async as version" (click)="confirmUpdate(version)">
|
||||
<ion-item button lines="none" *ngIf="updateAvailable$ | async as res" (click)="confirmUpdate(res)">
|
||||
<ion-label>
|
||||
New EmbassyOS Version {{version | displayEmver}} Available!
|
||||
New EmbassyOS Version {{res.versionLatest | displayEmver}} Available!
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
||||
import { Observable } from 'rxjs'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { WizardBaker } from '../install-wizard/prebaked-wizards'
|
||||
import { wizardModal } from '../install-wizard/install-wizard.component'
|
||||
import { ReqRes } from 'src/app/services/api/api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'update-os-banner',
|
||||
@@ -11,38 +12,24 @@ import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||
styleUrls: ['./update-os-banner.component.scss'],
|
||||
})
|
||||
export class UpdateOsBannerComponent {
|
||||
updateAvailable$: Observable<undefined | string>
|
||||
updateAvailable$: Observable<undefined | ReqRes.GetVersionLatestRes>
|
||||
constructor (
|
||||
private readonly osUpdateService: OsUpdateService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
) {
|
||||
this.updateAvailable$ = this.osUpdateService.watchForUpdateAvailable$()
|
||||
}
|
||||
|
||||
ngOnInit () { }
|
||||
|
||||
async confirmUpdate (versionLatest: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: `Update EmbassyOS`,
|
||||
message: `Update EmbassyOS to version ${displayEmver(versionLatest)}?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Update',
|
||||
handler: () => this.update(versionLatest),
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async update (versionLatest: string) {
|
||||
return this.loader.displayDuringP(
|
||||
this.osUpdateService.updateEmbassyOS(versionLatest),
|
||||
async confirmUpdate (res: ReqRes.GetVersionLatestRes) {
|
||||
await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.updateOS({
|
||||
version: res.versionLatest,
|
||||
releaseNotes: res.releaseNotes,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppReleaseNotesPage } from './app-release-notes.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppReleaseNotesPage],
|
||||
})
|
||||
export class AppReleaseNotesPageModule { }
|
||||
@@ -1,14 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title style="font-size: 16px;">{{ version }} Release Notes</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div [innerHTML]="releaseNotes | markdown"></div>
|
||||
</ion-content>
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'app-release-notes',
|
||||
templateUrl: './app-release-notes.page.html',
|
||||
styleUrls: ['./app-release-notes.page.scss'],
|
||||
})
|
||||
export class AppReleaseNotesPage {
|
||||
@Input() releaseNotes: string
|
||||
@Input() version: string
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
) { }
|
||||
|
||||
dismiss () {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,18 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title >
|
||||
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to {{ version }}!</ion-label>
|
||||
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.9!</ion-label>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
|
||||
<div>
|
||||
<h2>Highlights</h2>
|
||||
<p>
|
||||
0.2.8 is a small but important update designed to enhance awareness around potential pitfalls of using certain services.
|
||||
It introduces warnings for installing, uninstalling, backing up, and restoring backups of stateful services such as LND or c-lightning.
|
||||
Additionally, it draws a distinction between services that are designed to be launched inside the browser and those that are designed to run in the background
|
||||
</p>
|
||||
<p>
|
||||
0.2.8 also introduces automatic update checks. With this enabled, each time you visit your embassy you will be notified if new Embassy OS or service versions are available. This setting can be edited in your Embassy Config page.
|
||||
<ion-item lines="none" style="--border-radius: var(--icon-border-radius); margin-top: 15px">
|
||||
<ion-label>Auto Check for Updates</ion-label>
|
||||
<ion-toggle slot="end" [(ngModel)]="autoCheckUpdates"></ion-toggle>
|
||||
</ion-item>
|
||||
</p>
|
||||
|
||||
<div style="margin-top: 30px">
|
||||
<h5 style="color: var(--ion-color-danger)">Important</h5>
|
||||
<p>
|
||||
If you have LND or c-lightning installed, please update them to the latest versions.
|
||||
An oversight in Start9’s USB backups system has created a situation where <b>restoring</b> a LND or c-lightning backup could potentially result in permanent loss of channel funds.
|
||||
To be clear, <ion-text style="font-weight: 'bold';">DO NOT</ion-text> attempt to <b>restore</b> a LND or c-lightning backup until you have updated to the latest versions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Highlights</h2>
|
||||
<p class="main-content">
|
||||
0.2.9 introduces LAN support for services running on the Embassy. A service's LAN address (.local URL) can be accessed while connected to the same network.
|
||||
This is useful for two reasons: (1) LAN connections are significantly faster than Tor, and (2) if the Tor network is experiencing connectivity issues, you will not be locked out of your services.
|
||||
</p>
|
||||
|
||||
<div class="close-button">
|
||||
<ion-button fill="outline" (click)="dismiss()">
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
height: 100%;
|
||||
color: var(--ion-color-medium);
|
||||
}
|
||||
@@ -12,24 +12,17 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
export class OSWelcomePage {
|
||||
@Input() version: string
|
||||
|
||||
autoCheckUpdates = true
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly config: ConfigService,
|
||||
) { }
|
||||
|
||||
async dismiss () {
|
||||
this.apiService
|
||||
.patchServerConfig('autoCheckUpdates', this.autoCheckUpdates)
|
||||
.then(() => this.serverModel.update({ autoCheckUpdates: this.autoCheckUpdates }))
|
||||
.then(() => this.apiService.acknowledgeOSWelcome(this.config.version))
|
||||
.catch(console.error)
|
||||
this.apiService.acknowledgeOSWelcome(this.config.version).catch(console.error)
|
||||
|
||||
// return false to skip subsequent alert modals (e.g. check for updates modals)
|
||||
// return true to show subsequent alert modals
|
||||
return this.modalCtrl.dismiss(this.autoCheckUpdates)
|
||||
return this.modalCtrl.dismiss(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ function emptyAppInstalledFull (): Omit<AppInstalledFull, keyof AppInstalledPrev
|
||||
lastBackup: null,
|
||||
configuredRequirements: null,
|
||||
hasFetchedFull: false,
|
||||
actions: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,21 +34,36 @@ export interface AppAvailableVersionSpecificInfo {
|
||||
// installed
|
||||
|
||||
export interface AppInstalledPreview extends BaseApp {
|
||||
lanAddress?: string
|
||||
torAddress: string
|
||||
versionInstalled: string
|
||||
ui: boolean
|
||||
lanUi: boolean
|
||||
torUi: boolean
|
||||
// FE state only
|
||||
hasUI: boolean
|
||||
launchable: boolean
|
||||
}
|
||||
|
||||
export interface AppInstalledFull extends AppInstalledPreview {
|
||||
instructions: string | null
|
||||
lastBackup: string | null
|
||||
configuredRequirements: AppDependency[] | null // null if not yet configured
|
||||
hasFetchedFull: boolean
|
||||
startAlert?: string
|
||||
uninstallAlert?: string
|
||||
restoreAlert?: string
|
||||
actions: Actions
|
||||
// FE state only
|
||||
hasFetchedFull: boolean
|
||||
}
|
||||
// dependencies
|
||||
|
||||
export type Actions = ServiceAction[]
|
||||
export interface ServiceAction {
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
warning?: string
|
||||
allowedStatuses: AppStatus[]
|
||||
}
|
||||
export interface AppDependency extends InstalledAppDependency {
|
||||
// explanation of why it *is* optional. null represents it is required.
|
||||
optional: string | null
|
||||
|
||||
@@ -115,7 +115,7 @@ export interface S9Server {
|
||||
name: string
|
||||
origin: string
|
||||
versionInstalled: string
|
||||
versionLatest: string | undefined
|
||||
versionLatest: string | undefined // not on the api as of 0.2.8
|
||||
status: ServerStatus
|
||||
badge: number
|
||||
alternativeRegistryUrl: string | null
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppActionsPage } from './app-actions.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppActionsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
QRComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppActionsPage],
|
||||
})
|
||||
export class AppActionsPageModule { }
|
||||
@@ -0,0 +1,41 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Actions</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async) && {
|
||||
title: app.title | async,
|
||||
versionInstalled: app.versionInstalled | async,
|
||||
status: app.status | async,
|
||||
actions: app.actions | async
|
||||
} as vars">
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<!-- no metrics -->
|
||||
<ion-item *ngIf="!vars.actions.length">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>No Actions for {{ vars.title }} {{ vars.versionInstalled }}.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- actions -->
|
||||
<ion-item-group>
|
||||
<ion-item button *ngFor="let action of vars.actions" (click)="handleAction(action)" >
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2><ion-text color="primary">{{ action.name }}</ion-text><ion-icon *ngIf="!(action.allowedStatuses | includes: vars.status)" color="danger" name="close-outline"></ion-icon></h2>
|
||||
<p><ion-text color="dark">{{ action.description }}</ion-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
120
ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts
Normal file
120
ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService, isRpcFailure, isRpcSuccess } from 'src/app/services/api/api.service'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { ServiceAction, AppInstalledFull } from 'src/app/models/app-types'
|
||||
import { PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { AppStatus } from 'src/app/models/app-model'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
|
||||
@Component({
|
||||
selector: 'app-actions',
|
||||
templateUrl: './app-actions.page.html',
|
||||
styleUrls: ['./app-actions.page.scss'],
|
||||
})
|
||||
export class AppActionsPage extends Cleanup {
|
||||
error = ''
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
appId: string
|
||||
app: PropertySubject<AppInstalledFull>
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly loaderService: LoaderService,
|
||||
) { super() }
|
||||
|
||||
ngOnInit() {
|
||||
this.appId = this.route.snapshot.paramMap.get('appId')
|
||||
|
||||
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId)).pipe(
|
||||
map(app => this.app = app),
|
||||
).subscribe({ error: e => this.error = e.message })
|
||||
}
|
||||
|
||||
async handleAction(action: ServiceAction) {
|
||||
if (action.allowedStatuses.includes(this.app.status.getValue())) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Confirm',
|
||||
message: `Are you sure you want to execute action "${action.name}"? ${action.warning ? action.warning : ""}`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Execute',
|
||||
handler: () => {
|
||||
this.executeAction(action)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
} else {
|
||||
const joinStatuses = (statuses: AppStatus[]) => {
|
||||
const last = statuses.pop()
|
||||
let s = statuses.join(', ')
|
||||
if (last) {
|
||||
if (statuses.length > 1) { // oxford comma
|
||||
s += ','
|
||||
}
|
||||
s += ` or ${last}`
|
||||
}
|
||||
return s
|
||||
}
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Forbidden',
|
||||
message: `Action "${action.name}" can only be executed when service is ${joinStatuses(action.allowedStatuses)}`,
|
||||
buttons: ['OK'],
|
||||
cssClass: 'alert-error-message',
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
private async executeAction(action: ServiceAction) {
|
||||
try {
|
||||
const res = await this.loaderService.displayDuringP(
|
||||
this.apiService.serviceAction(this.appId, action),
|
||||
)
|
||||
|
||||
if (isRpcFailure(res)) {
|
||||
this.presentAlertActionFail(res.error.code, res.error.message)
|
||||
}
|
||||
|
||||
if (isRpcSuccess(res)) {
|
||||
const successAlert = await this.alertCtrl.create({
|
||||
header: 'Execution Complete',
|
||||
message: res.result.split('\n').join('</br ></br />'),
|
||||
buttons: ['OK'],
|
||||
cssClass: 'alert-success-message',
|
||||
})
|
||||
return await successAlert.present()
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof HttpErrorResponse) {
|
||||
this.presentAlertActionFail(e.status, e.message)
|
||||
} else {
|
||||
this.presentAlertActionFail(-1, e.message || JSON.stringify(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertActionFail(code: number, message: string): Promise<void> {
|
||||
const failureAlert = await this.alertCtrl.create({
|
||||
header: 'Execution Failed',
|
||||
message: `Error code ${code}. ${message}`,
|
||||
buttons: ['OK'],
|
||||
cssClass: 'alert-error-message',
|
||||
})
|
||||
return await failureAlert.present()
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import { RecommendationButtonComponentModule } from 'src/app/components/recommen
|
||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||
import { ErrorMessageComponentModule } from 'src/app/components/error-message/error-message.component.module'
|
||||
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
||||
import { AppReleaseNotesPageModule } from 'src/app/modals/app-release-notes/app-release-notes.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -35,7 +34,6 @@ const routes: Routes = [
|
||||
InstallWizardComponentModule,
|
||||
ErrorMessageComponentModule,
|
||||
InformationPopoverComponentModule,
|
||||
AppReleaseNotesPageModule,
|
||||
],
|
||||
declarations: [AppAvailableShowPage],
|
||||
})
|
||||
|
||||
@@ -53,13 +53,18 @@
|
||||
Install
|
||||
</ion-button>
|
||||
|
||||
<div *ngIf="vars.versionInstalled && vars.status !== 'INSTALLING' ">
|
||||
<ion-button *ngIf="installedStatus === 'installed-below'" class="main-action-button" expand="block" fill="outline" color="success" (click)="update('update')">
|
||||
Update to {{ vars.versionViewing | displayEmver }}
|
||||
</ion-button>
|
||||
<ion-button *ngIf="installedStatus === 'installed-above'" class="main-action-button" expand="block" fill="outline" color="warning" (click)="update('downgrade')">
|
||||
Downgrade to {{ vars.versionViewing | displayEmver }}
|
||||
<div *ngIf="vars.versionInstalled">
|
||||
<ion-button class="main-action-button" expand="block" fill="outline" [routerLink]="['/services', 'installed', vars.id]">
|
||||
Go to Service
|
||||
</ion-button>
|
||||
<div *ngIf="vars.status !== 'INSTALLING' ">
|
||||
<ion-button *ngIf="installedStatus === 'installed-below'" class="main-action-button" expand="block" fill="outline" color="success" (click)="update('update')">
|
||||
Update to {{ vars.versionViewing | displayEmver }}
|
||||
</ion-button>
|
||||
<ion-button *ngIf="installedStatus === 'installed-above'" class="main-action-button" expand="block" fill="outline" color="warning" (click)="update('downgrade')">
|
||||
Downgrade to {{ vars.versionViewing | displayEmver }}
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-item-group>
|
||||
@@ -81,6 +86,14 @@
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ion-item-divider style="color: var(--ion-color-dark); font-weight: bold;">New in {{ vars.versionViewing | displayEmver }}</ion-item-divider>
|
||||
<ion-item lines="none">
|
||||
<ion-label *ngIf="!($newVersionLoading$ | async)" style="display: flex; align-items: center; justify-content: space-between;" class="ion-text-wrap" >
|
||||
<div id='release-notes'color="dark" [innerHTML]="vars.releaseNotes | markdown"></div>
|
||||
</ion-label>
|
||||
<ion-spinner style="display: block; margin: auto;" name="lines" color="dark" *ngIf="$newVersionLoading$ | async"></ion-spinner>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider class="divider">Description</ion-item-divider>
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
@@ -90,13 +103,6 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Release Notes</ion-item-divider>
|
||||
<ion-item lines="none" button details="true" [disabled]="" (click)="presentModalReleaseNotes()" [disabled]="$newVersionLoading$ | async">
|
||||
<ion-icon slot="start" name="newspaper-outline" color="medium"></ion-icon>
|
||||
<ion-label *ngIf="!($newVersionLoading$ | async)"><ion-text color="medium">New in {{ vars.versionViewing | displayEmver }}</ion-text></ion-label>
|
||||
<ion-spinner style="display: block; margin: auto;" name="lines" color="dark" *ngIf="$newVersionLoading$ | async"></ion-spinner>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="(vars.serviceRequirements)?.length">
|
||||
<ion-item-divider class="divider">Service Dependencies
|
||||
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="medium" (click)="presentPopover(serviceDependencyDefintion, $event)">
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
// .recommendation-container {
|
||||
// margin-top: 3px;
|
||||
// display: grid;
|
||||
// grid-template-columns: auto auto;
|
||||
// justify-content: start;
|
||||
// grid-column-gap: 5px;
|
||||
// align-items: center;
|
||||
// }
|
||||
|
||||
.recommendation-text {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// @media (min-width:1000px) {
|
||||
// .recommendation-text {
|
||||
// font-size: small;
|
||||
// }
|
||||
// }
|
||||
|
||||
.recommendation-error {
|
||||
color: var(--ion-color-danger);
|
||||
}
|
||||
@@ -31,4 +16,9 @@
|
||||
font-size: medium;
|
||||
padding-left: 10px;
|
||||
font-weight: unset;
|
||||
}
|
||||
|
||||
#release-notes {
|
||||
overflow: auto;
|
||||
max-height: 160px;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, HostListener, NgZone } from '@angular/core'
|
||||
import { Component, NgZone } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AppAvailableFull, AppAvailableVersionSpecificInfo, AppDependency } from 'src/app/models/app-types'
|
||||
import { AppAvailableFull, AppAvailableVersionSpecificInfo } from 'src/app/models/app-types'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { AlertController, ModalController, NavController, PopoverController } from '@ionic/angular'
|
||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { BehaviorSubject, from, merge, Observable, of, Subscription } from 'rxjs'
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs'
|
||||
import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators'
|
||||
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
@@ -13,8 +13,6 @@ import { AppModel } from 'src/app/models/app-model'
|
||||
import { initPropertySubject, peekProperties, PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { AppReleaseNotesPage } from 'src/app/modals/app-release-notes/app-release-notes.page'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||
|
||||
@@ -40,8 +38,6 @@ export class AppAvailableShowPage extends Cleanup {
|
||||
|
||||
serviceDependencyDefintion = '<span style="font-style: italic">Service Dependencies</span> are other services that this service recommends or requires in order to run.'
|
||||
|
||||
showMoreReleaseNotes = false
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly apiService: ApiService,
|
||||
@@ -192,20 +188,6 @@ export class AppAvailableShowPage extends Cleanup {
|
||||
}
|
||||
}
|
||||
|
||||
async presentModalReleaseNotes () {
|
||||
const { releaseNotes, versionViewing } = peekProperties(this.$app$)
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppReleaseNotesPage,
|
||||
componentProps: {
|
||||
releaseNotes,
|
||||
version: versionViewing,
|
||||
},
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private fetchRecommendation (): Observable<any> {
|
||||
this.recommendation = history.state && history.state.installationRecommendation
|
||||
|
||||
|
||||
@@ -22,26 +22,32 @@
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let app of apps" sizeXs="4" sizeSm="3" sizeMd="2" sizeLg="2">
|
||||
<ng-container *ngIf="{ tor: app.subject.torAddress | async, status: app.subject.status | async, ui: app.subject.ui | async, iconURL: app.subject.iconURL | async | iconParse, title: app.subject.title | async } as vars" >
|
||||
<ng-container *ngIf="{
|
||||
status: app.subject.status | async,
|
||||
hasUI: app.subject.hasUI | async,
|
||||
launchable: app.subject.launchable | async,
|
||||
iconURL: app.subject.iconURL | async | iconParse,
|
||||
title: app.subject.title | async
|
||||
} as vars">
|
||||
|
||||
<ion-card class="installed-card" [class.installed-card-on]="vars.status === 'RUNNING'" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
|
||||
<div class="launch-container" *ngIf="vars.ui && !isConsulate">
|
||||
<div class="launch-button-triangle" (click)="launchUiTab(vars.tor, $event)" [class.disabled]="vars.status !== AppStatus.RUNNING || !isTor">
|
||||
<ion-icon class="launch-button-triangle-icon" name="globe-outline"></ion-icon>
|
||||
<ion-card class="installed-card" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
|
||||
<div class="launch-container" *ngIf="vars.hasUI">
|
||||
<div class="launch-button-triangle" (click)="launchUiTab(app.id, $event)" [class.disabled]="!vars.launchable">
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img style="position: absolute" class="main-img" [src]="vars.iconURL" [alt]="app.subject.title | async" />
|
||||
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'green'" src="assets/img/running-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'red'" src="assets/img/issue-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'yellow'" src="assets/img/warning-bulb.png"/>
|
||||
<img class="bulb-off" *ngIf="vars.status | displayBulb: 'off'" src="assets/img/off-bulb.png"/>
|
||||
<ion-card-header>
|
||||
<status [appStatus]="vars.status" size="small"></status>
|
||||
<p>{{ vars.title }}</p>
|
||||
</ion-card-header>
|
||||
</ion-card>
|
||||
<img style="position: absolute" class="main-img" [src]="vars.iconURL" [alt]="vars.title" />
|
||||
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'green'" src="assets/img/running-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'red'" src="assets/img/issue-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'yellow'" src="assets/img/warning-bulb.png"/>
|
||||
<img class="bulb-off" *ngIf="vars.status | displayBulb: 'off'" src="assets/img/off-bulb.png"/>
|
||||
<ion-card-header>
|
||||
<status [appStatus]="vars.status" size="small"></status>
|
||||
<p>{{ vars.title }}</p>
|
||||
</ion-card-header>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
@@ -53,7 +59,7 @@
|
||||
<p class="ion-text-wrap">Get started by installing your first service.</p>
|
||||
</div>
|
||||
<ion-button [routerLink]="['/services','marketplace']" style="width: 50%;" fill="outline">
|
||||
<ion-icon slot="start" name="cart-outline"></ion-icon>
|
||||
<ion-icon slot="start" name="storefront-outline"></ion-icon>
|
||||
Marketplace
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
@@ -35,19 +35,15 @@ export class AppInstalledListPage extends Cleanup {
|
||||
segmentValue: 'services' | 'embassy' = 'services'
|
||||
|
||||
showCertDownload : boolean
|
||||
isConsulate: boolean
|
||||
isTor: boolean
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly appModel: AppModel,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly syncDaemon: SyncDaemon,
|
||||
config: ConfigService,
|
||||
private readonly config: ConfigService,
|
||||
) {
|
||||
super()
|
||||
this.isConsulate = config.isConsulateAndroid || config.isConsulateIos
|
||||
this.isTor = config.isTor()
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
@@ -96,14 +92,21 @@ export class AppInstalledListPage extends Cleanup {
|
||||
this.error = e.message
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
async launchUiTab (address: string, event: Event) {
|
||||
async launchUiTab (id: string, event: Event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
address = address.startsWith('http') ? address : `http://${address}`
|
||||
return window.open(address, '_blank')
|
||||
|
||||
const app = this.apps.find(app => app.id === id).subject
|
||||
|
||||
let uiAddress: string
|
||||
if (this.config.isTor()) {
|
||||
uiAddress = `http://${app.torAddress.getValue()}`
|
||||
} else {
|
||||
uiAddress = `https://${app.lanAddress.getValue()}`
|
||||
}
|
||||
return window.open(uiAddress, '_blank')
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
hasFetchedFull: app.hasFetchedFull | async,
|
||||
iconURL: app.iconURL | async,
|
||||
title: app.title | async,
|
||||
ui: app.ui | async
|
||||
hasUI: app.hasUI | async,
|
||||
launchable: app.launchable | async,
|
||||
lanAddress: app.lanAddress | async
|
||||
} as vars" class="ion-padding-bottom">
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async)">
|
||||
<ion-refresher *ngIf="app && app.id" slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
@@ -53,62 +54,67 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px">
|
||||
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px;">
|
||||
<ion-label class="status-readout">
|
||||
<status size="bold-large" [appStatus]="vars.status"></status>
|
||||
<ion-button *ngIf="vars.status === AppStatus.NEEDS_CONFIG" expand="block" fill="outline" [routerLink]="['config']">
|
||||
<ion-button *ngIf="vars.status === AppStatus.NEEDS_CONFIG" expand="block" fill="outline" [routerLink]="['config']">
|
||||
Configure
|
||||
</ion-button>
|
||||
<ion-button *ngIf="[AppStatus.RUNNING, AppStatus.CRASHED, AppStatus.PAUSED, AppStatus.RESTARTING] | includes: vars.status" expand="block" fill="outline" (click)="stop()">
|
||||
<ion-button *ngIf="[AppStatus.RUNNING, AppStatus.CRASHED, AppStatus.PAUSED, AppStatus.RESTARTING] | includes: vars.status" expand="block" fill="outline" color="danger" (click)="stop()">
|
||||
Stop
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.CREATING_BACKUP" expand="block" fill="outline" (click)="presentAlertStopBackup()">
|
||||
<ion-button *ngIf="vars.status === AppStatus.CREATING_BACKUP" expand="block" fill="outline" (click)="presentAlertStopBackup()">
|
||||
Stop Backup
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.DEAD" expand="block" fill="outline" (click)="uninstall()">
|
||||
<ion-button *ngIf="vars.status === AppStatus.DEAD" expand="block" fill="outline" (click)="uninstall()">
|
||||
Force Uninstall
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.BROKEN_DEPENDENCIES" expand="block" fill="outline" (click)="scrollToRequirements()">
|
||||
<ion-button *ngIf="vars.status === AppStatus.BROKEN_DEPENDENCIES" expand="block" fill="outline" (click)="scrollToRequirements()">
|
||||
Fix
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.STOPPED" expand="block" fill="outline" (click)="start()">
|
||||
<ion-button *ngIf="vars.status === AppStatus.STOPPED" expand="block" fill="outline" color="success" (click)="tryStart()">
|
||||
Start
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="no-cushion-item" *ngIf="vars.ui && !isConsulate" lines="none">
|
||||
<ion-label style="margin-bottom: 10px; margin-top: 0px; display: flex; justify-content: left; align-items: center;" class="ion-text-wrap">
|
||||
<ion-button fill="clear" size="small" class="launch-explanation-button" (click)="presentLaunchPopover(vars.status, $event)">
|
||||
<ion-icon color="medium" name="information-circle-outline">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
<ion-button [disabled]="vars.status !== 'RUNNING' || !isTor" class="launch-button" [class.launch-button-off]="vars.status !== 'RUNNING' || !isTor" (click)="launchUiTab()">
|
||||
<ion-icon style="position: absolute; z-index: 1; left: 0;" name="globe-outline"></ion-icon>
|
||||
<ion-text>LAUNCH</ion-text>
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-button size="small" *ngIf="vars.hasUI" [disabled]="!vars.launchable" class="launch-button" expand="block" (click)="launchUiTab()">
|
||||
Launch Web Interface
|
||||
<ion-icon slot="end" name="rocket-outline"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="app && app.id && vars.status !== 'INSTALLING'">
|
||||
<ion-item-group class="ion-padding-bottom">
|
||||
<ion-item-divider>Tor Address</ion-item-divider>
|
||||
<ion-item lines="none">
|
||||
<ion-label style="display: flex; justify-content: space-between; align-items: center;" class="ion-text-wrap">
|
||||
<p style="color: var(--ion-color-dark)">{{ vars.torAddress }}</p>
|
||||
<ion-button slot="end" fill="clear" (click)="copyTor()">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
<!-- addresses -->
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>Tor Address</h2>
|
||||
<p>{{ vars.torAddress }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copyTor()">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>LAN Address</h2>
|
||||
<p *ngIf="!hideLAN">{{ vars.lanAddress }}</p>
|
||||
<p *ngIf="hideLAN"><ion-text color="warning">No LAN address for {{ vars.title }} {{ vars.versionInstalled }}</ion-text></p>
|
||||
</ion-label>
|
||||
<ion-button *ngIf="!hideLAN" slot="end" fill="clear" (click)="copyLAN()">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Backups</ion-item-divider>
|
||||
<!-- backups -->
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<!-- create backup -->
|
||||
<ion-item button [disabled]="[AppStatus.RESTORING_BACKUP, AppStatus.CREATING_BACKUP, AppStatus.INSTALLING, AppStatus.RESTARTING, AppStatus.STOPPING] | includes: vars.status" (click)="presentModalBackup('create')">
|
||||
<ion-icon slot="start" name="save-outline" color="primary"></ion-icon>
|
||||
<ion-label style="display: flex; flex-direction: column;">
|
||||
<ion-text color="primary">Create new Backup</ion-text>
|
||||
<ion-text color="primary">Create Backup</ion-text>
|
||||
<ion-text color="medium" style="font-size: x-small">
|
||||
Last Backup: {{vars.lastBackup ? (vars.lastBackup | date: 'short') : 'never'}}
|
||||
</ion-text>
|
||||
@@ -120,12 +126,7 @@
|
||||
<ion-label><ion-text color="primary">Restore from Backup</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>General</ion-item-divider>
|
||||
<!-- instructions -->
|
||||
<ion-item button details="true" (click)="checkForUpdates()">
|
||||
<ion-icon slot="start" name="refresh-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text style="font-weight: 500;" color="primary">Check for Updates</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<!-- instructions -->
|
||||
<ion-item [routerLink]="['instructions']">
|
||||
<ion-icon slot="start" name="list-outline" color="primary"></ion-icon>
|
||||
@@ -141,6 +142,11 @@
|
||||
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Properties</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- actions -->
|
||||
<ion-item [routerLink]="['actions']">
|
||||
<ion-icon slot="start" name="flash-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Actions</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- logs -->
|
||||
<ion-item [disabled]="vars.status === AppStatus.INSTALLING" [routerLink]="['logs']">
|
||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||
@@ -148,7 +154,7 @@
|
||||
</ion-item>
|
||||
<!-- marketplace -->
|
||||
<ion-item lines="none" [routerLink]="['/services', 'marketplace', appId]">
|
||||
<ion-icon slot="start" name="cart-outline" color="primary"></ion-icon>
|
||||
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
|
||||
@@ -40,22 +40,11 @@
|
||||
}
|
||||
|
||||
.launch-button {
|
||||
width: 100%;
|
||||
padding: 0px 10px;
|
||||
--background: linear-gradient(200deg, rgb(70 193 255), rgb(70 193 255 / 45%));
|
||||
width: calc(100% - 32px);
|
||||
border-radius: 8px;
|
||||
--border-radius: 8px;
|
||||
}
|
||||
|
||||
.launch-button-off {
|
||||
--background: #383838;
|
||||
color: var(--ion-color-medium)
|
||||
}
|
||||
|
||||
.launch-explanation-button {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: -2px;
|
||||
--border-radius: 100px;
|
||||
--background: rgb(70 193 255 / 75%);
|
||||
--background-hover: rgb(70 193 255);
|
||||
--background-hover-opacity: 100%;
|
||||
--border-style: none;
|
||||
--color: white;
|
||||
--border-radius: 10px;
|
||||
margin: 12px 10px;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@Component({
|
||||
@@ -34,13 +32,10 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
appId: string
|
||||
AppStatus = AppStatus
|
||||
showInstructions = false
|
||||
isConsulate: boolean
|
||||
isTor: boolean
|
||||
|
||||
hideLAN: boolean
|
||||
|
||||
dependencyDefintion = () => `<span style="font-style: italic">Dependencies</span> are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}`
|
||||
launchDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. If a service does not have this button, you must access it using another interface, such as a mobile app, desktop app, or another service on the Embassy. Please view the instructions for a service for details on how to use it.</p>`
|
||||
launchOffDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Get your service running in order to launch!</p>`
|
||||
launchLocalDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Visit your Embassy at its Tor address to launch this service!</p>`
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
@@ -56,12 +51,9 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly appModel: AppModel,
|
||||
private readonly popoverController: PopoverController,
|
||||
private readonly emver: Emver,
|
||||
config: ConfigService,
|
||||
private readonly config: ConfigService,
|
||||
) {
|
||||
super()
|
||||
this.isConsulate = config.isConsulateIos || config.isConsulateAndroid
|
||||
this.isTor = config.isTor()
|
||||
}
|
||||
|
||||
async ngOnInit () {
|
||||
@@ -70,8 +62,12 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
this.cleanup(
|
||||
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId))
|
||||
.pipe(
|
||||
tap(app => this.app = app),
|
||||
concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack
|
||||
tap(app => {
|
||||
this.app = app
|
||||
const appP = peekProperties(this.app)
|
||||
this.hideLAN = !appP.lanAddress || (appP.id === 'mastodon' && appP.versionInstalled === '3.3.0') // @TODO delete this hack in 0.3.0
|
||||
}),
|
||||
concatMap(() => this.syncWhenDependencyInstalls()), // must be final in stack
|
||||
catchError(e => of(this.setError(e))),
|
||||
).subscribe(),
|
||||
)
|
||||
@@ -103,61 +99,15 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
}
|
||||
|
||||
async launchUiTab () {
|
||||
let uiAddress = this.app.torAddress.getValue()
|
||||
uiAddress = uiAddress.startsWith('http') ? uiAddress : `http://${uiAddress}`
|
||||
let uiAddress: string
|
||||
if (this.config.isTor()) {
|
||||
uiAddress = `http://${this.app.torAddress.getValue()}`
|
||||
} else {
|
||||
uiAddress = `https://${this.app.lanAddress.getValue()}`
|
||||
}
|
||||
return window.open(uiAddress, '_blank')
|
||||
}
|
||||
|
||||
async checkForUpdates () {
|
||||
const app = peekProperties(this.app)
|
||||
|
||||
this.loader.of({
|
||||
message: `Checking for updates...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync(
|
||||
async () => {
|
||||
const { versionLatest } = await this.apiService.getAvailableApp(this.appId)
|
||||
if (this.emver.compare(versionLatest, app.versionInstalled) === 1) {
|
||||
this.presentAlertUpdate(app, versionLatest)
|
||||
} else {
|
||||
this.presentAlertUpToDate()
|
||||
}
|
||||
},
|
||||
).catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
async presentAlertUpdate (app: AppInstalledFull, versionLatest: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: 'Update Available',
|
||||
message: `New version ${displayEmver(versionLatest)} found for ${app.title}.`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'View in Store',
|
||||
cssClass: 'alert-success',
|
||||
handler: () => {
|
||||
this.navCtrl.navigateForward(['/services', 'marketplace', this.appId])
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertUpToDate () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Up To Date',
|
||||
message: `You are running the latest version of ${this.app.title.getValue()}!`,
|
||||
buttons: ['OK'],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async copyTor () {
|
||||
const app = peekProperties(this.app)
|
||||
let message = ''
|
||||
@@ -172,6 +122,20 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async copyLAN () {
|
||||
const app = peekProperties(this.app)
|
||||
let message = ''
|
||||
await copyToClipboard(app.lanAddress).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy' })
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async stop (): Promise<void> {
|
||||
const app = peekProperties(this.app)
|
||||
|
||||
@@ -200,15 +164,13 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
}).catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
async tryStart (): Promise<void> {
|
||||
const app = peekProperties(this.app)
|
||||
this.loader.of({
|
||||
message: `Starting ${app.title}...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringP(
|
||||
this.apiService.startApp(this.appId),
|
||||
).catch(e => this.setError(e))
|
||||
if (app.startAlert) {
|
||||
this.presentAlertStart(app)
|
||||
} else {
|
||||
this.start(app)
|
||||
}
|
||||
}
|
||||
|
||||
async presentModalBackup (type: 'create' | 'restore') {
|
||||
@@ -275,18 +237,6 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
return this.navCtrl.navigateRoot('/services/installed')
|
||||
}
|
||||
|
||||
async presentLaunchPopover (status: AppStatus, ev: any) {
|
||||
let desc: string
|
||||
if (!this.isTor) {
|
||||
desc = this.launchLocalDefinition
|
||||
} else if (status !== AppStatus.RUNNING) {
|
||||
desc = this.launchOffDefinition
|
||||
} else {
|
||||
desc = this.launchDefinition
|
||||
}
|
||||
return this.presentPopover(desc, ev)
|
||||
}
|
||||
|
||||
async presentPopover (information: string, ev: any) {
|
||||
const popover = await this.popoverController.create({
|
||||
component: InformationPopoverComponent,
|
||||
@@ -301,8 +251,39 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
return await popover.present()
|
||||
}
|
||||
|
||||
private setError (e: Error) {
|
||||
private async presentAlertStart (app: AppInstalledFull): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: app.startAlert,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Start',
|
||||
handler: () => {
|
||||
this.start(app)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async start (app: AppInstalledFull): Promise<void> {
|
||||
this.loader.of({
|
||||
message: `Starting ${app.title}...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringP(
|
||||
this.apiService.startApp(this.appId),
|
||||
).catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
private setError (e: Error): Observable<void> {
|
||||
this.$error$.next(e.message)
|
||||
return of()
|
||||
}
|
||||
|
||||
private clearError () {
|
||||
|
||||
@@ -2,10 +2,8 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { AppInstructionsPage } from './app-instructions.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -21,7 +19,6 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppInstructionsPage],
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Instructions</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ export class AppInstructionsPage {
|
||||
error = ''
|
||||
app: AppInstalledFull = { } as any
|
||||
appId: string
|
||||
instructions: any
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { AppLogsPage } from './app-logs.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -21,7 +18,6 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [AppLogsPage],
|
||||
})
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<ion-button (click)="getLogs()" color="primary">
|
||||
<ion-icon name="refresh-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { AppMetricsPage } from './app-metrics.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@@ -23,7 +20,6 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
QRComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Properties</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
|
||||
@@ -130,7 +130,6 @@ export class AppMetricsPage {
|
||||
|
||||
toggleMask (key: string) {
|
||||
this.unmasked[key] = !this.unmasked[key]
|
||||
console.log(this.unmasked)
|
||||
}
|
||||
|
||||
asIsOrder (a: any, b: any) {
|
||||
|
||||
@@ -43,6 +43,10 @@ const routes: Routes = [
|
||||
path: 'installed/:appId/metrics',
|
||||
loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:appId/actions',
|
||||
loadChildren: () => import('./app-actions/app-actions.module').then(m => m.AppActionsPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -4,7 +4,6 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { DevOptionsPage } from './dev-options.page'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -21,7 +20,6 @@ const routes: Routes = [
|
||||
ObjectConfigComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [DevOptionsPage],
|
||||
})
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Developer Options</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { DevSSHKeysPage } from './dev-ssh-keys.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,7 +18,6 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [DevSSHKeysPage],
|
||||
})
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>SSH Keys</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ExternalDrivesPage } from './external-drives.page'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
|
||||
// TODO: EJECT-DISKS
|
||||
|
||||
@@ -24,7 +23,6 @@ const routes: Routes = [
|
||||
ObjectConfigComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [ExternalDrivesPage],
|
||||
})
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Backup drives</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { LANPage } from './lan.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -20,7 +19,6 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [LANPage],
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Secure LAN Setup</ion-title>
|
||||
<ion-title>LAN Setup</ion-title>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
@@ -11,54 +8,60 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item lines="none" style="font-size: small; --background: var(--ion-background-color);">
|
||||
<ion-label size="small" class="ion-text-wrap">
|
||||
<ion-text color="medium">For a <ion-text style="font-style: italic;">faster</ion-text> experience, you can also securely communicate with your Embassy by visiting its Local Area Network (LAN) address.</ion-text>
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
If you are having issues connecting to your Embassy or services over LAN, you can try refreshing the network by clicking the button below.
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- info -->
|
||||
<ion-item>
|
||||
<ion-button slot="start" fill="clear" (click)="refreshLAN()">
|
||||
<ion-icon slot="start" name="refresh-outline"></ion-icon>
|
||||
Refresh Network
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>About</ion-item-divider>
|
||||
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2><ion-text color="warning">Instructions</ion-text></h2>
|
||||
<ng-container *ngIf="!lanDisabled">
|
||||
<ul style="font-size: smaller">
|
||||
<li>Download your Embassy's SSL Certificate Authority by clicking the download button below.</li>
|
||||
<li>Install and trust the CA.</li>
|
||||
<li>Connect this device to the same network as the Embassy. This should be your private home network.</li>
|
||||
<li>Navigate to your Embassy LAN address, indicated below.</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<div *ngIf="lanDisabled" class="ion-padding-top ion-padding-bottom">
|
||||
<p [innerHtml]="lanDisabledExplanation[lanDisabled]"></p>
|
||||
</div>
|
||||
<a *ngIf="!isConsulate" [href]="fullDocumentationLink" target="_blank">full documentation</a>
|
||||
<ion-button *ngIf="isConsulate" fill="outline" (click)="copyDocumentation()">full documentation</ion-button>
|
||||
|
||||
You can connect to your Embassy over your Local Area Network (LAN). This can be useful for achieving a faster experience, as well as a fallback in case the Tor network is experiencing issues.
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider style="margin-top: 0px"></ion-item-divider>
|
||||
<!-- Certificate -->
|
||||
<ion-item [disabled]="!!lanDisabled">
|
||||
<ion-item *ngIf="lanDisabled">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>SSL Certificate</h2>
|
||||
<p>Embassy Local CA</p>
|
||||
<ion-text color="warning" [innerHtml]="lanDisabledExplanation[lanDisabled]"></ion-text>
|
||||
</ion-label>
|
||||
<ion-button [disabled]="!!lanDisabled" slot="end" fill="clear" (click)="installCert()">
|
||||
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<!-- URL -->
|
||||
<ion-item [disabled]="!!lanDisabled" >
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>LAN Address</h2>
|
||||
<a [href]="lanAddress" target="_blank">{{ lanAddress }}</a>
|
||||
</ion-label>
|
||||
<ion-button [disabled]="!!lanDisabled" slot="end" fill="clear" (click)="copyLAN()">
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-item>
|
||||
<ion-button slot="start" fill="clear" color="primary">View Instructions</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="!lanDisabled">
|
||||
<ion-item-divider>Certificate and Address</ion-item-divider>
|
||||
<!-- Certificate -->
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>Root Certificate Authority</h2>
|
||||
<p>Embassy Local CA</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="installCert()">
|
||||
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<!-- URL -->
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>LAN Address</h2>
|
||||
<p>{{ lanAddress }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copyLAN()">
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- hidden element for downloading cert -->
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.tiny-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { isPlatform, ToastController } from '@ionic/angular'
|
||||
import { ServerModel } from 'src/app/models/server-model'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'lan',
|
||||
@@ -10,23 +12,23 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
styleUrls: ['./lan.page.scss'],
|
||||
})
|
||||
export class LANPage {
|
||||
torDocs = 'docs.privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion/user-manuals/embassyos/general/secure-lan'
|
||||
lanDocs = 'docs.start9labs.com/user-manuals/embassyos/general/secure-lan'
|
||||
torDocs = 'docs.privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion/user-manual/general/secure-lan'
|
||||
lanDocs = 'docs.start9labs.com/user-manual/general/secure-lan'
|
||||
|
||||
lanAddress: string
|
||||
isTor: boolean
|
||||
fullDocumentationLink: string
|
||||
isConsulate: boolean
|
||||
lanDisabled: LanSetupIssue = undefined
|
||||
lanDisabled: LanSetupIssue
|
||||
readonly lanDisabledExplanation: { [k in LanSetupIssue]: string } = {
|
||||
NotDesktop: `We have detected you are on a mobile device. To setup LAN on a mobile device, use the Start9 Setup App.`,
|
||||
NotTor: `We have detected you are not using a Tor connection. For security reasons, you must setup LAN over a Tor connection.<br /><br/>Navigate to your Embassy Tor Address and try again.`,
|
||||
NotTor: `We have detected you are not using a Tor connection. For security reasons, you must setup LAN over a Tor connection. Please navigate to your Embassy Tor Address and try again.`,
|
||||
}
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly config: ConfigService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly apiService: ApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
@@ -36,8 +38,6 @@ export class LANPage {
|
||||
this.lanDisabled = 'NotTor'
|
||||
}
|
||||
|
||||
this.isConsulate = this.config.isConsulateIos || this.config.isConsulateAndroid
|
||||
|
||||
if (this.config.isTor()) {
|
||||
this.fullDocumentationLink = `http://${this.torDocs}`
|
||||
} else {
|
||||
@@ -48,7 +48,19 @@ export class LANPage {
|
||||
this.lanAddress = `https://${server.serverId}.local`
|
||||
}
|
||||
|
||||
async copyLAN (): Promise < void > {
|
||||
async refreshLAN (): Promise<void> {
|
||||
this.loader.of({
|
||||
message: 'Refreshing Network',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync( async () => {
|
||||
await this.apiService.refreshLAN()
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
|
||||
async copyLAN (): Promise <void> {
|
||||
const message = await copyToClipboard(this.lanAddress).then(success => success ? 'copied to clipboard!' : 'failed to copy')
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
@@ -79,4 +91,4 @@ export class LANPage {
|
||||
}
|
||||
}
|
||||
|
||||
type LanSetupIssue = 'NotTor' | 'NotDesktop'
|
||||
type LanSetupIssue = 'NotTor' | 'NotDesktop'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user