Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions frontend/src/lib/validations.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { z } from 'zod';

// Username validation: lowercase letters only, 3-50 characters
// Username validation: letters, numbers, underscores, hyphens; 3-50 characters
const usernameSchema = z
.string({
message: 'Username must be a string',
})
.min(3, 'Username must be at least 3 characters')
.max(50, 'Username must be at most 50 characters')
.regex(/^[a-z]+$/, 'Username must contain only lowercase letters');
.regex(
/^[a-zA-Z0-9_-]+$/,
'Username can only contain letters, numbers, underscores, and hyphens'
);

// Password validation: min 8 chars, must contain uppercase, lowercase, number, and symbol
const passwordSchema = z
Expand Down
6 changes: 3 additions & 3 deletions internal/features/auth/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ import (
// tags are enforced by go-playground/validator in Service.Register; the DB
// config is validated separately (type-specific fields).
type RegisterInput struct {
Username string `validate:"required,min=3,max=50,lowercase,alpha"`
Username string `validate:"required,min=3,max=50,username_format"`
Password string `validate:"required,min=8,password_strength"`
DBConfig dbclient.Config `json:"DBConfig"`
}

// LoginInput is the payload expected when signing in an existing account.
type LoginInput struct {
Username string `validate:"required,lowercase,alpha"`
Username string `validate:"required,username_format"`
Password string `validate:"required"`
}

// ResetPasswordInput is the payload for recovering an account after a forgotten
// password. The user must prove ownership by supplying the recovery key that
// was shown at registration time.
type ResetPasswordInput struct {
Username string `validate:"required,lowercase,alpha"`
Username string `validate:"required,username_format"`
NewPassword string `validate:"required,min=8,password_strength"`
RecoveryKey string `validate:"required"`
}
Expand Down
14 changes: 12 additions & 2 deletions internal/features/auth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ func (s *Service) Startup(ctx context.Context) {
s.ctx = ctx
}

var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)

// validateUsernameFormat enforces that a username contains only letters,
// digits, underscores, and hyphens. It is registered as the "username_format"
// validator rule.
func validateUsernameFormat(fl validator.FieldLevel) bool {
return usernameRegex.MatchString(fl.Field().String())
}

// validatePasswordStrength enforces that a password contains at least one
// uppercase letter, one lowercase letter, one digit and one symbol. It is
// registered as the "password_strength" validator rule.
Expand All @@ -91,12 +100,13 @@ func validatePasswordStrength(fl validator.FieldLevel) bool {

// NewService wires a shared connection holder, the database-credentials
// keyring repository, the master-key keyring repository and a validator with
// the custom password strength rule into a ready-to-use auth Service.
// the custom validation rules into a ready-to-use auth Service.
func NewService(conn *dbclient.Connection) *Service {
validate := validator.New()

// Register custom password strength validator
// Register custom validators
_ = validate.RegisterValidation("password_strength", validatePasswordStrength)
_ = validate.RegisterValidation("username_format", validateUsernameFormat)

return &Service{
conn: conn,
Expand Down
Loading