diff --git a/api.go b/api.go
index 25bae7db..c99e89ba 100644
--- a/api.go
+++ b/api.go
@@ -222,6 +222,8 @@ func (api *API) Start(startRequest *models.StartRequest) error {
}
func (api *API) Setup(setupRequest *models.SetupRequest) error {
+ api.svc.cfg.SavePasswordCheck(setupRequest.UnlockPassword)
+
// only update non-empty values
if setupRequest.LNBackendType != "" {
api.svc.cfg.SetUpdate("LNBackendType", setupRequest.LNBackendType, "")
diff --git a/config.go b/config.go
index 4c60c12b..79d5e13a 100644
--- a/config.go
+++ b/config.go
@@ -12,9 +12,11 @@ import (
)
const (
- LNDBackendType = "LND"
- BreezBackendType = "BREEZ"
- CookieName = "alby_nwc_session"
+ LNDBackendType = "LND"
+ BreezBackendType = "BREEZ"
+ SessionCookieName = "session"
+ SessionCookieAuthKey = "authenticated"
+ UnlockPasswordCheck = "THIS STRING SHOULD MATCH IF PASSWORD IS CORRECT"
)
type AppConfig struct {
@@ -127,6 +129,16 @@ func (cfg *Config) SetUpdate(key string, value string, encryptionKey string) {
cfg.set(key, value, clauses, encryptionKey)
}
+func (cfg *Config) CheckUnlockPassword(encryptionKey string) bool {
+ decryptedValue, err := cfg.Get("UnlockPasswordCheck", encryptionKey)
+
+ return err == nil && decryptedValue == UnlockPasswordCheck
+}
+
+func (cfg *Config) SavePasswordCheck(encryptionKey string) {
+ cfg.SetUpdate("UnlockPasswordCheck", UnlockPasswordCheck, encryptionKey)
+}
+
func randomHex(n int) (string, error) {
bytes := make([]byte, n)
if _, err := rand.Read(bytes); err != nil {
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 77da9538..8bf0f723 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -17,6 +17,8 @@ import Start from "src/screens/Start";
import { AppsRedirect } from "src/components/redirects/AppsRedirect";
import { StartRedirect } from "src/components/redirects/StartRedirect";
import { HomeRedirect } from "src/components/redirects/HomeRedirect";
+import Unlock from "src/screens/Unlock";
+import { SetupRedirect } from "src/components/redirects/SetupRedirect";
function App() {
return (
@@ -35,7 +37,7 @@ function App() {
}
>
+ To continue, please enter your unlock password +
+ + > + ); +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 2afbb156..f1b1978e 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -90,6 +90,7 @@ export interface InfoResponse { backendType: BackendType; setupCompleted: boolean; running: boolean; + unlocked: boolean; } export interface CreateAppResponse { diff --git a/http_service.go b/http_service.go index 743e937b..50742db3 100644 --- a/http_service.go +++ b/http_service.go @@ -29,11 +29,9 @@ func NewHttpService(svc *Service) *HttpService { func (httpSvc *HttpService) validateUserMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - // TODO: check if login is required and check if user is logged in - //sess, _ := session.Get(CookieName, c) - // if user == nil { - // return c.NoContent(http.StatusUnauthorized) - // } + if !httpSvc.isUnlocked(c) { + return c.NoContent(http.StatusUnauthorized) + } return next(c) } } @@ -58,7 +56,11 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { e.GET("/api/info", httpSvc.infoHandler) e.POST("/api/logout", httpSvc.logoutHandler) e.POST("/api/setup", httpSvc.setupHandler) - e.POST("/api/start", httpSvc.startHandler) + + // allow one unlock request per second + unlockRateLimiter := middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(1)) + e.POST("/api/start", httpSvc.startHandler, unlockRateLimiter) + e.POST("/api/unlock", httpSvc.unlockHandler, unlockRateLimiter) frontend.RegisterHandlers(e) } @@ -75,6 +77,7 @@ func (httpSvc *HttpService) csrfHandler(c echo.Context) error { func (httpSvc *HttpService) infoHandler(c echo.Context) error { responseBody := httpSvc.api.GetInfo() + responseBody.Unlocked = httpSvc.isUnlocked(c) return c.JSON(http.StatusOK, responseBody) } @@ -92,11 +95,65 @@ func (httpSvc *HttpService) startHandler(c echo.Context) error { Message: fmt.Sprintf("Failed to start node: %s", err.Error()), }) } + + err = httpSvc.saveSessionCookie(c) + + if err != nil { + return c.JSON(http.StatusInternalServerError, ErrorResponse{ + Message: fmt.Sprintf("Failed to save session: %s", err.Error()), + }) + } + return c.NoContent(http.StatusNoContent) } +func (httpSvc *HttpService) unlockHandler(c echo.Context) error { + var unlockRequest api.UnlockRequest + if err := c.Bind(&unlockRequest); err != nil { + return c.JSON(http.StatusBadRequest, ErrorResponse{ + Message: fmt.Sprintf("Bad request: %s", err.Error()), + }) + } + + if !httpSvc.svc.cfg.CheckUnlockPassword(unlockRequest.UnlockPassword) { + return c.JSON(http.StatusUnauthorized, ErrorResponse{ + Message: "Invalid password", + }) + } + + err := httpSvc.saveSessionCookie(c) + + if err != nil { + return c.JSON(http.StatusInternalServerError, ErrorResponse{ + Message: fmt.Sprintf("Failed to save session: %s", err.Error()), + }) + } + + return c.NoContent(http.StatusNoContent) +} + +func (httpSvc *HttpService) isUnlocked(c echo.Context) bool { + sess, _ := session.Get(SessionCookieName, c) + return sess.Values[SessionCookieAuthKey] == true +} + +func (httpSvc *HttpService) saveSessionCookie(c echo.Context) error { + sess, _ := session.Get("session", c) + sess.Options = &sessions.Options{ + Path: "/", + MaxAge: 86400 * 7, + HttpOnly: true, + } + sess.Values[SessionCookieAuthKey] = true + err := sess.Save(c.Request(), c.Response()) + if err != nil { + httpSvc.svc.Logger.Errorf("Failed to save session: %v", err) + } + return err +} + func (httpSvc *HttpService) logoutHandler(c echo.Context) error { - sess, err := session.Get(CookieName, c) + sess, err := session.Get(SessionCookieName, c) if err != nil { return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: "Failed to get session", @@ -195,6 +252,10 @@ func (httpSvc *HttpService) setupHandler(c echo.Context) error { }) } + if httpSvc.svc.lnClient != nil && !httpSvc.isUnlocked(c) { + return c.NoContent(http.StatusUnauthorized) + } + err := httpSvc.api.Setup(&setupRequest) if err != nil { return c.JSON(http.StatusInternalServerError, ErrorResponse{ diff --git a/models/api/api.go b/models/api/api.go index ee348e80..5a41e96f 100644 --- a/models/api/api.go +++ b/models/api/api.go @@ -36,6 +36,10 @@ type StartRequest struct { UnlockPassword string `json:"unlockPassword"` } +type UnlockRequest struct { + UnlockPassword string `json:"unlockPassword"` +} + type SetupRequest struct { LNBackendType string `json:"backendType"` // Breez fields @@ -67,4 +71,5 @@ type InfoResponse struct { BackendType string `json:"backendType"` SetupCompleted bool `json:"setupCompleted"` Running bool `json:"running"` + Unlocked bool `json:"unlocked"` } diff --git a/start.go b/start.go index 844cacfc..a62d9816 100644 --- a/start.go +++ b/start.go @@ -1,6 +1,7 @@ package main import ( + "errors" "time" "github.com/nbd-wtf/go-nostr" @@ -104,6 +105,11 @@ func (svc *Service) StartNostr(encryptionKey string) error { } func (svc *Service) StartApp(encryptionKey string) error { + if !svc.cfg.CheckUnlockPassword(encryptionKey) { + svc.Logger.Errorf("Invalid password") + return errors.New("Invalid password") + } + err := svc.launchLNBackend(encryptionKey) if err != nil { svc.Logger.Errorf("Failed to launch LN backend: %v", err)