🌱🏠 a cloud service to enable your own server (owned by you and running on your computer) to be accessible on the internet in seconds, no credit card required https://greenhouse.server.garden/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

256 lines
7.8 KiB

package main
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"fmt"
"log"
"net/http"
"regexp"
"sort"
"strconv"
"strings"
"time"
base58 "github.com/shengdoushi/base58"
chart "github.com/wcharczuk/go-chart/v2"
chartdrawing "github.com/wcharczuk/go-chart/v2/drawing"
)
func registerProfileRoutes(app *FrontendApp) {
app.handleWithSession("/profile", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
if !session.EmailVerified {
// anti-XSS: only set path into the flash cookie if it matches a basic url pattern
if app.basicURLPathRegex.MatchString(request.URL.Path) {
msg := fmt.Sprintf("Please verify your email address in order to access %s%s", app.Domain, request.URL.Path)
app.setFlash(responseWriter, session, "info", msg)
}
http.Redirect(responseWriter, request, "/verify-email", http.StatusFound)
return
}
// email is verified, continue:
rawHash := sha256.Sum256([]byte(session.SessionId))
hashOfSessionId := fmt.Sprintf("%x", rawHash[:8])
tenant, err := app.Model.GetTenant(session.TenantId)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
billingYear, billingMonth, _, _, _ := getBillingTimeInfo()
usageTotal, err := app.Model.GetTenantUsageTotal(session.TenantId, billingYear, billingMonth)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
newAPIToken := (*session.Flash)["api-token"]
newAPITokenName := (*session.Flash)["api-token-name"]
if request.Method == "POST" {
postedHashOfSessionId := request.PostFormValue("hashOfSessionId")
if postedHashOfSessionId != hashOfSessionId {
app.setFlash(responseWriter, session, "error", "anti-CSRF (cross site request forgery) validation failed. Please try again.\n")
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
action := request.PostFormValue("action")
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")
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")
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)
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, "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", "unknown action\n")
}
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
apiTokens := []APIToken{}
for _, token := range tenant.APITokens {
//fmt.Printf("%s %s", token.Name, newAPITokenName)
if token.Name != newAPITokenName {
apiTokens = append(apiTokens, token)
}
}
data := struct {
Subdomain string
APITokens []APIToken
NewAPIToken string
NewAPITokenName string
BytesSoFar string
BillingAlarmSMS string
BillingAlarmEmail string
BillingAlarmThreshold string
BillingLimit string
HashOfSessionId string
}{
Subdomain: tenant.Subdomain,
APITokens: apiTokens,
NewAPIToken: newAPIToken,
NewAPITokenName: newAPITokenName,
BytesSoFar: ByteCountSI(usageTotal),
BillingAlarmSMS: tenant.SMSAlarmNumber,
BillingAlarmEmail: tenant.Email,
BillingAlarmThreshold: fmt.Sprintf("%.2f", float64(tenant.BillingAlarmCents)/float64(100)),
BillingLimit: fmt.Sprintf("%.2f", float64(tenant.ServiceLimitCents)/float64(100)),
HashOfSessionId: hashOfSessionId,
}
app.buildPageFromTemplate(responseWriter, session, "profile.html", data)
})
app.handleWithSession("/profile/usage_graph.png", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
// tenant, err := app.Model.GetTenant(session.TenantId)
// if err != nil {
// app.unhandledError(responseWriter, err)
// return
// }
_, _, start, end, _ := getBillingTimeInfo()
usageMetrics, err := app.Model.GetTenantUsageMetrics(session.TenantId, start, end)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
type datapoint struct {
Time time.Time
Bytes int64
}
i := 0
usageMetricObjects := make([]datapoint, len(usageMetrics))
for time, bytez := range usageMetrics {
usageMetricObjects[i] = datapoint{Time: time, Bytes: bytez}
i++
}
sort.Slice(usageMetricObjects, func(i, j int) bool {
return usageMetricObjects[j].Time.After(usageMetricObjects[i].Time)
})
xValues := make([]float64, len(usageMetricObjects)+3)
yValues := make([]float64, len(usageMetricObjects)+3)
var total int64 = 0
xValues[0] = float64(start.UnixNano())
yValues[0] = float64(0)
if len(usageMetricObjects) > 0 {
xValues[1] = float64(usageMetricObjects[0].Time.UnixNano() - int64(time.Second*10))
} else {
xValues[1] = float64(start.UnixNano())
}
yValues[1] = float64(0)
for i, obj := range usageMetricObjects {
total = total + obj.Bytes
xValues[i+2] = float64(obj.Time.UnixNano())
yValues[i+2] = float64(total)
}
xValues[len(xValues)-1] = float64(end.UnixNano())
yValues[len(yValues)-1] = float64(total)
// it will err if the y scale is from 0 to 0
if total == 0 {
total = 1000
}
graph := chart.Chart{
Width: 400,
Height: 200,
XAxis: chart.XAxis{
ValueFormatter: func(v interface{}) string {
if typed, isTyped := v.(float64); isTyped {
timeInstance := time.Unix(0, int64(typed)).UTC()
if timeInstance.After(end) {
timeInstance = end
}
return timeInstance.Format("Jan 2")
}
return ""
},
Range: &chart.ContinuousRange{
Min: float64(start.UnixNano()),
Max: float64(end.UnixNano()),
},
},
YAxis: chart.YAxis{
ValueFormatter: func(v interface{}) string {
if typed, isTyped := v.(float64); isTyped {
return ByteCountSI(int64(typed))
}
return ""
},
Range: &chart.ContinuousRange{
Min: float64(0),
Max: float64(total) * float64(1.2),
},
},
Series: []chart.Series{
chart.ContinuousSeries{
Name: "Bytes",
Style: chart.Style{
FillColor: chartdrawing.Color{R: 0, G: 100, B: 255, A: 70},
},
YAxis: chart.YAxisPrimary,
XValues: xValues,
YValues: yValues,
},
},
}
//log.Println(graph.YAxis.Range.GetDelta())
buffer := bytes.NewBuffer([]byte{})
err = graph.Render(chart.PNG, buffer)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
responseWriter.Header().Set("Content-Type", "image/png")
responseWriter.Header().Set("Content-Length", strconv.Itoa(buffer.Len()))
_, err = responseWriter.Write(buffer.Bytes())
if err != nil {
log.Printf("http Write error on usage_graph png: %+v", err)
}
})
}
func ByteCountSI(b int64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp])
}