initial commit

This commit is contained in:
Krzysztof
2025-08-01 18:01:55 +02:00
commit 9af1af7f92
73 changed files with 6531 additions and 0 deletions

69
handler/check_handler.go Normal file
View File

@@ -0,0 +1,69 @@
package handler
import (
"fmt"
"html/template"
"net/http"
"strconv"
"time"
"uptimemonitor"
"uptimemonitor/html"
)
func (h *Handler) ListChecks() http.HandlerFunc {
tmpl := template.Must(template.New("check.html").Funcs(template.FuncMap{
"sub": func(a, b int) int {
return a - b
},
}).ParseFS(html.FS, "check.html"))
type data struct {
Monitor uptimemonitor.Monitor
Checks []uptimemonitor.Check
Skeletons []int
MaxTime int64
StartTime string
EndTime string
}
return func(w http.ResponseWriter, r *http.Request) {
monitorID, err := strconv.Atoi(r.PathValue("monitor"))
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
monitor, err := h.Store.GetMonitorByID(r.Context(), monitorID)
if err != nil || monitor.ID == 0 {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
checks, err := h.Store.ListChecks(r.Context(), int64(monitorID), 60)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
maxTime := int64(0)
for _, check := range checks {
if check.ResponseTimeMs > maxTime {
maxTime = check.ResponseTimeMs
}
}
err = tmpl.ExecuteTemplate(w, "check_list", data{
Monitor: monitor,
Checks: checks,
Skeletons: make([]int, 60),
MaxTime: maxTime,
StartTime: time.Now().Add(-1 * time.Hour).Format("15:04"),
EndTime: time.Now().Format("15:04"),
})
if err != nil {
http.Error(w, fmt.Sprintf("%v", err), http.StatusInternalServerError)
return
}
}
}

29
handler/handler.go Normal file
View File

@@ -0,0 +1,29 @@
package handler
import (
"net/http"
"uptimemonitor"
"uptimemonitor/service"
"uptimemonitor/store"
)
type Handler struct {
Store *store.Store
Secure bool
}
func New(store *store.Store, service *service.Service, secure bool) *Handler {
return &Handler{
Store: store,
Secure: secure,
}
}
func getUserFromRequest(r *http.Request) uptimemonitor.User {
user, ok := r.Context().Value(userContextKey).(uptimemonitor.User)
if !ok {
return uptimemonitor.User{}
}
return user
}

28
handler/home_handler.go Normal file
View File

@@ -0,0 +1,28 @@
package handler
import (
"html/template"
"net/http"
"uptimemonitor"
"uptimemonitor/html"
)
func (h *Handler) HomePage() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "layout.html", "app.html", "home.html"))
type data struct {
User uptimemonitor.User
}
return func(w http.ResponseWriter, r *http.Request) {
count := h.Store.CountMonitors(r.Context())
if count == 0 {
http.Redirect(w, r, "/new", http.StatusSeeOther)
return
}
tmpl.Execute(w, data{
User: getUserFromRequest(r),
})
}
}

View File

@@ -0,0 +1,80 @@
package handler
import (
"fmt"
"html/template"
"net/http"
"strconv"
"uptimemonitor"
"uptimemonitor/html"
)
func (h *Handler) ListIncidents() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "incident.html"))
type data struct {
Incidents []uptimemonitor.Incident
}
return func(w http.ResponseWriter, r *http.Request) {
incidents, err := h.Store.ListOpenIncidents(r.Context())
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "incident_list", data{
Incidents: incidents,
})
}
}
func (h *Handler) DeleteIncident() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("incident"))
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
i, err := h.Store.GetIncidentByID(r.Context(), int64(id))
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
h.Store.DeleteIncident(r.Context(), int64(id))
w.Header().Set("HX-Redirect", fmt.Sprintf("/m/%s", i.Monitor.Uuid))
}
}
func (h *Handler) IncidentPage() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "layout.html", "app.html", "incident.html"))
type data struct {
User uptimemonitor.User
Incident uptimemonitor.Incident
Monitor uptimemonitor.Monitor
}
return func(w http.ResponseWriter, r *http.Request) {
uuid := r.PathValue("incident")
incident, err := h.Store.GetIncidentByUuid(r.Context(), uuid)
if err != nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
if incident.Monitor.Uuid != r.PathValue("monitor") {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
tmpl.Execute(w, data{
User: getUserFromRequest(r),
Incident: incident,
Monitor: incident.Monitor,
})
}
}

