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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@

DATABASE_URI=
CONFIG_PATH=config/local.yaml

JWT_PRIVATE_KEY=mykeyhdhd

CLIENT_URL=http://localhost:5173
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.24.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
Expand Down
22 changes: 17 additions & 5 deletions internal/api/middleware/auth_middleware.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
package middleware

import (
"context"
"net/http"

"github.com/gauravst/go-api-template/internal/config"
"github.com/gauravst/go-api-template/internal/utils/jwtToken"
"github.com/gauravst/go-api-template/internal/utils/response"
)

func Auth(cfg *config.Config, authService *services.AuthService) func(http.Handler) http.Handler {
type contextKey string

const userDataKey contextKey = "userData"

func Auth(cfg *config.Config) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract the token from the request headers
cookie, err := r.Cookie("accessToken")
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
response.WriteJson(w, http.StatusUnauthorized, response.GeneralError(err))
return
}
token := cookie.Value

// refresh token
// logout or set new access token here based on status

// If the token is valid, call the next handler
userData, err := jwtToken.VerifyJwtAndGetData(token, cfg.JwtPrivateKey)
if err != nil {
response.WriteJson(w, http.StatusUnauthorized, response.GeneralError(err))
return
}

ctx := context.WithValue(r.Context(), userDataKey, userData)
r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
Expand Down
33 changes: 33 additions & 0 deletions internal/api/middleware/cors_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package middleware

import (
"net/http"

"github.com/gauravst/go-api-template/internal/config"
)

func CORS(cfg *config.Config) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow only the specific frontend URL
w.Header().Set("Access-Control-Allow-Origin", cfg.ClientUrl)

// Allow credentials (IMPORTANT for cookies/tokens)
w.Header().Set("Access-Control-Allow-Credentials", "true")

// Allowed methods
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")

// Allowed headers
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

// Handle preflight requests
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}

next.ServeHTTP(w, r)
})
}
}
8 changes: 5 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ type HTTPServer struct {
}

type Config struct {
Env string `yaml:"env" env-required:"true" env-default:"production`
DatabaseUri string `env:"DATABASE_URI" env-required:"true"`
HTTPServer `yaml:"http_server"`
Env string `yaml:"env" env-required:"true" env-default:"production"`
DatabaseUri string `env:"DATABASE_URI" env-required:"true"`
JwtPrivateKey string `env:"JWT_PRIVATE_KEY" env-required:"true"`
ClientUrl string `env:"CLIENT_URL" env-required:"true"`
HTTPServer `yaml:"http_server"`
}

func ConfigMustLoad() *Config {
Expand Down
13 changes: 13 additions & 0 deletions internal/utils/email/email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package email

import "net/smtp"

func SendEmail(username, password, host, from, port string, toList []string, body []byte) error {
auth := smtp.PlainAuth("", username, password, host)
err := smtp.SendMail(host+":"+port, auth, from, toList, body)
if err != nil {
return err
}

return nil
}
23 changes: 23 additions & 0 deletions internal/utils/hashing/hashing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package hashing

import (
"fmt"

"golang.org/x/crypto/bcrypt"
)

func GenerateHashString(data string) (string, error) {
hashedValue, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
if err != nil {
return "", nil
}
return string(hashedValue), nil
}

func CompareHashString(hashedValue string, normalValue string) error {
err := bcrypt.CompareHashAndPassword([]byte(hashedValue), []byte(normalValue))
if err != nil {
return fmt.Errorf("invalid credentials")
}
return nil
}
69 changes: 69 additions & 0 deletions internal/utils/jwtToken/jwtToken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package jwtToken

import (
"errors"
"fmt"
"net/http"

"github.com/golang-jwt/jwt/v5"
)

func VerifyJwtAndGetData(jwtToken string, key string) (map[string]interface{}, error) {
token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
// Ensure the token uses the correct signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// Convert key (string) to []byte for HS256
return []byte(key), nil
})

// Extract claims regardless of token validity
var claims jwt.MapClaims
if token != nil {
if data, ok := token.Claims.(jwt.MapClaims); ok {
claims = data
}
}

// Check if there's an error, but still return claims if possible
if err != nil {
// If token expired, return claims with specific error
if errors.Is(err, jwt.ErrSignatureInvalid) {
return nil, fmt.Errorf("invalid token signature")
}
if claims != nil {
return claims, fmt.Errorf("token has expired")
}
return nil, err
}

return claims, nil
}

func CreateNewToken(data interface{}, key string) (string, error) {
claims, ok := data.(jwt.MapClaims)
if !ok {
return "", errors.New("invalid claims type")
}

token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
tokenString, err := token.SignedString(key)

if err != nil {
return "", err
}

return tokenString, nil
}

func SetAccessToken(w http.ResponseWriter, token string, secure bool) {
http.SetCookie(w, &http.Cookie{
Name: "accessToken",
Value: token,
Path: "/",
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
})
}
13 changes: 11 additions & 2 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export
OLD_IMPORT=github.com/gauravst/go-api-template
OLD_CMD_DIR=cmd/go-api-template

# info Variables
PROJECT_NAME=go-api-template
GITHUB_USERNAME=gauravst

# Variables
BINARY_NAME=go-api-template
GO_FILES=$(shell find . -name '*.go' -not -path './vendor/*')
Expand All @@ -21,12 +25,12 @@ all: build
# Build the application
build:
@echo "Building the application..."
go build -o bin/$(BINARY_NAME) cmd/go-api-template/main.go
go build -o bin/$(BINARY_NAME) cmd/$(PROJECT_NAME)/main.go

# Run the application
run:
@echo "Running the application..."
go run cmd/go-api-template/main.go
go run cmd/$(PROJECT_NAME)/main.go

# Run tests
test:
Expand Down Expand Up @@ -111,6 +115,11 @@ setup:
echo "Updating Makefile..."; \
sed -i 's|OLD_IMPORT=$(OLD_IMPORT)|OLD_IMPORT='$$NEW_IMPORT'|g' makefile; \
sed -i 's|OLD_CMD_DIR=$(OLD_CMD_DIR)|OLD_CMD_DIR='$$NEW_CMD_DIR'|g' makefile; \
sed -i 's|PROJECT_NAME=$(PROJECT_NAME)|PROJECT_NAME='$$LOWER_PROJECT'|g' makefile; \
sed -i 's|GITHUB_USERNAME=$(GITHUB_USERNAME)|GITHUB_USERNAME='$$LOWER_USERNAME'|g' makefile; \
sed -i 's|BINARY_NAME=$(BINARY_NAME)|BINARY_NAME='$$LOWER_PROJECT'|g' makefile; \
sed -i 's|APP_NAME=$(APP_NAME)|APP_NAME='$$LOWER_PROJECT'|g' makefile; \
sed -i 's|DOCKER_IMAGE_NAME=$(DOCKER_IMAGE_NAME)|DOCKER_IMAGE_NAME='$$LOWER_PROJECT'|g' makefile; \
echo "Setup completed!"

# Help (list all targets)
Expand Down
Loading