package h
import (
"bytes"
"fmt"
"html/template"
"io"
// "log/slog"
"net/url"
// "strings"
"gopkg.awl.red/lx/internal/config"
// "gopkg.awl.red/lx/internal/db"
)
var tmplfuncs = template.FuncMap{
"AsPrice": func(x int) string {
return fmt.Sprintf("$%.2f", float64(x)/1000)
},
"SecAsD": func(x int) string {
return fmt.Sprintf("%d days", x/(60*60*24))
},
}
var templateLogin = template.Must(template.New("template_login").Parse(`<!doctype html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{.StyleTag}}
</head><body>
<title>Login</title>
<h1>{{ .Config.Title }} Login</h1>
{{ if (or .Data.DidLogout .Data.BadLogin) }}
<script>
function removeToast(id) {
const el = document.getElementById(id)
if (el) {
el.remove()
}
}
{{ if .Data.DidLogout }}
function removeToastLogout() {
window.history.pushState({}, document.title, window.location.pathname);
removeToast('toast-logout')
}
setTimeout(removeToastLogout, 10000)
{{ end }}
{{ if .Data.BadLogin }}
function removeToastBadLogin() {
removeToast('toast-bad-login')
window.history.pushState({}, document.title, window.location.pathname);
}
setTimeout(removeToastBadLogin, 10000)
{{ end }}
</script>
<noscript>
<style>
.toast {
color: purple;
}
</style>
</noscript>
{{ end }}
{{ if .Data.DidLogout }}
<div class="toast" id="toast-logout" onclick="removeToastLogout()" style="position:fixed;top:0;right:0;padding:20px;background:green;">Logged out successfully ❌</div>
{{ end }}
{{ if .Data.BadLogin }}
<div class="toast" id="toast-bad-login" onclick="removeToastBadLogin()" style="position:fixed;top:0;right:0;padding:20px;background:red;">Error logging in ❌</div>
{{ end }}
<form method="post" action="/login">
<div>
<label for="email">Email: </label>
<input id="email" name="email" type="text" autocomplete="on"></input>
</div>
<div>
<label for="username">Username: </label>
<input id="username" name="username" type="text" autocomplete="on"></input>
</div>
<div>
<label for="password">Password: </label>
<input id="password" name="password" type="password"
{{ if .Config.PasswordRequired }}required{{ end }}
></input>
</div>
<div>
<input id="submit" name="submit" type="submit" value="Log in"></input>
</div>
</form>
</body></html>
`))
var templateSiteConfigEdit = template.Must(template.New("template_site_config_edit").Parse(`<!doctype html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{.StyleTag}}
<script>
function removeToast(id) {
const el = document.getElementById(id)
if (el) {
el.remove()
}
}
{{ if .Data.EditSaved }}
function removeToastEditSaved() {
window.history.pushState({}, document.title, window.location.pathname);
removeToast('toast-edit-saved')
}
setTimeout(removeToastEditSaved, 10000)
{{ end }}
{{ if .Data.BadEdit }}
function removeToastBadEdit() {
removeToast('toast-bad-edit')
window.history.pushState({}, document.title, window.location.pathname);
}
setTimeout(removeToastBadEdit, 10000)
{{ end }}
</script>
{{ if .Data.EditSaved }}
<div class="toast" id="toast-edit-saved" onclick="removeToastEditSaved()" style="position:fixed;top:0;right:0;padding:20px;background:green;">Config saved successfully ❌</div>
{{ end }}
{{ if .Data.BadEdit }}
<div class="toast" id="toast-bad-edit" onclick="removeToastBadEdit()" style="position:fixed;top:0;right:0;padding:20px;background:red;">Error saving edit ❌</div>
{{ end }}
</head><body>
<title>{{ .Config.Title }} Configuration</title>
<h1>{{ .Config.Title }} Configuration</h1>
<form method="post" action="/site_config">
<div>
<label for="title">Title: </label>
<input id="title" name="title" type="text" autocomplete="on" value="{{ .Config.Title }}"></input>
</div>
<div>
<label for="support_email">Support Email: </label>
<input id="support_email" name="support_email" type="text" autocomplete="on" value="{{ .Config.SupportEmail }}"></input>
</div>
<div>
<label for="support_phone">Support Phone: </label>
<input id="support_phone" name="support_phone" type="text" autocomplete="on" value="{{ .Config.SupportPhone }}"></input>
</div>
<div>
<label for="purpose">Site Purpose: </label>
<textarea id="purpose" name="purpose" style="resize:vertical; max-height:400px; min-height: 60px;" value="{{ .Config.Purpose }}"></textarea>
</div>
<div>
<label for="disclaimer">Site Disclaimer: </label>
<textarea id="disclaimer" name="disclaimer" style="resize:vertical; max-height:400px; min-height: 60px;" value="{{ .Config.Disclaimer }}"></textarea>
</div>
<div>
<label for="max_default_businesses">Max default businesses: </label>
<input id="max_default_businesses" name="max_default_businesses" type="number" value="{{ .Config.MaxDefaultBusinesses }}" min="1" step="1"></input>
</div>
<div>
<label for="listen_address">Listen Address: </label>
<input id="listen_address" name="listen_address" type="text" autocomplete="on" value="{{ .Config.ListenAddress }}"></input>
</div>
<div>
<label for="port">Port: </label>
<input id="port" name="port" type="number" value="{{ .Config.Port }}" min="1" min="65535" step="1"></input>
</div>
<div>
<label for="signup_needs_approve">Signup needs approve: </label>
<input id="signup_needs_approve" name="signup_needs_approve" type="checkbox" {{ if .Config.SignupNeedsApprove }}checked{{ end }}></input>
</div>
<div>
<label for="session_length_max">Session length max (seconds): </label>
<input id="session_length_max" name="session_length_max" type="number" value="{{ .Config.SessionLengthMax }}" min="300" step="60"></input>
</div>
<div>
<label for="email_confirm">Email needs confirm: </label>
<input id="email_confirm" name="email_confirm" type="checkbox" {{ if .Config.EmailConfirm }}checked{{ end }}></input>
</div>
<div>
<label for="email_required">Email needs required: </label>
<input id="email_required" name="email_required" type="checkbox" {{ if .Config.EmailRequired }}checked{{ end }}></input>
</div>
<div>
<label for="email_regex">Email regex(es, "@@@" separated): </label>
<input id="email_regex" name="email_regex" type="text" autocomplete="on" value="{{ .Config.EmailRegex }}"></input>
</div>
<div>
<label for="password_salt_length">Password Salt Length: </label>
<input id="password_salt_length" name="password_salt_length" type="number" value="{{ .Config.PasswordSaltLength }}" min="16" step="1"></input>
</div>
<div>
<label for="password_key_length">Password Key Length: </label>
<input id="password_key_length" name="password_key_length" type="number" value="{{ .Config.PasswordKeyLength }}" min="16" step="1"></input>
</div>
<div>
<label for="password_rounds">Password Hash Rounds: </label>
<input id="password_rounds" name="password_rounds" type="number" value="{{ .Config.PasswordRounds }}" min="1000" step="10"></input>
</div>
<div>
<fieldset>
<legend>Password hash strategy:</legend>
<label for="sha2_256">SHA2_256: </label>
<input id="sha2_256" name="password_hash_strategy" type="radio" value="SHA2_256" {{ if eq .Config.PasswordHashStrategy "SHA2_256" }}checked{{ end }} />
<label for="sha3_256">SHA3_256: </label>
<input id="sha3_256" name="password_hash_strategy" type="radio" value="SHA3_256" {{ if eq .Config.PasswordHashStrategy "SHA3_256" }}checked{{ end }} />
<label for="sha3_512">SHA3_512: </label>
<input id="sha3_512" name="password_hash_strategy" type="radio" value="SHA3_512" {{ if eq .Config.PasswordHashStrategy "SHA3_512" }}checked{{ end }} />
</fieldset>
</div>
<div>
<label for="password_attempts">Password Attempts: </label>
<input id="password_attempts" name="password_attempts" type="number" value="{{ .Config.PasswordAttempts }}" min="1" step="1"></input>
</div>
<div>
<label for="password_min">Password Min Length: </label>
<input id="password_min" name="password_min" type="number" value="{{ .Config.PasswordMin }}" min="1" step="1"></input>
</div>
<div>
<label for="password_regex">Password regex(es, "@@@" separated): </label>
<input id="password_regex" name="password_regex" type="text" autocomplete="on" value="{{ .Config.PasswordRegex }}"></input>
</div>
<div>
<label for="password_help">Password help (hint of password policy to users): </label>
<input id="password_help" name="password_help" type="text" autocomplete="on" value="{{ .Config.PasswordHelp }}"></input>
</div>
<div>
<label for="log_level">Log Level: </label>
<input id="log_level" name="log_level" type="text" autocomplete="on" value="{{ .Config.LogLevel }}"></input>
</div>
<div>
<label for="log_level_key">Log Level Key: </label>
<input id="log_level_key" name="log_level_key" type="text" autocomplete="on" value="{{ .Config.LogLevelKey }}"></input>
</div>
<div>
<label for="log_level_overrides">Log Level Overrides: </label>
<input id="log_level_overrides" name="log_level_overrides" type="text" autocomplete="on" value="{{ .Config.LogLevelOverrides }}"></input>
</div>
<div>
<label for="log_time_format">Log Time Format: </label>
<input id="log_time_format" name="log_time_format" type="text" autocomplete="on" value="{{ .Config.LogTimeFormat }}"></input>
</div>
<div>
<label for="log_time_key">Log Time Key: </label>
<input id="log_time_key" name="log_time_key" type="text" autocomplete="on" value="{{ .Config.LogTimeKey }}"></input>
</div>
<div>
<label for="log_add_source">Add source line to log: </label>
<input id="log_add_source" name="log_add_source" type="checkbox" {{ if .Config.LogAddSource }}checked{{ end }}></input>
</div>
<div>
<label for="person_style_text_color">Admin pages text color: </label>
<input id="person_style_text_color" name="person_style_text_color" type="text" autocomplete="on" value="{{ .Config.PersonStyleTextColor }}"></input>
</div>
<div>
<label for="person_style_text_dark_color">Admin pages text color (dark mode): </label>
<input id="person_style_text_dark_color" name="person_style_text_dark_color" type="text" autocomplete="on" value="{{ .Config.PersonStyleTextDarkColor }}"></input>
</div>
<div>
<label for="person_style_background_color">Admin pages background color: </label>
<input id="person_style_background_color" name="person_style_background_color" type="background" autocomplete="on" value="{{ .Config.PersonStyleBackgroundColor }}"></input>
</div>
<div>
<label for="person_style_background_dark_color">Admin pages background color (dark mode): </label>
<input id="person_style_background_dark_color" name="person_style_background_dark_color" type="background" autocomplete="on" value="{{ .Config.PersonStyleBackgroundDarkColor }}"></input>
</div>
<div>
<label for="config_reload_seconds">Reload config every # seconds (must restart service or wait for a config reload interval for it to apply): </label>
<input id="config_reload_seconds" name="config_reload_seconds" type="number" value="{{ .Config.ConfigReloadSeconds }}" min="3" step="1"></input>
</div>
<div>
<input id="submit" name="submit" type="submit" value="Save Configuration"></input>
</div>
</form>
</body></html>
`))
var templateSignup = template.Must(template.New("template_signup").Parse(`<!doctype html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{.StyleTag}}
</head><body>
<title>Signup</title>
<h1>{{ .Config.Title }} Account Signup</h1>
<form method="post" action="/signup">
<div>
<label for="username">Username: </label>
<input id="username" name="username" type="text" autocomplete="on"></input>
</div>
<div>
<label for="firstname">First name: </label>
<input id="firstname" name="firstname" type="firstname" autocomplete="on"></input>
</div>
<div>
<label for="lastname">Last name: </label>
<input id="lastname" name="lastname" type="lastname" autocomplete="on"></input>
</div>
<div>
<label for="password">Password: </label>
<input id="password" name="password" type="password"
{{ if .Config.PasswordRequired }}required{{ end }}
{{ if .Config.PasswordMin }}minlength="{{ .Config.PasswordMin }}"{{ end }}
></input>
</div>
<div>
<label for="password_confirm">Password Confirm: </label>
<input id="password_confirm" name="password_confirm" type="password"
{{ if .Config.PasswordRequired }}required{{ end }}
{{ if .Config.PasswordMin }}minlength="{{ .Config.PasswordMin }}"{{ end }}
></input>
</div>
<div>
<label for="email">Email: </label>
<input id="email" name="email" type="text"
{{ if .Config.EmailRequired }}required{{ end }}
autocomplete="on"></input>
</div>
{{ if .Config.SignupNeedsApprove }}
<div style="padding-bottom:0;">
<label for="request_text">(Optional): Account Purpose</label>
<textarea id="request_text" name="request_text" maxlength="255" style="resize:vertical; max-height:400px; min-height: 60px;"></textarea>
</div>
<p style='margin: 0; font-style: italic; padding-top: 10px;padding-bottom: 20px;'>You can include a short note to the admin about who you are, why you want to sign up, and what service(s) you are providing to which locale(s). If you are reaching out to the admin separately or the admin is already waiting for your signup, this may not be necessary.</p>
{{ end }}
<div>
<input id="submit" name="submit" type="submit"></input>
</div>
</form>
</body></html>
`))
var templatePricing = template.Must(template.New("template_pricing").Funcs(tmplfuncs).Parse(`<!doctype html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{.StyleTag}}
<style>
form .cell {
height: 40px;
}
#pay-schedule {
display: flex;
}
</style>
</head><body>
<title>{{ .Config.Title }} Pricing</title>
<h1>{{ .Config.Title }} Pricing</h1>
<form method="post" action="/pricing" style="width: 100%; max-width: 1000px;">
<div style="display:block;">
<div id="pay-schedule">
<label for="pay_annual">Pay monthly: </label>
<input id="pay_annual" type="radio" name="pay_schedule" checked>
<label for="pay_monthly">Pay monthly: </label>
<input id="pay_monthly" type="radio" name="pay_schedule">
</div>
<div style="display: flex; flex-direction: row;">
<div style="order: 0" style="flex-shrink:0; flex-grow:0;">
<div class="cell">Plan:</div>
<div class="cell">Price:</div>
<div class="cell">Anonymous links:</div>
<div class="cell">Named links:</div>
<div class="cell">Folders:</div>
<div class="cell">Analytics:</div>
<div class="cell">Analytics retention:</div>
<div class="cell">Analytics granularity:</div>
<div class="cell">Analytics View Limits:</div>
<div class="cell">API access <span tooltip="See https://domain.name/_ui/openapi">(?)</span>:</div>
</div>
{{ range .Config.BusinessTiers }}
{{ if or (le .FeatureLevel 0) (ge .FeatureLevel 9999) }} {{ continue }} {{ end }}
<div style="order: {{.FeatureLevel}}; flex: 1 0 auto;">
<div class="cell">{{ .DisplayName }}</div>
<div class="cell">{{ AsPrice .PriceByYear }} {{ AsPrice .PriceByMonth}}</div>
<div class="cell">{{ .BackhalvesAnonymous }}</div>
<div class="cell">{{ .BackhalvesNamed }}</div>
<div class="cell">{{ .Folders }}</div>
<div class="cell">Y</div>
<div class="cell">{{ SecAsD .AnalyticsRetention }} </div>
<div class="cell">{{ SecAsD .AnalyticsMaxGranularity }} </div>
<div class="cell">{{ .AnalyticsViewLimit }} </div>
<div class="cell">Y</div>
</div>
{{ end }}
</div>
<input type="submit" value="Choose plan">
</form>
</body></html>
`))
type ConfigTemplateData struct {
Config *config.Config `json:"config"`
Data any `json:"data"`
StyleTag template.HTML `json:"style_tag"`
}
func TLogin(w io.Writer, c *config.Config, q url.Values) error {
_, didLogout := q["did_logout"]
_, badLogin := q["bad_login"]
var stylesheetBuf bytes.Buffer
templateBaseStylesheet.Execute(&stylesheetBuf, ConfigTemplateData{Config: c})
return templateLogin.Execute(w, ConfigTemplateData{Config: c, StyleTag: template.HTML(stylesheetBuf.String()), Data: map[string]any{"DidLogout": didLogout, "BadLogin": badLogin}})
}
func TSignup(w io.Writer, c *config.Config) error {
var stylesheetBuf bytes.Buffer
templateBaseStylesheet.Execute(&stylesheetBuf, ConfigTemplateData{Config: c})
return templateSignup.Execute(w, ConfigTemplateData{Config: c, StyleTag: template.HTML(stylesheetBuf.String())})
}
func TSiteConfigEdit(w io.Writer, u url.Values, c *config.Config) error {
var stylesheetBuf bytes.Buffer
templateBaseStylesheet.Execute(&stylesheetBuf, ConfigTemplateData{Config: c})
_, badEdit := u["bad_edit"]
_, editSaved := u["edit_saved"]
return templateSiteConfigEdit.Execute(w, ConfigTemplateData{Config: c, StyleTag: template.HTML(stylesheetBuf.String()), Data: map[string]any{"BadEdit": badEdit, "EditSaved": editSaved}})
}
func TPricing(w io.Writer, u url.Values, c *config.Config) error {
var stylesheetBuf bytes.Buffer
templateBaseStylesheet.Execute(&stylesheetBuf, ConfigTemplateData{Config: c})
return templatePricing.Execute(w, ConfigTemplateData{Config: c, StyleTag: template.HTML(stylesheetBuf.String()), Data: map[string]any{}})
}