95
handler/login_handler.go Normal file
View File

@@ -0,0 +1,95 @@
package handler
import (
"html/template"
"net/http"
"time"
"uptimemonitor"
"uptimemonitor/form"
"uptimemonitor/html"
"golang.org/x/crypto/bcrypt"
)
func (h *Handler) LoginPage() http.HandlerFunc {
type data struct {
Form form.LoginForm
}
tmpl := template.Must(template.ParseFS(html.FS, "layout.html", "login.html"))
return func(w http.ResponseWriter, r *http.Request) {
tmpl.Execute(w, data{
Form: form.LoginForm{},
})
}
}
func (h *Handler) LoginForm() http.HandlerFunc {
type data struct {
Form form.LoginForm
}
tmpl := template.Must(template.ParseFS(html.FS, "login.html"))
return func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
f := form.LoginForm{
Email: r.PostFormValue("email"),
Password: r.PostFormValue("password"),
}
if !f.Validate() {
w.WriteHeader(http.StatusBadRequest)
tmpl.ExecuteTemplate(w, "login_form", data{
Form: f,
})
return
}
user, err := h.Store.GetUserByEmail(r.Context(), f.Email)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
f.Errors["Email"] = "The credentials do not match our records"
tmpl.ExecuteTemplate(w, "login_form", data{
Form: f,
})
return
}
if err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(f.Password)); err != nil {
w.WriteHeader(http.StatusBadRequest)
f.Errors["Email"] = "The credentials do not match our records"
tmpl.ExecuteTemplate(w, "login_form", data{
Form: f,
})
return
}
session, err := h.Store.CreateSession(r.Context(), uptimemonitor.Session{
UserID: user.ID,
ExpiresAt: time.Now().Add(time.Hour * 24 * 30),
User: user,
})
if err != nil {
w.WriteHeader(http.StatusBadRequest)
f.Errors["Email"] = "Something went wrong, try again later"
tmpl.ExecuteTemplate(w, "login_form", data{
Form: f,
})
return
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: session.Uuid,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: h.Secure,
Expires: session.ExpiresAt,
})
w.Header().Set("HX-Redirect", "/")
}
}

34
handler/logout_handler.go Normal file
View File

@@ -0,0 +1,34 @@
package handler
import (
"net/http"
"time"
"uptimemonitor"
)
func (h *Handler) Logout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, ok := r.Context().Value(sessionContextKey).(uptimemonitor.Session)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusServiceUnavailable)
return
}
err := h.Store.RemoveSessionByID(r.Context(), session.ID)
if err != nil {
// todo: log
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: h.Secure,
Expires: time.Now().Add(time.Hour * 24 * 30 * -1),
})
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
}

118
handler/middleware.go Normal file
View File

@@ -0,0 +1,118 @@
package handler
import (
"context"
"log/slog"
"net/http"
"strings"
"uptimemonitor"
)
type contextKey string
const (
sessionContextKey contextKey = "session"
userContextKey contextKey = "user"
)
func (m *Handler) Installed(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
count, err := m.Store.CountUsers(r.Context())
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if count == 0 && r.Method == http.MethodGet {
http.Redirect(w, r, "/setup", http.StatusSeeOther)
return
}
if count == 0 {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func (m *Handler) UserFromCookie(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("session")
if err != nil || c.Value == "" {
next.ServeHTTP(w, r)
return
}
session, err := m.Store.GetSessionByUuid(r.Context(), c.Value)
if err != nil {
next.ServeHTTP(w, r)
return
}
ctx := context.WithValue(r.Context(), sessionContextKey, session)
ctx = context.WithValue(ctx, userContextKey, session.User)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (m *Handler) Authenticated(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
value := r.Context().Value(sessionContextKey)
if value == nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
_, ok := value.(uptimemonitor.Session)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
})
}
func (m *Handler) Guest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
value := r.Context().Value(sessionContextKey)
if value != nil {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
})
}
func (m *Handler) Recoverer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
slog.Error("panic", "error", err)
w.WriteHeader(http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func (m *Handler) NoCache(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/static") {
w.Header().Set("Cache-Control", "public, max-age=31536000")
next.ServeHTTP(w, r)
return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
next.ServeHTTP(w, r)
})
}

