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 }