Merge pull request 'Delete lists and add users via email' (#22) from dev into main

This commit was merged in pull request #22.
This commit is contained in:
2026-08-21 21:51:52 +02:00
9 changed files with 187 additions and 26 deletions
+104 -6
View File
@@ -11,6 +11,7 @@ import (
type ListHandler struct { type ListHandler struct {
ListService *domain.ListService ListService *domain.ListService
UserService *domain.UserService
} }
func NewListHandler(listService *domain.ListService) *ListHandler { func NewListHandler(listService *domain.ListService) *ListHandler {
@@ -46,7 +47,7 @@ func (h *ListHandler) CreateList(w http.ResponseWriter, r *http.Request) {
return return
} }
list, err := h.ListService.Create(ctx, authContext.UserID, payload.Name, payload.UserIDs) list, err := h.ListService.CreateList(ctx, authContext.UserID, payload.Name, payload.UserIDs)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "failed to create list", slog.Any("error", err)) slog.ErrorContext(ctx, "failed to create list", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to create list") response.Error(ctx, w, http.StatusInternalServerError, "failed to create list")
@@ -57,6 +58,46 @@ func (h *ListHandler) CreateList(w http.ResponseWriter, r *http.Request) {
response.JSON(ctx, w, http.StatusCreated, list) response.JSON(ctx, w, http.StatusCreated, list)
} }
// DeleteList handles the deletion of a list
//
// @Summary Delete list route
// @Description Deletes a list, cascading to user associations and items
// @Tags List
// @Accept json
// @Produce json
// @Param id path int true "List ID"
// @Success 204
// @Error 400 {object} response.ErrorResponse "failed to decode request url"
// @Error 500 {object} response.ErrorResponse "failed to delete list"
// @Router /lists/{id} [delete]
func (h *ListHandler) DeleteList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
listID := r.PathValue("id")
if listID == "" {
slog.ErrorContext(ctx, "failed to read list id from path")
response.Error(ctx, w, http.StatusBadRequest, "failed to decode request url")
return
}
authContext, err := domain.GetAuthContext(ctx)
if err != nil {
slog.ErrorContext(ctx, "failed to get auth context", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to delete list")
return
}
err = h.ListService.DeleteList(ctx, authContext.UserID, listID)
if err != nil {
slog.ErrorContext(ctx, "failed to delete list", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to delete list")
return
}
slog.InfoContext(ctx, "deleted list successfully", slog.Any("list_id", listID))
response.Status(ctx, w, http.StatusNoContent)
}
// GetLists handles fetching lists for the current user // GetLists handles fetching lists for the current user
// //
// @Summary Returns all lists of the user // @Summary Returns all lists of the user
@@ -81,7 +122,7 @@ func (h *ListHandler) GetLists(w http.ResponseWriter, r *http.Request) {
lists, err := h.ListService.GetLists(ctx, authContext.UserID) lists, err := h.ListService.GetLists(ctx, authContext.UserID)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "failed to set list item completed", slog.Any("error", err)) slog.ErrorContext(ctx, "failed to get lists", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to read list items") response.Error(ctx, w, http.StatusInternalServerError, "failed to read list items")
return return
} }
@@ -99,6 +140,7 @@ func (h *ListHandler) GetLists(w http.ResponseWriter, r *http.Request) {
// @Param payload body request.AddUserToListPayload true "Add user to list payload" // @Param payload body request.AddUserToListPayload true "Add user to list payload"
// @Success 204 {object} nil // @Success 204 {object} nil
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 500 {object} response.ErrorResponse "failed to find email in system"
// @Error 500 {object} response.ErrorResponse "failed to add user to list" // @Error 500 {object} response.ErrorResponse "failed to add user to list"
// @Router /lists/user [post] // @Router /lists/user [post]
func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) { func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) {
@@ -118,14 +160,21 @@ func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) {
return return
} }
err = h.ListService.AddUserToList(ctx, authContext.UserID, payload.ListID, payload.UserID) user, err := h.UserService.GetUserByEmail(ctx, payload.Email)
if err != nil {
slog.ErrorContext(ctx, "failed to get user by email", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to find email in system")
return
}
err = h.ListService.AddUserToList(ctx, authContext.UserID, payload.ListID, user.ID)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "failed to add user to list", slog.Any("error", err)) slog.ErrorContext(ctx, "failed to add user to list", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to add user to list") response.Error(ctx, w, http.StatusInternalServerError, "failed to add user to list")
return return
} }
slog.InfoContext(ctx, "added user to list successfully", slog.Any("list_id", payload.ListID), slog.Any("user_id", payload.UserID)) slog.InfoContext(ctx, "added user to list successfully", slog.Any("list_id", payload.ListID), slog.Any("email", user.Email))
response.Status(ctx, w, http.StatusNoContent) response.Status(ctx, w, http.StatusNoContent)
} }
@@ -139,6 +188,7 @@ func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) {
// @Param payload body request.RemoveUserFromListPayload true "Remove user from list payload" // @Param payload body request.RemoveUserFromListPayload true "Remove user from list payload"
// @Success 204 {object} nil // @Success 204 {object} nil
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 500 {object} response.ErrorResponse "failed to find email in system"
// @Error 500 {object} response.ErrorResponse "failed to remove user from list" // @Error 500 {object} response.ErrorResponse "failed to remove user from list"
// @Router /lists/user [delete] // @Router /lists/user [delete]
func (h *ListHandler) RemoveUserFromList(w http.ResponseWriter, r *http.Request) { func (h *ListHandler) RemoveUserFromList(w http.ResponseWriter, r *http.Request) {
@@ -158,14 +208,21 @@ func (h *ListHandler) RemoveUserFromList(w http.ResponseWriter, r *http.Request)
return return
} }
err = h.ListService.RemoveUserFromList(ctx, authContext.UserID, payload.ListID, payload.UserID) user, err := h.UserService.GetUserByEmail(ctx, payload.Email)
if err != nil {
slog.ErrorContext(ctx, "failed to get user by email", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to find email in system")
return
}
err = h.ListService.RemoveUserFromList(ctx, authContext.UserID, payload.ListID, user.ID)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "failed to remove user from list", slog.Any("error", err)) slog.ErrorContext(ctx, "failed to remove user from list", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to remove user from list") response.Error(ctx, w, http.StatusInternalServerError, "failed to remove user from list")
return return
} }
slog.InfoContext(ctx, "removed user from list successfully", slog.Any("list_id", payload.ListID), slog.Any("user_id", payload.UserID)) slog.InfoContext(ctx, "removed user from list successfully", slog.Any("list_id", payload.ListID), slog.Any("email", user.Email))
response.Status(ctx, w, http.StatusNoContent) response.Status(ctx, w, http.StatusNoContent)
} }
@@ -209,6 +266,46 @@ func (h *ListHandler) CreateListItem(w http.ResponseWriter, r *http.Request) {
response.JSON(ctx, w, http.StatusCreated, item) response.JSON(ctx, w, http.StatusCreated, item)
} }
// DeleteListItem handles the deletion of a list item
//
// @Summary Delete list item route
// @Description Deletes an item
// @Tags List
// @Accept json
// @Produce json
// @Param id path int true "List Item ID"
// @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]
func (h *ListHandler) DeleteListItem(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
listItemID := r.PathValue("id")
if listItemID == "" {
slog.ErrorContext(ctx, "failed to read list item id from path")
response.Error(ctx, w, http.StatusBadRequest, "failed to decode request url")
return
}
authContext, err := domain.GetAuthContext(ctx)
if err != nil {
slog.ErrorContext(ctx, "failed to get auth context", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to delete list item")
return
}
err = h.ListService.DeleteListItem(ctx, authContext.UserID, listItemID)
if err != nil {
slog.ErrorContext(ctx, "failed to delete list item", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to delete list item")
return
}
slog.InfoContext(ctx, "deleted list item successfully", slog.Any("list_item_id", listItemID))
response.Status(ctx, w, http.StatusNoContent)
}
// UpdateListItem handles updating of a list item // UpdateListItem handles updating of a list item
// //
// @Summary Update list item // @Summary Update list item
@@ -257,6 +354,7 @@ func (h *ListHandler) UpdateListItem(w http.ResponseWriter, r *http.Request) {
// @Tags List // @Tags List
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path int true "List Item ID"
// @Param payload body request.SetListItemCompletedPayload true "Update list item is completed payload" // @Param payload body request.SetListItemCompletedPayload true "Update list item is completed payload"
// @Success 204 {object} nil // @Success 204 {object} nil
// @Error 400 {object} response.ErrorResponse "failed to decode request url" // @Error 400 {object} response.ErrorResponse "failed to decode request url"
+2 -2
View File
@@ -7,12 +7,12 @@ type CreateListPayload struct {
type AddUserToListPayload struct { type AddUserToListPayload struct {
ListID string `json:"list_id"` ListID string `json:"list_id"`
UserID string `json:"user_id"` Email string `json:"email"`
} }
type RemoveUserFromListPayload struct { type RemoveUserFromListPayload struct {
ListID string `json:"list_id"` ListID string `json:"list_id"`
UserID string `json:"user_id"` Email string `json:"email"`
} }
type CreateListItemPayload struct { type CreateListItemPayload struct {
+2
View File
@@ -42,10 +42,12 @@ func NewMux(cfg Config) http.Handler {
apiMux.Handle("POST /users", protected(userHandler.CreateUser)) apiMux.Handle("POST /users", protected(userHandler.CreateUser))
apiMux.Handle("POST /lists", protected(listHandler.CreateList)) apiMux.Handle("POST /lists", protected(listHandler.CreateList))
apiMux.Handle("DELETE /lists/{id}", protected(listHandler.DeleteList))
apiMux.Handle("GET /lists", protected(listHandler.GetLists)) apiMux.Handle("GET /lists", protected(listHandler.GetLists))
apiMux.Handle("POST /lists/user", protected(listHandler.AddUserToList)) apiMux.Handle("POST /lists/user", protected(listHandler.AddUserToList))
apiMux.Handle("DELETE /lists/user", protected(listHandler.RemoveUserFromList)) apiMux.Handle("DELETE /lists/user", protected(listHandler.RemoveUserFromList))
apiMux.Handle("POST /lists/item", protected(listHandler.CreateListItem)) 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("PUT /lists/item", protected(listHandler.UpdateListItem))
apiMux.Handle("POST /lists/items/{id}", protected(listHandler.SetListItemCompleted)) apiMux.Handle("POST /lists/items/{id}", protected(listHandler.SetListItemCompleted))
apiMux.Handle("GET /lists/{id}", protected(listHandler.GetListItems)) apiMux.Handle("GET /lists/{id}", protected(listHandler.GetListItems))
@@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS lists (
CREATE TABLE IF NOT EXISTS list_users ( CREATE TABLE IF NOT EXISTS list_users (
list_id UUID REFERENCES lists(id) ON DELETE CASCADE, list_id UUID REFERENCES lists(id) ON DELETE CASCADE,
user_id UUID NOT NULL, user_id UUID REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (list_id, user_id) PRIMARY KEY (list_id, user_id)
); );
+30 -6
View File
@@ -22,6 +22,8 @@ type List struct {
Name string `json:"name"` Name string `json:"name"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
ModifiedAt time.Time `json:"modified_at"` ModifiedAt time.Time `json:"modified_at"`
TotalItems int `json:"total_items"`
CompletedItems int `json:"completed_items"`
} }
type ListItem struct { type ListItem struct {
@@ -35,12 +37,14 @@ type ListItem struct {
type ListRepository interface { type ListRepository interface {
CreateList(ctx context.Context, name string, userIDs []string) (*List, error) CreateList(ctx context.Context, name string, userIDs []string) (*List, error)
DeleteList(ctx context.Context, listID string) error
GetLists(ctx context.Context, userID string) ([]List, error) GetLists(ctx context.Context, userID string) ([]List, error)
AddUserToList(ctx context.Context, listID string, userID string) error AddUserToList(ctx context.Context, listID string, userID string) error
RemoveUserFromList(ctx context.Context, listID string, userID string) error RemoveUserFromList(ctx context.Context, listID string, userID string) error
IsUserInList(ctx context.Context, listID string, userID string) (bool, error) IsUserInList(ctx context.Context, listID string, userID string) (bool, error)
IsUserInListByItemID(ctx context.Context, listItemID string, userID string) (bool, error) IsUserInListByItemID(ctx context.Context, listItemID string, userID string) (bool, error)
CreateListItem(ctx context.Context, listID string, title string) (*ListItem, error) CreateListItem(ctx context.Context, listID string, title string) (*ListItem, error)
DeleteListItem(ctx context.Context, listItemID string) error
UpdateListItem(ctx context.Context, listItemID string, title string, isCompleted bool) error UpdateListItem(ctx context.Context, listItemID string, title string, isCompleted bool) error
SetListItemCompleted(ctx context.Context, listItemID string, isCompleted bool) error SetListItemCompleted(ctx context.Context, listItemID string, isCompleted bool) error
GetListItems(ctx context.Context, listID string) ([]ListItem, error) GetListItems(ctx context.Context, listID string) ([]ListItem, error)
@@ -54,7 +58,7 @@ func NewListService(r ListRepository) *ListService {
return &ListService{repo: r} return &ListService{repo: r}
} }
func (s *ListService) Create(ctx context.Context, authUserID string, name string, userIDs []string) (*List, error) { func (s *ListService) CreateList(ctx context.Context, authUserID string, name string, userIDs []string) (*List, error) {
if name == "" { if name == "" {
return nil, ErrListNameEmpty return nil, ErrListNameEmpty
} }
@@ -66,6 +70,18 @@ func (s *ListService) Create(ctx context.Context, authUserID string, name string
return s.repo.CreateList(ctx, name, userIDs) return s.repo.CreateList(ctx, name, userIDs)
} }
func (s *ListService) DeleteList(ctx context.Context, authUserID string, listID string) error {
if listID == "" {
return ErrListIDEmpty
}
if err := s.userAllowedToAccessList(ctx, authUserID, listID); err != nil {
return err
}
return s.repo.DeleteList(ctx, listID)
}
func (s *ListService) GetLists(ctx context.Context, authUserID string) ([]List, error) { func (s *ListService) GetLists(ctx context.Context, authUserID string) ([]List, error) {
return s.repo.GetLists(ctx, authUserID) return s.repo.GetLists(ctx, authUserID)
} }
@@ -78,13 +94,9 @@ func (s *ListService) AddUserToList(ctx context.Context, authUserID string, list
return ErrUserIDEmpty return ErrUserIDEmpty
} }
inList, err := s.repo.IsUserInList(ctx, listID, authUserID) if err := s.userAllowedToAccessList(ctx, authUserID, listID); err != nil {
if err != nil {
return err return err
} }
if !inList {
return ErrUserNotInList
}
return s.repo.AddUserToList(ctx, listID, userID) return s.repo.AddUserToList(ctx, listID, userID)
} }
@@ -119,6 +131,18 @@ func (s *ListService) CreateListItem(ctx context.Context, authUserID string, lis
return s.repo.CreateListItem(ctx, listID, title) return s.repo.CreateListItem(ctx, listID, title)
} }
func (s *ListService) DeleteListItem(ctx context.Context, authUserID string, listItemID string) error {
if listItemID == "" {
return ErrListItemIDEmpty
}
if err := s.userAllowedToAccessListItem(ctx, authUserID, listItemID); err != nil {
return err
}
return s.repo.DeleteListItem(ctx, listItemID)
}
func (s *ListService) UpdateListItem(ctx context.Context, authUserID string, listItemID string, title string, isCompleted bool) error { func (s *ListService) UpdateListItem(ctx context.Context, authUserID string, listItemID string, title string, isCompleted bool) error {
if listItemID == "" { if listItemID == "" {
return ErrListItemIDEmpty return ErrListItemIDEmpty
+4 -4
View File
@@ -37,7 +37,7 @@ func TestListService_Create_Success(t *testing.T) {
repo.On("CreateList", mock.Anything, "My List", []string{"user1", "user2"}).Return(expectedList, nil) repo.On("CreateList", mock.Anything, "My List", []string{"user1", "user2"}).Return(expectedList, nil)
service := domain.NewListService(repo) service := domain.NewListService(repo)
list, err := service.Create(context.Background(), "My List", []string{"user1", "user2"}) list, err := service.CreateList(context.Background(), "My List", []string{"user1", "user2"})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedList, list) assert.Equal(t, expectedList, list)
@@ -48,7 +48,7 @@ func TestListService_Create_EmptyName(t *testing.T) {
repo := new(mockListRepo) repo := new(mockListRepo)
service := domain.NewListService(repo) service := domain.NewListService(repo)
list, err := service.Create(context.Background(), "", []string{"user1"}) list, err := service.CreateList(context.Background(), "", []string{"user1"})
require.Error(t, err) require.Error(t, err)
assert.EqualError(t, err, "list name must not be empty") assert.EqualError(t, err, "list name must not be empty")
@@ -60,7 +60,7 @@ func TestListService_Create_EmptyUsers(t *testing.T) {
repo := new(mockListRepo) repo := new(mockListRepo)
service := domain.NewListService(repo) service := domain.NewListService(repo)
list, err := service.Create(context.Background(), "My List", []string{}) list, err := service.CreateList(context.Background(), "My List", []string{})
require.Error(t, err) require.Error(t, err)
assert.EqualError(t, err, "users must have at least one associated user") assert.EqualError(t, err, "users must have at least one associated user")
@@ -75,7 +75,7 @@ func TestListService_Create_RepoError(t *testing.T) {
service := domain.NewListService(repo) service := domain.NewListService(repo)
list, err := service.Create(context.Background(), "My List", []string{"user1"}) list, err := service.CreateList(context.Background(), "My List", []string{"user1"})
require.Error(t, err) require.Error(t, err)
assert.ErrorIs(t, err, expectedErr) assert.ErrorIs(t, err, expectedErr)
+5
View File
@@ -17,6 +17,7 @@ type User struct {
type UserRepository interface { type UserRepository interface {
CreateUser(ctx context.Context, email string, name string, passwordHash string) (*User, error) CreateUser(ctx context.Context, email string, name string, passwordHash string) (*User, error)
GetUserByEmail(ctx context.Context, email string) (*User, error)
} }
type UserService struct { type UserService struct {
@@ -45,3 +46,7 @@ func (s *UserService) CreateUser(ctx context.Context, email string, name string,
return s.repo.CreateUser(ctx, email, name, string(hash)) return s.repo.CreateUser(ctx, email, name, string(hash))
} }
func (s *UserService) GetUserByEmail(ctx context.Context, email string) (*User, error) {
return s.repo.GetUserByEmail(ctx, email)
}
+21 -3
View File
@@ -59,9 +59,18 @@ func (r *ListRepo) CreateList(ctx context.Context, name string, userIDs []string
return list, nil 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)
if err != nil {
return fmt.Errorf("failed to delete list: %w", err)
}
return nil
}
func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List, error) { func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List, error) {
rows, err := r.db.QueryContext(ctx, rows, err := r.db.QueryContext(ctx,
"SELECT id, name, lists.created_at, modified_at FROM lists INNER JOIN list_users ON lists.id=list_users.list_id WHERE list_users.user_id = $1", "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, userID,
) )
if err != nil { if err != nil {
@@ -75,7 +84,7 @@ func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List,
var lists []domain.List var lists []domain.List
for rows.Next() { for rows.Next() {
var l domain.List var l domain.List
err = rows.Scan(&l.ID, &l.Name, &l.CreatedAt, &l.ModifiedAt) err = rows.Scan(&l.ID, &l.Name, &l.CreatedAt, &l.ModifiedAt, &l.TotalItems, &l.CompletedItems)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -153,6 +162,15 @@ func (r *ListRepo) CreateListItem(ctx context.Context, listID string, title stri
return l, nil 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)
if err != nil {
return fmt.Errorf("failed to delete list item: %w", err)
}
return nil
}
func (r *ListRepo) UpdateListItem(ctx context.Context, listItemID string, title string, isCompleted bool) 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.db.ExecContext(ctx, "UPDATE list_items SET title = $1, is_completed = $2, modified_at = NOW() WHERE id = $3",
title, isCompleted, listItemID, title, isCompleted, listItemID,
@@ -177,7 +195,7 @@ func (r *ListRepo) SetListItemCompleted(ctx context.Context, listItemID string,
func (r *ListRepo) GetListItems(ctx context.Context, listID string) ([]domain.ListItem, error) { func (r *ListRepo) GetListItems(ctx context.Context, listID string) ([]domain.ListItem, error) {
rows, err := r.db.QueryContext(ctx, rows, err := r.db.QueryContext(ctx,
"SELECT id, title, is_completed, created_at, modified_at FROM list_items WHERE list_id = $1", "SELECT id, title, is_completed, created_at, modified_at FROM list_items WHERE list_id = $1 ORDER BY is_completed, modified_at DESC",
listID, listID,
) )
if err != nil { if err != nil {
+14
View File
@@ -39,3 +39,17 @@ func (r *UserRepo) CreateUser(ctx context.Context, email string, name string, pa
return user, nil return user, nil
} }
func (r *UserRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
user := &domain.User{}
err := r.db.QueryRowContext(ctx,
"SELECT id, email, name FROM users WHERE email = $1",
email,
).Scan(&user.ID, &user.Email, &user.Name)
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
return user, nil
}