Added transactions and improved error handling #35
@@ -70,8 +70,8 @@ func main() {
|
||||
}
|
||||
|
||||
func seedAdminUser(db *sql.DB, email string, name string, password string) error {
|
||||
userRepo := repository.NewUserRepo(db)
|
||||
userService := domain.NewUserService(userRepo)
|
||||
store := repository.NewStore(db)
|
||||
userService := domain.NewUserService(store.User)
|
||||
|
||||
_, err := userService.CreateUser(context.Background(), email, name, password)
|
||||
return err
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
@@ -27,6 +28,8 @@ func NewAuthHandler(authService *domain.AuthService) *AuthHandler {
|
||||
// @Param payload body request.LoginPayload true "Login payload"
|
||||
// @Success 200 {object} domain.TokenPair
|
||||
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||
// @Error 401 {object} response.ErrorResponse "email not found"
|
||||
// @Error 401 {object} response.ErrorResponse "password is wrong"
|
||||
// @Error 500 {object} response.ErrorResponse "failed to login"
|
||||
// @Router /login [post]
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -41,8 +44,15 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tokens, err := h.AuthService.Login(ctx, payload.Email, payload.Password)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to login", slog.Any("error", err))
|
||||
if errors.Is(err, domain.ErrEmailNotFound) {
|
||||
response.Error(ctx, w, http.StatusUnauthorized, "email not found")
|
||||
} else if errors.Is(err, domain.ErrPasswordWrong) {
|
||||
response.Error(ctx, w, http.StatusUnauthorized, "password is wrong")
|
||||
} else {
|
||||
response.Error(ctx, w, http.StatusInternalServerError, "failed to login")
|
||||
}
|
||||
|
||||
slog.ErrorContext(ctx, "failed to login", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,6 +99,7 @@ func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
// @Tags Authorization
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body request.LogoutPayload true "Logout payload"
|
||||
// @Success 200 {object} nil
|
||||
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||
// @Error 500 {object} response.ErrorResponse "failed to logout"
|
||||
|
||||
@@ -47,7 +47,7 @@ func (h *ListHandler) CreateList(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
list, err := h.ListService.CreateList(ctx, authContext.UserID, payload.Name, payload.UserIDs)
|
||||
list, err := h.ListService.CreateList(ctx, authContext.UserID, payload.Name)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to create list", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusInternalServerError, "failed to create list")
|
||||
@@ -243,7 +243,7 @@ func (h *ListHandler) RemoveUserFromList(w http.ResponseWriter, r *http.Request)
|
||||
// @Success 204 {object} nil
|
||||
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||
// @Error 500 {object} response.ErrorResponse "failed to create list item"
|
||||
// @Router /lists/item [post]
|
||||
// @Router /lists/items [post]
|
||||
func (h *ListHandler) CreateListItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -283,7 +283,7 @@ func (h *ListHandler) CreateListItem(w http.ResponseWriter, r *http.Request) {
|
||||
// @Success 204
|
||||
// @Error 400 {object} response.ErrorResponse "failed to decode request url"
|
||||
// @Error 500 {object} response.ErrorResponse "failed to delete list item"
|
||||
// @Router /lists/item/{id} [delete]
|
||||
// @Router /lists/items/{id} [delete]
|
||||
func (h *ListHandler) DeleteListItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -324,7 +324,7 @@ func (h *ListHandler) DeleteListItem(w http.ResponseWriter, r *http.Request) {
|
||||
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||
// @Error 401 {object} response.ErrorResponse "not authorized"
|
||||
// @Error 500 {object} response.ErrorResponse "failed to update list item"
|
||||
// @Router /lists/item [put]
|
||||
// @Router /lists/items [put]
|
||||
func (h *ListHandler) UpdateListItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
@@ -12,11 +13,11 @@ import (
|
||||
type UserHandler struct {
|
||||
UserService *domain.UserService
|
||||
AuthService *domain.AuthService
|
||||
InviteService *domain.InviteService
|
||||
RegistrationService *domain.RegistrationService
|
||||
}
|
||||
|
||||
func NewUserHandler(userService *domain.UserService, authService *domain.AuthService, inviteService *domain.InviteService) *UserHandler {
|
||||
return &UserHandler{UserService: userService, AuthService: authService, InviteService: inviteService}
|
||||
func NewUserHandler(userService *domain.UserService, authService *domain.AuthService, registrationService *domain.RegistrationService) *UserHandler {
|
||||
return &UserHandler{UserService: userService, AuthService: authService, RegistrationService: registrationService}
|
||||
}
|
||||
|
||||
// CreateUser handles the creation of a user
|
||||
@@ -29,6 +30,8 @@ func NewUserHandler(userService *domain.UserService, authService *domain.AuthSer
|
||||
// @Param payload body request.CreateUserPayload true "Create user payload"
|
||||
// @Success 201 {object} domain.User
|
||||
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||
// @Error 400 {object} response.ErrorResponse "invite is expired"
|
||||
// @Error 409 {object} response.ErrorResponse "invite is already consumed"
|
||||
// @Error 400 {object} response.ErrorResponse "invite is invalid"
|
||||
// @Error 500 {object} response.ErrorResponse "failed to create user"
|
||||
// @Router /users [post]
|
||||
@@ -42,41 +45,26 @@ func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
invite, err := h.InviteService.GetInvite(ctx, payload.InviteCode)
|
||||
user, err := h.RegistrationService.Register(ctx, payload.InviteCode, payload.Email, payload.Name, payload.Password)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to get invite", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusBadRequest, "invite is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Creating user and consuming the invite must be in a transaction.
|
||||
// The repositories must support tx in context, and the handler must be able to start a transaction
|
||||
user, err := h.UserService.CreateUser(ctx, payload.Email, payload.Name, payload.Password)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to create user", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusInternalServerError, "failed to create user")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.InviteService.ConsumeInvite(ctx, invite.ID, user.ID)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to consume invite", slog.Any("error", err))
|
||||
|
||||
// TODO: This should be a transaction rollback, once we have db transactions in the handler
|
||||
err = h.UserService.DeleteUser(ctx, user.ID)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to delete user again",
|
||||
slog.ErrorContext(ctx, "failed to register user",
|
||||
slog.Any("error", err),
|
||||
slog.String("user_id", user.ID),
|
||||
)
|
||||
slog.Any("payload", payload))
|
||||
|
||||
if errors.Is(err, domain.ErrInviteExpired) {
|
||||
response.Error(ctx, w, http.StatusBadRequest, "invite is expired")
|
||||
} else if errors.Is(err, domain.ErrInviteConsumed) {
|
||||
response.Error(ctx, w, http.StatusConflict, "invite is already consumed")
|
||||
} else if errors.Is(err, domain.ErrInviteInvalid) {
|
||||
response.Error(ctx, w, http.StatusBadRequest, "invite is invalid")
|
||||
} else {
|
||||
response.Error(ctx, w, http.StatusInternalServerError, "failed to register")
|
||||
}
|
||||
response.Error(ctx, w, http.StatusInternalServerError, "failed to consume invite")
|
||||
return
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "created user successfully",
|
||||
slog.String("user_id", user.ID),
|
||||
slog.String("invite_id", invite.ID),
|
||||
)
|
||||
response.JSON(ctx, w, http.StatusCreated, user)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package request
|
||||
|
||||
type CreateListPayload struct {
|
||||
Name string `json:"name"`
|
||||
UserIDs []string `json:"user_ids"`
|
||||
}
|
||||
|
||||
type AddUserToListPayload struct {
|
||||
|
||||
@@ -16,20 +16,17 @@ type Config struct {
|
||||
}
|
||||
|
||||
func NewMux(cfg Config) http.Handler {
|
||||
authRepo := repository.NewAuthRepo(cfg.Database)
|
||||
authService := domain.NewAuthService(authRepo, []byte(cfg.JWTSecret))
|
||||
store := repository.NewStore(cfg.Database)
|
||||
|
||||
authService := domain.NewAuthService(store.Auth, []byte(cfg.JWTSecret))
|
||||
inviteService := domain.NewInviteService(store.Invite)
|
||||
userService := domain.NewUserService(store.User)
|
||||
registrationService := domain.NewRegistrationService(store, userService, inviteService)
|
||||
listService := domain.NewListService(store.List)
|
||||
|
||||
authHandler := handler.NewAuthHandler(authService)
|
||||
|
||||
inviteRepo := repository.NewInviteRepo(cfg.Database)
|
||||
inviteService := domain.NewInviteService(inviteRepo)
|
||||
inviteHandler := handler.NewInviteHandler(inviteService)
|
||||
|
||||
userRepo := repository.NewUserRepo(cfg.Database)
|
||||
userService := domain.NewUserService(userRepo)
|
||||
userHandler := handler.NewUserHandler(userService, authService, inviteService)
|
||||
|
||||
listRepo := repository.NewListRepo(cfg.Database)
|
||||
listService := domain.NewListService(listRepo)
|
||||
userHandler := handler.NewUserHandler(userService, authService, registrationService)
|
||||
listHandler := handler.NewListHandler(listService, userService)
|
||||
|
||||
protected := middleware.WithJWT(authService)
|
||||
@@ -41,7 +38,7 @@ func NewMux(cfg Config) http.Handler {
|
||||
apiMux.HandleFunc("POST /login", authHandler.Login)
|
||||
apiMux.HandleFunc("POST /login/refresh", authHandler.Refresh)
|
||||
apiMux.HandleFunc("POST /logout", authHandler.Logout)
|
||||
apiMux.HandleFunc("POST /logout/all", authHandler.LogoutAllDevices)
|
||||
apiMux.HandleFunc("POST /logout/all", protected(authHandler.LogoutAllDevices))
|
||||
|
||||
// Users
|
||||
apiMux.HandleFunc("POST /users", userHandler.CreateUser)
|
||||
@@ -60,9 +57,9 @@ func NewMux(cfg Config) http.Handler {
|
||||
apiMux.Handle("GET /lists", protected(listHandler.GetLists))
|
||||
apiMux.Handle("POST /lists/user", protected(listHandler.AddUserToList))
|
||||
apiMux.Handle("DELETE /lists/user", protected(listHandler.RemoveUserFromList))
|
||||
apiMux.Handle("POST /lists/item", protected(listHandler.CreateListItem))
|
||||
apiMux.Handle("DELETE /lists/item/{id}", protected(listHandler.DeleteListItem))
|
||||
apiMux.Handle("PUT /lists/item", protected(listHandler.UpdateListItem))
|
||||
apiMux.Handle("POST /lists/items", protected(listHandler.CreateListItem))
|
||||
apiMux.Handle("DELETE /lists/items/{id}", protected(listHandler.DeleteListItem))
|
||||
apiMux.Handle("PUT /lists/items", protected(listHandler.UpdateListItem))
|
||||
apiMux.Handle("POST /lists/items/{id}", protected(listHandler.SetListItemCompleted))
|
||||
apiMux.Handle("GET /lists/{id}", protected(listHandler.GetListItems))
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmailNotFound = errors.New("email not found")
|
||||
ErrPasswordWrong = errors.New("password is wrong")
|
||||
)
|
||||
|
||||
type AuthRepository interface {
|
||||
GetUserById(ctx context.Context, id string) (*AuthUser, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (*AuthUser, error)
|
||||
@@ -71,7 +76,7 @@ func (s *AuthService) Authenticate(ctx context.Context, email string, password s
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
|
||||
if err != nil {
|
||||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
return user, errors.New("invalid email or password")
|
||||
return user, ErrPasswordWrong
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
var (
|
||||
ErrInviteIDMissing = errors.New("invite id is required")
|
||||
ErrCodeMissing = errors.New("invite code is required")
|
||||
ErrInviteInvalid = errors.New("invite is invalid")
|
||||
ErrInviteExpired = errors.New("invite is expired")
|
||||
ErrInviteConsumed = errors.New("invite is already consumed")
|
||||
)
|
||||
|
||||
+28
-24
@@ -4,16 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrListIDEmpty = errors.New("list id must not be empty")
|
||||
ErrListNameEmpty = errors.New("list name must not be empty")
|
||||
ErrUserIDEmpty = errors.New("user id must not be empty")
|
||||
ErrListItemIDEmpty = errors.New("list item id must not be empty")
|
||||
ErrListItemTitleEmpty = errors.New("list item title must not be empty")
|
||||
ErrListIDMissing = errors.New("list id is required")
|
||||
ErrListNameMissing = errors.New("list name is required")
|
||||
ErrListItemIDMissing = errors.New("list item id is required")
|
||||
ErrListItemTitleMissing = errors.New("list item title is required")
|
||||
ErrUserNotInList = errors.New("user not in list")
|
||||
)
|
||||
|
||||
@@ -36,7 +34,7 @@ type ListItem struct {
|
||||
}
|
||||
|
||||
type ListRepository interface {
|
||||
CreateList(ctx context.Context, name string, userIDs []string) (*List, error)
|
||||
CreateList(ctx context.Context, name string) (*List, error)
|
||||
DeleteList(ctx context.Context, listID string) error
|
||||
GetLists(ctx context.Context, userID string) ([]List, error)
|
||||
AddUserToList(ctx context.Context, listID string, userID string) error
|
||||
@@ -58,21 +56,27 @@ func NewListService(r ListRepository) *ListService {
|
||||
return &ListService{repo: r}
|
||||
}
|
||||
|
||||
func (s *ListService) CreateList(ctx context.Context, authUserID string, name string, userIDs []string) (*List, error) {
|
||||
func (s *ListService) CreateList(ctx context.Context, authUserID string, name string) (*List, error) {
|
||||
if name == "" {
|
||||
return nil, ErrListNameEmpty
|
||||
return nil, ErrListNameMissing
|
||||
}
|
||||
|
||||
if !slices.Contains(userIDs, authUserID) {
|
||||
userIDs = append(userIDs, authUserID)
|
||||
list, err := s.repo.CreateList(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.repo.CreateList(ctx, name, userIDs)
|
||||
err = s.repo.AddUserToList(ctx, list.ID, authUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *ListService) DeleteList(ctx context.Context, authUserID string, listID string) error {
|
||||
if listID == "" {
|
||||
return ErrListIDEmpty
|
||||
return ErrListIDMissing
|
||||
}
|
||||
|
||||
if err := s.userAllowedToAccessList(ctx, authUserID, listID); err != nil {
|
||||
@@ -88,10 +92,10 @@ func (s *ListService) GetLists(ctx context.Context, authUserID string) ([]List,
|
||||
|
||||
func (s *ListService) AddUserToList(ctx context.Context, authUserID string, listID string, userID string) error {
|
||||
if listID == "" {
|
||||
return ErrListIDEmpty
|
||||
return ErrListIDMissing
|
||||
}
|
||||
if userID == "" {
|
||||
return ErrUserIDEmpty
|
||||
return ErrUserIDMissing
|
||||
}
|
||||
|
||||
if err := s.userAllowedToAccessList(ctx, authUserID, listID); err != nil {
|
||||
@@ -103,10 +107,10 @@ func (s *ListService) AddUserToList(ctx context.Context, authUserID string, list
|
||||
|
||||
func (s *ListService) RemoveUserFromList(ctx context.Context, authUserID string, listID string, userID string) error {
|
||||
if listID == "" {
|
||||
return ErrListIDEmpty
|
||||
return ErrListIDMissing
|
||||
}
|
||||
if userID == "" {
|
||||
return ErrUserIDEmpty
|
||||
return ErrUserIDMissing
|
||||
}
|
||||
|
||||
if err := s.userAllowedToAccessList(ctx, authUserID, listID); err != nil {
|
||||
@@ -118,10 +122,10 @@ func (s *ListService) RemoveUserFromList(ctx context.Context, authUserID string,
|
||||
|
||||
func (s *ListService) CreateListItem(ctx context.Context, authUserID string, listID string, title string) (*ListItem, error) {
|
||||
if listID == "" {
|
||||
return nil, ErrListIDEmpty
|
||||
return nil, ErrListIDMissing
|
||||
}
|
||||
if title == "" {
|
||||
return nil, ErrListItemTitleEmpty
|
||||
return nil, ErrListItemTitleMissing
|
||||
}
|
||||
|
||||
if err := s.userAllowedToAccessList(ctx, authUserID, listID); err != nil {
|
||||
@@ -133,7 +137,7 @@ func (s *ListService) CreateListItem(ctx context.Context, authUserID string, lis
|
||||
|
||||
func (s *ListService) DeleteListItem(ctx context.Context, authUserID string, listItemID string) error {
|
||||
if listItemID == "" {
|
||||
return ErrListItemIDEmpty
|
||||
return ErrListItemIDMissing
|
||||
}
|
||||
|
||||
if err := s.userAllowedToAccessListItem(ctx, authUserID, listItemID); err != nil {
|
||||
@@ -145,10 +149,10 @@ func (s *ListService) DeleteListItem(ctx context.Context, authUserID string, lis
|
||||
|
||||
func (s *ListService) UpdateListItem(ctx context.Context, authUserID string, listItemID string, title string, isCompleted bool) error {
|
||||
if listItemID == "" {
|
||||
return ErrListItemIDEmpty
|
||||
return ErrListItemIDMissing
|
||||
}
|
||||
if title == "" {
|
||||
return ErrListItemTitleEmpty
|
||||
return ErrListItemTitleMissing
|
||||
}
|
||||
|
||||
if err := s.userAllowedToAccessListItem(ctx, authUserID, listItemID); err != nil {
|
||||
@@ -160,7 +164,7 @@ func (s *ListService) UpdateListItem(ctx context.Context, authUserID string, lis
|
||||
|
||||
func (s *ListService) SetListItemCompleted(ctx context.Context, authUserID string, listItemID string, isCompleted bool) error {
|
||||
if listItemID == "" {
|
||||
return ErrListItemIDEmpty
|
||||
return ErrListItemIDMissing
|
||||
}
|
||||
|
||||
if err := s.userAllowedToAccessListItem(ctx, authUserID, listItemID); err != nil {
|
||||
@@ -172,7 +176,7 @@ func (s *ListService) SetListItemCompleted(ctx context.Context, authUserID strin
|
||||
|
||||
func (s *ListService) GetListItems(ctx context.Context, authUserID string, listID string) ([]ListItem, error) {
|
||||
if listID == "" {
|
||||
return nil, ErrListIDEmpty
|
||||
return nil, ErrListIDMissing
|
||||
}
|
||||
|
||||
if err := s.userAllowedToAccessList(ctx, authUserID, listID); err != nil {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type RegistrationService struct {
|
||||
tx Transactor
|
||||
UserService *UserService
|
||||
InviteService *InviteService
|
||||
}
|
||||
|
||||
func NewRegistrationService(tx Transactor, u *UserService, i *InviteService) *RegistrationService {
|
||||
return &RegistrationService{tx: tx, UserService: u, InviteService: i}
|
||||
}
|
||||
|
||||
func (s *RegistrationService) Register(ctx context.Context, inviteCode string, email string, username string, password string) (*User, error) {
|
||||
invite, err := s.InviteService.GetInvite(ctx, inviteCode)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to get invite", slog.Any("error", err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user *User
|
||||
err = s.tx.WithinTx(ctx, func(ctx context.Context) error {
|
||||
user, err = s.UserService.CreateUser(ctx, email, username, password)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to create user", slog.Any("error", err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.InviteService.ConsumeInvite(ctx, invite.ID, user.ID)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to consume invite", slog.Any("error", err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "user registration complete",
|
||||
slog.String("user_id", user.ID),
|
||||
slog.String("invite_id", invite.ID))
|
||||
return user, nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package domain
|
||||
|
||||
import "context"
|
||||
|
||||
type Transactor interface {
|
||||
WithinTx(ctx context.Context, fn func(ctx context.Context) error) error
|
||||
}
|
||||
+10
-11
@@ -11,17 +11,13 @@ import (
|
||||
)
|
||||
|
||||
type AuthRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewAuthRepo(db *sql.DB) *AuthRepo {
|
||||
return &AuthRepo{db: db}
|
||||
Repo
|
||||
}
|
||||
|
||||
func (r *AuthRepo) GetUserById(ctx context.Context, id string) (*domain.AuthUser, error) {
|
||||
user := &domain.AuthUser{}
|
||||
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"SELECT id, email, name, password_hash FROM users WHERE id = $1",
|
||||
id,
|
||||
).Scan(&user.ID, &user.Email, &user.Name, &user.PasswordHash)
|
||||
@@ -35,11 +31,14 @@ func (r *AuthRepo) GetUserById(ctx context.Context, id string) (*domain.AuthUser
|
||||
func (r *AuthRepo) GetUserByEmail(ctx context.Context, email string) (*domain.AuthUser, error) {
|
||||
user := &domain.AuthUser{}
|
||||
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"SELECT id, email, name, password_hash FROM users WHERE email = $1",
|
||||
email,
|
||||
).Scan(&user.ID, &user.Email, &user.Name, &user.PasswordHash)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrEmailNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
@@ -47,7 +46,7 @@ func (r *AuthRepo) GetUserByEmail(ctx context.Context, email string) (*domain.Au
|
||||
}
|
||||
|
||||
func (r *AuthRepo) StoreRefreshToken(ctx context.Context, userID string, tokenHash string, expiresAt time.Time) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
_, err := r.conn(ctx).ExecContext(ctx,
|
||||
"INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)",
|
||||
userID, tokenHash, expiresAt,
|
||||
)
|
||||
@@ -60,7 +59,7 @@ func (r *AuthRepo) StoreRefreshToken(ctx context.Context, userID string, tokenHa
|
||||
|
||||
func (r *AuthRepo) ConsumeRefreshToken(ctx context.Context, tokenHash string) (string, error) {
|
||||
var userID string
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"DELETE FROM refresh_tokens WHERE token_hash = $1 AND expires_at > NOW() RETURNING user_id",
|
||||
tokenHash,
|
||||
).Scan(&userID)
|
||||
@@ -75,7 +74,7 @@ func (r *AuthRepo) ConsumeRefreshToken(ctx context.Context, tokenHash string) (s
|
||||
}
|
||||
|
||||
func (r *AuthRepo) RevokeRefreshToken(ctx context.Context, tokenHash string) error {
|
||||
_, err := r.db.ExecContext(ctx, "DELETE FROM refresh_tokens WHERE token_hash = $1", tokenHash)
|
||||
_, err := r.conn(ctx).ExecContext(ctx, "DELETE FROM refresh_tokens WHERE token_hash = $1", tokenHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to revoke refresh token: %w", err)
|
||||
}
|
||||
@@ -84,7 +83,7 @@ func (r *AuthRepo) RevokeRefreshToken(ctx context.Context, tokenHash string) err
|
||||
}
|
||||
|
||||
func (r *AuthRepo) RevokeRefreshTokens(ctx context.Context, userID string) error {
|
||||
_, err := r.db.ExecContext(ctx, "DELETE FROM refresh_tokens WHERE user_id = $1", userID)
|
||||
_, err := r.conn(ctx).ExecContext(ctx, "DELETE FROM refresh_tokens WHERE user_id = $1", userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to revoke refresh tokens: %w", err)
|
||||
}
|
||||
|
||||
@@ -11,16 +11,12 @@ import (
|
||||
)
|
||||
|
||||
type InviteRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewInviteRepo(db *sql.DB) *InviteRepo {
|
||||
return &InviteRepo{db: db}
|
||||
Repo
|
||||
}
|
||||
|
||||
func (r *InviteRepo) CreateInvite(ctx context.Context, inviterUserID string, code string, expiresAt time.Time) (*domain.Invite, error) {
|
||||
var id string
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"INSERT INTO invites (inviter_user_id, code, expires_at) VALUES ($1, $2, $3) RETURNING id",
|
||||
inviterUserID, code, expiresAt,
|
||||
).Scan(&id)
|
||||
@@ -37,19 +33,28 @@ func (r *InviteRepo) CreateInvite(ctx context.Context, inviterUserID string, cod
|
||||
}
|
||||
|
||||
func (r *InviteRepo) DeleteInvite(ctx context.Context, userID string, inviteID string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
res, err := r.conn(ctx).ExecContext(ctx,
|
||||
"DELETE FROM invites WHERE id = $1 AND inviter_user_id = $2 AND consumed_at IS NULL",
|
||||
inviteID, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete invite: %w", err)
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get rows affected: %w", err)
|
||||
}
|
||||
if affected < 1 {
|
||||
// It's more of an assumption,
|
||||
// but unless I encounter this being wrong, I'll keep it.
|
||||
return domain.ErrInviteConsumed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InviteRepo) ConsumeInvite(ctx context.Context, inviteID string, inviteeUserID string) error {
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
res, err := r.conn(ctx).ExecContext(ctx,
|
||||
"UPDATE invites SET invitee_user_id=$1, consumed_at=NOW() WHERE id=$2 AND expires_at > NOW() AND consumed_at IS NULL",
|
||||
inviteeUserID, inviteID,
|
||||
)
|
||||
@@ -61,7 +66,7 @@ func (r *InviteRepo) ConsumeInvite(ctx context.Context, inviteID string, invitee
|
||||
return fmt.Errorf("could not get rows affected: %w", err)
|
||||
}
|
||||
if affected < 1 {
|
||||
return fmt.Errorf("invite not found or expired")
|
||||
return domain.ErrInviteInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -70,11 +75,14 @@ func (r *InviteRepo) ConsumeInvite(ctx context.Context, inviteID string, invitee
|
||||
func (r *InviteRepo) GetInvite(ctx context.Context, code string) (*domain.Invite, error) {
|
||||
var invite domain.Invite
|
||||
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"SELECT id, code, expires_at, consumed_at FROM invites WHERE code=$1",
|
||||
code,
|
||||
).Scan(&invite.ID, &invite.Code, &invite.ExpiresAt, &invite.ConsumedAt)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrInviteInvalid
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get invite: %w", err)
|
||||
}
|
||||
|
||||
@@ -82,7 +90,7 @@ func (r *InviteRepo) GetInvite(ctx context.Context, code string) (*domain.Invite
|
||||
}
|
||||
|
||||
func (r *InviteRepo) GetInvites(ctx context.Context, userID string, offset int, count int) ([]domain.Invite, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
rows, err := r.conn(ctx).QueryContext(ctx,
|
||||
"SELECT id, code, expires_at, consumed_at FROM invites WHERE inviter_user_id=$1 ORDER BY created_at DESC OFFSET $2 LIMIT $3",
|
||||
userID, offset, count,
|
||||
)
|
||||
@@ -110,7 +118,7 @@ func (r *InviteRepo) GetInvites(ctx context.Context, userID string, offset int,
|
||||
|
||||
func (r *InviteRepo) CountInvites(ctx context.Context, userID string) (int, error) {
|
||||
var count int
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"SELECT COUNT(*) FROM invites WHERE inviter_user_id=$1",
|
||||
userID,
|
||||
).Scan(&count)
|
||||
|
||||
+15
-46
@@ -5,29 +5,18 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/robindittmar/dttmr-api/internal/domain"
|
||||
)
|
||||
|
||||
type ListRepo struct {
|
||||
db *sql.DB
|
||||
Repo
|
||||
}
|
||||
|
||||
func NewListRepo(db *sql.DB) *ListRepo {
|
||||
return &ListRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *ListRepo) CreateList(ctx context.Context, name string, userIDs []string) (*domain.List, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
func (r *ListRepo) CreateList(ctx context.Context, name string) (*domain.List, error) {
|
||||
list := &domain.List{Name: name}
|
||||
|
||||
err = tx.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"INSERT INTO lists (name) VALUES ($1) RETURNING id, created_at, modified_at",
|
||||
name,
|
||||
).Scan(&list.ID, &list.CreatedAt, &list.ModifiedAt)
|
||||
@@ -35,32 +24,11 @@ func (r *ListRepo) CreateList(ctx context.Context, name string, userIDs []string
|
||||
return nil, fmt.Errorf("failed to insert list: %w", err)
|
||||
}
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, "INSERT INTO list_users (list_id, user_id) VALUES ($1, $2)")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare user/list association statement: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err := stmt.Close()
|
||||
if err != nil {
|
||||
slog.Error("failed to close user/list association statement", slog.Any("error", err))
|
||||
}
|
||||
}()
|
||||
|
||||
for _, userID := range userIDs {
|
||||
if _, err = stmt.ExecContext(ctx, list.ID, userID); err != nil {
|
||||
return nil, fmt.Errorf("failed to insert user/list association: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *ListRepo) DeleteList(ctx context.Context, listID string) error {
|
||||
_, err := r.db.ExecContext(ctx, "DELETE FROM lists WHERE id = $1", listID)
|
||||
_, err := r.conn(ctx).ExecContext(ctx, "DELETE FROM lists WHERE id = $1", listID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete list: %w", err)
|
||||
}
|
||||
@@ -69,7 +37,7 @@ func (r *ListRepo) DeleteList(ctx context.Context, listID string) error {
|
||||
}
|
||||
|
||||
func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
rows, err := r.conn(ctx).QueryContext(ctx,
|
||||
"SELECT l.id, l.name, l.created_at, l.modified_at, (SELECT COUNT(*) FROM list_items WHERE list_id=l.id), (SELECT COUNT(*) FROM list_items WHERE list_id=l.id AND is_completed=true) FROM lists AS l INNER JOIN list_users ON l.id=list_users.list_id WHERE list_users.user_id = $1",
|
||||
userID,
|
||||
)
|
||||
@@ -96,7 +64,7 @@ func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List,
|
||||
}
|
||||
|
||||
func (r *ListRepo) AddUserToList(ctx context.Context, listID string, userID string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
_, err := r.conn(ctx).ExecContext(ctx,
|
||||
"INSERT INTO list_users (list_id, user_id) VALUES ($1, $2)",
|
||||
listID, userID,
|
||||
)
|
||||
@@ -108,7 +76,7 @@ func (r *ListRepo) AddUserToList(ctx context.Context, listID string, userID stri
|
||||
}
|
||||
|
||||
func (r *ListRepo) RemoveUserFromList(ctx context.Context, listID string, userID string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
_, err := r.conn(ctx).ExecContext(ctx,
|
||||
"DELETE FROM list_users WHERE list_id = $1 AND user_id = $2",
|
||||
listID, userID,
|
||||
)
|
||||
@@ -122,7 +90,7 @@ func (r *ListRepo) RemoveUserFromList(ctx context.Context, listID string, userID
|
||||
func (r *ListRepo) IsUserInList(ctx context.Context, listID string, userID string) (bool, error) {
|
||||
var cnt int
|
||||
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"SELECT COUNT(*) FROM list_users WHERE list_id = $1 AND user_id = $2",
|
||||
listID, userID,
|
||||
).Scan(&cnt)
|
||||
@@ -136,7 +104,7 @@ func (r *ListRepo) IsUserInList(ctx context.Context, listID string, userID strin
|
||||
func (r *ListRepo) IsUserInListByItemID(ctx context.Context, listItemID string, userID string) (bool, error) {
|
||||
var cnt int
|
||||
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"SELECT COUNT(*) FROM list_users WHERE list_id = (SELECT list_id FROM list_items WHERE id = $1) AND user_id = $2",
|
||||
listItemID, userID,
|
||||
).Scan(&cnt)
|
||||
@@ -150,7 +118,7 @@ func (r *ListRepo) IsUserInListByItemID(ctx context.Context, listItemID string,
|
||||
func (r *ListRepo) CreateListItem(ctx context.Context, listID string, title string) (*domain.ListItem, error) {
|
||||
l := &domain.ListItem{Title: title}
|
||||
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"INSERT INTO list_items (list_id, title) VALUES ($1, $2) RETURNING id, is_completed, created_at, modified_at",
|
||||
listID, title,
|
||||
).Scan(&l.ID, &l.IsCompleted, &l.CreatedAt, &l.ModifiedAt)
|
||||
@@ -158,11 +126,12 @@ func (r *ListRepo) CreateListItem(ctx context.Context, listID string, title stri
|
||||
return nil, fmt.Errorf("failed to insert list item: %w", err)
|
||||
}
|
||||
|
||||
l.ListID = listID
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (r *ListRepo) DeleteListItem(ctx context.Context, listItemID string) error {
|
||||
_, err := r.db.ExecContext(ctx, "DELETE FROM list_items WHERE id = $1", listItemID)
|
||||
_, err := r.conn(ctx).ExecContext(ctx, "DELETE FROM list_items WHERE id = $1", listItemID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete list item: %w", err)
|
||||
}
|
||||
@@ -171,7 +140,7 @@ func (r *ListRepo) DeleteListItem(ctx context.Context, listItemID string) error
|
||||
}
|
||||
|
||||
func (r *ListRepo) UpdateListItem(ctx context.Context, listItemID string, title string, isCompleted bool) error {
|
||||
_, err := r.db.ExecContext(ctx, "UPDATE list_items SET title = $1, is_completed = $2, modified_at = NOW() WHERE id = $3",
|
||||
_, err := r.conn(ctx).ExecContext(ctx, "UPDATE list_items SET title = $1, is_completed = $2, modified_at = NOW() WHERE id = $3",
|
||||
title, isCompleted, listItemID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -182,7 +151,7 @@ func (r *ListRepo) UpdateListItem(ctx context.Context, listItemID string, title
|
||||
}
|
||||
|
||||
func (r *ListRepo) SetListItemCompleted(ctx context.Context, listItemID string, isCompleted bool) error {
|
||||
_, err := r.db.ExecContext(ctx, "UPDATE list_items SET is_completed = $1, modified_at = NOW() WHERE id = $2",
|
||||
_, err := r.conn(ctx).ExecContext(ctx, "UPDATE list_items SET is_completed = $1, modified_at = NOW() WHERE id = $2",
|
||||
isCompleted, listItemID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -193,7 +162,7 @@ func (r *ListRepo) SetListItemCompleted(ctx context.Context, listItemID string,
|
||||
}
|
||||
|
||||
func (r *ListRepo) GetListItems(ctx context.Context, listID string) ([]domain.ListItem, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
rows, err := r.conn(ctx).QueryContext(ctx,
|
||||
"SELECT id, title, is_completed, created_at, modified_at FROM list_items WHERE list_id = $1 ORDER BY is_completed, modified_at DESC",
|
||||
listID,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Transaction interface {
|
||||
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
|
||||
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
|
||||
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
|
||||
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
|
||||
}
|
||||
|
||||
type Repo struct {
|
||||
*Transactor
|
||||
}
|
||||
|
||||
func NewRepo(t *Transactor) Repo {
|
||||
return Repo{t}
|
||||
}
|
||||
|
||||
func (r *Repo) conn(ctx context.Context) Transaction {
|
||||
if tx, ok := ctx.Value(txKey{}).(*sql.Tx); ok {
|
||||
return tx
|
||||
}
|
||||
return r.db
|
||||
}
|
||||
|
||||
type txKey struct{}
|
||||
|
||||
type Transactor struct {
|
||||
db *sql.DB
|
||||
sp atomic.Uint64
|
||||
}
|
||||
|
||||
func NewTransactor(db *sql.DB) *Transactor {
|
||||
return &Transactor{db: db, sp: atomic.Uint64{}}
|
||||
}
|
||||
|
||||
func (t *Transactor) WithinTx(ctx context.Context, fn func(context.Context) error) error {
|
||||
if tx, ok := ctx.Value(txKey{}).(*sql.Tx); ok {
|
||||
return t.withinSavepoint(ctx, tx, fn)
|
||||
}
|
||||
|
||||
tx, err := t.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err = fn(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (t *Transactor) withinSavepoint(ctx context.Context, tx *sql.Tx, fn func(context.Context) error) error {
|
||||
name := fmt.Sprintf("sp_%d", t.sp.Add(1))
|
||||
if _, err := tx.ExecContext(ctx, "SAVEPOINT "+name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fn(ctx); err != nil {
|
||||
_, _ = tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT"+name)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := tx.ExecContext(ctx, "RELEASE SAVEPOINT"+name)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package repository
|
||||
|
||||
import "database/sql"
|
||||
|
||||
type Store struct {
|
||||
*Transactor
|
||||
Auth *AuthRepo
|
||||
Invite *InviteRepo
|
||||
List *ListRepo
|
||||
User *UserRepo
|
||||
}
|
||||
|
||||
func NewStore(db *sql.DB) *Store {
|
||||
t := NewTransactor(db)
|
||||
r := NewRepo(t)
|
||||
return &Store{
|
||||
Transactor: t,
|
||||
Auth: &AuthRepo{r},
|
||||
Invite: &InviteRepo{r},
|
||||
List: &ListRepo{r},
|
||||
User: &UserRepo{r},
|
||||
}
|
||||
}
|
||||
@@ -2,30 +2,19 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/robindittmar/dttmr-api/internal/domain"
|
||||
)
|
||||
|
||||
type UserRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewUserRepo(db *sql.DB) *UserRepo {
|
||||
return &UserRepo{db: db}
|
||||
Repo
|
||||
}
|
||||
|
||||
func (r *UserRepo) CreateUser(ctx context.Context, email string, name string, passwordHash string) (*domain.User, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
user := &domain.User{Email: email, Name: name}
|
||||
|
||||
err = tx.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"INSERT INTO users (email, name, password_hash) VALUES ($1, $2, $3) RETURNING id, created_at",
|
||||
email, name, passwordHash,
|
||||
).Scan(&user.ID, &user.CreatedAt)
|
||||
@@ -33,15 +22,11 @@ func (r *UserRepo) CreateUser(ctx context.Context, email string, name string, pa
|
||||
return nil, fmt.Errorf("failed to insert user: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepo) DeleteUser(ctx context.Context, userID string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
_, err := r.conn(ctx).ExecContext(ctx,
|
||||
"DELETE FROM users WHERE id = $1",
|
||||
userID,
|
||||
)
|
||||
@@ -53,7 +38,7 @@ func (r *UserRepo) DeleteUser(ctx context.Context, userID string) error {
|
||||
}
|
||||
|
||||
func (r *UserRepo) ChangePassword(ctx context.Context, userID string, passwordHash string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
_, err := r.conn(ctx).ExecContext(ctx,
|
||||
"UPDATE users SET password_hash = $1 WHERE id = $2",
|
||||
passwordHash, userID,
|
||||
)
|
||||
@@ -67,7 +52,7 @@ func (r *UserRepo) ChangePassword(ctx context.Context, userID string, passwordHa
|
||||
func (r *UserRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
user := &domain.User{}
|
||||
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
err := r.conn(ctx).QueryRowContext(ctx,
|
||||
"SELECT id, email, name FROM users WHERE email = $1",
|
||||
email,
|
||||
).Scan(&user.ID, &user.Email, &user.Name)
|
||||
|
||||
Reference in New Issue
Block a user