Skip to content

Commit

Permalink
Merge pull request #2 from prodemmi/feature/implement-auth
Browse files Browse the repository at this point in the history
[FEATURE] Implement otp and jwt authentication
  • Loading branch information
prodemmi committed Jun 9, 2023
2 parents 6ef169a + 0afae20 commit 4bf3d2c
Show file tree
Hide file tree
Showing 25 changed files with 484 additions and 23 deletions.
15 changes: 7 additions & 8 deletions app/console/commands/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
csrf "github.com/utrack/gin-csrf"
)

type ServeCommand struct {
Expand All @@ -20,13 +19,13 @@ func (s *ServeCommand) RunE(cmd *cobra.Command, args []string) error {

router.Use(middlewares.MaintenanceMode(s.container.Config))
router.Use(sessions.Sessions("mysession", s.container.Session.Store))
router.Use(csrf.Middleware(csrf.Options{
Secret: "SHsHZ28711587148418",
ErrorFunc: func(c *gin.Context) {
c.String(400, "CSRF token mismatch")
c.Abort()
},
}))
//router.Use(csrf.Middleware(csrf.Options{
// Secret: "SHsHZ28711587148418",
// ErrorFunc: func(c *gin.Context) {
// c.String(400, "CSRF token mismatch")
// c.Abort()
// },
//}))

routes.SetupApiRoutes(router, s.container)

Expand Down
93 changes: 93 additions & 0 deletions app/http/handlers/api/otp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package api

import (
"GoGinStarter/app/services/otp"
"GoGinStarter/app/services/user"
"GoGinStarter/internal/config"
"GoGinStarter/internal/container"
"GoGinStarter/internal/event"
"GoGinStarter/internal/jwt"
"GoGinStarter/internal/log"
"GoGinStarter/internal/response"
"github.com/gin-gonic/gin"
)

type OTPApiHandler struct {
userService user.Service
otpService otp.Service
response response.Response
log log.Log
config *config.Config
eventDispatcher event.Dispatcher
}

func NewTOPApiHandler(container *container.Container) *OTPApiHandler {
return &OTPApiHandler{
container.UserService,
container.OTPService,
container.Response,
container.Log,
container.Config,
container.EventDispatcher,
}
}

type SentRequest struct {
Mobile string `json:"mobile"`
}

func (o *OTPApiHandler) SentHandler(ctx *gin.Context) {
var data SentRequest
if err := ctx.BindJSON(&data); err != nil {
o.response.BadRequest(ctx, err.Error(), nil)
return
}

_, err := o.otpService.Create(ctx, data.Mobile)
if err != nil {
o.response.Error(ctx, 500, err.Error())
return
}

o.response.Success(ctx, nil, "OTP successfully sent")
}

type VerifyRequest struct {
Mobile string `json:"mobile"`
Token string `json:"token"`
}

func (o *OTPApiHandler) VerifyHandler(ctx *gin.Context) {
var data VerifyRequest
if err := ctx.BindJSON(&data); err != nil {
o.response.BadRequest(ctx, err.Error(), nil)
return
}

if err := o.otpService.VerifyByToken(ctx, data.Mobile, data.Token); err != nil {
o.response.Error(ctx, 500, err.Error())
return
}

user, err := o.userService.FirstOrCreate(ctx, data.Mobile)
if err != nil {
o.response.Error(ctx, 500, err.Error())
return
}

tokenString, err := jwt.CreateToken(user.ID, o.config.Auth.JWT.Secret, o.config.Auth.JWT.ExpirationTime)
if err != nil {
o.response.Error(ctx, 500, err.Error())
return
}

user, err = o.userService.AddRememberToken(ctx, user.ID, tokenString)
if err != nil {
o.response.Error(ctx, 500, err.Error())
return
}

ctx.Header("Authorization", "Bearer "+tokenString)

o.response.Success(ctx, nil, "Authentication successfully")
}
49 changes: 49 additions & 0 deletions app/http/middlewares/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package middlewares

import (
"GoGinStarter/app/services/user"
"GoGinStarter/internal/config"
"GoGinStarter/internal/response"
"fmt"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"strings"
)

func JWT(response response.Response, config *config.Config, userService user.Service) gin.HandlerFunc {
return func(ctx *gin.Context) {
authHeader := ctx.GetHeader("Authorization")
fmt.Println(authHeader)
if authHeader == "" {
response.Unauthorized(ctx, "Unauthorized 401")
ctx.Abort()
return
}

tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(config.Auth.JWT.Secret), nil
})

if err != nil {
response.Unauthorized(ctx, err.Error())
ctx.Abort()
return
}

if !token.Valid {
response.Unauthorized(ctx, "Invalid token")
ctx.Abort()
return
}

user, _ := userService.FindByRememberToken(ctx, token.Raw)

ctx.Set("remember_token", token)
ctx.Set("user", user)
ctx.Next()
}
}
18 changes: 13 additions & 5 deletions app/http/routes/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ import (
)

func SetupApiRoutes(router *gin.Engine, container *container.Container) {
OTPApiHandler := api.NewTOPApiHandler(container)
userApiHandler := api.NewUserApiHandler(container)

jwtMiddleware := middlewares.JWT(container.Response, container.Config, container.UserService)

apiRouter := router.Group("/api/v1", middlewares.RequestLogger(container.Log))
{
apiRouter.GET("/users", userApiHandler.UsersHandler)
apiRouter.GET("/users/:id", userApiHandler.SingleHandler)
apiRouter.POST("/users", userApiHandler.StoreHandler)
}

authRouter := apiRouter.Group("/auth")
authRouter.POST("/sent-otp", OTPApiHandler.SentHandler)
authRouter.POST("/verify-otp", OTPApiHandler.VerifyHandler)

userRouter := apiRouter.Group("/users", jwtMiddleware)
userRouter.GET("/index", userApiHandler.UsersHandler)
userRouter.POST("/store", userApiHandler.StoreHandler)
userRouter.GET("/:id/single", userApiHandler.SingleHandler)
}
18 changes: 18 additions & 0 deletions app/listeners/otp/created.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package otp

