Skip to content

Commit d51a834

Browse files
feat(examples): add basic auth example
1 parent b377d70 commit d51a834

File tree

7 files changed

+444
-283
lines changed

7 files changed

+444
-283
lines changed

Makefile

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1-
.PHONY: fmt lint test mocks test_coverage
1+
default: help
2+
3+
help: ## list makefile targets
4+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
5+
6+
7+
.PHONY: help fmt lint test mocks test_coverage
28

39
GO_PKGS := $(shell go list -f {{.Dir}} ./...)
410

5-
fmt:
11+
fmt: ## gofmt all files
612
@go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {}
713

8-
lint:
14+
lint: ## run golangci-lint
915
@golangci-lint run
1016

11-
test:
17+
test: ## run tests
1218
@go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS)
1319

14-
test_coverage:
20+
test_coverage: ## run tests with coverage
1521
@go test -race -v $(GO_FLAGS) -count=1 -coverprofile=coverage.out -covermode=atomic $(GO_PKGS)
1622
@go tool cover -html coverage.out
1723

18-
mocks:
24+
mocks: ## generate mocks
1925
@go generate ./...

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ Connect to `ws://localhost:8080/ws` for real-time job updates.
121121
A full-featured example demonstrating 14 different job types is available in the [examples](./examples/) directory:
122122

123123
```bash
124-
cd examples
124+
cd examples/getting_started
125125
go run main.go
126126
```
127127

@@ -140,6 +140,9 @@ go run main.go
140140

141141
Visit `http://localhost:8080` to see the UI in action.
142142

143+
### Basic Auth
144+
See the [basic_auth](./examples/basic_auth) example for how to secure GoCron-UI using a simple basic auth middleware with the credentials being sourced via environment variables.
145+
143146
## Deployment
144147

145148
### Binary Distribution

