DodoApp: Public API to fetch allowed networks

Update landing page to communicate with dodo-app backend.

Change-Id: I269ad5150b9203eca9c1c9cc9a8a99b55c583419
diff --git a/core/installer/welcome/app_tmpl.go b/core/installer/welcome/app_tmpl.go
index 252d431..dae56f8 100644
--- a/core/installer/welcome/app_tmpl.go
+++ b/core/installer/welcome/app_tmpl.go
@@ -14,6 +14,7 @@
 const tmplSuffix = ".gotmpl"
 
 type AppTmplStore interface {
+	Types() []string
 	Find(appType string) (AppTmpl, error)
 }
 
@@ -40,6 +41,14 @@
 	return &appTmplStoreFS{apps}, nil
 }
 
+func (s *appTmplStoreFS) Types() []string {
+	var ret []string
+	for t := range s.tmpls {
+		ret = append(ret, t)
+	}
+	return ret
+}
+
 func (s *appTmplStoreFS) Find(appType string) (AppTmpl, error) {
 	if app, ok := s.tmpls[appType]; ok {
 		return app, nil
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 4535be2..e6f3d37 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -37,6 +37,8 @@
 	loginPath      = "/login"
 	logoutPath     = "/logout"
 	staticPath     = "/static"
+	apiPublicData  = "/api/public-data"
+	apiCreateApp   = "/api/apps"
 	sessionCookie  = "dodo-app-session"
 	userCtx        = "user"
 )
@@ -90,7 +92,6 @@
 	jc                installer.JobCreator
 	workers           map[string]map[string]struct{}
 	appNs             map[string]string
-	sc                *securecookie.SecureCookie
 	tmplts            dodoAppTmplts
 	appTmpls          AppTmplStore
 }
@@ -117,10 +118,6 @@
 	if err != nil {
 		return nil, err
 	}
-	sc := securecookie.New(
-		securecookie.GenerateRandomKey(64),
-		securecookie.GenerateRandomKey(32),
-	)
 	apps, err := fs.Sub(appTmplsFS, "app-tmpl")
 	if err != nil {
 		return nil, err
@@ -148,7 +145,6 @@
 		jc,
 		map[string]map[string]struct{}{},
 		map[string]string{},
-		sc,
 		tmplts,
 		appTmpls,
 	}
@@ -175,6 +171,8 @@
 		r.Use(s.mwAuth)
 		r.PathPrefix(staticPath).Handler(http.FileServer(http.FS(staticResources)))
 		r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
+		r.HandleFunc(apiPublicData, s.handleAPIPublicData)
+		r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
 		r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
 		r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
 		r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
@@ -184,10 +182,9 @@
 	}()
 	go func() {
 		r := mux.NewRouter()
-		r.HandleFunc("/update", s.handleApiUpdate)
-		r.HandleFunc("/api/apps/{app-name}/workers", s.handleApiRegisterWorker).Methods(http.MethodPost)
-		r.HandleFunc("/api/apps", s.handleApiCreateApp).Methods(http.MethodPost)
-		r.HandleFunc("/api/add-admin-key", s.handleApiAddAdminKey).Methods(http.MethodPost)
+		r.HandleFunc("/update", s.handleAPIUpdate)
+		r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
+		r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
 		e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
 	}()
 	return <-e
@@ -195,6 +192,7 @@
 
 type UserGetter interface {
 	Get(r *http.Request) string
+	Encode(w http.ResponseWriter, user string) error
 }
 
 type externalUserGetter struct {
@@ -202,7 +200,10 @@
 }
 
 func NewExternalUserGetter() UserGetter {
-	return &externalUserGetter{}
+	return &externalUserGetter{securecookie.New(
+		securecookie.GenerateRandomKey(64),
+		securecookie.GenerateRandomKey(32),
+	)}
 }
 
 func (ug *externalUserGetter) Get(r *http.Request) string {
@@ -217,6 +218,22 @@
 	return user
 }
 
+func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
+	if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
+		cookie := &http.Cookie{
+			Name:     sessionCookie,
+			Value:    encoded,
+			Path:     "/",
+			Secure:   true,
+			HttpOnly: true,
+		}
+		http.SetCookie(w, cookie)
+		return nil
+	} else {
+		return err
+	}
+}
+
 type internalUserGetter struct{}
 
 func NewInternalUserGetter() UserGetter {
@@ -227,9 +244,17 @@
 	return r.Header.Get("X-User")
 }
 
