Installer: Refactor and give each searver its own directory
Change-Id: I1db2929e7a35b6f92022dec0c6506d68e0297563
diff --git a/core/installer/server/welcome/server.go b/core/installer/server/welcome/server.go
new file mode 100644
index 0000000..198f637
--- /dev/null
+++ b/core/installer/server/welcome/server.go
@@ -0,0 +1,298 @@
+package welcome
+
+import (
+ "bytes"
+ "embed"
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "net/http"
+ "os"
+
+ "github.com/gorilla/mux"
+
+ "github.com/giolekva/pcloud/core/installer"
+ "github.com/giolekva/pcloud/core/installer/server"
+ "github.com/giolekva/pcloud/core/installer/soft"
+)
+
+//go:embed templates/*
+var tmpls embed.FS
+
+//go:embed static/*
+var staticAssets embed.FS
+
+type templates struct {
+ createAccount *template.Template
+ createAccountSuccess *template.Template
+}
+
+func parseTemplatesWelcome(fs embed.FS) (templates, error) {
+ base, err := template.New("base.html").ParseFS(fs, "templates/base.html")
+ if err != nil {
+ return templates{}, err
+ }
+ parse := func(path string) (*template.Template, error) {
+ if b, err := base.Clone(); err != nil {
+ return nil, err
+ } else {
+ return b.ParseFS(fs, path)
+ }
+ }
+ createAccount, err := parse("templates/create-account.html")
+ if err != nil {
+ return templates{}, err
+ }
+ createAccountSuccess, err := parse("templates/create-account-success.html")
+ if err != nil {
+ return templates{}, err
+ }
+ return templates{createAccount, createAccountSuccess}, nil
+}
+
+type Server struct {
+ port int
+ repo soft.RepoIO
+ nsCreator installer.NamespaceCreator
+ hf installer.HelmFetcher
+ createAccountAddr string
+ loginAddr string
+ membershipsAddr string
+ tmpl templates
+}
+
+func NewServer(
+ port int,
+ repo soft.RepoIO,
+ nsCreator installer.NamespaceCreator,
+ hf installer.HelmFetcher,
+ createAccountAddr string,
+ loginAddr string,
+ membershipsAddr string,
+) (*Server, error) {
+ tmplts, err := parseTemplatesWelcome(tmpls)
+ if err != nil {
+ return nil, err
+ }
+ return &Server{
+ port,
+ repo,
+ nsCreator,
+ hf,
+ createAccountAddr,
+ loginAddr,
+ membershipsAddr,
+ tmplts,
+ }, nil
+}
+
+func (s *Server) Start() {
+ r := mux.NewRouter()
+ r.PathPrefix("/static/").Handler(server.NewCachingHandler(http.FileServer(http.FS(staticAssets))))
+ r.Path("/").Methods("POST").HandlerFunc(s.createAccount)
+ r.Path("/").Methods("GET").HandlerFunc(s.createAccountForm)
+ http.Handle("/", r)
+ log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
+}
+
+func (s *Server) createAccountForm(w http.ResponseWriter, r *http.Request) {
+ s.renderRegistrationForm(w, formData{})
+}
+
+type formData struct {
+ UsernameErrors []string
+ PasswordErrors []string
+ Data createAccountReq
+}
+
+type cpFormData struct {
+ UsernameErrors []string
+ PasswordErrors []string
+ Password string
+}
+
+type createAccountReq struct {
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+ SecretToken string `json:"secretToken,omitempty"`
+}
+
+type apiCreateAccountReq struct {
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+}
+
+type ValidationError struct {
+ Field string `json:"field"`
+ Message string `json:"message"`
+}
+
+type ErrorResponse struct {
+ Errors []ValidationError `json:"errors"`
+}
+
+func extractReq(r *http.Request) (createAccountReq, error) {
+ var req createAccountReq
+ if err := func() error {
+ var err error
+ if err = r.ParseForm(); err != nil {
+ return err
+ }
+ if req.Username, err = server.GetFormValue(r.PostForm, "username"); err != nil {
+ return err
+ }
+ if req.Password, err = server.GetFormValue(r.PostForm, "password"); err != nil {
+ return err
+ }
+ if req.SecretToken, err = server.GetFormValue(r.PostForm, "secret-token"); err != nil {
+ return err
+ }
+ return nil
+ }(); err != nil {
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ return createAccountReq{}, err
+ }
+ }
+ return req, nil
+}
+
+func (s *Server) renderRegistrationForm(w http.ResponseWriter, data formData) {
+ if err := s.tmpl.createAccount.Execute(w, data); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func (s *Server) renderRegistrationSuccess(w http.ResponseWriter, loginAddr string) {
+ data := struct {
+ LoginAddr string
+ }{
+ LoginAddr: loginAddr,
+ }
+ if err := s.tmpl.createAccountSuccess.Execute(w, data); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func (s *Server) createAccount(w http.ResponseWriter, r *http.Request) {
+ req, err := extractReq(r)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ {
+ var buf bytes.Buffer
+ cr := apiCreateAccountReq{req.Username, req.Password}
+ if err := json.NewEncoder(&buf).Encode(cr); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ resp, err := http.Post(s.createAccountAddr, "application/json", &buf)
+ if err != nil {
+ var respBody bytes.Buffer
+ if _, err := io.Copy(&respBody, resp.Body); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ respStr := respBody.String()
+ log.Println(respStr)
+ http.Error(w, respStr, http.StatusInternalServerError)
+ return
+ }
+ if resp.StatusCode != http.StatusOK {
+ var errResponse ErrorResponse
+ if err := json.NewDecoder(resp.Body).Decode(&errResponse); err != nil {
+ http.Error(w, "Error Decoding JSON", http.StatusInternalServerError)
+ return
+ }
+ var usernameErrors, passwordErrors []string
+ for _, err := range errResponse.Errors {
+ if err.Field == "username" {
+ usernameErrors = append(usernameErrors, err.Message)
+ }
+ if err.Field == "password" {
+ passwordErrors = append(passwordErrors, err.Message)
+ }
+ }
+ s.renderRegistrationForm(w, formData{
+ usernameErrors,
+ passwordErrors,
+ req,
+ })
+ return
+ }
+ }
+ if err := s.createUser(req.Username); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ s.renderRegistrationSuccess(w, s.loginAddr)
+}
+
+type firstAccount struct {
+ Created bool `json:"created"`
+ Domain string `json:"domain"`
+ Groups []string `json:"groups"`
+}
+
+type initRequest struct {
+ User string `json:"user"`
+ Email string `json:"email"`
+ Groups []string `json:"groups"`
+}
+
+type createUserRequest struct {
+ User string `json:"user"`
+ Email string `json:"email"`
+}
+
+func (s *Server) createUser(username string) error {
+ _, err := s.repo.Do(func(r soft.RepoFS) (string, error) {
+ var fa firstAccount
+ if err := soft.ReadYaml(r, "first-account.yaml", &fa); err != nil {
+ return "", err
+ }
+ var resp *http.Response
+ var err error
+ if fa.Created {
+ req := createUserRequest{username, fmt.Sprintf("%s@%s", username, fa.Domain)}
+ var buf bytes.Buffer
+ if err := json.NewEncoder(&buf).Encode(req); err != nil {
+ return "", err
+ }
+ resp, err = http.Post(
+ fmt.Sprintf("%s/api/users", s.membershipsAddr),
+ "applications/json",
+ &buf,
+ )
+ } else {
+ req := initRequest{username, fmt.Sprintf("%s@%s", username, fa.Domain), fa.Groups}
+ var buf bytes.Buffer
+ if err := json.NewEncoder(&buf).Encode(req); err != nil {
+ return "", err
+ }
+ resp, err = http.Post(
+ fmt.Sprintf("%s/api/init", s.membershipsAddr),
+ "applications/json",
+ &buf,
+ )
+ fa.Created = true
+ if err := soft.WriteYaml(r, "first-account.yaml", fa); err != nil {
+ return "", err
+ }
+ }
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ fmt.Printf("Memberships resp: %d", resp.StatusCode)
+ io.Copy(os.Stdout, resp.Body)
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("memberships error")
+ }
+ return "initialized groups for first account", nil
+ })
+ return err
+}