Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
# realtime-messaging
Real time pubsub messaging with Golang and Websockets

### Build and run the project
```
docker compose up --build
```

### Obtain a JWT token
```
docker compose run auth-cli /app/auth-cli-bin -action create -username demo-user
```

### Access Grafana dashboard: localhost:3002/d/realtime-metrics-dashboard
50 changes: 50 additions & 0 deletions auth-cli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/iulian509/realtime-messaging/config"
)

func main() {
action := flag.String("action", "", "Action to perform (create)")
username := flag.String("username", "", "Username for the token")

flag.Parse()

if *action != "create" {
flag.Usage()
os.Exit(1)
}

if *username == "" {
log.Fatal("username is required for create action")
}

token, err := generateJWT(*username)
if err != nil {
log.Fatalf("failed to generate JWT: %v", err)
}

fmt.Printf("generated JWT for user %s: %s\n", *username, token)
}

func generateJWT(username string) (string, error) {
config, err := config.LoadConfig()
if err != nil {
log.Fatalf("failed to load YAML configuration")
}

claims := jwt.MapClaims{
"username": username,
"exp": time.Now().Add(time.Hour * 1).Unix(),
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.JWT.SecretKey))
}
3 changes: 3 additions & 0 deletions config/config-dev.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
nats:
host: 'nats://realtime-messaging-nats:4222'

jwt:
secretKey: "your-very-secure-secret-key" # Do not store real keys here
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ type Config struct {
Nats struct {
Host string `yaml:"host"`
}
JWT struct {
SecretKey string `yaml:"secretKey"`
}
}

func LoadConfig() (*Config, error) {
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ services:
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
- ./grafana/dashboards:/var/lib/grafana/dashboards
auth-cli:
build:
context: .
dockerfile: docker/auth-cli/Dockerfile
11 changes: 11 additions & 0 deletions docker/auth-cli/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM golang:1.24.2-alpine

WORKDIR /app

COPY config/ ./config/
COPY auth-cli/ ./auth-cli
COPY go.mod go.sum ./

RUN go mod download

RUN CGO_ENABLED=0 GOOS=linux go build -o auth-cli-bin -ldflags "-s -w" ./auth-cli/
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.24.2

require (
github.com/goccy/go-yaml v1.17.1
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/gorilla/websocket v1.5.3
github.com/nats-io/nats.go v1.40.1
github.com/prometheus/client_golang v1.21.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
Expand Down
46 changes: 46 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package auth

import (
"fmt"
"log"
"net/http"

"github.com/golang-jwt/jwt/v5"
"github.com/iulian509/realtime-messaging/config"
)

func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "missing Authorization header", http.StatusUnauthorized)
return
}

const bearerPrefix = "Bearer "
if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
http.Error(w, "invalid Authorization header format", http.StatusUnauthorized)
return
}

tokenString := authHeader[len(bearerPrefix):]
config, err := config.LoadConfig()
if err != nil {
log.Fatalf("failed to load YAML configuration")
}

token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(config.JWT.SecretKey), nil
})

if err != nil || !token.Valid {
http.Error(w, "invalid or expired token", http.StatusUnauthorized)
return
}

next.ServeHTTP(w, r)
}
}
3 changes: 2 additions & 1 deletion publisher/cmd/publisher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"

"github.com/iulian509/realtime-messaging/config"
"github.com/iulian509/realtime-messaging/internal/auth"
"github.com/iulian509/realtime-messaging/internal/metrics"
"github.com/iulian509/realtime-messaging/publisher/internal/handlers"
"github.com/iulian509/realtime-messaging/publisher/internal/mq"
Expand All @@ -28,7 +29,7 @@ func main() {
}

http.HandleFunc("/metrics", metrics.PromHandler())
http.HandleFunc("/publish", metrics.PrometheusMiddleware(deps.PublisherHandler))
http.HandleFunc("/publish", metrics.PrometheusMiddleware(auth.AuthMiddleware(deps.PublisherHandler)))

err = http.ListenAndServe(":3000", nil)
log.Println("publisher service running on :3000")
Expand Down
3 changes: 2 additions & 1 deletion subscriber/cmd/subscriber/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"

"github.com/iulian509/realtime-messaging/config"
"github.com/iulian509/realtime-messaging/internal/auth"
"github.com/iulian509/realtime-messaging/internal/metrics"
"github.com/iulian509/realtime-messaging/subscriber/internal/handlers"
"github.com/iulian509/realtime-messaging/subscriber/internal/mq"
Expand All @@ -28,7 +29,7 @@ func main() {
}

http.HandleFunc("/metrics", metrics.PromHandler())
http.HandleFunc("/subscribe", metrics.PrometheusMiddleware(deps.SubscriberHandler))
http.HandleFunc("/subscribe", metrics.PrometheusMiddleware(auth.AuthMiddleware(deps.SubscriberHandler)))

err = http.ListenAndServe(":3001", nil)
log.Println("subscriber service running on :3001")
Expand Down