examples/basic_auth/README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# GoCronUI Basic Auth Example
2+
3+
This is a simple example demonstrating securing gocron-ui with basic auth:
4+
5+
```go
6+
> GOCRON_UI_USERNAME=admin GOCRON_UI_PASSWORD=password go run main.go
7+
```
8+
9+
In another terminal:
10+
11+
**valid credentials**
12+
```bash
13+
curl -v -u "admin:password" 127.0.0.1:8080/api/jobs | jq .
14+
\ % Total % Received % Xferd Average Speed Time Time Time Current
15+
Dload Upload Total Spent Left Speed
16+
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 127.0.0.1:8080...
17+
* Connected to 127.0.0.1 (127.0.0.1) port 8080
18+
* Server auth using Basic with user 'admin'
19+
> GET /api/jobs HTTP/1.1
20+
> Host: 127.0.0.1:8080
21+
> Authorization: Basic YWRtaW46cGFzc3dvcmQ=
22+
> User-Agent: curl/8.7.1
23+
> Accept: */*
24+
>
25+
* Request completely sent off
26+
< HTTP/1.1 200 OK
27+
< Content-Type: application/json
28+
< Vary: Origin
29+
< Date: Sun, 09 Nov 2025 23:09:13 GMT
30+
< Content-Length: 1187
31+
<
32+
{ [1187 bytes data]
33+
100 1187 100 1187 0 0 1837k 0 --:--:-- --:--:-- --:--:-- 1159k
34+
* Connection #0 to host 127.0.0.1 left intact
35+
[
36+
{
37+
"id": "3681db0c-fd30-48ed-af21-2082f6a9f3d7",
38+
"name": "simple-10s-interval",
39+
"tags": [
40+
"interval",
41+
"simple"
42+
],
43+
"nextRun": "2025-11-10T10:09:07+11:00",
44+
"lastRun": "2025-11-10T10:09:07+11:00",
45+
"nextRuns": [
46+
"2025-11-10T10:09:07+11:00",
47+
"2025-11-10T10:09:17+11:00",
48+
"2025-11-10T10:09:27+11:00",
49+
"2025-11-10T10:09:37+11:00",
50+
"2025-11-10T10:09:47+11:00"
51+
],
52+
"schedule": "Every 10 seconds",
53+
"scheduleDetail": "Duration: 10s"
54+
},
55+
{
56+
"id": "7ff3624c-248e-47f6-90bd-49da5451ddcf",
57+
"name": "simple-20s-interval",
58+
"tags": [
59+
"interval",
60+
"simple"
61+
],
62+
"nextRun": "2025-11-10T10:09:07+11:00",
63+
"lastRun": "2025-11-10T10:09:07+11:00",
64+
"nextRuns": [
65+
"2025-11-10T10:09:07+11:00",
66+
"2025-11-10T10:09:27+11:00",
67+
"2025-11-10T10:09:47+11:00",
68+
"2025-11-10T10:10:07+11:00",
69+
"2025-11-10T10:10:27+11:00"
70+
],
71+
"schedule": "Every 20 seconds",
72+
"scheduleDetail": "Duration: 20s"
73+
},
74+
{
75+
"id": "ad6ddb39-e1a3-4dc4-8539-cf1d9a4667eb",
76+
"name": "simple-5s-interval",
77+
"tags": [
78+
"interval",
79+
"simple"
80+
],
81+
"nextRun": "2025-11-10T10:09:17+11:00",
82+
"lastRun": "2025-11-10T10:09:12+11:00",
83+
"nextRuns": [
84+
"2025-11-10T10:09:17+11:00",
85+
"2025-11-10T10:09:22+11:00",
86+
"2025-11-10T10:09:27+11:00",
87+
"2025-11-10T10:09:32+11:00",
88+
"2025-11-10T10:09:37+11:00"
89+
],
90+
"schedule": "Every 5 seconds",
91+
"scheduleDetail": "Duration: 5s"
92+
}
93+
]
94+
```
95+
96+
**missing or invalid credentials**
97+
```bash
98+
curl -v 127.0.0.1:8080/api/jobs
99+
* Trying 127.0.0.1:8080...
100+
* Connected to 127.0.0.1 (127.0.0.1) port 8080
101+
> GET /api/jobs HTTP/1.1
102+
> Host: 127.0.0.1:8080
103+
> User-Agent: curl/8.7.1
104+
> Accept: */*
105+
>
106+
* Request completely sent off
107+
< HTTP/1.1 401 Unauthorized
108+
< Content-Type: text/plain; charset=utf-8
109+
< Www-Authenticate: Basic realm="restricted", charset="UTF-8"
110+
< X-Content-Type-Options: nosniff
111+
< Date: Sun, 09 Nov 2025 23:10:02 GMT
112+
< Content-Length: 13
113+
<
114+
Unauthorized
115+
* Connection #0 to host 127.0.0.1 left intact
116+
```

