mirror of
https://github.com/go-shiori/shiori.git
synced 2024-11-10 17:36:02 +08:00
update account
This commit is contained in:
parent
7c09384617
commit
fee6cf7f9c
12 changed files with 224 additions and 48 deletions
|
@ -119,7 +119,7 @@ func initShiori(ctx context.Context, cmd *cobra.Command) (*config.Config, *depen
|
|||
account := model.AccountDTO{
|
||||
Username: "shiori",
|
||||
Password: "gopher",
|
||||
Owner: true,
|
||||
Owner: model.Ptr[bool](true),
|
||||
}
|
||||
|
||||
if _, err := dependencies.Domains.Accounts.CreateAccount(cmd.Context(), account); err != nil {
|
||||
|
|
|
@ -103,6 +103,9 @@ type DB interface {
|
|||
// SaveAccount saves new account in database
|
||||
SaveAccount(ctx context.Context, a model.Account) (*model.Account, error)
|
||||
|
||||
// UpdateAccount updates account in database
|
||||
UpdateAccount(ctx context.Context, a model.Account) error
|
||||
|
||||
// SaveAccountSettings saves settings for specific user in database
|
||||
SaveAccountSettings(ctx context.Context, a model.Account) error
|
||||
|
||||
|
@ -110,7 +113,7 @@ type DB interface {
|
|||
ListAccounts(ctx context.Context, opts ListAccountsOptions) ([]model.Account, error)
|
||||
|
||||
// GetAccount fetch account with matching username.
|
||||
GetAccount(ctx context.Context, id model.DBID) (model.Account, bool, error)
|
||||
GetAccount(ctx context.Context, id model.DBID) (*model.Account, bool, error)
|
||||
|
||||
// DeleteAccount removes account with matching id
|
||||
DeleteAccount(ctx context.Context, id model.DBID) error
|
||||
|
|
|
@ -36,6 +36,7 @@ func testDatabase(t *testing.T, dbFactory testDatabaseFactory) {
|
|||
"testDeleteAccount": testDeleteAccount,
|
||||
"testDeleteNonExistantAccount": testDeleteNonExistantAccount,
|
||||
"testSaveAccount": testSaveAccount,
|
||||
"testUpdateAccount": testUpdateAccount,
|
||||
"testSaveAccountSetting": testSaveAccountSettings,
|
||||
"testGetAccount": testGetAccount,
|
||||
"testListAccounts": testListAccounts,
|
||||
|
@ -390,6 +391,50 @@ func testSaveAccount(t *testing.T, db DB) {
|
|||
require.NotEmpty(t, account.ID)
|
||||
}
|
||||
|
||||
func testUpdateAccount(t *testing.T, db DB) {
|
||||
ctx := context.TODO()
|
||||
|
||||
acc := model.Account{
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
Owner: true,
|
||||
Config: model.UserConfig{
|
||||
ShowId: true,
|
||||
},
|
||||
}
|
||||
|
||||
account, err := db.SaveAccount(ctx, acc)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, account)
|
||||
require.NotEmpty(t, account.ID)
|
||||
|
||||
account, _, err = db.GetAccount(ctx, account.ID)
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
acc := model.Account{
|
||||
ID: account.ID,
|
||||
Username: "asdlasd",
|
||||
Owner: false,
|
||||
Password: "another",
|
||||
Config: model.UserConfig{
|
||||
ShowId: false,
|
||||
},
|
||||
}
|
||||
|
||||
err := db.UpdateAccount(ctx, acc)
|
||||
require.Nil(t, err)
|
||||
|
||||
updatedAccount, exists, err := db.GetAccount(ctx, account.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists)
|
||||
require.Equal(t, acc.Username, updatedAccount.Username)
|
||||
require.Equal(t, acc.Owner, updatedAccount.Owner)
|
||||
require.Equal(t, acc.Config, updatedAccount.Config)
|
||||
require.NotEqual(t, acc.Password, account.Password)
|
||||
})
|
||||
}
|
||||
|
||||
func testSaveAccountSettings(t *testing.T, db DB) {
|
||||
ctx := context.TODO()
|
||||
|
||||
|
|
|
@ -603,6 +603,27 @@ func (db *MySQLDatabase) SaveAccount(ctx context.Context, account model.Account)
|
|||
return &account, nil
|
||||
}
|
||||
|
||||
// UpdateAccount update account in database
|
||||
func (db *MySQLDatabase) UpdateAccount(ctx context.Context, account model.Account) error {
|
||||
if account.ID == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, `UPDATE account
|
||||
SET username = ?, password = ?, owner = ?, config = ?
|
||||
WHERE id = ?`,
|
||||
account.Username, account.Password, account.Owner, account.Config, account.ID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAccountSettings update settings for specific account in database. Returns error if any happened
|
||||
func (db *MySQLDatabase) SaveAccountSettings(ctx context.Context, account model.Account) (err error) {
|
||||
// Update account config in database for specific user
|
||||
|
@ -651,14 +672,14 @@ func (db *MySQLDatabase) ListAccounts(ctx context.Context, opts ListAccountsOpti
|
|||
|
||||
// GetAccount fetch account with matching username.
|
||||
// Returns the account and boolean whether it's exist or not.
|
||||
func (db *MySQLDatabase) GetAccount(ctx context.Context, id model.DBID) (model.Account, bool, error) {
|
||||
func (db *MySQLDatabase) GetAccount(ctx context.Context, id model.DBID) (*model.Account, bool, error) {
|
||||
account := model.Account{}
|
||||
err := db.GetContext(ctx, &account, `SELECT
|
||||
id, username, password, owner, config FROM account WHERE id = ?`,
|
||||
id,
|
||||
)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return account, false, errors.WithStack(err)
|
||||
return &account, false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Use custom not found error if that's the result of the query
|
||||
|
@ -666,7 +687,7 @@ func (db *MySQLDatabase) GetAccount(ctx context.Context, id model.DBID) (model.A
|
|||
err = ErrNotFound
|
||||
}
|
||||
|
||||
return account, account.ID != 0, err
|
||||
return &account, account.ID != 0, err
|
||||
}
|
||||
|
||||
// DeleteAccount removes record with matching username.
|
||||
|
|
|
@ -616,6 +616,29 @@ func (db *PGDatabase) SaveAccount(ctx context.Context, account model.Account) (*
|
|||
return &account, nil
|
||||
}
|
||||
|
||||
// UpdateAccount updates account in database.
|
||||
func (db *PGDatabase) UpdateAccount(ctx context.Context, account model.Account) error {
|
||||
if account.ID == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, `UPDATE account
|
||||
SET username = $1, password = $2, owner = $3, config = $4
|
||||
WHERE id = $5`,
|
||||
account.Username, account.Password, account.Owner, account.Config, account.ID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAccountSettings update settings for specific account in database. Returns error if any happened
|
||||
func (db *PGDatabase) SaveAccountSettings(ctx context.Context, account model.Account) (err error) {
|
||||
|
||||
|
@ -665,14 +688,14 @@ func (db *PGDatabase) ListAccounts(ctx context.Context, opts ListAccountsOptions
|
|||
|
||||
// GetAccount fetch account with matching username.
|
||||
// Returns the account and boolean whether it's exist or not.
|
||||
func (db *PGDatabase) GetAccount(ctx context.Context, id model.DBID) (model.Account, bool, error) {
|
||||
func (db *PGDatabase) GetAccount(ctx context.Context, id model.DBID) (*model.Account, bool, error) {
|
||||
account := model.Account{}
|
||||
err := db.GetContext(ctx, &account, `SELECT
|
||||
id, username, password, owner, config FROM account WHERE id = $1`,
|
||||
id,
|
||||
)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return account, false, errors.WithStack(err)
|
||||
return &account, false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Use custom not found error if that's the result of the query
|
||||
|
@ -680,7 +703,7 @@ func (db *PGDatabase) GetAccount(ctx context.Context, id model.DBID) (model.Acco
|
|||
err = ErrNotFound
|
||||
}
|
||||
|
||||
return account, account.ID != 0, err
|
||||
return &account, account.ID != 0, err
|
||||
}
|
||||
|
||||
// DeleteAccount removes record with matching username.
|
||||
|
|
|
@ -733,6 +733,32 @@ func (db *SQLiteDatabase) SaveAccountSettings(ctx context.Context, account model
|
|||
return nil
|
||||
}
|
||||
|
||||
func (db *SQLiteDatabase) UpdateAccount(ctx context.Context, account model.Account) error {
|
||||
if account.ID == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
queryString := "UPDATE account SET username = ?, password = ?, owner = ?, config = ? WHERE id = ?"
|
||||
|
||||
updateQuery, err := tx.PrepareContext(ctx, queryString)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err = updateQuery.ExecContext(ctx, account.Username, account.Password, account.Owner, account.Config, account.ID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAccounts fetch list of account (without its password) based on submitted options.
|
||||
func (db *SQLiteDatabase) ListAccounts(ctx context.Context, opts ListAccountsOptions) ([]model.Account, error) {
|
||||
// Create query
|
||||
|
@ -770,14 +796,14 @@ func (db *SQLiteDatabase) ListAccounts(ctx context.Context, opts ListAccountsOpt
|
|||
|
||||
// GetAccount fetch account with matching username.
|
||||
// Returns the account and boolean whether it's exist or not.
|
||||
func (db *SQLiteDatabase) GetAccount(ctx context.Context, id model.DBID) (model.Account, bool, error) {
|
||||
func (db *SQLiteDatabase) GetAccount(ctx context.Context, id model.DBID) (*model.Account, bool, error) {
|
||||
account := model.Account{}
|
||||
err := db.GetContext(ctx, &account, `SELECT
|
||||
id, username, password, owner, config FROM account WHERE id = ?`,
|
||||
id,
|
||||
)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return account, false, errors.WithStack(err)
|
||||
return &account, false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Use custom not found error if that's the result of the query
|
||||
|
@ -785,7 +811,7 @@ func (db *SQLiteDatabase) GetAccount(ctx context.Context, id model.DBID) (model.
|
|||
err = ErrNotFound
|
||||
}
|
||||
|
||||
return account, account.ID != 0, err
|
||||
return &account, account.ID != 0, err
|
||||
}
|
||||
|
||||
// DeleteAccount removes record with matching username.
|
||||
|
|
|
@ -36,11 +36,18 @@ func (d *AccountsDomain) CreateAccount(ctx context.Context, account model.Accoun
|
|||
return nil, fmt.Errorf("error hashing provided password: %w", err)
|
||||
}
|
||||
|
||||
storedAccount, err := d.deps.Database.SaveAccount(ctx, model.Account{
|
||||
acc := model.Account{
|
||||
Username: account.Username,
|
||||
Password: string(hashedPassword),
|
||||
Owner: account.Owner,
|
||||
})
|
||||
}
|
||||
if account.Owner != nil {
|
||||
acc.Owner = *account.Owner
|
||||
}
|
||||
if account.Config != nil {
|
||||
acc.Config = *account.Config
|
||||
}
|
||||
|
||||
storedAccount, err := d.deps.Database.SaveAccount(ctx, acc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating account: %v", err)
|
||||
}
|
||||
|
@ -64,23 +71,51 @@ func (d *AccountsDomain) DeleteAccount(ctx context.Context, id int) error {
|
|||
}
|
||||
|
||||
func (d *AccountsDomain) UpdateAccount(ctx context.Context, account model.AccountDTO) (*model.AccountDTO, error) {
|
||||
updatedAccount := model.Account{
|
||||
ID: account.ID,
|
||||
// Get account from database
|
||||
storedAccount, _, err := d.deps.Database.GetAccount(ctx, account.ID)
|
||||
if errors.Is(err, database.ErrNotFound) {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting account for update: %w", err)
|
||||
}
|
||||
|
||||
// Update password as well
|
||||
if account.Password != "" {
|
||||
// Hash password with bcrypt
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(account.Password), 10)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error hashing provided password: %w", err)
|
||||
}
|
||||
updatedAccount.Password = string(hashedPassword)
|
||||
storedAccount.Password = string(hashedPassword)
|
||||
}
|
||||
|
||||
// TODO
|
||||
if account.Username != "" {
|
||||
storedAccount.Username = account.Username
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
if account.Owner != nil {
|
||||
storedAccount.Owner = *account.Owner
|
||||
}
|
||||
|
||||
if account.Config != nil {
|
||||
storedAccount.Config = *account.Config
|
||||
}
|
||||
|
||||
// Save updated account
|
||||
err = d.deps.Database.UpdateAccount(ctx, *storedAccount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating account: %w", err)
|
||||
}
|
||||
|
||||
// Get updated account from database
|
||||
updatedAccount, _, err := d.deps.Database.GetAccount(ctx, account.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting updated account: %w", err)
|
||||
}
|
||||
|
||||
account = updatedAccount.ToDTO()
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func NewAccountsDomain(deps *dependencies.Dependencies) model.AccountsDomain {
|
||||
|
|
|
@ -22,7 +22,7 @@ func (r *AccountsAPIRoutes) Setup(g *gin.RouterGroup) model.Routes {
|
|||
g.GET("/", r.listHandler)
|
||||
g.POST("/", r.createHandler)
|
||||
g.DELETE("/:id", r.deleteHandler)
|
||||
// g.PUT("/:id", r.updateHandler)
|
||||
g.PUT("/:id", r.updateHandler)
|
||||
|
||||
return r
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ func (p *createAccountPayload) ToAccountDTO() model.AccountDTO {
|
|||
return model.AccountDTO{
|
||||
Username: p.Username,
|
||||
Password: p.Password,
|
||||
Owner: !p.IsVisitor,
|
||||
Owner: model.Ptr[bool](!p.IsVisitor),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,22 +144,37 @@ func (r *AccountsAPIRoutes) deleteHandler(c *gin.Context) {
|
|||
response.Send(c, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// func (r *AccountsAPIRoutes) updateHandler(c *gin.Context) {
|
||||
// id := c.Param("id")
|
||||
// updateHandler godoc
|
||||
//
|
||||
// @Summary Update an account
|
||||
// @Tags accounts
|
||||
// @Produce json
|
||||
// @Success 200 {array} model.AccountDTO
|
||||
// @Failure 400 {string} string "Bad Request"
|
||||
// @Failure 500 {string} string "Internal Server Error"
|
||||
// @Router /api/v1/accounts/{id} [put,patch]
|
||||
func (r *AccountsAPIRoutes) updateHandler(c *gin.Context) {
|
||||
accountID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
r.logger.WithError(err).Error("error parsing id")
|
||||
response.SendError(c, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
// var payload model.AccountDTO
|
||||
// if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
// r.logger.WithError(err).Error("error binding json")
|
||||
// c.AbortWithStatus(http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
var payload model.AccountDTO
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
r.logger.WithError(err).Error("error binding json")
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
payload.ID = model.DBID(accountID)
|
||||
|
||||
// account, err := r.deps.Domains.Accounts.UpdateAccount(c.Request.Context(), id, payload)
|
||||
// if err != nil {
|
||||
// r.logger.WithError(err).Error("error updating account")
|
||||
// c.AbortWithStatus(http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
account, err := r.deps.Domains.Accounts.UpdateAccount(c.Request.Context(), payload)
|
||||
if err != nil {
|
||||
r.logger.WithError(err).Error("error updating account")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// response.Send(c, http.StatusOK, account)
|
||||
// }
|
||||
response.Send(c, http.StatusOK, account)
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ func TestAccountsRoute(t *testing.T) {
|
|||
account := model.AccountDTO{
|
||||
Username: "shiori",
|
||||
Password: "gopher",
|
||||
Owner: true,
|
||||
Owner: model.Ptr(true),
|
||||
}
|
||||
|
||||
_, accountInsertErr := deps.Domains.Accounts.CreateAccount(ctx, account)
|
||||
|
@ -269,7 +269,7 @@ func TestSettingsHandler(t *testing.T) {
|
|||
require.Equal(t, user.Config, account.Config)
|
||||
|
||||
// Send Request to update config for user
|
||||
token, err := deps.Domains.Auth.CreateTokenForAccount(&user, time.Now().Add(time.Minute))
|
||||
token, err := deps.Domains.Auth.CreateTokenForAccount(user, time.Now().Add(time.Minute))
|
||||
require.NoError(t, err)
|
||||
|
||||
payloadJSON := []byte(`{
|
||||
|
|
|
@ -46,19 +46,22 @@ func (c UserConfig) Value() (driver.Value, error) {
|
|||
|
||||
// ToDTO converts Account to AccountDTO.
|
||||
func (a Account) ToDTO() AccountDTO {
|
||||
owner := a.Owner
|
||||
config := a.Config
|
||||
|
||||
return AccountDTO{
|
||||
ID: a.ID,
|
||||
Username: a.Username,
|
||||
Owner: a.Owner,
|
||||
Config: a.Config,
|
||||
Owner: &owner,
|
||||
Config: &config,
|
||||
}
|
||||
}
|
||||
|
||||
// AccountDTO is data transfer object for Account.
|
||||
type AccountDTO struct {
|
||||
ID DBID `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"` // Used only to store, not to retrieve
|
||||
Owner bool `json:"owner"`
|
||||
Config UserConfig `json:"config"`
|
||||
ID DBID `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"` // Used only to store, not to retrieve
|
||||
Owner *bool `json:"owner"`
|
||||
Config *UserConfig `json:"config"`
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ type AuthDomain interface {
|
|||
type AccountsDomain interface {
|
||||
ListAccounts(ctx context.Context) ([]AccountDTO, error)
|
||||
CreateAccount(ctx context.Context, account AccountDTO) (*AccountDTO, error)
|
||||
// UpdateAccount(ctx context.Context, account AccountDTO) (*AccountDTO, error)
|
||||
UpdateAccount(ctx context.Context, account AccountDTO) (*AccountDTO, error)
|
||||
DeleteAccount(ctx context.Context, id int) error
|
||||
}
|
||||
|
||||
|
|
5
internal/model/ptr.go
Normal file
5
internal/model/ptr.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package model
|
||||
|
||||
func Ptr[t any](a t) *t {
|
||||
return &a
|
||||
}
|
Loading…
Reference in a new issue