diff --git a/api/controller/auth.go b/api/controller/auth.go index cadc458..64ec680 100644 --- a/api/controller/auth.go +++ b/api/controller/auth.go @@ -1,25 +1,95 @@ 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) { - // metrics.WithLabelValues("new").Inc() var email = r.FormValue("email") var password = r.FormValue("password") - if email == "" || password == "" { - http.Error(w, "Missing required fields", http.StatusBadRequest) + _, 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 } @@ -27,23 +97,46 @@ func PostSignup(db *sql.DB) http.HandlerFunc { rand.Read(salt) hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 1, 16) - hashStr := base64.StdEncoding.EncodeToString(hash) - user_uuid, err := uuid.NewRandom() + _, 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 { - http.Error(w, err.Error(), http.StatusInternalServerError) - log.Printf("Could not generate UUID: %v", err) - return - } + if strings.Contains(err.Error(), "email") { + metricsAuthSignUp.WithLabelValues("email-dup").Inc() + http.Error(w, "Email already exists", http.StatusBadRequest) + return + } - _, err = db.Exec("INSERT INTO user (user_uuid, email, email_verified, is_admin, password, salt, created_at) VALUES (?, ?, FALSE, FALSE, ?, ?, CURRENT_DATE)", user_uuid, email, hash, salt) - if err != nil { + metricsError.WithLabelValues("signup_sql").Inc() http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("Could not insert user: %v", err) return } - w.WriteHeader(http.StatusCreated) - w.Write([]byte(hashStr)) + 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) +} diff --git a/api/controller/workout.go b/api/controller/workout.go index bd9e661..a65582b 100644 --- a/api/controller/workout.go +++ b/api/controller/workout.go @@ -13,7 +13,7 @@ import ( ) var ( - metrics = promauto.NewCounterVec( + metricsWorkout = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "mefit_api_workout_total", Help: "The total number of workout api requests processed", @@ -24,7 +24,7 @@ var ( func NewWorkout(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - metrics.WithLabelValues("new").Inc() + metricsWorkout.WithLabelValues("new").Inc() var dateStr = r.FormValue("date") var typeStr = r.FormValue("type") @@ -67,7 +67,7 @@ func NewWorkout(db *sql.DB) http.HandlerFunc { func GetWorkouts(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - metrics.WithLabelValues("get").Inc() + metricsWorkout.WithLabelValues("get").Inc() // token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token) // var userId = token.UID @@ -109,7 +109,7 @@ func GetWorkouts(db *sql.DB) http.HandlerFunc { func DeleteWorkout(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - metrics.WithLabelValues("delete").Inc() + metricsWorkout.WithLabelValues("delete").Inc() // token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token) // var userId = token.UID diff --git a/api/go.mod b/api/go.mod index 21d7346..6dbe7f3 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,6 +3,7 @@ module api go 1.22.5 require ( + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-migrate/migrate/v4 v4.17.1 github.com/google/uuid v1.4.0 github.com/mattn/go-sqlite3 v1.14.22 diff --git a/api/go.sum b/api/go.sum index 6ac2ee7..451d0b4 100644 --- a/api/go.sum +++ b/api/go.sum @@ -4,6 +4,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= diff --git a/api/migrations/002_users.sql b/api/migrations/002_users.up.sql similarity index 76% rename from api/migrations/002_users.sql rename to api/migrations/002_users.up.sql index 47c3ba5..843117e 100644 --- a/api/migrations/002_users.sql +++ b/api/migrations/002_users.up.sql @@ -2,7 +2,7 @@ CREATE TABLE user ( user_uuid TEXT NOT NULL UNIQUE PRIMARY KEY, - email TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, email_verified BOOLEAN NOT NULL, is_admin BOOLEAN NOT NULL, @@ -10,5 +10,5 @@ CREATE TABLE user ( password BLOB NOT NULL, salt BLOB NOT NULL, - created_at DATETIME NOT NULL, + created_at DATETIME NOT NULL ) WITHOUT ROWID; diff --git a/api/utils/db.go b/api/utils/db.go index f105f88..04d7c9a 100644 --- a/api/utils/db.go +++ b/api/utils/db.go @@ -29,4 +29,6 @@ func RunMigrations(db *sql.DB) { log.Fatal("Could not run migrations: ", err) } } + + log.Println("Migrations ran successfully") }