Skip to content

Commit 69226d4

Browse files
committed
feat: add authentication middleware
1 parent 9ae7684 commit 69226d4

File tree

6 files changed

+113
-21
lines changed

6 files changed

+113
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ A backend service built with **Go** for managing IoT devices, collecting telemet
1515
- **Telemetry Collection** (time-series sensor data)
1616
- **Command Dispatch** to devices
1717
- **Dockerized** for consistent deployment
18-
- **Testing** (unit & integration)
18+
- **Testing** (integration)
1919
- **CI/CD** pipeline (GitHub Actions)
2020

2121
## Tech Stack

internal/app/bootstrap.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ func BuildApp(log *logger.Logger, dbpool *pgxpool.Pool, cfg config.Config) http.
2121
userService := user.NewService(userRepo)
2222
userHandler := transporthttp.NewUserHandler(log, cfg, userService, tokenService)
2323

24-
router := transporthttp.NewRouter(log, userHandler)
24+
userMiddleware := transporthttp.NewUserMiddleware(tokenService)
25+
26+
router := transporthttp.NewRouter(log, userMiddleware, userHandler)
2527

2628
return router
2729
}

internal/domain/user/user.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ type Password struct {
3535
hash []byte
3636
}
3737

38+
var AnonymousUser = &User{}
39+
40+
func (u *User) IsAnonymous() bool {
41+
return u == AnonymousUser
42+
}
43+
3844
func newUser(email, username, password string) (*User, error) {
3945
addr, err := NewEmail(email)
4046
if err != nil {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package http
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"time"
7+
8+
chimw "github.com/go-chi/chi/v5/middleware"
9+
"github.com/raphico/go-device-telemetry-api/internal/logger"
10+
)
11+
12+
func loggingMiddleware(log *logger.Logger) func(http.Handler) http.Handler {
13+
return func(next http.Handler) http.Handler {
14+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15+
start := time.Now()
16+
next.ServeHTTP(w, r)
17+
log.Info(fmt.Sprintf(
18+
"HTTP request: method=%s, path=%s, remote=%s, duration=%s, reqID=%s",
19+
r.Method, r.URL.Path, r.RemoteAddr, time.Since(start), chimw.GetReqID(r.Context()),
20+
))
21+
})
22+
}
23+
}

internal/transport/http/router.go

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,40 @@ import (
1010
"github.com/raphico/go-device-telemetry-api/internal/logger"
1111
)
1212

13-
func NewRouter(log *logger.Logger, userHandler *UserHandler) http.Handler {
13+
func NewRouter(log *logger.Logger, userMw *UserMiddleware, userHandler *UserHandler) http.Handler {
1414
r := chi.NewRouter()
1515

1616
r.Use(chimw.RequestID)
1717
r.Use(chimw.RealIP)
1818
r.Use(chimw.Recoverer)
19+
r.Use(userMw.AuthMiddleware)
1920
r.Use(loggingMiddleware(log))
2021
r.Use(chimw.Timeout(60 * time.Second))
2122

2223
r.Route("/api/v1", func(r chi.Router) {
24+
r.Route("/auth", func(r chi.Router) {
25+
r.Post("/register", userHandler.RegisterUser)
26+
r.Post("/login", userHandler.LoginUser)
27+
r.Post("/refresh", userHandler.RefreshAccessToken)
28+
})
29+
30+
r.Group(func(r chi.Router) {
31+
r.Use(userMw.RequireAuthMiddleware)
32+
33+
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
34+
w.WriteHeader(http.StatusOK)
35+
_, _ = w.Write([]byte("OK"))
36+
})
37+
})
38+
2339
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
2440
w.WriteHeader(http.StatusOK)
2541
if _, err := w.Write([]byte("OK")); err != nil {
2642
log.Error(fmt.Sprintf("failed to write health response: %v", err))
2743
}
2844
})
2945

30-
r.Route("/auth", func(r chi.Router) {
31-
r.Post("/register", userHandler.RegisterUser)
32-
r.Post("/login", userHandler.LoginUser)
33-
r.Post("/refresh", userHandler.RefreshAccessToken)
34-
})
3546
})
3647

3748
return r
3849
}
39-
40-
func loggingMiddleware(log *logger.Logger) func(http.Handler) http.Handler {
41-
return func(next http.Handler) http.Handler {
42-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43-
start := time.Now()
44-
next.ServeHTTP(w, r)
45-
log.Info(fmt.Sprintf(
46-
"HTTP request: method=%s, path=%s, remote=%s, duration=%s, reqID=%s",
47-
r.Method, r.URL.Path, r.RemoteAddr, time.Since(start), chimw.GetReqID(r.Context()),
48-
))
49-
})
50-
}
51-
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package http
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/raphico/go-device-telemetry-api/internal/domain/token"
9+
"github.com/raphico/go-device-telemetry-api/internal/domain/user"
10+
)
11+
12+
type UserMiddleware struct {
13+
tokenService *token.Service
14+
}
15+
16+
type contextKey struct{}
17+
18+
var userCtxKey = &contextKey{}
19+
20+
func NewUserMiddleware(tokenService *token.Service) *UserMiddleware {
21+
return &UserMiddleware{
22+
tokenService: tokenService,
23+
}
24+
}
25+
26+
func (um *UserMiddleware) AuthMiddleware(next http.Handler) http.Handler {
27+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28+
w.Header().Add("Vary", "Authorization")
29+
30+
authHeader := r.Header.Get("Authorization")
31+
if !strings.HasPrefix(authHeader, "Bearer ") {
32+
next.ServeHTTP(w, r)
33+
return
34+
}
35+
36+
accessToken := strings.TrimPrefix(authHeader, "Bearer ")
37+
userId, err := um.tokenService.ValidateAccessToken(accessToken)
38+
if err != nil {
39+
next.ServeHTTP(w, r)
40+
return
41+
}
42+
43+
ctx := context.WithValue(r.Context(), userCtxKey, userId)
44+
next.ServeHTTP(w, r.WithContext(ctx))
45+
})
46+
}
47+
48+
func (um *UserMiddleware) RequireAuthMiddleware(next http.Handler) http.Handler {
49+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50+
_, ok := r.Context().Value(userCtxKey).(user.UserID)
51+
if !ok {
52+
WriteJSONError(w, http.StatusUnauthorized, "unauthorized", "authentication required")
53+
return
54+
}
55+
56+
next.ServeHTTP(w, r)
57+
})
58+
}
59+
60+
func GetUserID(ctx context.Context) (user.UserID, bool) {
61+
userID, ok := ctx.Value(userCtxKey).(user.UserID)
62+
return userID, ok
63+
}

0 commit comments

Comments
 (0)