tsk-3: Add Token support (#10)

Closes #3

Reviewed-on: phoenix/textsender-api#10
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
This commit is contained in:
phoenix
2025-11-04 17:51:35 +00:00
committed by phoenix
parent 96fc87ea5e
commit d332cde84f
8 changed files with 113 additions and 4 deletions
+1
View File
@@ -1,3 +1,4 @@
JWT_SECRET=NULqYIzgt28bTiyziCd7IOO7b6LnWDW!
DB_NAME=textsender_db DB_NAME=textsender_db
DB_USER=textsender DB_USER=textsender
DB_PASSWORD=password DB_PASSWORD=password
+2
View File
@@ -94,10 +94,12 @@ jobs:
DB_HOST: postgres DB_HOST: postgres
DB_PORT: 5432 DB_PORT: 5432
DB_SSLMODE: disable DB_SSLMODE: disable
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: | run: |
echo "Parent directory" echo "Parent directory"
echo `pwd` echo `pwd`
echo "JWT_SECRET=$SECRET_KEY" >> .env
echo "DB_NAME=$DB_NAME" > .env echo "DB_NAME=$DB_NAME" > .env
echo "DB_USER=$DB_USER" >> .env echo "DB_USER=$DB_USER" >> .env
echo "DB_PASSWORD=$DB_PASSWORD" >> .env echo "DB_PASSWORD=$DB_PASSWORD" >> .env
+8 -2
View File
@@ -18,13 +18,17 @@ import (
"git.kundeng.us/phoenix/textsender-api/internal/handler" "git.kundeng.us/phoenix/textsender-api/internal/handler"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
mdlware "git.kundeng.us/phoenix/textsender-api/internal/middleware" mdlware "git.kundeng.us/phoenix/textsender-api/internal/middleware"
"git.kundeng.us/phoenix/textsender-api/internal/services"
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
) )
func main() { func main() {
cfg := config.Load() cfg := config.Load()
if cfg == nil { if cfg == nil {
fmt.Errorf("Error initializing config") fmt.Println("Error initializing config")
os.Exit(-1)
} else if cfg.JWTSecret == "" {
fmt.Println("Error: JWTSecret not initialized")
os.Exit(-1) os.Exit(-1)
} }
@@ -45,6 +49,8 @@ func main() {
return return
} }
jwtService := services.NewJWTService(cfg.JWTSecret)
contactStore := store.NewContactStore(database.Pool) contactStore := store.NewContactStore(database.Pool)
contactHandler := handler.NewContactHandler(contactStore) contactHandler := handler.NewContactHandler(contactStore)
@@ -55,7 +61,7 @@ func main() {
router.Use(middleware.Timeout(60 * time.Second)) router.Use(middleware.Timeout(60 * time.Second))
router.Use(mdlware.JSONContentType) router.Use(mdlware.JSONContentType)
router.Post(endpoint.ADD_CONTACT_ENDPOINT, contactHandler.AddContact) router.Handle(endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact)))
// Start server // Start server
server := &http.Server{ server := &http.Server{
+1
View File
@@ -7,6 +7,7 @@ require github.com/google/uuid v1.6.0
require ( require (
git.kundeng.us/phoenix/textsender-models v0.0.4 git.kundeng.us/phoenix/textsender-models v0.0.4
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v5 v5.7.6 github.com/jackc/pgx/v5 v5.7.6
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
+2 -2
View File
@@ -1,5 +1,3 @@
git.kundeng.us/phoenix/textsender-models v0.0.4-1-0e41936506-556 h1:htIAVchoU7LzTlDuAOwwUYmJH8U9UXCwjt3lO00juEc=
git.kundeng.us/phoenix/textsender-models v0.0.4-1-0e41936506-556/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no=
git.kundeng.us/phoenix/textsender-models v0.0.4 h1:r3kqo/Xe5hEcRceknPZ/gw1y4VHSrhG+UYYgrgGTsVg= git.kundeng.us/phoenix/textsender-models v0.0.4 h1:r3kqo/Xe5hEcRceknPZ/gw1y4VHSrhG+UYYgrgGTsVg=
git.kundeng.us/phoenix/textsender-models v0.0.4/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no= git.kundeng.us/phoenix/textsender-models v0.0.4/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -8,6 +6,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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+2
View File
@@ -16,6 +16,7 @@ type Config struct {
DBConnString string DBConnString string
ServerPort string ServerPort string
ResetDB bool ResetDB bool
JWTSecret string `env:"JWT_SECRET" required:"true"`
} }
type ConnectionInfo struct { type ConnectionInfo struct {
@@ -68,6 +69,7 @@ func Load() *Config {
DBConnString: dbConnString, DBConnString: dbConnString,
ServerPort: *port, ServerPort: *port,
ResetDB: *resetDb, ResetDB: *resetDb,
JWTSecret: os.Getenv("JWT_SECRET"),
} }
} }
+47
View File
@@ -0,0 +1,47 @@
package middleware
import (
"context"
"net/http"
"strings"
"git.kundeng.us/phoenix/textsender-api/internal/services"
)
type contextKey string
const (
UserContextKey contextKey = "user"
)
func AuthMiddleware(authService *services.JWTService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// Extract token from "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
token := parts[1]
// Validate token with auth service
user, err := authService.ValidateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add user to context
ctx := context.WithValue(r.Context(), UserContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
+50
View File
@@ -0,0 +1,50 @@
package services
import (
"time"
"github.com/golang-jwt/jwt/v5"
txtmodels_token "git.kundeng.us/phoenix/textsender-models/pkg/token"
txtmodels_user "git.kundeng.us/phoenix/textsender-models/pkg/user"
)
type JWTService struct {
secretKey []byte
}
func NewJWTService(secretKey string) *JWTService {
return &JWTService{
secretKey: []byte(secretKey),
}
}
func (s *JWTService) ValidateToken(tokenString string) (*txtmodels_user.User, error) {
// TODO: Include more user information in the claims to populate user
claims := &txtmodels_token.Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return s.secretKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, jwt.ErrSignatureInvalid
}
// Check token expiration
if time.Now().After(claims.ExpiresAt.Time) {
return nil, jwt.ErrTokenExpired
}
return &txtmodels_user.User{
Id: claims.UserId,
}, nil
}