fix: /logout routes; improved error handling for invites

This commit is contained in:
2026-09-01 15:28:18 +02:00
parent 4b703b077c
commit b35dc4841f
6 changed files with 35 additions and 4 deletions
+1
View File
@@ -89,6 +89,7 @@ func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
// @Tags Authorization // @Tags Authorization
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param payload body request.LogoutPayload true "Logout payload"
// @Success 200 {object} nil // @Success 200 {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 logout" // @Error 500 {object} response.ErrorResponse "failed to logout"
+13 -1
View File
@@ -1,6 +1,7 @@
package handler package handler
import ( import (
"errors"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -29,6 +30,8 @@ func NewUserHandler(userService *domain.UserService, authService *domain.AuthSer
// @Param payload body request.CreateUserPayload true "Create user payload" // @Param payload body request.CreateUserPayload true "Create user payload"
// @Success 201 {object} domain.User // @Success 201 {object} domain.User
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @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 400 {object} response.ErrorResponse "invite is invalid"
// @Error 500 {object} response.ErrorResponse "failed to create user" // @Error 500 {object} response.ErrorResponse "failed to create user"
// @Router /users [post] // @Router /users [post]
@@ -47,7 +50,16 @@ func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
slog.ErrorContext(ctx, "failed to register user", slog.ErrorContext(ctx, "failed to register user",
slog.Any("error", err), slog.Any("error", err),
slog.Any("payload", payload)) slog.Any("payload", payload))
response.Error(ctx, w, http.StatusInternalServerError, "failed to register")
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")
}
return return
} }
+1 -1
View File
@@ -38,7 +38,7 @@ func NewMux(cfg Config) http.Handler {
apiMux.HandleFunc("POST /login", authHandler.Login) apiMux.HandleFunc("POST /login", authHandler.Login)
apiMux.HandleFunc("POST /login/refresh", authHandler.Refresh) apiMux.HandleFunc("POST /login/refresh", authHandler.Refresh)
apiMux.HandleFunc("POST /logout", authHandler.Logout) apiMux.HandleFunc("POST /logout", authHandler.Logout)
apiMux.HandleFunc("POST /logout/all", authHandler.LogoutAllDevices) apiMux.HandleFunc("POST /logout/all", protected(authHandler.LogoutAllDevices))
// Users // Users
apiMux.HandleFunc("POST /users", userHandler.CreateUser) apiMux.HandleFunc("POST /users", userHandler.CreateUser)
+1
View File
@@ -9,6 +9,7 @@ import (
var ( var (
ErrInviteIDMissing = errors.New("invite id is required") ErrInviteIDMissing = errors.New("invite id is required")
ErrCodeMissing = errors.New("invite code is required") ErrCodeMissing = errors.New("invite code is required")
ErrInviteInvalid = errors.New("invite is invalid")
ErrInviteExpired = errors.New("invite is expired") ErrInviteExpired = errors.New("invite is expired")
ErrInviteConsumed = errors.New("invite is already consumed") ErrInviteConsumed = errors.New("invite is already consumed")
) )
+8
View File
@@ -3,6 +3,7 @@ package domain
import ( import (
"context" "context"
"log/slog" "log/slog"
"time"
) )
type RegistrationService struct { type RegistrationService struct {
@@ -22,6 +23,13 @@ func (s *RegistrationService) Register(ctx context.Context, inviteCode string, e
return nil, err return nil, err
} }
if invite.ConsumedAt != nil {
return nil, ErrInviteConsumed
}
if invite.ExpiresAt.Before(time.Now()) {
return nil, ErrInviteExpired
}
var user *User var user *User
err = s.tx.WithinTx(ctx, func(ctx context.Context) error { err = s.tx.WithinTx(ctx, func(ctx context.Context) error {
user, err = s.UserService.CreateUser(ctx, email, username, password) user, err = s.UserService.CreateUser(ctx, email, username, password)
+11 -2
View File
@@ -33,13 +33,22 @@ func (r *InviteRepo) CreateInvite(ctx context.Context, inviterUserID string, cod
} }
func (r *InviteRepo) DeleteInvite(ctx context.Context, userID string, inviteID string) error { func (r *InviteRepo) DeleteInvite(ctx context.Context, userID string, inviteID string) error {
_, err := r.conn(ctx).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", "DELETE FROM invites WHERE id = $1 AND inviter_user_id = $2 AND consumed_at IS NULL",
inviteID, userID, inviteID, userID,
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to delete invite: %w", err) 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 return nil
} }
@@ -57,7 +66,7 @@ func (r *InviteRepo) ConsumeInvite(ctx context.Context, inviteID string, invitee
return fmt.Errorf("could not get rows affected: %w", err) return fmt.Errorf("could not get rows affected: %w", err)
} }
if affected < 1 { if affected < 1 {
return fmt.Errorf("invite not found or expired") return domain.ErrInviteInvalid
} }
return nil return nil