Compare commits
7 Commits
853f25ee67
...
b437955378
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b437955378 | ||
|
|
5462db565a | ||
|
|
6ff2bc7647 | ||
|
|
291e60adb2 | ||
|
|
fd3ca22012 | ||
|
|
68c2a4df36 | ||
|
|
6031167c78 |
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Jeffrey Paul <sneak@sneak.berlin>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
25
README.md
25
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
webhooker is a Go web application by [@sneak](https://sneak.berlin) that
|
webhooker is a Go web application by [@sneak](https://sneak.berlin) that
|
||||||
receives, stores, and proxies webhooks to configured targets with retry
|
receives, stores, and proxies webhooks to configured targets with retry
|
||||||
support, observability, and a management web UI. License: pending.
|
support, observability, and a management web UI. License: MIT.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -88,15 +88,14 @@ monolithic database:
|
|||||||
- **Main application database** — Stores application configuration and
|
- **Main application database** — Stores application configuration and
|
||||||
all standard webapp data: users, sessions, API keys, and global
|
all standard webapp data: users, sessions, API keys, and global
|
||||||
settings.
|
settings.
|
||||||
- **Per-processor databases** — Each processor (working name — a better
|
- **Per-webhook databases** — Each webhook gets its own dedicated SQLite
|
||||||
term is needed) gets its own dedicated SQLite database file containing:
|
database file containing: input logs, webhook logs, and all output
|
||||||
input logs, processor logs, and all output queues for that specific
|
queues for that specific webhook.
|
||||||
processor.
|
|
||||||
|
|
||||||
This separation provides several benefits: processor databases can be
|
This separation provides several benefits: webhook databases can be
|
||||||
independently backed up, rotated, or archived; a high-volume processor
|
independently backed up, rotated, or archived; a high-volume webhook
|
||||||
won't cause lock contention or bloat affecting the main application; and
|
won't cause lock contention or bloat affecting the main application; and
|
||||||
individual processor data can be cleanly deleted when a processor is
|
individual webhook data can be cleanly deleted when a webhook is
|
||||||
removed.
|
removed.
|
||||||
|
|
||||||
### Package Layout
|
### Package Layout
|
||||||
@@ -120,9 +119,9 @@ The main entry point is `cmd/webhooker/main.go`.
|
|||||||
### Data Model
|
### Data Model
|
||||||
|
|
||||||
- **Users** — Service users with username/password (Argon2id hashing)
|
- **Users** — Service users with username/password (Argon2id hashing)
|
||||||
- **Processors** — Webhook processing units, many-to-one with users
|
- **Webhooks** — Webhook processing units, many-to-one with users
|
||||||
- **Webhooks** — Inbound URL endpoints feeding into processors
|
- **Entrypoints** — Inbound URL endpoints feeding into webhooks
|
||||||
- **Targets** — Delivery destinations per processor (HTTP, retry, database, log)
|
- **Targets** — Delivery destinations per webhook (HTTP, retry, database, log)
|
||||||
- **Events** — Captured webhook payloads
|
- **Events** — Captured webhook payloads
|
||||||
- **Deliveries** — Pairing of events with targets
|
- **Deliveries** — Pairing of events with targets
|
||||||
- **Delivery Results** — Outcome of each delivery attempt
|
- **Delivery Results** — Outcome of each delivery attempt
|
||||||
@@ -170,7 +169,7 @@ The main entry point is `cmd/webhooker/main.go`.
|
|||||||
- [ ] Analytics dashboard (success rates, response times)
|
- [ ] Analytics dashboard (success rates, response times)
|
||||||
|
|
||||||
### Phase 5: API
|
### Phase 5: API
|
||||||
- [ ] RESTful CRUD for processors, webhooks, targets
|
- [ ] RESTful CRUD for webhooks, entrypoints, targets
|
||||||
- [ ] Event viewing and filtering endpoints
|
- [ ] Event viewing and filtering endpoints
|
||||||
- [ ] API documentation (OpenAPI)
|
- [ ] API documentation (OpenAPI)
|
||||||
|
|
||||||
@@ -182,7 +181,7 @@ The main entry point is `cmd/webhooker/main.go`.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Pending — to be determined by the author (MIT, GPL, or WTFPL).
|
MIT License. See [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
"sneak.berlin/go/webhooker/internal/globals"
|
"sneak.berlin/go/webhooker/internal/globals"
|
||||||
"sneak.berlin/go/webhooker/internal/logger"
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
pkgconfig "sneak.berlin/go/webhooker/pkg/config"
|
pkgconfig "sneak.berlin/go/webhooker/pkg/config"
|
||||||
|
|
||||||
// spooky action at a distance!
|
// Populates the environment from a ./.env file automatically for
|
||||||
// this populates the environment
|
// development configuration. Kept in one place only (here).
|
||||||
// from a ./.env file automatically
|
|
||||||
// for development configuration.
|
|
||||||
// .env contents should be things like
|
|
||||||
// `DBURL=postgres://user:pass@.../`
|
|
||||||
// (without the backticks, of course)
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,6 +61,40 @@ func (c *Config) IsProd() bool {
|
|||||||
return c.Environment == EnvironmentProd
|
return c.Environment == EnvironmentProd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// envString returns the env var value if set, otherwise falls back to pkgconfig.
|
||||||
|
func envString(envKey, configKey string) string {
|
||||||
|
if v := os.Getenv(envKey); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return pkgconfig.GetString(configKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// envSecretString returns the env var value if set, otherwise falls back to pkgconfig secrets.
|
||||||
|
func envSecretString(envKey, configKey string) string {
|
||||||
|
if v := os.Getenv(envKey); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return pkgconfig.GetSecretString(configKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// envBool returns the env var value parsed as bool, otherwise falls back to pkgconfig.
|
||||||
|
func envBool(envKey, configKey string) bool {
|
||||||
|
if v := os.Getenv(envKey); v != "" {
|
||||||
|
return strings.EqualFold(v, "true") || v == "1"
|
||||||
|
}
|
||||||
|
return pkgconfig.GetBool(configKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// envInt returns the env var value parsed as int, otherwise falls back to pkgconfig.
|
||||||
|
func envInt(envKey, configKey string, defaultValue ...int) int {
|
||||||
|
if v := os.Getenv(envKey); v != "" {
|
||||||
|
if i, err := strconv.Atoi(v); err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pkgconfig.GetInt(configKey, defaultValue...)
|
||||||
|
}
|
||||||
|
|
||||||
// nolint:revive // lc parameter is required by fx even if unused
|
// nolint:revive // lc parameter is required by fx even if unused
|
||||||
func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
||||||
log := params.Logger.Get()
|
log := params.Logger.Get()
|
||||||
@@ -80,30 +111,30 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
|||||||
EnvironmentDev, EnvironmentProd, environment)
|
EnvironmentDev, EnvironmentProd, environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the environment in the config package
|
// Set the environment in the config package (for fallback resolution)
|
||||||
pkgconfig.SetEnvironment(environment)
|
pkgconfig.SetEnvironment(environment)
|
||||||
|
|
||||||
// Load configuration values
|
// Load configuration values — env vars take precedence over config.yaml
|
||||||
s := &Config{
|
s := &Config{
|
||||||
DBURL: pkgconfig.GetString("dburl"),
|
DBURL: envString("DBURL", "dburl"),
|
||||||
Debug: pkgconfig.GetBool("debug"),
|
Debug: envBool("DEBUG", "debug"),
|
||||||
MaintenanceMode: pkgconfig.GetBool("maintenanceMode"),
|
MaintenanceMode: envBool("MAINTENANCE_MODE", "maintenanceMode"),
|
||||||
DevelopmentMode: pkgconfig.GetBool("developmentMode"),
|
DevelopmentMode: envBool("DEVELOPMENT_MODE", "developmentMode"),
|
||||||
DevAdminUsername: pkgconfig.GetString("devAdminUsername"),
|
DevAdminUsername: envString("DEV_ADMIN_USERNAME", "devAdminUsername"),
|
||||||
DevAdminPassword: pkgconfig.GetString("devAdminPassword"),
|
DevAdminPassword: envString("DEV_ADMIN_PASSWORD", "devAdminPassword"),
|
||||||
Environment: pkgconfig.GetString("environment", environment),
|
Environment: environment,
|
||||||
MetricsUsername: pkgconfig.GetString("metricsUsername"),
|
MetricsUsername: envString("METRICS_USERNAME", "metricsUsername"),
|
||||||
MetricsPassword: pkgconfig.GetString("metricsPassword"),
|
MetricsPassword: envString("METRICS_PASSWORD", "metricsPassword"),
|
||||||
Port: pkgconfig.GetInt("port", 8080),
|
Port: envInt("PORT", "port", 8080),
|
||||||
SentryDSN: pkgconfig.GetSecretString("sentryDSN"),
|
SentryDSN: envSecretString("SENTRY_DSN", "sentryDSN"),
|
||||||
SessionKey: pkgconfig.GetSecretString("sessionKey"),
|
SessionKey: envSecretString("SESSION_KEY", "sessionKey"),
|
||||||
log: log,
|
log: log,
|
||||||
params: ¶ms,
|
params: ¶ms,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate database URL
|
// Validate database URL
|
||||||
if s.DBURL == "" {
|
if s.DBURL == "" {
|
||||||
return nil, fmt.Errorf("database URL (dburl) is required")
|
return nil, fmt.Errorf("database URL (DBURL) is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In production, require session key
|
// In production, require session key
|
||||||
@@ -118,8 +149,6 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
|||||||
|
|
||||||
if s.Debug {
|
if s.Debug {
|
||||||
params.Logger.EnableDebugLogging()
|
params.Logger.EnableDebugLogging()
|
||||||
s.log = params.Logger.Get()
|
|
||||||
log.Debug("Debug mode enabled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log configuration summary (without secrets)
|
// Log configuration summary (without secrets)
|
||||||
|
|||||||
14
internal/database/model_entrypoint.go
Normal file
14
internal/database/model_entrypoint.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
// Entrypoint represents an inbound URL endpoint that feeds into a webhook
|
||||||
|
type Entrypoint struct {
|
||||||
|
BaseModel
|
||||||
|
|
||||||
|
WebhookID string `gorm:"type:uuid;not null" json:"webhook_id"`
|
||||||
|
Path string `gorm:"uniqueIndex;not null" json:"path"` // URL path for this entrypoint
|
||||||
|
Description string `json:"description"`
|
||||||
|
Active bool `gorm:"default:true" json:"active"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Webhook Webhook `json:"webhook,omitempty"`
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
// Event represents a webhook event
|
// Event represents a captured webhook event
|
||||||
type Event struct {
|
type Event struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
ProcessorID string `gorm:"type:uuid;not null" json:"processor_id"`
|
WebhookID string `gorm:"type:uuid;not null" json:"webhook_id"`
|
||||||
WebhookID string `gorm:"type:uuid;not null" json:"webhook_id"`
|
EntrypointID string `gorm:"type:uuid;not null" json:"entrypoint_id"`
|
||||||
|
|
||||||
// Request data
|
// Request data
|
||||||
Method string `gorm:"not null" json:"method"`
|
Method string `gorm:"not null" json:"method"`
|
||||||
@@ -14,7 +14,7 @@ type Event struct {
|
|||||||
ContentType string `json:"content_type"`
|
ContentType string `json:"content_type"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Processor Processor `json:"processor,omitempty"`
|
|
||||||
Webhook Webhook `json:"webhook,omitempty"`
|
Webhook Webhook `json:"webhook,omitempty"`
|
||||||
|
Entrypoint Entrypoint `json:"entrypoint,omitempty"`
|
||||||
Deliveries []Delivery `json:"deliveries,omitempty"`
|
Deliveries []Delivery `json:"deliveries,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
// Processor represents an event processor
|
|
||||||
type Processor struct {
|
|
||||||
BaseModel
|
|
||||||
|
|
||||||
UserID string `gorm:"type:uuid;not null" json:"user_id"`
|
|
||||||
Name string `gorm:"not null" json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
RetentionDays int `gorm:"default:30" json:"retention_days"` // Days to retain events
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
User User `json:"user,omitempty"`
|
|
||||||
Webhooks []Webhook `json:"webhooks,omitempty"`
|
|
||||||
Targets []Target `json:"targets,omitempty"`
|
|
||||||
}
|
|
||||||
@@ -10,14 +10,14 @@ const (
|
|||||||
TargetTypeLog TargetType = "log"
|
TargetTypeLog TargetType = "log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Target represents a delivery target for a processor
|
// Target represents a delivery target for a webhook
|
||||||
type Target struct {
|
type Target struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
ProcessorID string `gorm:"type:uuid;not null" json:"processor_id"`
|
WebhookID string `gorm:"type:uuid;not null" json:"webhook_id"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Type TargetType `gorm:"not null" json:"type"`
|
Type TargetType `gorm:"not null" json:"type"`
|
||||||
Active bool `gorm:"default:true" json:"active"`
|
Active bool `gorm:"default:true" json:"active"`
|
||||||
|
|
||||||
// Configuration fields (JSON stored based on type)
|
// Configuration fields (JSON stored based on type)
|
||||||
Config string `gorm:"type:text" json:"config"` // JSON configuration
|
Config string `gorm:"type:text" json:"config"` // JSON configuration
|
||||||
@@ -27,6 +27,6 @@ type Target struct {
|
|||||||
MaxQueueSize int `json:"max_queue_size,omitempty"`
|
MaxQueueSize int `json:"max_queue_size,omitempty"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Processor Processor `json:"processor,omitempty"`
|
Webhook Webhook `json:"webhook,omitempty"`
|
||||||
Deliveries []Delivery `json:"deliveries,omitempty"`
|
Deliveries []Delivery `json:"deliveries,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ type User struct {
|
|||||||
Password string `gorm:"not null" json:"-"` // Argon2 hashed
|
Password string `gorm:"not null" json:"-"` // Argon2 hashed
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Processors []Processor `json:"processors,omitempty"`
|
Webhooks []Webhook `json:"webhooks,omitempty"`
|
||||||
APIKeys []APIKey `json:"api_keys,omitempty"`
|
APIKeys []APIKey `json:"api_keys,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
// Webhook represents a webhook endpoint that feeds into a processor
|
// Webhook represents a webhook processing unit that groups entrypoints and targets
|
||||||
type Webhook struct {
|
type Webhook struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
ProcessorID string `gorm:"type:uuid;not null" json:"processor_id"`
|
UserID string `gorm:"type:uuid;not null" json:"user_id"`
|
||||||
Path string `gorm:"uniqueIndex;not null" json:"path"` // URL path for this webhook
|
Name string `gorm:"not null" json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Active bool `gorm:"default:true" json:"active"`
|
RetentionDays int `gorm:"default:30" json:"retention_days"` // Days to retain events
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Processor Processor `json:"processor,omitempty"`
|
User User `json:"user,omitempty"`
|
||||||
|
Entrypoints []Entrypoint `json:"entrypoints,omitempty"`
|
||||||
|
Targets []Target `json:"targets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ func (d *Database) Migrate() error {
|
|||||||
return d.db.AutoMigrate(
|
return d.db.AutoMigrate(
|
||||||
&User{},
|
&User{},
|
||||||
&APIKey{},
|
&APIKey{},
|
||||||
&Processor{},
|
|
||||||
&Webhook{},
|
&Webhook{},
|
||||||
|
&Entrypoint{},
|
||||||
&Target{},
|
&Target{},
|
||||||
&Event{},
|
&Event{},
|
||||||
&Delivery{},
|
&Delivery{},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func (h *Handlers) HandleLoginPage() http.HandlerFunc {
|
|||||||
"Error": "",
|
"Error": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
h.renderTemplate(w, r, []string{"templates/base.html", "templates/login.html"}, data)
|
h.renderTemplate(w, r, "login.html", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ func (h *Handlers) HandleLoginSubmit() http.HandlerFunc {
|
|||||||
"Error": "Username and password are required",
|
"Error": "Username and password are required",
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
h.renderTemplate(w, r, []string{"templates/base.html", "templates/login.html"}, data)
|
h.renderTemplate(w, r, "login.html", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ func (h *Handlers) HandleLoginSubmit() http.HandlerFunc {
|
|||||||
"Error": "Invalid username or password",
|
"Error": "Invalid username or password",
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
h.renderTemplate(w, r, []string{"templates/base.html", "templates/login.html"}, data)
|
h.renderTemplate(w, r, "login.html", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ func (h *Handlers) HandleLoginSubmit() http.HandlerFunc {
|
|||||||
"Error": "Invalid username or password",
|
"Error": "Invalid username or password",
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
h.renderTemplate(w, r, []string{"templates/base.html", "templates/login.html"}, data)
|
h.renderTemplate(w, r, "login.html", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/healthcheck"
|
"sneak.berlin/go/webhooker/internal/healthcheck"
|
||||||
"sneak.berlin/go/webhooker/internal/logger"
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
"sneak.berlin/go/webhooker/internal/session"
|
"sneak.berlin/go/webhooker/internal/session"
|
||||||
|
"sneak.berlin/go/webhooker/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
// nolint:revive // HandlersParams is a standard fx naming convention
|
// nolint:revive // HandlersParams is a standard fx naming convention
|
||||||
@@ -26,11 +27,20 @@ type HandlersParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
params *HandlersParams
|
params *HandlersParams
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
hc *healthcheck.Healthcheck
|
hc *healthcheck.Healthcheck
|
||||||
db *database.Database
|
db *database.Database
|
||||||
session *session.Session
|
session *session.Session
|
||||||
|
templates map[string]*template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePageTemplate parses a page-specific template set from the embedded FS.
|
||||||
|
// Each page template is combined with the shared base, htmlheader, and navbar templates.
|
||||||
|
func parsePageTemplate(pageFile string) *template.Template {
|
||||||
|
return template.Must(
|
||||||
|
template.ParseFS(templates.Templates, "htmlheader.html", "navbar.html", "base.html", pageFile),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
|
func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
|
||||||
@@ -40,9 +50,16 @@ func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
|
|||||||
s.hc = params.Healthcheck
|
s.hc = params.Healthcheck
|
||||||
s.db = params.Database
|
s.db = params.Database
|
||||||
s.session = params.Session
|
s.session = params.Session
|
||||||
|
|
||||||
|
// 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"),
|
||||||
|
}
|
||||||
|
|
||||||
lc.Append(fx.Hook{
|
lc.Append(fx.Hook{
|
||||||
OnStart: func(ctx context.Context) error {
|
OnStart: func(ctx context.Context) error {
|
||||||
// FIXME compile some templates here or something
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -80,16 +97,11 @@ type UserInfo struct {
|
|||||||
Username string
|
Username string
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderTemplate renders a template with common data
|
// renderTemplate renders a pre-parsed template with common data
|
||||||
func (s *Handlers) renderTemplate(w http.ResponseWriter, r *http.Request, templateFiles []string, data interface{}) {
|
func (s *Handlers) renderTemplate(w http.ResponseWriter, r *http.Request, pageTemplate string, data interface{}) {
|
||||||
// Always include the common templates
|
tmpl, ok := s.templates[pageTemplate]
|
||||||
allTemplates := []string{"templates/htmlheader.html", "templates/navbar.html"}
|
if !ok {
|
||||||
allTemplates = append(allTemplates, templateFiles...)
|
s.log.Error("template not found", "template", pageTemplate)
|
||||||
|
|
||||||
// Parse templates
|
|
||||||
tmpl, err := template.ParseFiles(allTemplates...)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error("failed to parse template", "error", err)
|
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -108,6 +120,16 @@ func (s *Handlers) renderTemplate(w http.ResponseWriter, r *http.Request, templa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If data is a map, merge user info into it
|
||||||
|
if m, ok := data.(map[string]interface{}); ok {
|
||||||
|
m["User"] = userInfo
|
||||||
|
if err := tmpl.Execute(w, m); err != nil {
|
||||||
|
s.log.Error("failed to execute template", "error", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap data with base template data
|
// Wrap data with base template data
|
||||||
type templateDataWrapper struct {
|
type templateDataWrapper struct {
|
||||||
User *UserInfo
|
User *UserInfo
|
||||||
@@ -119,17 +141,6 @@ func (s *Handlers) renderTemplate(w http.ResponseWriter, r *http.Request, templa
|
|||||||
Data: data,
|
Data: data,
|
||||||
}
|
}
|
||||||
|
|
||||||
// If data is a map, merge user info into it
|
|
||||||
if m, ok := data.(map[string]interface{}); ok {
|
|
||||||
m["User"] = userInfo
|
|
||||||
if err := tmpl.Execute(w, m); err != nil {
|
|
||||||
s.log.Error("failed to execute template", "error", err)
|
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise use wrapper
|
|
||||||
if err := tmpl.Execute(w, wrapper); err != nil {
|
if err := tmpl.Execute(w, wrapper); err != nil {
|
||||||
s.log.Error("failed to execute template", "error", err)
|
s.log.Error("failed to execute template", "error", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -87,10 +87,11 @@ func TestRenderTemplate(t *testing.T) {
|
|||||||
"Version": "1.0.0",
|
"Version": "1.0.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
// When templates don't exist, renderTemplate should return an error
|
// When a non-existent template name is requested, renderTemplate
|
||||||
h.renderTemplate(w, req, []string{"nonexistent.html"}, data)
|
// should return an internal server error
|
||||||
|
h.renderTemplate(w, req, "nonexistent.html", data)
|
||||||
|
|
||||||
// Should return internal server error when template parsing fails
|
// Should return internal server error when template is not found
|
||||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (s *Handlers) HandleIndex() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the template
|
// Render the template
|
||||||
s.renderTemplate(w, req, []string{"templates/base.html", "templates/index.html"}, data)
|
s.renderTemplate(w, req, "index.html", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,6 @@ func (h *Handlers) HandleProfile() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the profile page
|
// Render the profile page
|
||||||
h.renderTemplate(w, r, []string{"templates/base.html", "templates/profile.html"}, data)
|
h.renderTemplate(w, r, "profile.html", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,66 +4,66 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleSourceList shows a list of user's webhook sources
|
// HandleSourceList shows a list of user's webhooks
|
||||||
func (h *Handlers) HandleSourceList() http.HandlerFunc {
|
func (h *Handlers) HandleSourceList() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Implement source list page
|
// TODO: Implement webhook list page
|
||||||
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSourceCreate shows the form to create a new webhook source
|
// HandleSourceCreate shows the form to create a new webhook
|
||||||
func (h *Handlers) HandleSourceCreate() http.HandlerFunc {
|
func (h *Handlers) HandleSourceCreate() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Implement source creation form
|
// TODO: Implement webhook creation form
|
||||||
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSourceCreateSubmit handles the source creation form submission
|
// HandleSourceCreateSubmit handles the webhook creation form submission
|
||||||
func (h *Handlers) HandleSourceCreateSubmit() http.HandlerFunc {
|
func (h *Handlers) HandleSourceCreateSubmit() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Implement source creation logic
|
// TODO: Implement webhook creation logic
|
||||||
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSourceDetail shows details for a specific webhook source
|
// HandleSourceDetail shows details for a specific webhook
|
||||||
func (h *Handlers) HandleSourceDetail() http.HandlerFunc {
|
func (h *Handlers) HandleSourceDetail() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Implement source detail page
|
// TODO: Implement webhook detail page
|
||||||
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSourceEdit shows the form to edit a webhook source
|
// HandleSourceEdit shows the form to edit a webhook
|
||||||
func (h *Handlers) HandleSourceEdit() http.HandlerFunc {
|
func (h *Handlers) HandleSourceEdit() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Implement source edit form
|
// TODO: Implement webhook edit form
|
||||||
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSourceEditSubmit handles the source edit form submission
|
// HandleSourceEditSubmit handles the webhook edit form submission
|
||||||
func (h *Handlers) HandleSourceEditSubmit() http.HandlerFunc {
|
func (h *Handlers) HandleSourceEditSubmit() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Implement source update logic
|
// TODO: Implement webhook update logic
|
||||||
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSourceDelete handles webhook source deletion
|
// HandleSourceDelete handles webhook deletion
|
||||||
func (h *Handlers) HandleSourceDelete() http.HandlerFunc {
|
func (h *Handlers) HandleSourceDelete() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Implement source deletion logic
|
// TODO: Implement webhook deletion logic
|
||||||
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSourceLogs shows the request/response logs for a webhook source
|
// HandleSourceLogs shows the request/response logs for a webhook
|
||||||
func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
|
func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Implement source logs page
|
// TODO: Implement webhook logs page
|
||||||
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import (
|
|||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleWebhook handles incoming webhook requests
|
// HandleWebhook handles incoming webhook requests at entrypoint URLs
|
||||||
func (h *Handlers) HandleWebhook() http.HandlerFunc {
|
func (h *Handlers) HandleWebhook() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Get webhook UUID from URL
|
// Get entrypoint UUID from URL
|
||||||
webhookUUID := chi.URLParam(r, "uuid")
|
entrypointUUID := chi.URLParam(r, "uuid")
|
||||||
if webhookUUID == "" {
|
if entrypointUUID == "" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the incoming webhook request
|
// Log the incoming webhook request
|
||||||
h.log.Info("webhook request received",
|
h.log.Info("webhook request received",
|
||||||
"uuid", webhookUUID,
|
"entrypoint_uuid", entrypointUUID,
|
||||||
"method", r.Method,
|
"method", r.Method,
|
||||||
"remote_addr", r.RemoteAddr,
|
"remote_addr", r.RemoteAddr,
|
||||||
"user_agent", r.UserAgent(),
|
"user_agent", r.UserAgent(),
|
||||||
@@ -32,7 +32,7 @@ func (h *Handlers) HandleWebhook() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement webhook handling logic
|
// TODO: Implement webhook handling logic
|
||||||
// For now, return "unimplemented" for all webhook POST requests
|
// Look up entrypoint by UUID, find parent webhook, fan out to targets
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
_, err := w.Write([]byte("unimplemented"))
|
_, err := w.Write([]byte("unimplemented"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ type LoggerParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
params LoggerParams
|
levelVar *slog.LevelVar
|
||||||
|
params LoggerParams
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:revive // lc parameter is required by fx even if unused
|
// nolint:revive // lc parameter is required by fx even if unused
|
||||||
@@ -26,24 +27,30 @@ func New(lc fx.Lifecycle, params LoggerParams) (*Logger, error) {
|
|||||||
l := new(Logger)
|
l := new(Logger)
|
||||||
l.params = params
|
l.params = params
|
||||||
|
|
||||||
|
// Use slog.LevelVar for dynamic log level changes
|
||||||
|
l.levelVar = new(slog.LevelVar)
|
||||||
|
l.levelVar.Set(slog.LevelInfo)
|
||||||
|
|
||||||
// Determine if we're running in a terminal
|
// Determine if we're running in a terminal
|
||||||
tty := false
|
tty := false
|
||||||
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
||||||
tty = true
|
tty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replaceAttr := func(_ []string, a slog.Attr) slog.Attr { // nolint:revive // groups unused
|
||||||
|
// Always use UTC for timestamps
|
||||||
|
if a.Key == slog.TimeKey {
|
||||||
|
if t, ok := a.Value.Any().(time.Time); ok {
|
||||||
|
return slog.Time(slog.TimeKey, t.UTC())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
var handler slog.Handler
|
var handler slog.Handler
|
||||||
opts := &slog.HandlerOptions{
|
opts := &slog.HandlerOptions{
|
||||||
Level: slog.LevelInfo,
|
Level: l.levelVar,
|
||||||
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { // nolint:revive // groups unused
|
ReplaceAttr: replaceAttr,
|
||||||
// Always use UTC for timestamps
|
|
||||||
if a.Key == slog.TimeKey {
|
|
||||||
if t, ok := a.Value.Any().(time.Time); ok {
|
|
||||||
return slog.Time(slog.TimeKey, t.UTC())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tty {
|
if tty {
|
||||||
@@ -63,34 +70,7 @@ func New(lc fx.Lifecycle, params LoggerParams) (*Logger, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) EnableDebugLogging() {
|
func (l *Logger) EnableDebugLogging() {
|
||||||
// Recreate logger with debug level
|
l.levelVar.Set(slog.LevelDebug)
|
||||||
tty := false
|
|
||||||
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
|
||||||
tty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var handler slog.Handler
|
|
||||||
opts := &slog.HandlerOptions{
|
|
||||||
Level: slog.LevelDebug,
|
|
||||||
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { // nolint:revive // groups unused
|
|
||||||
// Always use UTC for timestamps
|
|
||||||
if a.Key == slog.TimeKey {
|
|
||||||
if t, ok := a.Value.Any().(time.Time); ok {
|
|
||||||
return slog.Time(slog.TimeKey, t.UTC())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if tty {
|
|
||||||
handler = slog.NewTextHandler(os.Stdout, opts)
|
|
||||||
} else {
|
|
||||||
handler = slog.NewJSONHandler(os.Stdout, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.logger = slog.New(handler)
|
|
||||||
slog.SetDefault(l.logger)
|
|
||||||
l.logger.Debug("debug logging enabled", "debug", true)
|
l.logger.Debug("debug logging enabled", "debug", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/globals"
|
"sneak.berlin/go/webhooker/internal/globals"
|
||||||
"sneak.berlin/go/webhooker/internal/logger"
|
"sneak.berlin/go/webhooker/internal/logger"
|
||||||
|
"sneak.berlin/go/webhooker/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
// nolint:revive // MiddlewareParams is a standard fx naming convention
|
// nolint:revive // MiddlewareParams is a standard fx naming convention
|
||||||
@@ -24,17 +25,20 @@ type MiddlewareParams struct {
|
|||||||
Logger *logger.Logger
|
Logger *logger.Logger
|
||||||
Globals *globals.Globals
|
Globals *globals.Globals
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
|
Session *session.Session
|
||||||
}
|
}
|
||||||
|
|
||||||
type Middleware struct {
|
type Middleware struct {
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
params *MiddlewareParams
|
params *MiddlewareParams
|
||||||
|
session *session.Session
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(lc fx.Lifecycle, params MiddlewareParams) (*Middleware, error) {
|
func New(lc fx.Lifecycle, params MiddlewareParams) (*Middleware, error) {
|
||||||
s := new(Middleware)
|
s := new(Middleware)
|
||||||
s.params = ¶ms
|
s.params = ¶ms
|
||||||
s.log = params.Logger.Get()
|
s.log = params.Logger.Get()
|
||||||
|
s.session = params.Session
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,11 +122,27 @@ func (s *Middleware) CORS() func(http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Middleware) Auth() func(http.Handler) http.Handler {
|
// RequireAuth returns middleware that checks for a valid session.
|
||||||
|
// Unauthenticated users are redirected to the login page.
|
||||||
|
func (s *Middleware) RequireAuth() func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: implement proper authentication
|
sess, err := s.session.Get(r)
|
||||||
s.log.Debug("AUTH: before request")
|
if err != nil {
|
||||||
|
s.log.Debug("auth middleware: failed to get session", "error", err)
|
||||||
|
http.Redirect(w, r, "/pages/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.session.IsAuthenticated(sess) {
|
||||||
|
s.log.Debug("auth middleware: unauthenticated request",
|
||||||
|
"path", r.URL.Path,
|
||||||
|
"method", r.Method,
|
||||||
|
)
|
||||||
|
http.Redirect(w, r, "/pages/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,23 +90,23 @@ func (s *Server) SetupRoutes() {
|
|||||||
r.Get("/", s.h.HandleProfile())
|
r.Get("/", s.h.HandleProfile())
|
||||||
})
|
})
|
||||||
|
|
||||||
// Webhook source management routes (require authentication)
|
// Webhook management routes (require authentication)
|
||||||
s.router.Route("/sources", func(r chi.Router) {
|
s.router.Route("/sources", func(r chi.Router) {
|
||||||
// TODO: Add authentication middleware here
|
r.Use(s.mw.RequireAuth())
|
||||||
r.Get("/", s.h.HandleSourceList()) // List all sources
|
r.Get("/", s.h.HandleSourceList()) // List all webhooks
|
||||||
r.Get("/new", s.h.HandleSourceCreate()) // Show create form
|
r.Get("/new", s.h.HandleSourceCreate()) // Show create form
|
||||||
r.Post("/new", s.h.HandleSourceCreateSubmit()) // Handle create submission
|
r.Post("/new", s.h.HandleSourceCreateSubmit()) // Handle create submission
|
||||||
})
|
})
|
||||||
|
|
||||||
s.router.Route("/source/{sourceID}", func(r chi.Router) {
|
s.router.Route("/source/{sourceID}", func(r chi.Router) {
|
||||||
// TODO: Add authentication middleware here
|
r.Use(s.mw.RequireAuth())
|
||||||
r.Get("/", s.h.HandleSourceDetail()) // View source details
|
r.Get("/", s.h.HandleSourceDetail()) // View webhook details
|
||||||
r.Get("/edit", s.h.HandleSourceEdit()) // Show edit form
|
r.Get("/edit", s.h.HandleSourceEdit()) // Show edit form
|
||||||
r.Post("/edit", s.h.HandleSourceEditSubmit()) // Handle edit submission
|
r.Post("/edit", s.h.HandleSourceEditSubmit()) // Handle edit submission
|
||||||
r.Post("/delete", s.h.HandleSourceDelete()) // Delete source
|
r.Post("/delete", s.h.HandleSourceDelete()) // Delete webhook
|
||||||
r.Get("/logs", s.h.HandleSourceLogs()) // View source logs
|
r.Get("/logs", s.h.HandleSourceLogs()) // View webhook logs
|
||||||
})
|
})
|
||||||
|
|
||||||
// Webhook endpoint - accepts all HTTP methods
|
// Entrypoint endpoint - accepts incoming webhook POST requests
|
||||||
s.router.HandleFunc("/webhook/{uuid}", s.h.HandleWebhook())
|
s.router.HandleFunc("/webhook/{uuid}", s.h.HandleWebhook())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,6 @@ import (
|
|||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
|
||||||
// spooky action at a distance!
|
|
||||||
// this populates the environment
|
|
||||||
// from a ./.env file automatically
|
|
||||||
// for development configuration.
|
|
||||||
// .env contents should be things like
|
|
||||||
// `DBURL=postgres://user:pass@.../`
|
|
||||||
// (without the backticks, of course)
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerParams is a standard fx naming convention for dependency injection
|
// ServerParams is a standard fx naming convention for dependency injection
|
||||||
|
|||||||
8
templates/templates.go
Normal file
8
templates/templates.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.html
|
||||||
|
var Templates embed.FS
|
||||||
Reference in New Issue
Block a user