package auth import ( "context" "fmt" "net/http" "strings" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "switchboard-core/models" "switchboard-core/store" ) const bcryptCost = 12 type BuiltinProvider struct{} func NewBuiltinProvider() *BuiltinProvider { return &BuiltinProvider{} } func (p *BuiltinProvider) Mode() Mode { return ModeBuiltin } func (p *BuiltinProvider) SupportsRegistration() bool { return true } func (p *BuiltinProvider) Authenticate(c *gin.Context, stores store.Stores) (*Result, error) { var req struct { Login string `json:"login" binding:"required"` Password string `json:"password" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { return nil, err } user, err := stores.Users.GetByLogin(c.Request.Context(), req.Login) if err != nil { return nil, ErrInvalidCreds } if !user.IsActive { return nil, ErrInactive } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { return nil, ErrInvalidCreds } return &Result{User: user, VaultHint: req.Password}, nil } func (p *BuiltinProvider) Register(c *gin.Context, stores store.Stores) (*Result, error) { var req struct { Username string `json:"username" binding:"required"` Email string `json:"email" binding:"required"` Password string `json:"password" binding:"required,min=8"` } if err := c.ShouldBindJSON(&req); err != nil { return nil, err } ctx := c.Request.Context() allowed, _ := stores.Policies.GetBool(ctx, "allow_registration") if !allowed { return nil, ErrRegistrationOff } if existing, _ := stores.Users.GetByUsername(ctx, req.Username); existing != nil { return nil, ErrDuplicate } if existing, _ := stores.Users.GetByEmail(ctx, req.Email); existing != nil { return nil, ErrDuplicate } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcryptCost) if err != nil { return nil, err } handle := UniqueHandle(ctx, stores.Users, models.HandleFromName(req.Username)) defaultActive, _ := stores.Policies.GetBool(ctx, "default_user_active") user := &models.User{ Username: strings.ToLower(req.Username), Email: strings.ToLower(req.Email), PasswordHash: string(hash), IsActive: defaultActive, AuthSource: string(ModeBuiltin), Handle: handle, } if err := stores.Users.Create(ctx, user); err != nil { return nil, err } EnsureEveryoneGroup(ctx, stores, user.ID) return &Result{User: user, IsNewUser: true, VaultHint: req.Password}, nil } // UniqueHandle appends -2, -3, etc. on collision. func UniqueHandle(ctx context.Context, users store.UserStore, base string) string { if base == "" { base = "user" } if u, _ := users.GetByHandle(ctx, base); u == nil { return base } for i := 2; i < 1000; i++ { candidate := fmt.Sprintf("%s-%d", base, i) if u, _ := users.GetByHandle(ctx, candidate); u == nil { return candidate } } return fmt.Sprintf("%s-%s", base, store.NewID()[:8]) } func ErrorToHTTPStatus(err error) int { switch err { case ErrInvalidCreds: return http.StatusUnauthorized case ErrInactive: return http.StatusForbidden case ErrRegistrationOff: return http.StatusForbidden case ErrDuplicate: return http.StatusConflict case ErrNotSupported: return http.StatusMethodNotAllowed default: return http.StatusBadRequest } }