Improve installer extraction, login failures, and upload handling
This commit is contained in:
@@ -59,6 +59,15 @@ type Captcha struct {
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
type LoginFailure string
|
||||
|
||||
const (
|
||||
LoginFailureNone LoginFailure = ""
|
||||
LoginFailureLocked LoginFailure = "locked"
|
||||
LoginFailureCaptcha LoginFailure = "captcha"
|
||||
LoginFailureCredentials LoginFailure = "credentials"
|
||||
)
|
||||
|
||||
func NewService(store *db.Store) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
@@ -103,21 +112,26 @@ func (s *Service) NewCaptcha() (Captcha, error) {
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, username, password, captchaID, captcha string, clientKeys ...string) (string, string, bool, error) {
|
||||
sessionID, csrf, failure, err := s.LoginDetailed(ctx, username, password, captchaID, captcha, clientKeys...)
|
||||
return sessionID, csrf, err == nil && failure == LoginFailureNone, err
|
||||
}
|
||||
|
||||
func (s *Service) LoginDetailed(ctx context.Context, username, password, captchaID, captcha string, clientKeys ...string) (string, string, LoginFailure, error) {
|
||||
attemptKey := loginAttemptKey(username, clientKeys...)
|
||||
if s.loginLocked(attemptKey) {
|
||||
return "", "", false, nil
|
||||
return "", "", LoginFailureLocked, nil
|
||||
}
|
||||
if !s.consumeCaptcha(captchaID, captcha) {
|
||||
s.recordLoginFailure(attemptKey)
|
||||
return "", "", false, nil
|
||||
return "", "", LoginFailureCaptcha, nil
|
||||
}
|
||||
user, ok, err := s.store.VerifyAdminPassword(ctx, username, password)
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
return "", "", LoginFailureNone, err
|
||||
}
|
||||
if !ok {
|
||||
s.recordLoginFailure(attemptKey)
|
||||
return "", "", false, nil
|
||||
return "", "", LoginFailureCredentials, nil
|
||||
}
|
||||
sessionID := randomToken(32)
|
||||
csrf := randomToken(32)
|
||||
@@ -126,7 +140,7 @@ func (s *Service) Login(ctx context.Context, username, password, captchaID, capt
|
||||
s.sessions[sessionID] = sessionEntry{username: user.Username, csrf: csrf, expiresAt: time.Now().Add(sessionTTL)}
|
||||
delete(s.loginAttempts, attemptKey)
|
||||
s.mu.Unlock()
|
||||
return sessionID, csrf, true, nil
|
||||
return sessionID, csrf, LoginFailureNone, nil
|
||||
}
|
||||
|
||||
func (s *Service) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -120,8 +120,29 @@ func TestLoginLocksAfterRepeatedFailures(t *testing.T) {
|
||||
service.mu.Lock()
|
||||
answer := service.captchas[captcha.ID].answer
|
||||
service.mu.Unlock()
|
||||
if _, _, ok, err := service.Login(context.Background(), "admin", "admin", captcha.ID, answer, "127.0.0.1"); err != nil || ok {
|
||||
t.Fatalf("locked login should fail without error, ok=%v err=%v", ok, err)
|
||||
if _, _, failure, err := service.LoginDetailed(context.Background(), "admin", "admin", captcha.ID, answer, "127.0.0.1"); err != nil || failure != LoginFailureLocked {
|
||||
t.Fatalf("locked login returned failure=%q err=%v", failure, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginDetailedDistinguishesCaptchaFailure(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
store, err := db.Open(&config.Config{
|
||||
StorageDir: root,
|
||||
Database: config.DatabaseConfig{Provider: "sqlite", SQLitePath: filepath.Join(root, "captcha.sqlite"), HealthIntervalSec: 3600},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
if err := store.EnsureDefaultAdmin(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
service := NewService(store)
|
||||
_, _, failure, err := service.LoginDetailed(context.Background(), "admin", "admin", "missing", "00000", "127.0.0.1")
|
||||
if err != nil || failure != LoginFailureCaptcha {
|
||||
t.Fatalf("captcha login returned failure=%q err=%v", failure, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user