+func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
+	return nil
+}
+
 func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		if strings.HasSuffix(r.URL.Path, loginPath) || strings.HasPrefix(r.URL.Path, logoutPath) || strings.HasPrefix(r.URL.Path, staticPath) {
+		if strings.HasSuffix(r.URL.Path, loginPath) ||
+			strings.HasPrefix(r.URL.Path, logoutPath) ||
+			strings.HasPrefix(r.URL.Path, staticPath) ||
+			strings.HasPrefix(r.URL.Path, apiPublicData) ||
+			strings.HasPrefix(r.URL.Path, apiCreateApp) {
 			next.ServeHTTP(w, r)
 			return
 		}
@@ -249,6 +274,7 @@
 }
 
 func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
+	// TODO(gio): move to UserGetter
 	http.SetCookie(w, &http.Cookie{
 		Name:     sessionCookie,
 		Value:    "",
@@ -309,15 +335,9 @@
 		http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
 		return
 	}
-	if encoded, err := s.sc.Encode(sessionCookie, user); err == nil {
-		cookie := &http.Cookie{
-			Name:     sessionCookie,
-			Value:    encoded,
-			Path:     "/",
-			Secure:   true,
-			HttpOnly: true,
-		}
-		http.SetCookie(w, cookie)
+	if err := s.ug.Encode(w, user); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
 }
@@ -388,7 +408,7 @@
 	After string `json:"after"`
 }
 
-func (s *DodoAppServer) handleApiUpdate(w http.ResponseWriter, r *http.Request) {
+func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
 	fmt.Println("update")
 	var req apiUpdateReq
 	var contents strings.Builder
@@ -434,7 +454,7 @@
 	Address string `json:"address"`
 }
 
-func (s *DodoAppServer) handleApiRegisterWorker(w http.ResponseWriter, r *http.Request) {
+func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
 	vars := mux.Vars(r)
 	appName, ok := vars["app-name"]
 	if !ok || appName == "" {
@@ -525,7 +545,7 @@
 	Password string `json:"password"`
 }
 
-func (s *DodoAppServer) handleApiCreateApp(w http.ResponseWriter, r *http.Request) {
+func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
 	var req apiCreateAppReq
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest)
@@ -573,6 +593,7 @@
 		AppName:  appName,
 		Password: password,
 	}
+	w.Header().Set("Access-Control-Allow-Origin", "*")
 	if err := json.NewEncoder(w).Encode(resp); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -699,7 +720,7 @@
 	Public string `json:"public"`
 }
 
-func (s *DodoAppServer) handleApiAddAdminKey(w http.ResponseWriter, r *http.Request) {
+func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
 	var req apiAddAdminKeyReq
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest)
@@ -783,6 +804,36 @@
 	return s.nf.Filter(user, networks)
 }
 
+type publicNetworkData struct {
+	Name   string `json:"name"`
+	Domain string `json:"domain"`
+}
+
+type publicData struct {
+	Networks []publicNetworkData `json:"networks"`
+	Types    []string            `json:"types"`
+}
+
+func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
+	networks, err := s.getNetworks("")
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	var ret publicData
+	for _, n := range networks {
+		ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
+	}
+	for _, t := range s.appTmpls.Types() {
+		ret.Types = append(ret.Types, strings.ReplaceAll(t, "-", ":"))
+	}
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	if err := json.NewEncoder(w).Encode(ret); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
 func pickNetwork(networks []installer.Network, network string) []installer.Network {
 	for _, n := range networks {
 		if n.Name == network {
@@ -802,7 +853,7 @@
 	return noNetworkFilter{}
 }
 
-func (f noNetworkFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
+func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
 	return networks, nil
 }
 
@@ -815,6 +866,9 @@
 }
 
 func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
+	if user == "" {
+		return networks, nil
+	}
 	network, err := f.st.GetUserNetwork(user)
 	if err != nil {
 		return nil, err
@@ -836,7 +890,7 @@
 	return &allowListFilter{allowed}
 }
 
-func (f *allowListFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
+func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
 	ret := []installer.Network{}
 	for _, n := range networks {
 		if slices.Contains(f.allowed, n.Name) {
@@ -854,11 +908,11 @@
 	return &combinedNetworkFilter{filters}
 }
 
-func (f *combinedNetworkFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
+func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
 	ret := networks
 	var err error
 	for _, f := range f.filters {
-		ret, err = f.Filter(app, ret)
+		ret, err = f.Filter(user, ret)
 		if err != nil {
 			return nil, err
 		}