feat: redirect root path based on auth state #52
@@ -652,7 +652,7 @@ against a misbehaving sender).
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | --------------------------- | ----------- |
|
||||
| `GET` | `/` | Web UI index page (server-rendered) |
|
||||
| `GET` | `/` | Root redirect (authenticated → `/sources`, unauthenticated → `/pages/login`) |
|
||||
| `GET` | `/.well-known/healthcheck` | Health check (JSON: status, uptime, version) |
|
||||
| `GET` | `/s/*` | Static file serving (embedded CSS, JS) |
|
||||
| `ANY` | `/webhook/{uuid}` | Webhook receiver endpoint (accepts all methods) |
|
||||
|
||||
@@ -65,7 +65,6 @@ func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
|
||||
|
||||
// Parse all page templates once at startup
|
||||
s.templates = map[string]*template.Template{
|
||||
"index.html": parsePageTemplate("index.html"),
|
||||
"login.html": parsePageTemplate("login.html"),
|
||||
"profile.html": parsePageTemplate("profile.html"),
|
||||
"sources_list.html": parsePageTemplate("sources_list.html"),
|
||||
|
||||
@@ -4,10 +4,8 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxtest"
|
||||
"sneak.berlin/go/webhooker/internal/config"
|
||||
@@ -26,6 +24,7 @@ func (n *noopNotifier) Notify([]delivery.DeliveryTask) {}
|
||||
|
||||
func TestHandleIndex(t *testing.T) {
|
||||
var h *Handlers
|
||||
var sess *session.Session
|
||||
|
||||
app := fxtest.New(
|
||||
t,
|
||||
@@ -44,15 +43,47 @@ func TestHandleIndex(t *testing.T) {
|
||||
func() delivery.Notifier { return &noopNotifier{} },
|
||||
New,
|
||||
),
|
||||
fx.Populate(&h),
|
||||
fx.Populate(&h, &sess),
|
||||
)
|
||||
app.RequireStart()
|
||||
defer app.RequireStop()
|
||||
|
||||
// Since we can't test actual template rendering without templates,
|
||||
// let's test that the handler is created and doesn't panic
|
||||
handler := h.HandleIndex()
|
||||
assert.NotNil(t, handler)
|
||||
t.Run("unauthenticated redirects to login", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := h.HandleIndex()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusSeeOther, w.Code)
|
||||
assert.Equal(t, "/pages/login", w.Header().Get("Location"))
|
||||
})
|
||||
|
||||
t.Run("authenticated redirects to sources", func(t *testing.T) {
|
||||
// Create a request, set up an authenticated session, then test
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Get a session and mark it as authenticated
|
||||
s, err := sess.Get(req)
|
||||
assert.NoError(t, err)
|
||||
sess.SetUser(s, "test-user-id", "testuser")
|
||||
err = sess.Save(req, w, s)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Build a new request with the session cookie from the response
|
||||
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for _, cookie := range w.Result().Cookies() {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
w2 := httptest.NewRecorder()
|
||||
|
||||
handler := h.HandleIndex()
|
||||
handler.ServeHTTP(w2, req2)
|
||||
|
||||
assert.Equal(t, http.StatusSeeOther, w2.Code)
|
||||
assert.Equal(t, "/sources", w2.Header().Get("Location"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRenderTemplate(t *testing.T) {
|
||||
@@ -96,37 +127,3 @@ func TestRenderTemplate(t *testing.T) {
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatUptime(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
duration string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "minutes only",
|
||||
duration: "45m",
|
||||
expected: "45m",
|
||||
},
|
||||
{
|
||||
name: "hours and minutes",
|
||||
duration: "2h30m",
|
||||
expected: "2h 30m",
|
||||
},
|
||||
{
|
||||
name: "days, hours and minutes",
|
||||
duration: "25h45m",
|
||||
expected: "1d 1h 45m",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d, err := time.ParseDuration(tt.duration)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := formatUptime(d)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,20 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/webhooker/internal/database"
|
||||
)
|
||||
|
||||
// HandleIndex returns a handler for the root path that redirects based
|
||||
// on authentication state: authenticated users go to /sources (the
|
||||
// dashboard), unauthenticated users go to the login page.
|
||||
func (s *Handlers) HandleIndex() http.HandlerFunc {
|
||||
// Calculate server start time
|
||||
startTime := time.Now()
|
||||
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
// Calculate uptime
|
||||
uptime := time.Since(startTime)
|
||||
uptimeStr := formatUptime(uptime)
|
||||
|
||||
// Get user count from database
|
||||
var userCount int64
|
||||
s.db.DB().Model(&database.User{}).Count(&userCount)
|
||||
|
||||
// Prepare template data
|
||||
data := map[string]interface{}{
|
||||
"Version": s.params.Globals.Version,
|
||||
"Uptime": uptimeStr,
|
||||
"UserCount": userCount,
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
sess, err := s.session.Get(r)
|
||||
if err == nil && s.session.IsAuthenticated(sess) {
|
||||
http.Redirect(w, r, "/sources", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Render the template
|
||||
s.renderTemplate(w, req, "index.html", data)
|
||||
http.Redirect(w, r, "/pages/login", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// formatUptime formats a duration into a human-readable string
|
||||
func formatUptime(d time.Duration) string {
|
||||
days := int(d.Hours()) / 24
|
||||
hours := int(d.Hours()) % 24
|
||||
minutes := int(d.Minutes()) % 60
|
||||
|
||||
if days > 0 {
|
||||
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
|
||||
}
|
||||
if hours > 0 {
|
||||
return fmt.Sprintf("%dh %dm", hours, minutes)
|
||||
}
|
||||
return fmt.Sprintf("%dm", minutes)
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Home - Webhooker{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="max-w-4xl mx-auto px-6 py-12">
|
||||
<div class="text-center mb-10">
|
||||
<h1 class="text-4xl font-medium text-gray-900">Welcome to Webhooker</h1>
|
||||
<p class="mt-3 text-lg text-gray-500">A reliable webhook proxy service for event delivery</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Server Status Card -->
|
||||
<div class="card-elevated p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="rounded-full bg-success-50 p-3 mr-4">
|
||||
<svg class="w-6 h-6 text-success-500" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M1.333 2.667C1.333 1.194 4.318 0 8 0s6.667 1.194 6.667 2.667V4c0 1.473-2.985 2.667-6.667 2.667S1.333 5.473 1.333 4V2.667z"/>
|
||||
<path d="M1.333 6.334v3C1.333 10.805 4.318 12 8 12s6.667-1.194 6.667-2.667V6.334a6.51 6.51 0 0 1-1.458.79C11.81 7.684 9.967 8 8 8c-1.966 0-3.809-.317-5.208-.876a6.508 6.508 0 0 1-1.458-.79z"/>
|
||||
<path d="M14.667 11.668a6.51 6.51 0 0 1-1.458.789c-1.4.56-3.242.876-5.21.876-1.966 0-3.809-.316-5.208-.876a6.51 6.51 0 0 1-1.458-.79v1.666C1.333 14.806 4.318 16 8 16s6.667-1.194 6.667-2.667v-1.665z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-medium text-gray-900">Server Status</h2>
|
||||
<span class="badge-success">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Uptime</p>
|
||||
<p class="text-2xl font-medium text-gray-900">{{.Uptime}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Version</p>
|
||||
<p class="font-mono text-sm text-gray-700">{{.Version}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users Card -->
|
||||
<div class="card-elevated p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="rounded-full bg-primary-50 p-3 mr-4">
|
||||
<svg class="w-6 h-6 text-primary-500" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022zM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0zM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816zM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275zM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-medium text-gray-900">Users</h2>
|
||||
<p class="text-sm text-gray-500">Registered accounts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-4xl font-medium text-gray-900">{{.UserCount}}</p>
|
||||
<p class="text-sm text-gray-500 mt-1">Total users</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if not .User}}
|
||||
<div class="text-center mt-10">
|
||||
<p class="text-gray-500 mb-4">Ready to get started?</p>
|
||||
<a href="/pages/login" class="btn-primary">Login to your account</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user