397
handler/monitor_handler.go Normal file
View File

@@ -0,0 +1,397 @@
package handler
import (
"fmt"
"html/template"
"net/http"
"strconv"
"uptimemonitor"
"uptimemonitor/form"
"uptimemonitor/html"
)
func (h *Handler) ListMonitors() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "monitor.html"))
type data struct {
Monitors []uptimemonitor.Monitor
Skeletons []int
}
return func(w http.ResponseWriter, r *http.Request) {
monitors, err := h.Store.ListMonitors(r.Context())
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "monitor_list", data{
Monitors: monitors,
Skeletons: make([]int, 60),
})
}
}
func (h *Handler) CreateMonitorPage() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "layout.html", "app.html", "new.html"))
type data struct {
Form form.MonitorForm
User uptimemonitor.User
}
return func(w http.ResponseWriter, r *http.Request) {
tmpl.Execute(w, data{
Form: form.MonitorForm{
HttpHeaders: `{
"Content-Type": "application/json"
}`,
HttpBody: `{}`,
WebhookHeaders: `{
"Content-Type": "application/json"
}`,
WebhookBody: `{
"url": "{{ .Url }}",
"code": "{{ .StatusCode }}"
}`,
},
User: getUserFromRequest(r),
})
}
}
func (h *Handler) CreateMonitorForm() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "new.html"))
type data struct {
Form form.MonitorForm
}
return func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
f := form.MonitorForm{
HttpMethod: r.PostFormValue("http_method"),
Url: r.PostFormValue("url"),
HasCustomHeaders: r.PostFormValue("has_custom_headers") == "on",
HasCustomBody: r.PostFormValue("has_custom_body") == "on",
HttpHeaders: r.PostFormValue("http_headers"),
HttpBody: r.PostFormValue("http_body"),
HasWebhook: r.PostFormValue("has_webhook") == "on",
WebhookMethod: r.PostFormValue("webhook_method"),
WebhookUrl: r.PostFormValue("webhook_url"),
WebhookHeaders: r.PostFormValue("webhook_headers"),
WebhookBody: r.PostFormValue("webhook_body"),
}
if !f.Validate() {
w.WriteHeader(http.StatusBadRequest)
tmpl.ExecuteTemplate(w, "new_form", data{Form: f})
return
}
monitor := uptimemonitor.Monitor{
HttpMethod: f.HttpMethod,
Url: f.Url,
}
if f.HasCustomHeaders {
monitor.HttpHeaders = f.HttpHeaders
}
if f.HasCustomBody {
monitor.HttpBody = f.HttpBody
}
if f.HasWebhook {
monitor.WebhookUrl = f.WebhookUrl
monitor.WebhookMethod = f.WebhookMethod
monitor.WebhookHeaders = f.WebhookHeaders
monitor.WebhookBody = f.WebhookBody
}
m, err := h.Store.CreateMonitor(r.Context(), monitor)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Set("HX-Redirect", m.URI())
}
}
func (h *Handler) MonitorPage() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "layout.html", "app.html", "monitor.html"))
type data struct {
Monitor uptimemonitor.Monitor
Skeletons []int
User uptimemonitor.User
}
return func(w http.ResponseWriter, r *http.Request) {
m, err := h.Store.GetMonitorByUuid(r.Context(), r.PathValue("monitor"))
if err != nil || m.ID == 0 {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
tmpl.Execute(w, data{
Monitor: m,
Skeletons: make([]int, 60),
User: getUserFromRequest(r),
})
}
}
func (h *Handler) MonitorStats() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "monitor.html"))
type data struct {
ID int64
AvgResponseTime int64
Uptime string
Count int64
ChecksCount int64
FailureCount int64
}
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("monitor"))
if err != nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
m, err := h.Store.GetMonitorByID(r.Context(), id)
if err != nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
tmpl.ExecuteTemplate(w, "monitor_stats", data{
ID: int64(id),
AvgResponseTime: int64(m.AvgResponseTimeMs),
Uptime: fmt.Sprintf("%.1f", m.Uptime),
Count: m.IncidentsCount,
ChecksCount: m.N,
FailureCount: m.IncidentsCount,
})
}
}
func (h *Handler) ListMonitorIncidents() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "monitor.html"))
type data struct {
ID int64
Monitor uptimemonitor.Monitor
Incidents []uptimemonitor.Incident
}
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("monitor"))
if err != nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
m, err := h.Store.GetMonitorByID(r.Context(), id)
if err != nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
incidents, err := h.Store.ListMonitorIncidents(r.Context(), int64(id))
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "monitor_incident_list", data{
ID: int64(id),
Monitor: m,
Incidents: incidents,
})
}
}
func (h *Handler) EditMonitorPage() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "layout.html", "app.html", "edit.html"))
type data struct {
Form form.MonitorForm
User uptimemonitor.User
Monitor uptimemonitor.Monitor
}
return func(w http.ResponseWriter, r *http.Request) {
m, err := h.Store.GetMonitorByUuid(r.Context(), r.PathValue("monitor"))
if err != nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
f := form.MonitorForm{
Url: m.Url,
HttpMethod: m.HttpMethod,
HttpHeaders: m.HttpHeaders,
HttpBody: m.HttpBody,
HasCustomHeaders: m.HttpHeaders != "",
HasCustomBody: m.HttpBody != "",
HasWebhook: m.WebhookUrl != "",
WebhookUrl: m.WebhookUrl,
WebhookMethod: m.WebhookMethod,
WebhookHeaders: m.WebhookHeaders,
WebhookBody: m.WebhookBody,
}
if !f.HasCustomBody {
f.HttpBody = "{}"
}
if !f.HasCustomHeaders {
f.HttpHeaders = "{}"
}
tmpl.Execute(w, data{
Monitor: m,
Form: f,
User: getUserFromRequest(r),
})
}
}
func (h *Handler) EditMonitorForm() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "edit.html"))
type data struct {
Form form.MonitorForm
User uptimemonitor.User
Monitor uptimemonitor.Monitor
}
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("monitor"))
if err != nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
monitor, err := h.Store.GetMonitorByID(r.Context(), id)
if err != nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
r.ParseForm()
customHeaders := r.PostFormValue("http_headers")
customBody := r.PostFormValue("http_body")
if r.PostFormValue("has_custom_headers") != "on" {
customHeaders = ""
}
if r.PostFormValue("has_custom_body") != "on" {
customBody = ""
}
f := form.MonitorForm{
HttpMethod: r.PostFormValue("http_method"),
Url: r.PostFormValue("url"),
HasCustomHeaders: r.PostFormValue("has_custom_headers") == "on",
HasCustomBody: r.PostFormValue("has_custom_body") == "on",
HttpHeaders: customHeaders,
HttpBody: customBody,
HasWebhook: r.PostFormValue("has_webhook") == "on",
WebhookMethod: r.PostFormValue("webhook_method"),
WebhookUrl: r.PostFormValue("webhook_url"),
WebhookHeaders: r.PostFormValue("webhook_headers"),
WebhookBody: r.PostFormValue("webhook_body"),
}
if !f.Validate() {
w.WriteHeader(http.StatusBadRequest)
tmpl.ExecuteTemplate(w, "edit_form", data{
Monitor: monitor,
Form: f,
User: getUserFromRequest(r),
})
return
}
monitor.Url = f.Url
monitor.HttpMethod = f.HttpMethod
if f.HasCustomHeaders {
monitor.HttpHeaders = f.HttpHeaders
} else {
monitor.HttpHeaders = ""
}
if f.HasCustomBody {
monitor.HttpBody = f.HttpBody
} else {
monitor.HttpBody = ""
}
if f.HasWebhook {
monitor.WebhookUrl = f.WebhookUrl
monitor.WebhookMethod = f.WebhookMethod
monitor.WebhookHeaders = f.WebhookHeaders
monitor.WebhookBody = f.WebhookBody
} else {
monitor.WebhookUrl = ""
monitor.WebhookMethod = ""
monitor.WebhookHeaders = ""
monitor.WebhookBody = ""
}
err = h.Store.UpdateMonitor(r.Context(), monitor)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Set("HX-Redirect", monitor.URI())
}
}
func (h *Handler) DeleteMonitorPage() http.HandlerFunc {
tmpl := template.Must(template.ParseFS(html.FS, "layout.html", "app.html", "delete.html"))
type data struct {
User uptimemonitor.User
Monitor uptimemonitor.Monitor
}
return func(w http.ResponseWriter, r *http.Request) {
uuid := r.PathValue("monitor")
m, err := h.Store.GetMonitorByUuid(r.Context(), uuid)
if err != nil || m.ID == 0 {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
tmpl.Execute(w, data{
User: getUserFromRequest(r),
Monitor: m,
})
}
}
func (h *Handler) DeleteMonitorForm() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("monitor"))
m, err := h.Store.GetMonitorByID(r.Context(), id)
if err != nil || m.ID == 0 {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
h.Store.DeleteMonitor(r.Context(), int64(id))
w.Header().Add("HX-Redirect", "/")
}
}

