DodoApp: Public API to fetch allowed networks
Update landing page to communicate with dodo-app backend.
Change-Id: I269ad5150b9203eca9c1c9cc9a8a99b55c583419
diff --git a/apps/landing/hugo.toml b/apps/landing/hugo.toml
index 62892eb..b7cb8bc 100644
--- a/apps/landing/hugo.toml
+++ b/apps/landing/hugo.toml
@@ -1,3 +1,9 @@
baseURL = 'https://example.org/'
languageCode = 'en-us'
title = 'Dodo'
+[[headers]]
+ for = '/**'
+ [headers.values]
+ Content-Security-Policy = 'connect-src app.v1.dodo.cloud'
+ Referrer-Policy = 'strict-origin-when-cross-origin'
+ X-Content-Type-Options = 'nosniff'
diff --git a/apps/landing/layouts/partials/register-form.html b/apps/landing/layouts/partials/register-form.html
index 1e1638c..3f35896 100644
--- a/apps/landing/layouts/partials/register-form.html
+++ b/apps/landing/layouts/partials/register-form.html
@@ -1,14 +1,19 @@
<div class="form-container-footer">
- <form method="POST" action="/register" class="form-group-footer">
+ <form id="register-form" method="POST" action="/register" class="form-group-footer" onsubmit="return register()">
<label>
domain
- <select name="domain">
+ <select id="network" name="domain">
<option value="dodoapp.xyz">dodoapp.xyz</option>
</select>
</label>
<label>
subdomain
- <input type="text" name="subdomain" />
+ <input id="subdomain" type="text" name="subdomain" />
+ </label>
+ <label>
+ application type
+ <select id="app-type" name="app-type">
+ </select>
</label>
<label>
ssh public key
diff --git a/apps/landing/static/js/main.js b/apps/landing/static/js/main.js
index a068560..24e651e 100644
--- a/apps/landing/static/js/main.js
+++ b/apps/landing/static/js/main.js
@@ -139,3 +139,55 @@
fact.addEventListener("mouseover", () => handleMouseover(facts[index].params.image, index, facts[index].params.title));
});
});
+
+async function loadPublicData() {
+ let networkSelect = document.querySelector("select#network");
+ if (networkSelect === undefined) {
+ return;
+ }
+ networkSelect.innerHTML = "";
+ let appTypeSelect = document.querySelector("select#app-type");
+ if (appTypeSelect === undefined) {
+ return;
+ }
+ appTypeSelect.innerHTML = "";
+ let resp = await fetch("https://app.v1.dodo.cloud/api/public-data");
+ if (!resp.ok) {
+ return;
+ }
+ let data = await resp.json();
+ data.networks.forEach((network) => {
+ let opt = document.createElement("option");
+ opt.setAttribute("value", network.domain);
+ opt.innerHTML = network.domain;
+ networkSelect.appendChild(opt);
+ });
+ data.types.forEach((t) => {
+ let opt = document.createElement("option");
+ opt.setAttribute("value", t);
+ opt.innerHTML = t;
+ appTypeSelect.appendChild(opt);
+ });
+}
+
+function register() {
+ var data = {
+ type: document.getElementById("app-type").value,
+ adminPublicKey: document.getElementById("public-key").value,
+ network: document.getElementById("network").value,
+ subdomain: document.getElementById("subdomain").value,
+ };
+ fetch("https://app.v1.dodo.cloud/api/apps", {
+ method: "POST",
+ body: JSON.stringify(data),
+ }).then((resp) => {
+ resp.json().then((r) => {
+ console.log(r);
+ });
+ });
+ return false;
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ loadPublicData();
+});
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
}