Browse Source

peparing for alpha release and changes based on initial usability test

master
forest 1 month ago
parent
commit
675fcc988e
19 changed files with 293 additions and 144 deletions
  1. +38
    -0
      backend.go
  2. +39
    -19
      frontend.go
  3. +104
    -50
      frontend/alpha-profile.gotemplate.html
  4. +8
    -0
      frontend/app-connect.gotemplate.html
  5. +2
    -2
      frontend/install-linux.gotemplate.html
  6. +5
    -4
      frontend/static/greenhouse.css
  7. +21
    -18
      frontend/static/image-sources/generate_webp.sh
  8. BIN
      frontend/static/images/amd.webp
  9. BIN
      frontend/static/images/arm.webp
  10. BIN
      frontend/static/images/intel.webp
  11. BIN
      frontend/static/images/macos_disk_image.webp
  12. BIN
      frontend/static/images/mascot-sad.webp
  13. BIN
      frontend/static/images/windows-installer.webp
  14. +2
    -4
      frontend_howto.go
  15. +53
    -4
      frontend_login.go
  16. +5
    -11
      frontend_profile.go
  17. +6
    -30
      ingress_service.go
  18. +2
    -1
      main.go
  19. +8
    -1
      public_api.go

+ 38
- 0
backend.go View File

