package controller import ( "crypto/ecdsa" "crypto/rand" "crypto/x509" "database/sql" "encoding/base64" "log" "net/http" "net/mail" "os" "strings" "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "golang.org/x/crypto/argon2" ) var ( metricsAuthSignUp = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "mefit_api_auth_signup_total", Help: "The total number of auth signup api requests processed", }, []string{"result"}, ) metricsError = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "mefit_api_error_total", Help: "The total number of errors", }, []string{"result"}, ) // metricsAuthSignIn = promauto.NewCounterVec( // prometheus.CounterOpts{ // Name: "mefit_api_auth_signin_total", // }, // []string{"result"}, // ) privateKey = func() *ecdsa.PrivateKey { keyBase64 := os.Getenv("PRIVATE_KEY") if keyBase64 == "" { log.Fatal("PRIVATE_KEY not defined") } keyData, err := base64.StdEncoding.DecodeString(keyBase64) if err != nil { log.Fatalf("Could not decode private key: %v", err) } key, err := x509.ParseECPrivateKey(keyData) if err != nil { log.Fatalf("Could not parse private key: %v", err) } log.Println("Successfully imported private key") return key }() ) func PostSignup(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var email = r.FormValue("email") var password = r.FormValue("password") _, err := mail.ParseAddress(email) if err != nil { metricsAuthSignUp.WithLabelValues("email").Inc() http.Error(w, "Invalid email", http.StatusBadRequest) return } if len(password) < 8 || !strings.ContainsAny(password, "0123456789") || !strings.ContainsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") || !strings.ContainsAny(password, "abcdefghijklmnopqrstuvwxyz") || !strings.ContainsAny(password, "!@#$%^&*()_+-=[]{}\\|;:'\",.<>/?") { metricsAuthSignUp.WithLabelValues("password").Inc() http.Error(w, "Password needs to be 8 characters long, contain at least one number, one special, one uppercase and one lowercase character", http.StatusBadRequest) return } user_uuid, err := uuid.NewRandom() if err != nil { metricsError.WithLabelValues("signup_uuid").Inc() http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("Could not generate UUID: %v", err) return } salt := make([]byte, 16) rand.Read(salt) hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 1, 16) _, err = db.Exec("INSERT INTO user (user_uuid, email, email_verified, is_admin, password, salt, created_at) VALUES (?, ?, FALSE, FALSE, ?, ?, datetime())", user_uuid, email, hash, salt) if err != nil { if strings.Contains(err.Error(), "email") { metricsAuthSignUp.WithLabelValues("email-dup").Inc() http.Error(w, "Email already exists", http.StatusBadRequest) return } metricsError.WithLabelValues("signup_sql").Inc() http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("Could not insert user: %v", err) return } token, err := generateJwt(email, user_uuid.String()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) metricsError.WithLabelValues("signup_jwt").Inc() log.Printf("Could not generate JWT: %v", err) return } metricsAuthSignUp.WithLabelValues("success").Inc() w.WriteHeader(http.StatusOK) w.Write([]byte(token)) } } func generateJwt(user_email string, user_id string) (string, error) { var now = time.Now() t := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{ "iss": "mefit", "iat": now.Unix(), "exp": now.Add(time.Hour * 24 * 7).Unix(), "sub": user_email, "user_id": user_id, }) return t.SignedString(privateKey) }