import (
"fmt"
)

type CreatedListener struct{}

func (l CreatedListener) HandleEvent(data interface{}) {
if d, ok := data.(map[string]interface{}); ok {
token := d["token"].(string)
mobile := d["mobile"].(string)

// Implement OTP send

fmt.Println("OTP created is " + token + " for mobile " + mobile)
}
}
18 changes: 18 additions & 0 deletions app/models/otp_tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package models

import (
"gorm.io/gorm"
"time"
)

type OtpTokens struct {
gorm.Model
Mobile string `json:"mobile" gorm:"index:not null"`
Token string `json:"token" gorm:"size:6;not null"`
SentAt time.Time `json:"sent_at"`
ExpiresAt time.Time `json:"expires_at"`
}

func (o *OtpTokens) TableName() string {
return "otp_tokens"
}
12 changes: 7 additions & 5 deletions app/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (

type User struct {
gorm.Model
FirstName string `json:"first_name" gorm:"not null"`
LastName string `json:"last_name" gorm:"not null"`
Email string `json:"email" gorm:"unique_index;not null"`
Password string `json:"-" gorm:"not null"`
DeletedAt gorm.DeletedAt `json:"deleted_at"`
FirstName string `json:"first_name" gorm:"not null"`
LastName string `json:"last_name" gorm:"not null"`
Mobile string `json:"mobile;" gorm:"unique_index"`
Email string `json:"email" gorm:"unique_index;not null"`
Password string `json:"-" gorm:"not null"`
RememberToken string `json:"-" gorm:"unique"`
DeletedAt gorm.DeletedAt `json:"deleted_at"`
}

func (u *User) TableName() string {
Expand Down
57 changes: 57 additions & 0 deletions app/repositories/otp/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package otp

import (
"GoGinStarter/app/models"
"GoGinStarter/internal/config"
"GoGinStarter/internal/log"
"GoGinStarter/internal/otp"
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
time "time"
)

type Repository interface {
Create(ctx *gin.Context, mobile string) (*models.OtpTokens, error)
VerifyByToken(ctx *gin.Context, mobile string, token string) error
}

type repository struct {
db *gorm.DB
log log.Log
config *config.Config
}

func (r *repository) Create(ctx *gin.Context, mobile string) (*models.OtpTokens, error) {
otpToken := models.OtpTokens{
Mobile: mobile,
Token: otp.GenerateOTP(r.config.Auth.OTP.TokenLength),
SentAt: time.Now(),
ExpiresAt: time.Now().Add(time.Second * time.Duration(r.config.Auth.OTP.ExpirationTime)),
}
r.db.Create(&otpToken)

return &otpToken, nil
}

func (r *repository) VerifyByToken(ctx *gin.Context, mobile string, token string) error {
otpToken := models.OtpTokens{}
db := r.db.Where("mobile = ? AND token = ?", mobile, token).First(&otpToken)
if err := db.Error; err != nil {
return err
}

if time.Now().After(otpToken.ExpiresAt) {
return fmt.Errorf("token has exipred")
}

if err := db.Delete(&otpToken).Error; err != nil {
return err
}

return nil
}

func ProvideOtpRepository(db *gorm.DB, log log.Log, config *config.Config) Repository {
return &repository{db, log, config}
}
32 changes: 32 additions & 0 deletions app/repositories/user/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (
type Repository interface {
Index(ctx *gin.Context) (*paginator.Paginator, error)
FindById(ctx *gin.Context, id int) (*models.User, error)
FirstOrCreate(ctx *gin.Context, mobile string) (*models.User, error)
AddRememberToken(ctx *gin.Context, id uint, rememberToken string) (*models.User, error)
FindByRememberToken(ctx *gin.Context, rememberToken string) (*models.User, error)
}

type repository struct {
Expand Down Expand Up @@ -44,6 +47,35 @@ func (r *repository) FindById(ctx *gin.Context, id int) (*models.User, error) {
return &user, nil
}

func (r *repository) FirstOrCreate(ctx *gin.Context, mobile string) (*models.User, error) {
var user models.User
if err := r.db.Where("mobile = ?", mobile).FirstOrCreate(&user, models.User{
Mobile: mobile,
}).Error; err != nil {
return nil, err
}

return &user, nil
}

func (r *repository) AddRememberToken(ctx *gin.Context, id uint, rememberToken string) (*models.User, error) {
var user models.User
if err := r.db.Model(&user).Where("id = ?", id).Update("remember_token", rememberToken).Error; err != nil {
return nil, err
}

return &user, nil
}

func (r *repository) FindByRememberToken(ctx *gin.Context, rememberToken string) (*models.User, error) {
var user models.User
if err := r.db.Where("remember_token = ?", rememberToken).First(&user).Error; err != nil {
return nil, err
}

return &user, nil
}

func ProvideUserRepository(db *gorm.DB, log log.Log) Repository {
return &repository{db, log}
}
Loading

0 comments on commit 4bf3d2c

Please sign in to comment.