Implement Sprint 2 security enhancements
Add user-based rate limiting middleware (100 req/min) for authenticated endpoints using httprate library. Implement common password validation blocking 50+ weak passwords. Improve test coverage from 38.3% to 44.6% with comprehensive refresh token tests. Security improvements: - Rate limiting by user ID for /habits and /stats endpoints - X-RateLimit-Limit header in responses - Common password blacklist in password validation - Refresh token test suite with 5 scenarios (valid, invalid, expired, revoked, empty)
This commit is contained in:
@@ -11,9 +11,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type mockEntryRepo struct {
|
type mockEntryRepo struct {
|
||||||
createFunc func(ctx context.Context, entry *entities.HabitEntry) error
|
createFunc func(ctx context.Context, entry *entities.HabitEntry) error
|
||||||
findByDateRangeFunc func(ctx context.Context, habitID string, from, to time.Time) ([]*entities.HabitEntry, error)
|
findByDateRangeFunc func(ctx context.Context, habitID string, from, to time.Time) ([]*entities.HabitEntry, error)
|
||||||
updateFunc func(ctx context.Context, entry *entities.HabitEntry) error
|
updateFunc func(ctx context.Context, entry *entities.HabitEntry) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockEntryRepo) Create(ctx context.Context, entry *entities.HabitEntry) error {
|
func (m *mockEntryRepo) Create(ctx context.Context, entry *entities.HabitEntry) error {
|
||||||
@@ -537,5 +537,3 @@ func TestMarkHabitHandler_CounterFirstMarkWithNegative(t *testing.T) {
|
|||||||
t.Fatalf("Expected no error, got %v", err)
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -262,9 +262,9 @@ func TestRegisterUserHandler_EdgeCases(t *testing.T) {
|
|||||||
handler := NewRegisterUserHandler(repo, hasher)
|
handler := NewRegisterUserHandler(repo, hasher)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cmd RegisterUserCommand
|
cmd RegisterUserCommand
|
||||||
wantErr error
|
wantErr error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"email with plus addressing",
|
"email with plus addressing",
|
||||||
|
|||||||
@@ -137,4 +137,3 @@ func TestUnmarkHabitHandler_ReturnsErrorWhenEntryNotFound(t *testing.T) {
|
|||||||
t.Errorf("Expected ErrNotFound for missing entry, got %v", err)
|
t.Errorf("Expected ErrNotFound for missing entry, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -278,4 +278,3 @@ func TestGetHabitEntriesHandler_RequiresPaginationWithLongDateRange(t *testing.T
|
|||||||
t.Errorf("Expected ErrInvalidInput for date range > 1 year without pagination, got %v", err)
|
t.Errorf("Expected ErrInvalidInput for date range > 1 year without pagination, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HabitStatsDTO struct {
|
type HabitStatsDTO struct {
|
||||||
HabitID string `json:"habit_id"`
|
HabitID string `json:"habit_id"`
|
||||||
HabitName string `json:"habit_name"`
|
HabitName string `json:"habit_name"`
|
||||||
TotalCompletions int `json:"total_completions"`
|
TotalCompletions int `json:"total_completions"`
|
||||||
CurrentStreak int `json:"current_streak"`
|
CurrentStreak int `json:"current_streak"`
|
||||||
LongestStreak int `json:"longest_streak"`
|
LongestStreak int `json:"longest_streak"`
|
||||||
CompletionRate float64 `json:"completion_rate"`
|
CompletionRate float64 `json:"completion_rate"`
|
||||||
CompletionsThisWeek int `json:"completions_this_week"`
|
CompletionsThisWeek int `json:"completions_this_week"`
|
||||||
CompletionsThisMonth int `json:"completions_this_month"`
|
CompletionsThisMonth int `json:"completions_this_month"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetHabitStatsQuery struct {
|
type GetHabitStatsQuery struct {
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apocapoc-api/internal/domain/entities"
|
||||||
|
"apocapoc-api/internal/shared/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockRefreshTokenRepository struct {
|
||||||
|
findByTokenFunc func(ctx context.Context, token string) (*entities.RefreshToken, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRefreshTokenRepository) Create(ctx context.Context, token *entities.RefreshToken) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRefreshTokenRepository) FindByToken(ctx context.Context, token string) (*entities.RefreshToken, error) {
|
||||||
|
if m.findByTokenFunc != nil {
|
||||||
|
return m.findByTokenFunc(ctx, token)
|
||||||
|
}
|
||||||
|
return nil, errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRefreshTokenRepository) FindByUserID(ctx context.Context, userID string) ([]*entities.RefreshToken, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRefreshTokenRepository) RevokeByToken(ctx context.Context, token string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRefreshTokenRepository) RevokeAllByUserID(ctx context.Context, userID string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRefreshTokenRepository) DeleteExpired(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockUserRepositoryForRefresh struct {
|
||||||
|
findByIDFunc func(ctx context.Context, id string) (*entities.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepositoryForRefresh) Create(ctx context.Context, user *entities.User) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepositoryForRefresh) FindByID(ctx context.Context, id string) (*entities.User, error) {
|
||||||
|
if m.findByIDFunc != nil {
|
||||||
|
return m.findByIDFunc(ctx, id)
|
||||||
|
}
|
||||||
|
return nil, errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepositoryForRefresh) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepositoryForRefresh) Update(ctx context.Context, user *entities.User) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshTokenHandler_Success(t *testing.T) {
|
||||||
|
refreshTokenRepo := &mockRefreshTokenRepository{
|
||||||
|
findByTokenFunc: func(ctx context.Context, token string) (*entities.RefreshToken, error) {
|
||||||
|
return entities.NewRefreshToken("user-123", token, time.Now().Add(24*time.Hour)), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
userRepo := &mockUserRepositoryForRefresh{
|
||||||
|
findByIDFunc: func(ctx context.Context, id string) (*entities.User, error) {
|
||||||
|
user := entities.NewUser("test@example.com", "hash", "UTC")
|
||||||
|
user.ID = id
|
||||||
|
return user, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := NewRefreshTokenHandler(refreshTokenRepo, userRepo)
|
||||||
|
|
||||||
|
query := RefreshTokenQuery{
|
||||||
|
RefreshToken: "valid-token",
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := handler.Handle(context.Background(), query)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.UserID != "user-123" {
|
||||||
|
t.Errorf("Expected userID 'user-123', got %s", result.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Email != "test@example.com" {
|
||||||
|
t.Errorf("Expected email 'test@example.com', got %s", result.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshTokenHandler_InvalidToken(t *testing.T) {
|
||||||
|
refreshTokenRepo := &mockRefreshTokenRepository{}
|
||||||
|
userRepo := &mockUserRepositoryForRefresh{}
|
||||||
|
|
||||||
|
handler := NewRefreshTokenHandler(refreshTokenRepo, userRepo)
|
||||||
|
|
||||||
|
query := RefreshTokenQuery{
|
||||||
|
RefreshToken: "invalid-token",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := handler.Handle(context.Background(), query)
|
||||||
|
if err != errors.ErrNotFound {
|
||||||
|
t.Errorf("Expected ErrNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshTokenHandler_ExpiredToken(t *testing.T) {
|
||||||
|
refreshTokenRepo := &mockRefreshTokenRepository{
|
||||||
|
findByTokenFunc: func(ctx context.Context, token string) (*entities.RefreshToken, error) {
|
||||||
|
return entities.NewRefreshToken("user-123", token, time.Now().Add(-1*time.Hour)), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
userRepo := &mockUserRepositoryForRefresh{}
|
||||||
|
|
||||||
|
handler := NewRefreshTokenHandler(refreshTokenRepo, userRepo)
|
||||||
|
|
||||||
|
query := RefreshTokenQuery{
|
||||||
|
RefreshToken: "expired-token",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := handler.Handle(context.Background(), query)
|
||||||
|
if err != errors.ErrNotFound {
|
||||||
|
t.Errorf("Expected ErrNotFound for expired token, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshTokenHandler_RevokedToken(t *testing.T) {
|
||||||
|
refreshToken := entities.NewRefreshToken("user-123", "revoked-token", time.Now().Add(24*time.Hour))
|
||||||
|
refreshToken.Revoke()
|
||||||
|
|
||||||
|
refreshTokenRepo := &mockRefreshTokenRepository{
|
||||||
|
findByTokenFunc: func(ctx context.Context, token string) (*entities.RefreshToken, error) {
|
||||||
|
return refreshToken, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
userRepo := &mockUserRepositoryForRefresh{}
|
||||||
|
|
||||||
|
handler := NewRefreshTokenHandler(refreshTokenRepo, userRepo)
|
||||||
|
|
||||||
|
query := RefreshTokenQuery{
|
||||||
|
RefreshToken: "revoked-token",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := handler.Handle(context.Background(), query)
|
||||||
|
if err != errors.ErrNotFound {
|
||||||
|
t.Errorf("Expected ErrNotFound for revoked token, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshTokenHandler_EmptyToken(t *testing.T) {
|
||||||
|
refreshTokenRepo := &mockRefreshTokenRepository{}
|
||||||
|
userRepo := &mockUserRepositoryForRefresh{}
|
||||||
|
|
||||||
|
handler := NewRefreshTokenHandler(refreshTokenRepo, userRepo)
|
||||||
|
|
||||||
|
query := RefreshTokenQuery{
|
||||||
|
RefreshToken: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := handler.Handle(context.Background(), query)
|
||||||
|
if err != errors.ErrInvalidInput {
|
||||||
|
t.Errorf("Expected ErrInvalidInput, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,4 +43,3 @@ func TestNewHabitEntry_BooleanHabit(t *testing.T) {
|
|||||||
t.Error("Value should be nil for boolean habit")
|
t.Error("Value should be nil for boolean habit")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
|
|
||||||
func TestHabitType_MarshalJSON(t *testing.T) {
|
func TestHabitType_MarshalJSON(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
habitType HabitType
|
habitType HabitType
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{"Boolean", HabitTypeBoolean, `"BOOLEAN"`},
|
{"Boolean", HabitTypeBoolean, `"BOOLEAN"`},
|
||||||
{"Counter", HabitTypeCounter, `"COUNTER"`},
|
{"Counter", HabitTypeCounter, `"COUNTER"`},
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ type Claims struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type JWTService struct {
|
type JWTService struct {
|
||||||
secret []byte
|
secret []byte
|
||||||
expiry time.Duration
|
expiry time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJWTService(secret string, expiryHours int) *JWTService {
|
func NewJWTService(secret string, expiryHours int) *JWTService {
|
||||||
return &JWTService{
|
return &JWTService{
|
||||||
secret: []byte(secret),
|
secret: []byte(secret),
|
||||||
expiry: time.Duration(expiryHours) * time.Hour,
|
expiry: time.Duration(expiryHours) * time.Hour,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AuthHandlers struct {
|
type AuthHandlers struct {
|
||||||
registerHandler *commands.RegisterUserHandler
|
registerHandler *commands.RegisterUserHandler
|
||||||
loginHandler *queries.LoginUserHandler
|
loginHandler *queries.LoginUserHandler
|
||||||
refreshTokenHandler *queries.RefreshTokenHandler
|
refreshTokenHandler *queries.RefreshTokenHandler
|
||||||
revokeTokenHandler *commands.RevokeTokenHandler
|
revokeTokenHandler *commands.RevokeTokenHandler
|
||||||
revokeAllTokensHandler *commands.RevokeAllTokensHandler
|
revokeAllTokensHandler *commands.RevokeAllTokensHandler
|
||||||
jwtService *auth.JWTService
|
jwtService *auth.JWTService
|
||||||
refreshTokenRepo repositories.RefreshTokenRepository
|
refreshTokenRepo repositories.RefreshTokenRepository
|
||||||
refreshTokenExpiry time.Duration
|
refreshTokenExpiry time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthHandlers(
|
func NewAuthHandlers(
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateHabitRequest struct {
|
type CreateHabitRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Type value_objects.HabitType `json:"type"`
|
Type value_objects.HabitType `json:"type"`
|
||||||
Frequency value_objects.Frequency `json:"frequency"`
|
Frequency value_objects.Frequency `json:"frequency"`
|
||||||
SpecificDays []int `json:"specific_days,omitempty"`
|
SpecificDays []int `json:"specific_days,omitempty"`
|
||||||
SpecificDates []int `json:"specific_dates,omitempty"`
|
SpecificDates []int `json:"specific_dates,omitempty"`
|
||||||
CarryOver bool `json:"carry_over"`
|
CarryOver bool `json:"carry_over"`
|
||||||
IsNegative bool `json:"is_negative"`
|
IsNegative bool `json:"is_negative"`
|
||||||
TargetValue *float64 `json:"target_value,omitempty"`
|
TargetValue *float64 `json:"target_value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateHabitRequest struct {
|
type UpdateHabitRequest struct {
|
||||||
@@ -28,19 +28,19 @@ type UpdateHabitRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HabitResponse struct {
|
type HabitResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Type value_objects.HabitType `json:"type"`
|
Type value_objects.HabitType `json:"type"`
|
||||||
Frequency value_objects.Frequency `json:"frequency"`
|
Frequency value_objects.Frequency `json:"frequency"`
|
||||||
SpecificDays []int `json:"specific_days,omitempty"`
|
SpecificDays []int `json:"specific_days,omitempty"`
|
||||||
SpecificDates []int `json:"specific_dates,omitempty"`
|
SpecificDates []int `json:"specific_dates,omitempty"`
|
||||||
CarryOver bool `json:"carry_over"`
|
CarryOver bool `json:"carry_over"`
|
||||||
IsNegative bool `json:"is_negative"`
|
IsNegative bool `json:"is_negative"`
|
||||||
TargetValue *float64 `json:"target_value,omitempty"`
|
TargetValue *float64 `json:"target_value,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarkHabitRequest struct {
|
type MarkHabitRequest struct {
|
||||||
@@ -59,14 +59,14 @@ type TodaysHabitResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserHabitResponse struct {
|
type UserHabitResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type value_objects.HabitType `json:"type"`
|
Type value_objects.HabitType `json:"type"`
|
||||||
Frequency value_objects.Frequency `json:"frequency"`
|
Frequency value_objects.Frequency `json:"frequency"`
|
||||||
SpecificDays []int `json:"specific_days,omitempty"`
|
SpecificDays []int `json:"specific_days,omitempty"`
|
||||||
TargetValue *float64 `json:"target_value,omitempty"`
|
TargetValue *float64 `json:"target_value,omitempty"`
|
||||||
CarryOver bool `json:"carry_over"`
|
CarryOver bool `json:"carry_over"`
|
||||||
IsNegative bool `json:"is_negative"`
|
IsNegative bool `json:"is_negative"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HabitEntryResponse struct {
|
type HabitEntryResponse struct {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apocapoc-api/internal/infrastructure/auth"
|
||||||
|
|
||||||
|
"github.com/go-chi/httprate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RateLimitByUser(jwtService *auth.JWTService, requestsPerMinute int, duration time.Duration) func(http.Handler) http.Handler {
|
||||||
|
limiter := httprate.NewRateLimiter(
|
||||||
|
requestsPerMinute,
|
||||||
|
duration,
|
||||||
|
httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
|
||||||
|
userID, ok := GetUserIDFromContext(r.Context())
|
||||||
|
if !ok {
|
||||||
|
return r.RemoteAddr, nil
|
||||||
|
}
|
||||||
|
return "user:" + userID, nil
|
||||||
|
}),
|
||||||
|
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusTooManyRequests)
|
||||||
|
w.Write([]byte(`{"error":"Rate limit exceeded. Please try again later."}`))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("X-RateLimit-Limit", strconv.Itoa(requestsPerMinute))
|
||||||
|
|
||||||
|
limiter.Handler(next).ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,8 @@ func NewRouter(corsOrigins string, habitHandlers *HabitHandlers, authHandlers *A
|
|||||||
|
|
||||||
r.Route("/api/v1/habits", func(r chi.Router) {
|
r.Route("/api/v1/habits", func(r chi.Router) {
|
||||||
r.Use(AuthMiddleware(jwtService))
|
r.Use(AuthMiddleware(jwtService))
|
||||||
|
r.Use(RateLimitByUser(jwtService, 100, 1*time.Minute))
|
||||||
|
|
||||||
r.Post("/", habitHandlers.CreateHabit)
|
r.Post("/", habitHandlers.CreateHabit)
|
||||||
r.Get("/", habitHandlers.GetUserHabits)
|
r.Get("/", habitHandlers.GetUserHabits)
|
||||||
r.Get("/today", habitHandlers.GetTodaysHabits)
|
r.Get("/today", habitHandlers.GetTodaysHabits)
|
||||||
@@ -59,6 +61,7 @@ func NewRouter(corsOrigins string, habitHandlers *HabitHandlers, authHandlers *A
|
|||||||
|
|
||||||
r.Route("/api/v1/stats", func(r chi.Router) {
|
r.Route("/api/v1/stats", func(r chi.Router) {
|
||||||
r.Use(AuthMiddleware(jwtService))
|
r.Use(AuthMiddleware(jwtService))
|
||||||
|
r.Use(RateLimitByUser(jwtService, 100, 1*time.Minute))
|
||||||
r.Get("/habits/{id}", statsHandlers.GetHabitStats)
|
r.Get("/habits/{id}", statsHandlers.GetHabitStats)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package errors
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotFound = errors.New("resource not found")
|
ErrNotFound = errors.New("resource not found")
|
||||||
ErrAlreadyExists = errors.New("resource already exists")
|
ErrAlreadyExists = errors.New("resource already exists")
|
||||||
ErrInvalidInput = errors.New("invalid input")
|
ErrInvalidInput = errors.New("invalid input")
|
||||||
ErrUnauthorized = errors.New("unauthorized")
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,25 +21,25 @@ func TestShouldAppearToday_Weekly(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Monday when Monday is specified",
|
name: "Monday when Monday is specified",
|
||||||
specificDays: []int{1}, // Monday
|
specificDays: []int{1}, // Monday
|
||||||
targetDate: time.Date(2025, 1, 6, 0, 0, 0, 0, time.UTC), // Monday
|
targetDate: time.Date(2025, 1, 6, 0, 0, 0, 0, time.UTC), // Monday
|
||||||
expected: true,
|
expected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Tuesday when Monday is specified",
|
name: "Tuesday when Monday is specified",
|
||||||
specificDays: []int{1}, // Monday
|
specificDays: []int{1}, // Monday
|
||||||
targetDate: time.Date(2025, 1, 7, 0, 0, 0, 0, time.UTC), // Tuesday
|
targetDate: time.Date(2025, 1, 7, 0, 0, 0, 0, time.UTC), // Tuesday
|
||||||
expected: false,
|
expected: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Wednesday when Mon/Wed/Fri specified",
|
name: "Wednesday when Mon/Wed/Fri specified",
|
||||||
specificDays: []int{1, 3, 5}, // Mon, Wed, Fri
|
specificDays: []int{1, 3, 5}, // Mon, Wed, Fri
|
||||||
targetDate: time.Date(2025, 1, 8, 0, 0, 0, 0, time.UTC), // Wednesday
|
targetDate: time.Date(2025, 1, 8, 0, 0, 0, 0, time.UTC), // Wednesday
|
||||||
expected: true,
|
expected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Sunday when Mon/Wed/Fri specified",
|
name: "Sunday when Mon/Wed/Fri specified",
|
||||||
specificDays: []int{1, 3, 5}, // Mon, Wed, Fri
|
specificDays: []int{1, 3, 5}, // Mon, Wed, Fri
|
||||||
targetDate: time.Date(2025, 1, 5, 0, 0, 0, 0, time.UTC), // Sunday
|
targetDate: time.Date(2025, 1, 5, 0, 0, 0, 0, time.UTC), // Sunday
|
||||||
expected: false,
|
expected: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
var commonPasswords = map[string]bool{
|
||||||
|
"123456": true,
|
||||||
|
"password": true,
|
||||||
|
"123456789": true,
|
||||||
|
"12345678": true,
|
||||||
|
"12345": true,
|
||||||
|
"1234567": true,
|
||||||
|
"password1": true,
|
||||||
|
"123123": true,
|
||||||
|
"1234567890": true,
|
||||||
|
"000000": true,
|
||||||
|
"abc123": true,
|
||||||
|
"1234": true,
|
||||||
|
"qwerty": true,
|
||||||
|
"111111": true,
|
||||||
|
"123321": true,
|
||||||
|
"dragon": true,
|
||||||
|
"master": true,
|
||||||
|
"monkey": true,
|
||||||
|
"letmein": true,
|
||||||
|
"login": true,
|
||||||
|
"princess": true,
|
||||||
|
"qwertyuiop": true,
|
||||||
|
"solo": true,
|
||||||
|
"passw0rd": true,
|
||||||
|
"starwars": true,
|
||||||
|
"iloveyou": true,
|
||||||
|
"welcome": true,
|
||||||
|
"admin": true,
|
||||||
|
"sunshine": true,
|
||||||
|
"password123": true,
|
||||||
|
"123qwe": true,
|
||||||
|
"654321": true,
|
||||||
|
"superman": true,
|
||||||
|
"1qaz2wsx": true,
|
||||||
|
"trustno1": true,
|
||||||
|
"charlie": true,
|
||||||
|
"666666": true,
|
||||||
|
"qazwsx": true,
|
||||||
|
"freedom": true,
|
||||||
|
"football": true,
|
||||||
|
"baseball": true,
|
||||||
|
"whatever": true,
|
||||||
|
"jordan": true,
|
||||||
|
"killer": true,
|
||||||
|
"summer": true,
|
||||||
|
"hockey": true,
|
||||||
|
"bailey": true,
|
||||||
|
"shadow": true,
|
||||||
|
"master123": true,
|
||||||
|
"ninja": true,
|
||||||
|
"mustang": true,
|
||||||
|
"password!": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsCommonPassword(password string) bool {
|
||||||
|
lower := strings.ToLower(password)
|
||||||
|
return commonPasswords[lower]
|
||||||
|
}
|
||||||
@@ -92,6 +92,10 @@ func ValidatePassword(password string) error {
|
|||||||
return ValidationError{Field: "password", Message: "password must contain at least one special character"}
|
return ValidationError{Field: "password", Message: "password must contain at least one special character"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if IsCommonPassword(password) {
|
||||||
|
return ValidationError{Field: "password", Message: "password is too common, please choose a more secure password"}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user