diff --git a/consts/consts.go b/consts/consts.go new file mode 100644 index 00000000..64712255 --- /dev/null +++ b/consts/consts.go @@ -0,0 +1,14 @@ +package consts + +import "time" + +const ( + InitialSetupFlagKey = "InitialSetupKey" + + JWTSecretKey = "JWTSecretKey" + JWTIssuer = "CloudSonic" + JWTTokenExpiration = 30 * time.Minute + + InitialUserName = "admin" + InitialName = "Admin" +) diff --git a/server/app/app.go b/server/app/app.go index 49320c08..462712b0 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -2,18 +2,15 @@ package app import ( "context" - "fmt" "net/http" "net/url" "strings" - "github.com/cloudsonic/sonic-server/log" "github.com/cloudsonic/sonic-server/model" "github.com/cloudsonic/sonic-server/server" "github.com/deluan/rest" "github.com/go-chi/chi" "github.com/go-chi/jwtauth" - "github.com/google/uuid" ) var initialUser = model.User{ @@ -31,7 +28,6 @@ type Router struct { func New(ds model.DataStore, path string) *Router { r := &Router{ds: ds, path: path} r.mux = r.routes() - r.createDefaultUser() return r } @@ -61,21 +57,6 @@ func (app *Router) routes() http.Handler { return r } -func (app *Router) createDefaultUser() { - c, err := app.ds.User().CountAll() - if err != nil { - panic(fmt.Sprintf("Could not access User table: %s", err)) - } - if c == 0 { - id, _ := uuid.NewRandom() - initialPassword, _ := uuid.NewRandom() - log.Warn("Creating initial user. Please change the password!", "user", initialUser.UserName, "password", initialPassword) - initialUser.ID = id.String() - initialUser.Password = initialPassword.String() - app.ds.User().Put(&initialUser) - } -} - func R(r chi.Router, pathPrefix string, newRepository rest.RepositoryConstructor) { r.Route(pathPrefix, func(r chi.Router) { r.Get("/", rest.GetAll(newRepository)) diff --git a/server/app/auth.go b/server/app/auth.go index 085806c3..b46e893c 100644 --- a/server/app/auth.go +++ b/server/app/auth.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "net/http" - "os" "strings" + "sync" "time" + "github.com/cloudsonic/sonic-server/consts" "github.com/cloudsonic/sonic-server/model" "github.com/deluan/rest" "github.com/dgrijalva/jwt-go" @@ -16,16 +17,14 @@ import ( ) var ( - tokenExpiration = 30 * time.Minute - issuer = "CloudSonic" -) - -var ( + once sync.Once jwtSecret []byte TokenAuth *jwtauth.JWTAuth ) func Login(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) { + initTokenAuth(ds) + return func(w http.ResponseWriter, r *http.Request) { data := make(map[string]string) decoder := json.NewDecoder(r.Body) @@ -56,11 +55,22 @@ func Login(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) { map[string]interface{}{ "message": "User '" + username + "' authenticated successfully", "token": tokenString, - "user": strings.Title(user.UserName), + "name": strings.Title(user.Name), "username": username, }) } } + +func initTokenAuth(ds model.DataStore) { + once.Do(func() { + secret, err := ds.Property().DefaultGet(consts.JWTSecretKey, "not so secret") + if err != nil { + log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err) + } + jwtSecret = []byte(secret) + TokenAuth = jwtauth.New("HS256", jwtSecret, nil) + }) +} func validateLogin(userRepo model.UserRepository, userName, password string) (*model.User, error) { u, err := userRepo.FindByUsername(userName) if err == model.ErrNotFound { @@ -86,14 +96,14 @@ func validateLogin(userRepo model.UserRepository, userName, password string) (*m func createToken(u *model.User) (string, error) { token := jwt.New(jwt.SigningMethodHS256) claims := token.Claims.(jwt.MapClaims) - claims["iss"] = issuer + claims["iss"] = consts.JWTIssuer claims["sub"] = u.UserName return touchToken(token) } func touchToken(token *jwt.Token) (string, error) { - expireIn := time.Now().Add(tokenExpiration).Unix() + expireIn := time.Now().Add(consts.JWTTokenExpiration).Unix() claims := token.Claims.(jwt.MapClaims) claims["exp"] = expireIn @@ -135,14 +145,3 @@ func Authenticator(next http.Handler) http.Handler { next.ServeHTTP(w, r.WithContext(newCtx)) }) } - -func init() { - // TODO Store jwtSecret in the DB - secret := os.Getenv("JWT_SECRET") - if secret == "" { - secret = "not so secret" - log.Warn("No JWT_SECRET env var found. Please set one.") - } - jwtSecret = []byte(secret) - TokenAuth = jwtauth.New("HS256", jwtSecret, nil) -} diff --git a/server/initial_setup.go b/server/initial_setup.go new file mode 100644 index 00000000..f4ce6296 --- /dev/null +++ b/server/initial_setup.go @@ -0,0 +1,69 @@ +package server + +import ( + "fmt" + "time" + + "github.com/cloudsonic/sonic-server/consts" + "github.com/cloudsonic/sonic-server/log" + "github.com/cloudsonic/sonic-server/model" + "github.com/google/uuid" +) + +func initialSetup(ds model.DataStore) { + _ = ds.WithTx(func(tx model.DataStore) error { + _, err := ds.Property().Get(consts.InitialSetupFlagKey) + if err == nil { + return nil + } + log.Warn("Running initial setup") + if err = createDefaultUser(ds); err != nil { + return err + } + if err = createJWTSecret(ds); err != nil { + return err + } + + err = ds.Property().Put(consts.InitialSetupFlagKey, time.Now().String()) + return err + }) +} + +func createJWTSecret(ds model.DataStore) error { + _, err := ds.Property().Get(consts.JWTSecretKey) + if err == nil { + return nil + } + jwtSecret, _ := uuid.NewRandom() + log.Warn("Creating JWT secret, used for encrypting UI sessions") + err = ds.Property().Put(consts.JWTSecretKey, jwtSecret.String()) + if err != nil { + log.Error("Could not save JWT secret in DB", err) + } + return err +} + +func createDefaultUser(ds model.DataStore) error { + c, err := ds.User().CountAll() + if err != nil { + panic(fmt.Sprintf("Could not access User table: %s", err)) + } + if c == 0 { + id, _ := uuid.NewRandom() + initialPassword, _ := uuid.NewRandom() + log.Warn("Creating initial user. Please change the password!", "user", consts.InitialUserName, "password", initialPassword) + initialUser := model.User{ + ID: id.String(), + UserName: consts.InitialUserName, + Name: consts.InitialName, + Email: "", + Password: initialPassword.String(), + IsAdmin: true, + } + err := ds.User().Put(&initialUser) + if err != nil { + log.Error("Could not create initial user", "user", initialUser, err) + } + } + return err +} diff --git a/server/server.go b/server/server.go index cfd283c4..7346c982 100644 --- a/server/server.go +++ b/server/server.go @@ -8,6 +8,7 @@ import ( "github.com/cloudsonic/sonic-server/conf" "github.com/cloudsonic/sonic-server/log" + "github.com/cloudsonic/sonic-server/model" "github.com/cloudsonic/sonic-server/scanner" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" @@ -19,14 +20,16 @@ const Version = "0.2" type Server struct { Scanner *scanner.Scanner router *chi.Mux + ds model.DataStore } -func New(scanner *scanner.Scanner) *Server { - a := &Server{Scanner: scanner} +func New(scanner *scanner.Scanner, ds model.DataStore) *Server { + a := &Server{Scanner: scanner, ds: ds} if !conf.Sonic.DevDisableBanner { showBanner(Version) } initMimeTypes() + initialSetup(ds) a.initRoutes() a.initScanner() return a diff --git a/wire_gen.go b/wire_gen.go index c151ff6a..65bb0031 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -20,7 +20,7 @@ import ( func CreateServer(musicFolder string) *server.Server { dataStore := persistence.New() scannerScanner := scanner.New(dataStore) - serverServer := server.New(scannerScanner) + serverServer := server.New(scannerScanner, dataStore) return serverServer }