Added GET /lists, POST /list/items/{id} and GET /list/{id} #16
@@ -60,7 +60,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
// @Success 200 {object} domain.TokenPair
|
||||
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||
// @Error 500 {object} response.ErrorResponse "failed to refresh token"
|
||||
// @Router /api/v1/refresh [post]
|
||||
// @Router /api/v1/login/refresh [post]
|
||||
func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -81,3 +81,65 @@ func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
response.JSON(ctx, w, http.StatusOK, tokens)
|
||||
}
|
||||
|
||||
// Logout handles logging out a user
|
||||
//
|
||||
// @Summary Logout route
|
||||
// @Description Logout current user
|
||||
// @Tags Authorization
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} nil
|
||||
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||
// @Error 500 {object} response.ErrorResponse "failed to logout"
|
||||
// @Router /api/v1/logout [post]
|
||||
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
payload, err := request.DecodeJSON[request.LogoutPayload](r)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to decode logout payload", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusBadRequest, "failed to decode request body")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.AuthService.Logout(ctx, payload.RefreshToken)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to logout", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusInternalServerError, "failed to logout")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(ctx, w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
// LogoutAllDevices handles logging out a user on all devices
|
||||
//
|
||||
// @Summary Logout all route
|
||||
// @Description Logout user from all devices (revokes all refresh tokens)
|
||||
// @Tags Authorization
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} nil
|
||||
// @Error 401 {object} response.ErrorResponse "failed to get auth context"
|
||||
// @Error 500 {object} response.ErrorResponse "failed to logout"
|
||||
// @Router /api/v1/logout/all [post]
|
||||
func (h *AuthHandler) LogoutAllDevices(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
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.StatusUnauthorized, "failed to get auth context")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.AuthService.LogoutAllDevices(ctx, authContext.UserID)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to logout", slog.Any("error", err))
|
||||
response.Error(ctx, w, http.StatusInternalServerError, "failed to logout")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(ctx, w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
@@ -8,3 +8,7 @@ type LoginPayload struct {
|
||||
type RefreshPayload struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type LogoutPayload struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
@@ -33,8 +33,11 @@ func NewMux(cfg Config) http.Handler {
|
||||
apiMux := http.NewServeMux()
|
||||
apiMux.HandleFunc("/", handler.DefaultHandler)
|
||||
apiMux.HandleFunc("GET /health", handler.HealthHandler)
|
||||
|
||||
apiMux.HandleFunc("POST /login", authHandler.Login)
|
||||
apiMux.HandleFunc("POST /auth/refresh", authHandler.Refresh)
|
||||
apiMux.HandleFunc("POST /login/refresh", authHandler.Refresh)
|
||||
apiMux.HandleFunc("POST /logout", authHandler.Logout)
|
||||
apiMux.HandleFunc("POST /logout/all", authHandler.LogoutAllDevices)
|
||||
|
||||
apiMux.Handle("POST /users", protected(userHandler.CreateUser))
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ type AuthRepository interface {
|
||||
GetUserByEmail(ctx context.Context, email string) (*AuthUser, error)
|
||||
StoreRefreshToken(ctx context.Context, userID string, tokenHash string, expiresAt time.Time) error
|
||||
ConsumeRefreshToken(ctx context.Context, tokenHash string) (string, error)
|
||||
RevokeRefreshToken(ctx context.Context, tokenHash string) error
|
||||
RevokeRefreshTokens(ctx context.Context, userID string) error
|
||||
}
|
||||
|
||||
type AuthService struct {
|
||||
@@ -102,6 +104,16 @@ func (s *AuthService) Refresh(ctx context.Context, refreshToken string) (TokenPa
|
||||
return s.issueTokens(ctx, authUser)
|
||||
}
|
||||
|
||||
func (s *AuthService) Logout(ctx context.Context, refreshToken string) error {
|
||||
tokenHash := hashToken(refreshToken)
|
||||
|
||||
return s.repo.RevokeRefreshToken(ctx, tokenHash)
|
||||
}
|
||||
|
||||
func (s *AuthService) LogoutAllDevices(ctx context.Context, userID string) error {
|
||||
return s.repo.RevokeRefreshTokens(ctx, userID)
|
||||
}
|
||||
|
||||
type JWTClaims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
|
||||
@@ -57,6 +57,7 @@ func (r *AuthRepo) StoreRefreshToken(ctx context.Context, userID string, tokenHa
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *AuthRepo) ConsumeRefreshToken(ctx context.Context, tokenHash string) (string, error) {
|
||||
var userID string
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
@@ -72,3 +73,21 @@ func (r *AuthRepo) ConsumeRefreshToken(ctx context.Context, tokenHash string) (s
|
||||
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (r *AuthRepo) RevokeRefreshToken(ctx context.Context, tokenHash string) error {
|
||||
_, err := r.db.ExecContext(ctx, "DELETE FROM refresh_tokens WHERE token_hash = $1", tokenHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to revoke refresh token: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *AuthRepo) RevokeRefreshTokens(ctx context.Context, userID string) error {
|
||||
_, err := r.db.ExecContext(ctx, "DELETE FROM refresh_tokens WHERE user_id = $1", userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to revoke refresh tokens: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user