104
handler/setup_handler.go Normal file
View File

@@ -0,0 +1,104 @@
package handler
import (
"html/template"
"net/http"
"time"
"uptimemonitor"
"uptimemonitor/form"
"uptimemonitor/html"
"golang.org/x/crypto/bcrypt"
)
func (h *Handler) SetupPage() http.HandlerFunc {
type data struct {
Form form.SetupForm
}
tmpl := template.Must(template.ParseFS(html.FS, "layout.html", "setup.html"))
return func(w http.ResponseWriter, r *http.Request) {
count, err := h.Store.CountUsers(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if count > 0 {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
tmpl.Execute(w, data{
Form: form.SetupForm{},
})
}
}
func (h *Handler) SetupForm() http.HandlerFunc {
type data struct {
Form form.SetupForm
}
tmpl := template.Must(template.ParseFS(html.FS, "setup.html"))
return func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
f := form.SetupForm{
Name: r.PostFormValue("name"),
Email: r.PostFormValue("email"),
Password: r.PostFormValue("password"),
}
if !f.Validate() {
w.WriteHeader(http.StatusBadRequest)
tmpl.ExecuteTemplate(w, "setup_form", data{
Form: f,
})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(f.Password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
user, err := h.Store.CreateUser(r.Context(), uptimemonitor.User{
Name: f.Name,
Email: f.Email,
PasswordHash: string(hash),
})
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session, err := h.Store.CreateSession(r.Context(), uptimemonitor.Session{
UserID: user.ID,
ExpiresAt: time.Now().Add(time.Hour * 24 * 30),
User: user,
})
if err != nil {
w.WriteHeader(http.StatusBadRequest)
f.Errors["Email"] = "Something went wrong, try again later"
tmpl.ExecuteTemplate(w, "login_form", data{
Form: f,
})
return
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: session.Uuid,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: h.Secure,
Expires: session.ExpiresAt,
})
w.Header().Set("HX-Redirect", "/")
}
}

View File

@@ -0,0 +1,58 @@
package handler
import (
"encoding/json"
"html/template"
"net/http"
"uptimemonitor"
"uptimemonitor/html"
)
func (*Handler) ListSponsors() http.HandlerFunc {
layout := template.Must(template.ParseFS(html.FS, "layout.html"))
sponsor := template.Must(template.ParseFS(html.FS, "sponsor.html"))
type data struct {
Sponsors []uptimemonitor.Sponsor
}
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("HX-Request") != "true" {
layout.ExecuteTemplate(w, "sponsors", nil)
return
}
d := data{
Sponsors: []uptimemonitor.Sponsor{
{
Name: "AIR Labs",
Url: "https://airlabs.pl",
Image: "/static/img/airlabs.svg",
},
},
}
req, err := http.NewRequest(http.MethodGet, "https://sponsors.uptimemonitor.dev", nil)
if err != nil {
sponsor.Execute(w, d)
return
}
res, err := http.DefaultClient.Do(req)
if err != nil {
sponsor.Execute(w, d)
return
}
var sponsors []uptimemonitor.Sponsor
err = json.NewDecoder(res.Body).Decode(&sponsors)
if err != nil {
sponsor.Execute(w, d)
return
}
sponsor.Execute(w, data{
Sponsors: sponsors,
})
}
}