update account

This commit is contained in:
Felipe M. 2024-06-04 00:33:58 +02:00
parent 7c09384617
commit fee6cf7f9c
No known key found for this signature in database
GPG key ID: CCFBC5637D4000A8
12 changed files with 224 additions and 48 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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()

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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 {

View file

@ -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)
}

View file

@ -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(`{

View file

@ -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"`
}

View file

@ -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
View file

@ -0,0 +1,5 @@
package model
func Ptr[t any](a t) *t {
return &a
}