examples/basic_auth/main.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package main
2+
3+
import (
4+
"crypto/sha256"
5+
"crypto/subtle"
6+
"flag"
7+
"fmt"
8+
"log"
9+
"net/http"
10+
"os"
11+
"os/signal"
12+
"strings"
13+
"syscall"
14+
"time"
15+
16+
"github.com/go-co-op/gocron-ui/server"
17+
"github.com/go-co-op/gocron/v2"
18+
)
19+
20+
const (
21+
usernameEnv = "GOCRON_UI_USERNAME"
22+
passwordEnv = "GOCRON_UI_PASSWORD"
23+
)
24+
25+
var jobs = []struct {
26+
name string
27+
definition gocron.JobDefinition
28+
task gocron.Task
29+
options []gocron.JobOption
30+
}{
31+
{
32+
"simple-10s-interval", gocron.DurationJob(10 * time.Second), gocron.NewTask(func() { log.Println("Running 10-second interval job") }), []gocron.JobOption{gocron.WithName("simple-10s-interval"), gocron.WithTags("interval", "simple")},
33+
},
34+
{
35+
"simple-5s-interval", gocron.DurationJob(5 * time.Second), gocron.NewTask(func() { log.Println("Running 5-second interval job") }), []gocron.JobOption{gocron.WithName("simple-5s-interval"), gocron.WithTags("interval", "simple")},
36+
},
37+
{
38+
"simple-20s-interval", gocron.DurationJob(20 * time.Second), gocron.NewTask(func() { log.Println("Running 20-second interval job") }), []gocron.JobOption{gocron.WithName("simple-20s-interval"), gocron.WithTags("interval", "simple")},
39+
},
40+
}
41+
42+
func main() {
43+
username, usernameOK := os.LookupEnv(usernameEnv)
44+
password, passwordOK := os.LookupEnv(passwordEnv)
45+
46+
if (!usernameOK || !passwordOK) || username == "" || password == "" {
47+
log.Fatalf("Environment variables %s and %s must be set for basic authentication", usernameEnv, passwordEnv)
48+
}
49+
50+
port := flag.Int("port", 8080, "Port to run the server on")
51+
title := flag.String("title", "GoCron Scheduler", "Custom title for the UI")
52+
flag.Parse()
53+
54+
// create the gocron scheduler
55+
scheduler, err := gocron.NewScheduler()
56+
if err != nil {
57+
log.Fatalf("Failed to create scheduler: %v", err)
58+
}
59+
60+
// add jobs to the scheduler
61+
for _, job := range jobs {
62+
if _, err := scheduler.NewJob(job.definition, job.task, job.options...); err != nil {
63+
log.Printf("Error creating job: %v", err)
64+
}
65+
}
66+
67+
// start the scheduler
68+
scheduler.Start()
69+
log.Println("Scheduler started with", len(scheduler.Jobs()), "jobs")
70+
71+
// create and start the API server with custom title
72+
srv := server.NewServer(scheduler, *port, server.WithTitle(*title))
73+
74+
// start server in a goroutine
75+
go func() {
76+
addr := fmt.Sprintf(":%d", *port)
77+
log.Println("\n" + strings.Repeat("=", 70))
78+
log.Printf("GoCron UI Server Started with Basic Authentication")
79+
log.Println(strings.Repeat("=", 70))
80+
log.Printf("Web UI: http://localhost%s", addr)
81+
log.Printf("API: http://localhost%s/api", addr)
82+
log.Printf("WebSocket: ws://localhost%s/ws", addr)
83+
log.Printf("Total Jobs: %d", len(scheduler.Jobs()))
84+
log.Println(strings.Repeat("=", 70) + "\n")
85+
86+
if err := http.ListenAndServe(addr, basicAuthMiddleware(srv.Router, username, password)); err != nil {
87+
log.Fatalf("Server failed to start: %v", err)
88+
}
89+
}()
90+
91+
// wait for interrupt signal to gracefully shutdown
92+
quit := make(chan os.Signal, 1)
93+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
94+
<-quit
95+
96+
log.Println("\nShutting down server...")
97+
98+
// shutdown scheduler
99+
if err := scheduler.Shutdown(); err != nil {
100+
log.Printf("Error shutting down scheduler: %v", err)
101+
}
102+
103+
log.Println("Server stopped gracefully")
104+
}
105+
106+
// https://www.alexedwards.net/blog/basic-authentication-in-go
107+
func basicAuthMiddleware(next http.Handler, expectedUsername, expectedPassword string) http.HandlerFunc {
108+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
109+
username, password, ok := r.BasicAuth()
110+
if ok {
111+
usernameHash := sha256.Sum256([]byte(username))
112+
passwordHash := sha256.Sum256([]byte(password))
113+
114+
expectedUsernameHash := sha256.Sum256([]byte(expectedUsername))
115+
expectedPasswordHash := sha256.Sum256([]byte(expectedPassword))
116+
117+
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
118+
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
119+
120+
if usernameMatch && passwordMatch {
121+
next.ServeHTTP(w, r)
122+
return
123+
}
124+
}
125+
126+
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
127+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
128+
})
129+
}
File renamed without changes.

0 commit comments

Comments
 (0)