shiori/internal/http/routes/api/v1/auth.go
Monirzadeh c05d617fbd
feat: allow per-user settings and store them in database (#639)
* create needed field in sqlite database

* update account model

* update Account struct for save Account options

* update sqlite database return account settings

* save configure in sqlite as text and return that

* read configure from user account and defualt configure for shiori

* add api/ui for update settings in database user can save settings in database (in sqlite database)

* check configures be in json format before save in database

* support MariaDB

* fix wrong comment

* support PostgreSQL

* revert unneeded change in new logic

* change configures to config

* change SaveAccount to SaveAccountSettings

* add migrate database scripts

* change default in migration scrtipts

* update model

* read config field as json from database

* fix parse value config value & update config update

* update default value for new user

* update settings variable name to reflect database value in UI

* fix typo

* not panic if user not exist and update worng comment

* visitor user can update there settings now

* remove unneeded loading dialog

* fix typo

* update function for pg and mysql

* remove IsJson

* move scan method to model

* simplify jsonify

* simplify assignees value to account.Config

* missing part of function

* fix some typo and unneeded field in struct

* add down migrate script for all database

* change createEbook to CreateEbook

* use json instead of text in mysql and postgres

* implement

* remove unneeded part

* remove unneeded jsonify in code

* return SelectContext and GetContext

* remove defualt config in reques for new user it will be set in backend

* New API

* remove legacy API

* remove validateSessionWithoutOwnerStatus

* remove Jsonify function don't need that anymore

* add unit test for database

* update migrate script name

* change put to patch

* return PUT

* fix Patch problem and now use PATCH instead of PUT

* remove unneeded retuen

* more cleaner code for request new settings

* fix bug to handle string in Scan method thanks to fmartingr

* fix Authorization & use GetAccount & remove username from request

* shiori-settings remove and it read from shiori-account

* add swagger documentation

* API unit test

* fix typo

* remove unneeded coment

Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com>

* better Documentation

Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com>

* shiori-toke remove on logout

* fix typo

* add unit test check update config in database

* update swag documentation

* fix swag formaing error

---------

Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com>
2023-10-15 15:27:54 +02:00

198 lines
5.4 KiB
Go

package api_v1
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/go-shiori/shiori/internal/config"
"github.com/go-shiori/shiori/internal/http/context"
"github.com/go-shiori/shiori/internal/http/response"
"github.com/go-shiori/shiori/internal/model"
"github.com/sirupsen/logrus"
)
type AuthAPIRoutes struct {
logger *logrus.Logger
deps *config.Dependencies
legacyLoginHandler model.LegacyLoginHandler
}
func (r *AuthAPIRoutes) Setup(group *gin.RouterGroup) model.Routes {
group.GET("/me", r.meHandler)
group.POST("/login", r.loginHandler)
group.POST("/refresh", r.refreshHandler)
group.PATCH("/account", r.settingsHandler)
return r
}
type loginRequestPayload struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
RememberMe bool `json:"remember_me"`
}
func (p *loginRequestPayload) IsValid() error {
if p.Username == "" {
return fmt.Errorf("username should not be empty")
}
if p.Password == "" {
return fmt.Errorf("password should not be empty")
}
return nil
}
type loginResponseMessage struct {
Token string `json:"token"`
SessionID string `json:"session"` // Deprecated, used only for legacy APIs
Expiration int64 `json:"expires"` // Deprecated, used only for legacy APIs
}
type settingRequestPayload struct {
Config model.UserConfig `json:"config"`
}
// loginHandler godoc
//
// @Summary Login to an account using username and password
// @Tags Auth
// @Accept json
// @Produce json
// @Param payload body loginRequestPayload false "Login data"
// @Success 200 {object} loginResponseMessage "Login successful"
// @Failure 400 {object} nil "Invalid login data"
// @Router /api/v1/auth/login [post]
func (r *AuthAPIRoutes) loginHandler(c *gin.Context) {
var payload loginRequestPayload
if err := c.ShouldBindJSON(&payload); err != nil {
response.SendInternalServerError(c)
return
}
if err := payload.IsValid(); err != nil {
response.SendError(c, http.StatusBadRequest, err.Error())
return
}
account, err := r.deps.Domains.Auth.GetAccountFromCredentials(c, payload.Username, payload.Password)
if err != nil {
response.SendError(c, http.StatusBadRequest, err.Error())
return
}
expiration := time.Now().Add(time.Hour)
if payload.RememberMe {
expiration = time.Now().Add(time.Hour * 24 * 30)
}
token, err := r.deps.Domains.Auth.CreateTokenForAccount(account, expiration)
if err != nil {
response.SendInternalServerError(c)
return
}
sessionID, err := r.legacyLoginHandler(*account, time.Hour*24*30)
if err != nil {
r.logger.WithError(err).Error("failed execute legacy login handler")
response.SendInternalServerError(c)
return
}
responseMessage := loginResponseMessage{
Token: token,
SessionID: sessionID,
Expiration: expiration.Unix(),
}
response.Send(c, http.StatusOK, responseMessage)
}
// refreshHandler godoc
//
// @Summary Refresh a token for an account
// @Tags Auth
// @securityDefinitions.apikey ApiKeyAuth
// @Produce json
// @Success 200 {object} loginResponseMessage "Refresh successful"
// @Failure 403 {object} nil "Token not provided/invalid"
// @Router /api/v1/auth/refresh [post]
func (r *AuthAPIRoutes) refreshHandler(c *gin.Context) {
ctx := context.NewContextFromGin(c)
if !ctx.UserIsLogged() {
response.SendError(c, http.StatusForbidden, nil)
return
}
expiration := time.Now().Add(time.Hour * 72)
account, _ := c.Get(model.ContextAccountKey)
token, err := r.deps.Domains.Auth.CreateTokenForAccount(account.(*model.Account), expiration)
if err != nil {
response.SendInternalServerError(c)
return
}
responseMessage := loginResponseMessage{
Token: token,
}
response.Send(c, http.StatusAccepted, responseMessage)
}
// meHandler godoc
//
// @Summary Get information for the current logged in user
// @Tags Auth
// @securityDefinitions.apikey ApiKeyAuth
// @Produce json
// @Success 200 {object} model.Account
// @Failure 403 {object} nil "Token not provided/invalid"
// @Router /api/v1/auth/me [get]
func (r *AuthAPIRoutes) meHandler(c *gin.Context) {
ctx := context.NewContextFromGin(c)
if !ctx.UserIsLogged() {
response.SendError(c, http.StatusForbidden, nil)
return
}
response.Send(c, http.StatusOK, ctx.GetAccount())
}
// settingsHandler godoc
//
// @Summary Perform actions on the currently logged-in user.
// @Tags Auth
// @securityDefinitions.apikey ApiKeyAuth
// @Param payload body settingRequestPayload false "Config data"
// @Produce json
// @Success 200 {object} model.Account
// @Failure 403 {object} nil "Token not provided/invalid"
// @Router /api/v1/auth/account [patch]
func (r *AuthAPIRoutes) settingsHandler(c *gin.Context) {
ctx := context.NewContextFromGin(c)
if !ctx.UserIsLogged() {
response.SendError(c, http.StatusForbidden, nil)
}
var payload settingRequestPayload
if err := c.ShouldBindJSON(&payload); err != nil {
response.SendInternalServerError(c)
}
account := ctx.GetAccount()
account.Config = payload.Config
err := r.deps.Database.SaveAccountSettings(c, *account)
if err != nil {
response.SendInternalServerError(c)
}
response.Send(c, http.StatusOK, ctx.GetAccount())
}
func NewAuthAPIRoutes(logger *logrus.Logger, deps *config.Dependencies, loginHandler model.LegacyLoginHandler) *AuthAPIRoutes {
return &AuthAPIRoutes{
logger: logger,
deps: deps,
legacyLoginHandler: loginHandler,
}
}