@ -3,6 +3,8 @@ package main
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/json"
@ -21,6 +23,7 @@ import (
"git.sequentialread.com/forest/greenhouse/pki"
errors "git.sequentialread.com/forest/pkg-errors"
base58 "github.com/shengdoushi/base58"
)
type BaseHTTPService struct {
@ -172,6 +175,41 @@ func initBackend(
return &toReturn
}
func (app *BackendApp) CreateAPIToken(tenantId int, name string) (string, string, error) {
apiTokenBuffer := make([]byte, 16)
rand.Read(apiTokenBuffer)
apiToken := base58.Encode(apiTokenBuffer, base58.BitcoinAlphabet)
rawHash := sha256.Sum256([]byte(apiToken))
hashedAPIToken := fmt.Sprintf("%x", rawHash)
tenant, err := app.Model.GetTenant(tenantId)
if err != nil {
return "", "", err
}
i := 0
for i < 100 {
conflict := false
for _, token := range tenant.APITokens {
if token.Name == name {
conflict = true
}
}
if conflict == false {
break
}
i++
nameWithoutNumberSuffix := regexp.MustCompile("_[0-9]+$").ReplaceAllString(name, "")
name = fmt.Sprintf("%s_%d", nameWithoutNumberSuffix, i)
}
if i >= 100 {
return "", name, errors.Errorf("too many %s tokens", name)
}
err = app.Model.CreateAPIToken(tenantId, name, hashedAPIToken)
return apiToken, name, err
}
func (app *BackendApp) InitializeTenant(tenantId int, email string) error {
emailSplit := strings.Split(email, "@")
if len(emailSplit) != 2 {


+ 39
- 19
frontend.go View File

@ -31,6 +31,11 @@ type Session struct {
Flash *map[string]string
}
type ApplicationAuthSession struct {
TenantId int
Name string
}
type FrontendApp struct {
Port int
TLSCertificate string
@ -49,27 +54,31 @@ type FrontendApp struct {
SessionCacheMutex *sync.Mutex
basicURLPathRegex *regexp.Regexp
AdminTenantId int
ApplicationAuthSessions map[string]*ApplicationAuthSession
}
func initFrontend(workingDirectory string, config *Config, model *DBModel, backend *BackendApp, emailService *EmailService, ingress *IngressService) FrontendApp {
app := FrontendApp{
Port: config.FrontendPort,
TLSCertificate: config.FrontendTLSCertificate,
TLSKey: config.FrontendTLSKey,
Domain: config.FrontendDomain,
WorkingDirectory: workingDirectory,
Router: mux.NewRouter(),
EmailService: emailService,
Model: model,
Backend: backend,
Ingress: ingress,
HTMLTemplates: map[string]*template.Template{},
PasswordHashSalt: "Ko0jOdSCzEyDtK4rmoocfcR9LxwOrIZsaVPBjImkb6AhRW6yNSmgsU122ArU1URBjcJ1EnskZ5r7",
SessionCache: map[string]*Session{},
SessionIdByTenantId: map[int]string{},
SessionCacheMutex: &sync.Mutex{},
basicURLPathRegex: regexp.MustCompile("(?i)[a-z0-9/?&_+-]+"),
Port: config.FrontendPort,
TLSCertificate: config.FrontendTLSCertificate,
TLSKey: config.FrontendTLSKey,
Domain: config.FrontendDomain,
AdminTenantId: config.AdminTenantId,
WorkingDirectory: workingDirectory,
Router: mux.NewRouter(),
EmailService: emailService,
Model: model,
Backend: backend,
Ingress: ingress,
HTMLTemplates: map[string]*template.Template{},
PasswordHashSalt: "Ko0jOdSCzEyDtK4rmoocfcR9LxwOrIZsaVPBjImkb6AhRW6yNSmgsU122ArU1URBjcJ1EnskZ5r7",
SessionCache: map[string]*Session{},
SessionIdByTenantId: map[int]string{},
SessionCacheMutex: &sync.Mutex{},
basicURLPathRegex: regexp.MustCompile("(?i)[a-z0-9/?&_+-]+"),
ApplicationAuthSessions: map[string]*ApplicationAuthSession{},
}
app.handleWithSessionNotRequired("/", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
@ -247,6 +256,10 @@ func (app *FrontendApp) unhandledError(responseWriter http.ResponseWriter, err e
}
func (app *FrontendApp) handleWithSpecificUser(path string, userId int, handler func(http.ResponseWriter, *http.Request, Session)) {
//log.Printf("handleWithSpecificUser: %d", userId)
if userId == 0 {
panic("handleWithSpecificUser called with userId 0: this is a security issue!")
}
app.handleWithSessionImpl(path, true, userId, handler)
}
@ -268,13 +281,20 @@ func (app *FrontendApp) handleWithSessionImpl(path string, required bool, requir
if err != nil {
app.unhandledError(responseWriter, err)
} else {
if (required && session.TenantId == 0) || (requireUserId != 0 && requireUserId != session.TenantId) {
log.Printf("%d, %d, %t, %t", requireUserId, session.TenantId, requireUserId != 0, session.TenantId != requireUserId)
if (required && session.TenantId == 0) || (requireUserId != 0 && session.TenantId != requireUserId) {
pathAndQuery := request.URL.Path
query := request.URL.Query().Encode()
if query != "" {
pathAndQuery = fmt.Sprintf("%s?%s", request.URL.Path, query)
}
// anti-XSS: only set returnTo if it matches a basic url pattern
if app.basicURLPathRegex.MatchString(request.URL.Path) {
if app.basicURLPathRegex.MatchString(pathAndQuery) {
msg := fmt.Sprintf("Please log in in order to access %s%s", app.Domain, request.URL.Path)
app.setFlash(responseWriter, session, "info", msg)
app.setFlash(responseWriter, session, "returnTo", request.URL.Path)
app.setFlash(responseWriter, session, "returnTo", pathAndQuery)
}
http.Redirect(responseWriter, request, "/login", http.StatusFound)


+ 104
- 50
frontend/alpha-profile.gotemplate.html View File

@ -9,6 +9,11 @@
<label class="tab" for="account-status">alpha version of control panel</label>
<div class="vertical tab-content">
<p>
Your bandwidth usage this month: <b>{{ .BytesSoFar }}</b>
</p>
<img src="/profile/usage_graph.png"/>
<div class="horizontal justify-right align-center margin-bottom">
<form class="profile-form" method="POST" action="#">
<label for="subdomain">Personal Subdomain: </label>
@ -22,7 +27,44 @@
</div>
</form>
</div>
<div class="horizontal align-center margin-bottom">
<form class="profile-form" method="POST" action="#">
{{ if or .APITokens .NewAPIToken }}
<label for="api-tokens">API Tokens: </label>
<div id="api-tokens">
{{ range $token := .APITokens }}
<div class="api-token"><span>{{ $token.Name }}</span></div>
{{ end }}
{{ if .NewAPIToken }}
<div class="api-token new-api-token"><div>{{ .NewAPITokenName }}</div> <div class="new-api-token-token">{{ .NewAPIToken }}</div></div>
{{ end }}
</div>
{{ else }}
<p>You do not have any API Tokens yet.</p>
{{ end }}
<label for="key_name">New API Token: </label><br/>
<input type="hidden" name="hashOfSessionId" value="{{ .HashOfSessionId }}"/>
<input type="hidden" name="action" value="create_api_token"/>
<div><input type="text" name="key_name" id="key_name" placeholder="API Token Name" value="untitled" />
<input type="submit" name="submit" alt="Submit" class="create-token-button" value="create" /></div>
</form>
</div>
<div class="horizontal justify-right align-center margin-bottom">
<span class="fine-print">
{{ if .NewAPIToken }}
This API Token will not be displayed again, <br/>
so make sure to copy and paste it or write it down now!
{{ end }}
</span>
</div>
<h3>Advanced Options:</h3>
<div class="horizontal justify-right align-center margin-bottom">
<form class="profile-form" method="POST" action="#">
{{ if .ExternalDomains }}
@ -34,8 +76,6 @@
</div>
{{ end }}
</div>
{{ else }}
<p>You do not have any External Domains yet.</p>
{{ end }}
<label for="external-domain">Add External Domain: </label>
<div class="horizontal align-center margin-bottom">
@ -59,49 +99,6 @@
</div>
</form>
</div>
<p>
Your bandwidth usage this month:
</p>
<img src="/profile/usage_graph.png"/>
<p>
{{ .BytesSoFar }} used this month
</p>
<div class="horizontal align-center margin-bottom">
<form class="profile-form" method="POST" action="#">
{{ if or .APITokens .NewAPIToken }}
<label for="api-tokens">API Tokens: </label>
<div id="api-tokens">
{{ range $token := .APITokens }}
<div class="api-token"><span>{{ $token.Name }}</span></div>
{{ end }}
{{ if .NewAPIToken }}
<div class="api-token new-api-token"><div>{{ .NewAPITokenName }}</div> <div class="new-api-token-token">{{ .NewAPIToken }}</div></div>
{{ end }}
</div>
{{ else }}
<p>You do not have any API Tokens yet.</p>
{{ end }}
<label for="key_name">New API Token: </label><br/>
<input type="hidden" name="hashOfSessionId" value="{{ .HashOfSessionId }}"/>
<input type="hidden" name="action" value="create_api_token"/>
<input type="text" name="key_name" id="key_name" />
<input type="submit" name="submit" alt="Submit" class="create-token-button" value="create" />
</form>
</div>
<div class="horizontal justify-right align-center margin-bottom">
<span class="fine-print">
{{ if .NewAPIToken }}
This API Token will not be displayed again, <br/>
so make sure to copy and paste it or write it down now!
{{ end }}
</span>
</div>
</div>
</div>
@ -118,8 +115,33 @@
</label>
<div class="vertical tab-content">
<p>
how to install on windows
You may download our installer:
</p>
<blockquote>
<a class="horizontal align-center" href="https://picopublish.sequentialread.com/files/greenhouse-setup-alpha-rc0.exe">
<img class="os-image" src="static/images/windows-installer.webp"/> greenhouse-setup-alpha-rc0.exe
</a>
</blockquote>
<br/>
<br/>
<div class="mascot admonition">
<div class="emoji-icon">
<img src="/static/images/mascot-laptop.webp"/>
</div>
<p>
Some computers ship with the windows operating system pre-configured to only allow applications from the Microsoft Store.
This is called "Windows S Mode". If your computer is like this, you won't be able to run our installer until you turn S Mode off.
</p>
<p>
Microsoft Support:
<a href="https://support.microsoft.com/en-us/windows/switching-out-of-s-mode-in-windows-4f56d9be-99ec-6983-119f-031bfb28a307#WindowsVersion=Windows_10">
Switching out of S mode in Windows</a>.
</p>
<p>
We also have our own installation guide / FAQ here:
<a href="/install-greenhouse-self-hosting-software-windows">installing the greenhouse self-hosting software on Windows</a>.
</p>
</div>
</div>
<input type="radio" name="client" value="linux" id="client-linux" {{ if eq .OperatingSystem "linux" }}checked="checked"{{ end }}></input>
@ -133,10 +155,14 @@
During the alpha test phase, we're only offering a shell script installer / uninstaller for linux.
</p>
<p>
<pre class="small install-command">curl https://greenhouse-alpha.server.garden/install.sh | sudo sh</pre><br/>
<blockquote>
<pre class="small install-command">curl https://greenhouse-alpha.server.garden/install.sh | sudo sh</pre>
</blockquote>
<br/>
<br/>
<div class="horizontal align-center wrap">CPU Support:
<span class="cpu-support"><img alt="AMD" src="/static/images/amd.webp"/> / <img alt="intel" src="/static/images/intel.webp"/> 64-bit</span>
<span class="cpu-support"><img alt="raspberry-pi" src="/static/images/raspberrypi.webp"/> / <img alt="arm" src="/static/images/arm.webp"/> 32-bit and 64-bit </span>
<span class="white-pill">AMD / intel 64-bit</span>
<span class="white-pill"><img alt="raspberry-pi" src="/static/images/raspberrypi.webp"/> / arm 32-bit and 64-bit </span>
</div>
</p>
@ -160,8 +186,36 @@
</label>
<div class="vertical tab-content">
<p>
how to install on mac os
During the alpha test phase, we're only offering a simple application download for MacOS.
</p>
<blockquote>
<a class="horizontal align-center" href="https://picopublish.sequentialread.com/files/greenhouse-desktop-alpha-rc0.dmg">
<img class="os-image" src="static/images/macos_disk_image.webp"/> greenhouse-desktop-alpha-rc0.dmg
</a>
</blockquote>
<br/>
<br/>
<div class="mascot admonition">
<div class="emoji-icon">
<img src="/static/images/mascot-sad.webp"/>
</div>
<p>
Unfortunately Apple has made it very hard to run applications that did not come from their App Store.
</p>
<p>
The Greenhouse application is also not "code-signed" by Apple yet, making it a 3rd-class citizen on thier platform.
So you will probably have to go through a lot of extra steps to be able to run it.
</p>
<p>
Apple Support:
<a href="https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac">
Open a Mac app from an unidentified developer</a>.
</p>
<p>
We also have our own installation guide / FAQ here:
<a href="/install-greenhouse-self-hosting-software-mac">installing the greenhouse self-hosting software on MacOS</a>.
</p>
</div>
</div>
</div>


+ 8
- 0
frontend/app-connect.gotemplate.html View File

@ -0,0 +1,8 @@
<div>
<p>Success! Your Greenhouse application should be logged in shortly!</p>
<p>You may now close this browser tab.</p>
</div>

+ 2
- 2
frontend/install-linux.gotemplate.html View File

@ -23,8 +23,8 @@
<p>
<pre class="install-command">curl https://greenhouse-alpha.server.garden/install.sh | sudo sh</pre>
<div class="horizontal align-center wrap">CPU Support:
<span class="cpu-support"><img alt="AMD" src="/static/images/amd.webp"/> / <img alt="intel" src="/static/images/intel.webp"/> 64-bit</span>
<span class="cpu-support"><img alt="raspberry-pi" src="/static/images/raspberrypi.webp"/> / <img alt="arm" src="/static/images/arm.webp"/> 32-bit and 64-bit </span>
<span class="white-pill">AMD/ intel 64-bit</span>
<span class="white-pill"><img alt="raspberry-pi" src="/static/images/raspberrypi.webp"/> / arm 32-bit and 64-bit </span>
</div>
</p>
<div class="mascot admonition">


+ 5
- 4
frontend/static/greenhouse.css View File

@ -215,7 +215,7 @@ pre.flash.info {
.install-command.small {
font-size: 12px;
}
.cpu-support {
.white-pill {
border: 1px solid gray;
border-radius: 12px;
background: white;
@ -229,7 +229,7 @@ pre.flash.info {
margin: 0 5px;
}
.cpu-support img {
.white-pill img {
height: 16px;
margin: 0 5px;
}
@ -388,9 +388,10 @@ form.vertical .js-form-submit-button {
.api-token,
.external-domain {
border-bottom: 1px solid #44332277;
border-bottom: 2px solid #44332255;
margin: 0.5em;
padding: 0.05em;
padding: 0.15em;
}
.new-api-token {


+ 21
- 18
frontend/static/image-sources/generate_webp.sh View File

@ -36,29 +36,32 @@ pngToWebp () {
cwebp -q "$quality" -m 6 -preset text -alpha_filter best "$pngFilename" -o "$webpFilename"
}
# svgToWebp "greenhouse-border.svg" "$quality" "icon"
# svgToWebp "free.svg" "$quality" "icon"
# # svgToWebp "blah.svg" "$quality" "120" "105"
svgToWebp "greenhouse-border.svg" "$quality" "icon"
svgToWebp "free.svg" "$quality" "icon"
# svgToWebp "blah.svg" "$quality" "120" "105"
#pngToWebp "mascot-angry.png" "$lowerquality" "icon"
pngToWebp "mascot-angry.png" "$lowerquality" "icon"
pngToWebp "mascot-dead.png" "$lowerquality" "icon"
#pngToWebp "mascot-laptop.png" "$lowerquality" "icon"
#pngToWebp "mascot-reading.png" "$lowerquality" "icon"
pngToWebp "mascot-laptop.png" "$lowerquality" "icon"
pngToWebp "mascot-reading.png" "$lowerquality" "icon"
pngToWebp "mascot-sad.png" "$lowerquality" "icon"
pngToWebp "mascot-science.png" "$lowerquality" "icon"
pngToWebp "mascot-surfing.png" "$lowerquality" "icon"
# pngToWebp "beginner.png" "$quality" "icon"
# pngToWebp "custodialcloud.png" "$lowerquality" "text"
# pngToWebp "hybridcloud.png" "$lowerquality" "text"
# pngToWebp "selfhost.png" "$lowerquality" "text"
# pngToWebp "macos.png" "$quality" "icon"
# pngToWebp "namecheap-1.png" "$quality" "text"
# pngToWebp "namecheap-2.png" "$quality" "text"
# pngToWebp "namecheap-3.png" "$quality" "text"
# pngToWebp "shine.png" "$quality" "icon"
# pngToWebp "tux.png" "$quality" "icon"
# pngToWebp "windows.png" "$quality" "icon"
pngToWebp "beginner.png" "$quality" "icon"
pngToWebp "custodialcloud.png" "$lowerquality" "text"
pngToWebp "hybridcloud.png" "$lowerquality" "text"
pngToWebp "selfhost.png" "$lowerquality" "text"
pngToWebp "namecheap-1.png" "$quality" "text"
pngToWebp "namecheap-2.png" "$quality" "text"
pngToWebp "namecheap-3.png" "$quality" "text"
pngToWebp "shine.png" "$quality" "icon"
pngToWebp "macos.png" "$quality" "icon"
pngToWebp "macos_disk_image.png" "$quality" "icon"
pngToWebp "tux.png" "$quality" "icon"
pngToWebp "windows.png" "$quality" "icon"
pngToWebp "windows-installer.png" "$quality" "icon"
# pngToWebp "intel.png" "$quality" "text"
# pngToWebp "amd.png" "$quality" "text"
# pngToWebp "arm.png" "$quality" "text"
# pngToWebp "raspberrypi.png" "$quality" "icon"
pngToWebp "raspberrypi.png" "$quality" "icon"

BIN
frontend/static/images/amd.webp View File

Before After

BIN
frontend/static/images/arm.webp View File

Before After

BIN
frontend/static/images/intel.webp View File

Before After

BIN
frontend/static/images/macos_disk_image.webp View File

Before After

BIN
frontend/static/images/mascot-sad.webp View File

Before After

BIN
frontend/static/images/windows-installer.webp View File

Before After

+ 2
- 4
frontend_howto.go View File

@ -35,17 +35,15 @@ func registerHowtoRoutes(app *FrontendApp) {
)
app.handleWithSessionNotRequired(
"/install-greenhouse-client-software-linux",
"/install-greenhouse-self-hosting-software-linux",
func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
app.buildPageFromTemplate(responseWriter, session, "install.html", struct{}{})
app.buildPageFromTemplate(responseWriter, session, "install-linux.html", struct{}{})
},
)
serveScripts := []string{
"install.sh",
"uninstall.sh",
"install-macos.sh",
"uninstall-macos.sh",
}
for _, scriptFileName := range serveScripts {
app.handleWithSessionNotRequired(


+ 53
- 4
frontend_login.go View File

@ -9,6 +9,7 @@ import (
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"time"
@ -221,13 +222,61 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
app.handleWithSession("/verify-email/{token}", verifyEmailHandler)
app.handleWithSessionNotRequired("/logout", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
err := app.Model.LogoutTenant(session.TenantId)
if err != nil {
app.unhandledError(responseWriter, err)
return
if session.TenantId != 0 {
err := app.Model.LogoutTenant(session.TenantId)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
}
app.deleteCookie(responseWriter, "sessionId")
app.deleteCookie(responseWriter, "sessionIdLax")
http.Redirect(responseWriter, request, "/", http.StatusFound)
})
app.handleWithSessionNotRequired("/app-auth-session", func(responseWriter http.ResponseWriter, request *http.Request, _ Session) {
appAuthSessionId := request.URL.Query().Get("session")
if request.Method == "POST" {
name := request.URL.Query().Get("name")
if !regexp.MustCompile("[A-Za-z0-9_-]+").MatchString(name) {
http.Error(responseWriter, "400 Bad Request: app-auth-session name may only contain letters, numbers, dashes, and underscores", http.StatusBadRequest)
return
}
app.ApplicationAuthSessions[appAuthSessionId] = &ApplicationAuthSession{
Name: name,
}
responseWriter.Write([]byte("OK"))
} else {
appAuthSession, hasAppAuthSession := app.ApplicationAuthSessions[appAuthSessionId]
if hasAppAuthSession {
if appAuthSession.TenantId != 0 {
apiToken, _, err := app.Backend.CreateAPIToken(appAuthSession.TenantId, appAuthSession.Name)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
responseWriter.Header().Set("Content-Type", "text/plain")
responseWriter.Write([]byte(apiToken))
} else {
http.Error(responseWriter, "401 unauthorized", http.StatusUnauthorized)
}
} else {
http.Error(responseWriter, "404 not found", http.StatusNotFound)
}
}
})
app.handleWithSession("/app-connect", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
appAuthSessionId := request.URL.Query().Get("session")
appAuthSession, hasAppAuthSession := app.ApplicationAuthSessions[appAuthSessionId]
if hasAppAuthSession && appAuthSession.TenantId == 0 {
app.ApplicationAuthSessions[appAuthSessionId].TenantId = session.TenantId
app.buildPageFromTemplate(responseWriter, session, "app-connect.html", nil)
} else {
http.Error(responseWriter, "404 not found", http.StatusNotFound)
}
})
}

+ 5
- 11
frontend_profile.go View File

@ -2,7 +2,6 @@ package main
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"fmt"
"log"
@ -13,7 +12,6 @@ import (
"strings"
"time"
base58 "github.com/shengdoushi/base58"
chart "github.com/wcharczuk/go-chart/v2"
chartdrawing "github.com/wcharczuk/go-chart/v2/drawing"
os_from_user_agent_header "zgo.at/gadget"
@ -161,27 +159,23 @@ func registerProfileRoutes(app *FrontendApp) {
} else if action == "create_api_token" {
keyName := strings.TrimSpace(request.PostFormValue("key_name"))
if len(keyName) == 0 {
app.setFlash(responseWriter, session, "error", "key name is required\n")
app.setFlash(responseWriter, session, "error", "API Token Name is required\n")
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
if !regexp.MustCompile("[A-Za-z0-9_-]+").MatchString(keyName) {
app.setFlash(responseWriter, session, "error", "key name may only contain letters, numbers, dashes, and underscores\n")
app.setFlash(responseWriter, session, "error", "API Token Name may only contain letters, numbers, dashes, and underscores\n")
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
apiTokenBuffer := make([]byte, 16)
rand.Read(apiTokenBuffer)
apiToken := base58.Encode(apiTokenBuffer, base58.BitcoinAlphabet)
rawHash := sha256.Sum256([]byte(apiToken))
hashedAPIToken := fmt.Sprintf("%x", rawHash)
err := app.Model.CreateAPIToken(session.TenantId, keyName, hashedAPIToken)
apiToken, name, err := app.Backend.CreateAPIToken(session.TenantId, keyName)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
app.setFlash(responseWriter, session, "api-token", apiToken)
app.setFlash(responseWriter, session, "api-token-name", keyName)
app.setFlash(responseWriter, session, "api-token-name", name)
app.setFlash(responseWriter, session, "info", fmt.Sprintf("Success! Your new '%s' API Token is %s. It will not be displayed again, so make sure to copy and paste it or write it down now!\n", keyName, apiToken))
} else {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("unknown action '%s'\n", action))


+ 6
- 30
ingress_service.go View File

@ -3,8 +3,6 @@ package main
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"fmt"
"log"
@ -15,11 +13,11 @@ import (
"time"
errors "git.sequentialread.com/forest/pkg-errors"
base58 "github.com/shengdoushi/base58"
)
type IngressService struct {
BaseHTTPService
Backend *BackendApp
Model *DBModel
DaemonIsRunning bool
AdminTenantId int
@ -47,9 +45,10 @@ const adminThresholdNodeId = "greenhouse_internal_node"
const greenhouseExternalDomain = "greenhouse-alpha.server.garden"
func NewIngressService(config *Config, model *DBModel) *IngressService {
func NewIngressService(config *Config, backend *BackendApp, model *DBModel) *IngressService {
toReturn := &IngressService{
Model: model,
Backend: backend,
AdminTenantId: config.AdminTenantId,
FrontendPort: config.FrontendPort,
}
@ -173,37 +172,14 @@ func (service *IngressService) ConfigureGreenhouseDaemon() error {
"responseStatus.NeedsAPIToken (%t) || !hasMatchingAPIToken (%t): now reconfiguring the greenhouse daemon's API token...\n",
responseStatus.NeedsAPIToken, !hasMatchingAPIToken,
)
i := 0
newTokenName := "greenhouse_builtin_ingress"
for i < 100 {
conflict := false
for _, token := range tenant.APITokens {
if token.Name == newTokenName {
conflict = true
}
}
if conflict == false {
break
}
i++
newTokenName = fmt.Sprintf("greenhouse_builtin_ingress_%d", i)
}
if i >= 100 {
return errors.New("too many greenhouse_builtin_ingress tokens")
}
apiTokenBuffer := make([]byte, 16)
rand.Read(apiTokenBuffer)
apiToken := base58.Encode(apiTokenBuffer, base58.BitcoinAlphabet)
rawHash := sha256.Sum256([]byte(apiToken))
hashedAPIToken := fmt.Sprintf("%x", rawHash)
err = service.Model.CreateAPIToken(service.AdminTenantId, newTokenName, hashedAPIToken)
newTokenName := "greenhouse_builtin_ingress"
apiToken, _, err := service.Backend.CreateAPIToken(service.AdminTenantId, newTokenName)
if err != nil {
return err
}
_, err := service.MyHTTP200(
_, err = service.MyHTTP200(
"POST",
fmt.Sprintf("http://unix/register?serverName=%s", adminThresholdNodeId),
nil,


+ 2
- 1
main.go View File

@ -64,10 +64,11 @@ func main() {
easypkiInstance := NewGreenhouseEasyPKI(model)
pkiService := pki.NewPKIService(easypkiInstance)
ingressService := NewIngressService(config, model)
backendApp := initBackend(workingDirectory, config, pkiService, model, emailService)
ingressService := NewIngressService(config, backendApp, model)
frontendApp := initFrontend(workingDirectory, config, model, backendApp, emailService, ingressService)
scheduledTasks := NewScheduledTasks(ingressService, backendApp, model)


+ 8
- 1
public_api.go View File

@ -6,6 +6,7 @@ import (
"fmt"
"log"
"net/http"
"regexp"
"strings"
)
@ -34,12 +35,18 @@ func AddAPIRoutesToFrontend(app *FrontendApp) {
http.Error(responseWriter, "405 Method Not Allowed, try POST", http.StatusMethodNotAllowed)
return
}
newNodeId := request.URL.Query().Get("serverName")
newNodeId := strings.ToLower(request.URL.Query().Get("serverName"))
if newNodeId == "" {
http.Error(responseWriter, "404 Not Found, a server name must be provided, like /api/client_config?serverName=my_server", http.StatusNotFound)
return
}
subdomainRegex := regexp.MustCompile("^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$")
if !subdomainRegex.MatchString(newNodeId) {
http.Error(responseWriter, "400 Bad Request, the server name must be a valid subdomain. It should only contain letters, numbers, and dashes", http.StatusBadRequest)
return
}
// used to use fmt.Sprintf("%s.%s", tenant.Subdomain, freeSubdomainDomain) as the greenhouseDomain
// tenant, err := app.Model.GetTenant(user.TenantId)
// if err != nil {


Loading…
Cancel
Save