github.com/soulteary/pocket-bookcase@v0.0.0-20240428065142-0b5a9a0fc98a/internal/http/routes/api/v1/auth.go (about)

     1  package api_v1
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"time"
     7  
     8  	"github.com/gin-gonic/gin"
     9  	"github.com/sirupsen/logrus"
    10  	"github.com/soulteary/pocket-bookcase/internal/dependencies"
    11  	"github.com/soulteary/pocket-bookcase/internal/http/context"
    12  	"github.com/soulteary/pocket-bookcase/internal/http/response"
    13  	"github.com/soulteary/pocket-bookcase/internal/model"
    14  )
    15  
    16  type AuthAPIRoutes struct {
    17  	logger             *logrus.Logger
    18  	deps               *dependencies.Dependencies
    19  	legacyLoginHandler model.LegacyLoginHandler
    20  }
    21  
    22  func (r *AuthAPIRoutes) Setup(group *gin.RouterGroup) model.Routes {
    23  	group.GET("/me", r.meHandler)
    24  	group.POST("/login", r.loginHandler)
    25  	group.POST("/refresh", r.refreshHandler)
    26  	group.PATCH("/account", r.settingsHandler)
    27  	return r
    28  }
    29  
    30  type loginRequestPayload struct {
    31  	Username   string `json:"username"    validate:"required"`
    32  	Password   string `json:"password"    validate:"required"`
    33  	RememberMe bool   `json:"remember_me"`
    34  }
    35  
    36  func (p *loginRequestPayload) IsValid() error {
    37  	if p.Username == "" {
    38  		return fmt.Errorf("username should not be empty")
    39  	}
    40  	if p.Password == "" {
    41  		return fmt.Errorf("password should not be empty")
    42  	}
    43  	return nil
    44  }
    45  
    46  type loginResponseMessage struct {
    47  	Token      string `json:"token"`
    48  	SessionID  string `json:"session"` // Deprecated, used only for legacy APIs
    49  	Expiration int64  `json:"expires"` // Deprecated, used only for legacy APIs
    50  }
    51  
    52  type settingRequestPayload struct {
    53  	Config model.UserConfig `json:"config"`
    54  }
    55  
    56  // loginHandler godoc
    57  //
    58  //	@Summary	Login to an account using username and password
    59  //	@Tags		Auth
    60  //	@Accept		json
    61  //	@Produce	json
    62  //	@Param		payload	body		loginRequestPayload		false	"Login data"
    63  //	@Success	200		{object}	loginResponseMessage	"Login successful"
    64  //	@Failure	400		{object}	nil						"Invalid login data"
    65  //	@Router		/api/v1/auth/login [post]
    66  func (r *AuthAPIRoutes) loginHandler(c *gin.Context) {
    67  	var payload loginRequestPayload
    68  	if err := c.ShouldBindJSON(&payload); err != nil {
    69  		response.SendInternalServerError(c)
    70  		return
    71  	}
    72  
    73  	if err := payload.IsValid(); err != nil {
    74  		response.SendError(c, http.StatusBadRequest, err.Error())
    75  		return
    76  	}
    77  
    78  	account, err := r.deps.Domains.Auth.GetAccountFromCredentials(c, payload.Username, payload.Password)
    79  	if err != nil {
    80  		response.SendError(c, http.StatusBadRequest, err.Error())
    81  		return
    82  	}
    83  
    84  	expiration := time.Now().Add(time.Hour)
    85  	if payload.RememberMe {
    86  		expiration = time.Now().Add(time.Hour * 24 * 30)
    87  	}
    88  
    89  	token, err := r.deps.Domains.Auth.CreateTokenForAccount(account, expiration)
    90  	if err != nil {
    91  		response.SendInternalServerError(c)
    92  		return
    93  	}
    94  
    95  	sessionID, err := r.legacyLoginHandler(*account, time.Hour*24*30)
    96  	if err != nil {
    97  		r.logger.WithError(err).Error("failed execute legacy login handler")
    98  		response.SendInternalServerError(c)
    99  		return
   100  	}
   101  
   102  	responseMessage := loginResponseMessage{
   103  		Token:      token,
   104  		SessionID:  sessionID,
   105  		Expiration: expiration.Unix(),
   106  	}
   107  
   108  	response.Send(c, http.StatusOK, responseMessage)
   109  }
   110  
   111  // refreshHandler godoc
   112  //
   113  //	@Summary					Refresh a token for an account
   114  //	@Tags						Auth
   115  //	@securityDefinitions.apikey	ApiKeyAuth
   116  //	@Produce					json
   117  //	@Success					200	{object}	loginResponseMessage	"Refresh successful"
   118  //	@Failure					403	{object}	nil						"Token not provided/invalid"
   119  //	@Router						/api/v1/auth/refresh [post]
   120  func (r *AuthAPIRoutes) refreshHandler(c *gin.Context) {
   121  	ctx := context.NewContextFromGin(c)
   122  	if !ctx.UserIsLogged() {
   123  		response.SendError(c, http.StatusForbidden, nil)
   124  		return
   125  	}
   126  
   127  	expiration := time.Now().Add(time.Hour * 72)
   128  	account, _ := c.Get(model.ContextAccountKey)
   129  	token, err := r.deps.Domains.Auth.CreateTokenForAccount(account.(*model.Account), expiration)
   130  	if err != nil {
   131  		response.SendInternalServerError(c)
   132  		return
   133  	}
   134  
   135  	responseMessage := loginResponseMessage{
   136  		Token: token,
   137  	}
   138  
   139  	response.Send(c, http.StatusAccepted, responseMessage)
   140  }
   141  
   142  // meHandler godoc
   143  //
   144  //	@Summary					Get information for the current logged in user
   145  //	@Tags						Auth
   146  //	@securityDefinitions.apikey	ApiKeyAuth
   147  //	@Produce					json
   148  //	@Success					200	{object}	model.Account
   149  //	@Failure					403	{object}	nil	"Token not provided/invalid"
   150  //	@Router						/api/v1/auth/me [get]
   151  func (r *AuthAPIRoutes) meHandler(c *gin.Context) {
   152  	ctx := context.NewContextFromGin(c)
   153  	if !ctx.UserIsLogged() {
   154  		response.SendError(c, http.StatusForbidden, nil)
   155  		return
   156  	}
   157  
   158  	response.Send(c, http.StatusOK, ctx.GetAccount())
   159  }
   160  
   161  // settingsHandler godoc
   162  //
   163  //	@Summary					Perform actions on the currently logged-in user.
   164  //	@Tags						Auth
   165  //	@securityDefinitions.apikey	ApiKeyAuth
   166  //	@Param						payload	body	settingRequestPayload	false	"Config data"
   167  //	@Produce					json
   168  //	@Success					200	{object}	model.Account
   169  //	@Failure					403	{object}	nil	"Token not provided/invalid"
   170  //	@Router						/api/v1/auth/account [patch]
   171  func (r *AuthAPIRoutes) settingsHandler(c *gin.Context) {
   172  	ctx := context.NewContextFromGin(c)
   173  	if !ctx.UserIsLogged() {
   174  		response.SendError(c, http.StatusForbidden, nil)
   175  	}
   176  	var payload settingRequestPayload
   177  	if err := c.ShouldBindJSON(&payload); err != nil {
   178  		response.SendInternalServerError(c)
   179  	}
   180  
   181  	account := ctx.GetAccount()
   182  	account.Config = payload.Config
   183  
   184  	err := r.deps.Database.SaveAccountSettings(c, *account)
   185  	if err != nil {
   186  		response.SendInternalServerError(c)
   187  	}
   188  
   189  	response.Send(c, http.StatusOK, ctx.GetAccount())
   190  }
   191  
   192  func NewAuthAPIRoutes(logger *logrus.Logger, deps *dependencies.Dependencies, loginHandler model.LegacyLoginHandler) *AuthAPIRoutes {
   193  	return &AuthAPIRoutes{
   194  		logger:             logger,
   195  		deps:               deps,
   196  		legacyLoginHandler: loginHandler,
   197  	}
   198  }