From e8931f937227fecd57970182815d7bc0c304fdd5 Mon Sep 17 00:00:00 2001 From: Ali Shah <89487837+Alishah634@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:29:05 -0400 Subject: [PATCH 1/8] Fixed minor test convention error --- internal/database/mysql_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/database/mysql_test.go b/internal/database/mysql_test.go index a77b455..56dfc1e 100644 --- a/internal/database/mysql_test.go +++ b/internal/database/mysql_test.go @@ -208,10 +208,10 @@ func TestAuthenticateLogin(t *testing.T) { expectedID int happyPath bool }{ - {name: "valid_login", username: happyUser, password: happyPass, expectedID: 1, happyPath: true}, - {name: "bad_user", username: "im a mistake", password: happyPass, expectedID: 0, happyPath: false}, - {name: "bad_pass", username: happyUser, password: "im a mistake", expectedID: 0, happyPath: false}, - {name: "bad_user_and_pass", username: "we're both", password: "mistakes", expectedID: 0, happyPath: false}, + {name: "ValidLogin", username: happyUser, password: happyPass, expectedID: 1, happyPath: true}, + {name: "BadUser", username: "im a mistake", password: happyPass, expectedID: 0, happyPath: false}, + {name: "BadPass", username: happyUser, password: "im a mistake", expectedID: 0, happyPath: false}, + {name: "BadUser_and_BadPass", username: "we're both", password: "mistakes", expectedID: 0, happyPath: false}, } for _, testCase := range testCases { From bbfa1c83d864e7c7bf6859f1fbfd15eaa465fa18 Mon Sep 17 00:00:00 2001 From: Ali Shah <89487837+Alishah634@users.noreply.github.com> Date: Tue, 2 Jul 2024 02:33:21 -0400 Subject: [PATCH 2/8] updated the necessary includes --- internal/database/mysql.go | 65 +++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/internal/database/mysql.go b/internal/database/mysql.go index 50f1a77..b5287c9 100644 --- a/internal/database/mysql.go +++ b/internal/database/mysql.go @@ -5,10 +5,8 @@ package database import ( "database/sql" "errors" - "fmt" "log" "os" - "strconv" "strings" _ "github.com/go-sql-driver/mysql" @@ -120,12 +118,14 @@ func (db *MySQLDatabase) GetUser(id int) (user *internal.User) { return } -func (db *MySQLDatabase) getOpenID() (id int) { - err := db.underlyingDB.QueryRow("SELECT id FROM openid LIMIT 1").Scan(&id) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - log.Println("Failed to query for an openid -", err) - } - return +func (db *MySQLDatabase) getOpenID(tx *sql.Tx) (int, error) { + var id int + err := tx.QueryRow("SELECT id FROM openid LIMIT 1").Scan(&id) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + log.Println("Failed to query for an openid -", err) + return 0, err + } + return id, nil } func (db *MySQLDatabase) AddUser(username string, email string, password string) (err error) { @@ -134,26 +134,55 @@ func (db *MySQLDatabase) AddUser(username string, email string, password string) if len(username) == 0 || len(email) == 0 || len(password) == 0 { return errors.New("empty fields") } - // TODO(@seoyoungcho213): For efficiency, we might be able to return the new id here with only a single query?`` - query := "INSERT INTO users (username, email, password" - if openID := db.getOpenID(); openID == 0 { - log.Println("All existing IDs in use, assigning new ID to {" + username + "}") - query += `) VALUES ("%s", "%s", "%s")` - } else { - log.Println("Re-using open id:", openID, "for {"+username+"}") - query += `, id) VALUES ("%s", "%s", "%s", ` + strconv.Itoa(openID) + `)` + + // Start a transaction: + tx, err := db.underlyingDB.Begin() + if err != nil { + log.Println("Error starting transaction -", err) + return errors.New("internal server error") } + defer func() { + if err != nil { + tx.Rollback() + } + }() + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { log.Println("Error hashing password -", err) return errors.New("internal server error") } - if _, err = db.underlyingDB.Exec(fmt.Sprintf(query, username, email, hashedPassword)); err != nil { + + // Get openID within the transaction + openID, err := db.getOpenID(tx) + if err != nil { + log.Println("Error getting open ID -", err) + return errors.New("internal server error") + } + + if openID == 0 { + log.Println("All existing IDs in use, assigning new ID to {" + username + "}") + query := "INSERT INTO users (username, email, password) VALUES (?, ?, ?)" + _, err = tx.Exec(query, username, email, hashedPassword) + } else { + log.Println("Re-using open id:", openID, "for {"+username+"}") + query := "INSERT INTO users (username, email, password, id) VALUES (?, ?, ?, ?)" + _, err = tx.Exec(query, username, email, hashedPassword, openID) + } + + if err != nil { log.Println("Error adding user -", err) return errors.New("internal server error") } + + // Commit the transaction + err = tx.Commit() + if err != nil { + log.Println("Error committing transaction -", err) + return errors.New("internal server error") + } // TODO(@seoyoungcho213): Make sure to delete the openID if used here ("delete if exists" would be perfect). - return + return nil } func (db *MySQLDatabase) GetUserID(varName string, variable string) (id int) { From 7c3174d4a4cc8e509934631eb11268239fcc5ff5 Mon Sep 17 00:00:00 2001 From: Ali Shah <89487837+Alishah634@users.noreply.github.com> Date: Tue, 2 Jul 2024 02:43:52 -0400 Subject: [PATCH 3/8] adding test cases for new functionality --- internal/database/mysql_test.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/internal/database/mysql_test.go b/internal/database/mysql_test.go index 56dfc1e..18ee055 100644 --- a/internal/database/mysql_test.go +++ b/internal/database/mysql_test.go @@ -162,7 +162,13 @@ func TestAddUser(t *testing.T) { {"AddEmptyPassword", "empty_pass_user", "empty_pass_user@example.com", "", 0, true}, // TODO: The functionality for this test need to be implemented - // {"AddInvalidEmail", "invalid_email_user", "invalidemail.com", "test_pass7", 0, true}, + // {"AddInvalidEmail", "invalid_email_user", "invalidemail.com", "test_pass7", 0, true}, // TODO: The functionality for this test need to be implemented + {"MultipleFailures1", "user_fail", "user_fail@example.com", "", 0, true}, // Should fail + {"MultipleFailures2", "user_fail", "", "password", 0, true}, // Should fail + {"MultipleFailures3", "", "user_fail@example.com", "password", 0, true}, // Should fail + {"ValidAfterFailures", "valid_user", "valid_user@example.com", "password", 3, false}, // Should be ID 3 after failures + {"CreateAndReuseOpenID1", "temp_user", "temp_user@example.com", "password", 4, false}, // Should be ID 4 + {"ReuseOpenID", "new_user", "new_user@example.com", "password", 4, false}, // Should reuse ID 4 } for _, tc := range testCases { @@ -185,6 +191,13 @@ func TestAddUser(t *testing.T) { if id != tc.expectedID { t.Fatalf("Expected user id %d but got %d for case: %s", tc.expectedID, id, tc.name) } + // Specific case to delete the user to test reusing open ID + if tc.name == "CreateAndReuseOpenID1" { + _, err = probe.Exec("DELETE FROM users WHERE id = ?", id) + if err != nil { + t.Fatalf("Failed to delete user - %v for case: %s", err, tc.name) + } + } } }, ) From 21a82d5589280a29fcb091beace5e5c4db29df6c Mon Sep 17 00:00:00 2001 From: Ali Shah <89487837+Alishah634@users.noreply.github.com> Date: Sat, 6 Jul 2024 18:35:59 -0400 Subject: [PATCH 4/8] Still issues regarding duplicate test cases, small edits made however WIP. Not a working commit --- internal/database/mysql.go | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/internal/database/mysql.go b/internal/database/mysql.go index b5287c9..2f7291b 100644 --- a/internal/database/mysql.go +++ b/internal/database/mysql.go @@ -118,13 +118,18 @@ func (db *MySQLDatabase) GetUser(id int) (user *internal.User) { return } -func (db *MySQLDatabase) getOpenID(tx *sql.Tx) (int, error) { +func getOpenID(tx *sql.Tx) (int, error) { var id int err := tx.QueryRow("SELECT id FROM openid LIMIT 1").Scan(&id) if err != nil && !errors.Is(err, sql.ErrNoRows) { log.Println("Failed to query for an openid -", err) return 0, err } + // HERE We need to delete the openID if it exists in the OpenID table. + if _, err = tx.Exec("DELETE FROM openid WHERE id = ? LIMIT 1", id); err != nil { + log.Println("Error deleting the openID -", err) + return 0, err + } return id, nil } @@ -134,29 +139,26 @@ func (db *MySQLDatabase) AddUser(username string, email string, password string) if len(username) == 0 || len(email) == 0 || len(password) == 0 { return errors.New("empty fields") } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + log.Println("Error hashing password -", err) + return errors.New("internal server error") + } // Start a transaction: tx, err := db.underlyingDB.Begin() if err != nil { log.Println("Error starting transaction -", err) - return errors.New("internal server error") - } - defer func() { - if err != nil { - tx.Rollback() - } - }() - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - log.Println("Error hashing password -", err) + tx.Rollback() return errors.New("internal server error") } // Get openID within the transaction - openID, err := db.getOpenID(tx) + openID, err := getOpenID(tx) if err != nil { log.Println("Error getting open ID -", err) + tx.Rollback() return errors.New("internal server error") } @@ -172,6 +174,7 @@ func (db *MySQLDatabase) AddUser(username string, email string, password string) if err != nil { log.Println("Error adding user -", err) + tx.Rollback() return errors.New("internal server error") } @@ -179,6 +182,7 @@ func (db *MySQLDatabase) AddUser(username string, email string, password string) err = tx.Commit() if err != nil { log.Println("Error committing transaction -", err) + tx.Rollback() return errors.New("internal server error") } // TODO(@seoyoungcho213): Make sure to delete the openID if used here ("delete if exists" would be perfect). @@ -196,6 +200,9 @@ func (db *MySQLDatabase) GetUserID(varName string, variable string) (id int) { } func (db *MySQLDatabase) DeleteUser(id int) (err error) { + if id < 1{ + return errors.New("invalid id") + } if _, err = db.underlyingDB.Exec("INSERT INTO openid (id) VALUES(?)", id); err != nil { log.Println("Error inserting openID", id, " to the table -", err) return From 05693b6805fb3655d282a4fc171dd0e07cc539c9 Mon Sep 17 00:00:00 2001 From: Ali Shah <89487837+Alishah634@users.noreply.github.com> Date: Wed, 10 Jul 2024 02:54:32 -0400 Subject: [PATCH 5/8] Now ensure the duplicate email check occurs before adding user to database, hence solving the auto increment ID issue --- internal/database/mysql.go | 21 +++++++++++++++++++++ internal/database/mysql_test.go | 9 ++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/internal/database/mysql.go b/internal/database/mysql.go index df7564a..8195bc3 100644 --- a/internal/database/mysql.go +++ b/internal/database/mysql.go @@ -119,6 +119,18 @@ func (db *MySQLDatabase) GetUser(id uint64) (user *internal.User) { return } +// EmailExists checks if an email already exists in the users table. +func (db *MySQLDatabase) EmailExists(email string) (bool, error) { + var exists bool + query := "SELECT EXISTS (SELECT 1 FROM users WHERE email = ?)" + err := db.underlyingDB.QueryRow(query, email).Scan(&exists) + if err != nil { + log.Println("Error checking email existence -", err) + return false, err + } + return exists, nil +} + func (db *MySQLDatabase) getOpenID() (id uint64, err error) { err = db.underlyingDB.QueryRow("SELECT id FROM openid LIMIT 1").Scan(&id) if err != nil && !errors.Is(err, sql.ErrNoRows) { @@ -133,6 +145,14 @@ func (db *MySQLDatabase) AddUser(firstName, middleName, lastName, email, passwor if len(firstName) == 0 || len(lastName) == 0 || len(email) == 0 || len(password) == 0 || len(dateOfBirth) == 0 { return 0, errors.New("empty fields") } + // Check if email already exists + + if emailExists, err := db.EmailExists(email); err != nil { + return 0, errors.New("internal server error") + } else if emailExists { + return 0, errors.New("email already exists") + } + query := "INSERT INTO users (first_name, middle_name, last_name, email, password, date_of_birth" if openID, err := db.getOpenID(); err != nil && !errors.Is(err, sql.ErrNoRows) { log.Println("Error getting open id -", err) @@ -153,6 +173,7 @@ func (db *MySQLDatabase) AddUser(firstName, middleName, lastName, email, passwor log.Println("Error hashing password -", err) return id, errors.New("internal server error") } + result, err := db.underlyingDB.Exec(query, firstName, middleName, lastName, email, hashedPassword, dateOfBirth) if err != nil { log.Println("Error adding user -", err) diff --git a/internal/database/mysql_test.go b/internal/database/mysql_test.go index f6ea58f..c026df6 100644 --- a/internal/database/mysql_test.go +++ b/internal/database/mysql_test.go @@ -168,8 +168,7 @@ func TestAddUser(t *testing.T) { {"AddDuplicateName", "testname", "", "user", "dupl_name@example.com","test_pass3", "2024-10-22", 3, false}, {"AddDuplicateEmail", "dupl_email", "", "user", "test_user@example2.com","test_pass4", "2024-10-22", 0, true}, {"AddEmptyFirstname", "", "", "user", "empty_first@example.com","test_pass5", "2024-08-22", 0, true}, - // TODO : This supposed to be ID:3, but duplicate email increment ID by one. Fix this error. - {"AddEmptyMiddlename", "empty", "", "middle", "empty_mid@example.com","test_pass6", "2024-07-22", 5, false}, + {"AddEmptyMiddlename", "empty", "", "middle", "empty_mid@example.com","test_pass6", "2024-07-22", 4, false}, {"AddEmptyLastname", "empty", "email", "", "empty_last@example.com","test_pass7", "2024-07-22", 0, true}, {"AddEmptyEmail", "empty", "", "email", "", "test_pass7","2024-07-22", 0, true}, {"AddEmptyPassword", "empty", "pass", "user","empty_pass_user@example.com", "", "2021-07-22", 0, true}, @@ -179,9 +178,9 @@ func TestAddUser(t *testing.T) { {"MultipleFailures1", "user_fail_1","middle","last", "user_fail@example.com","","2024-10-22", 0, true}, // Should fail {"MultipleFailures2", "user_fail","middle","last", "","password","2024-10-22", 0, true}, // Should fail {"MultipleFailures3", "","middle","last", "user_fail@example.com","password", "2024-10-22", 0, true}, // Should fail - {"ValidAfterFailures", "valid_user", "middle","last", "valid_user@example.com","password", "2024-10-22", 3, false}, // Should be ID 3 after failures - {"CreateAndReuseOpenID1", "temp_user", "middle","last", "temp_user@example.com","password", "2024-10-22", 4, false}, // Should be ID 4 - {"ReuseOpenID", "new_user", "middle","last", "new_user@example.com", "password","2024-10-22", 4, false}, // Should reuse ID 4 + {"ValidAfterFailures", "valid_user", "middle","last", "valid_user@example.com","password", "2024-10-22", 5, false}, // Should be ID 5 after failures + {"CreateAndReuseOpenID1", "temp_user", "middle","last", "temp_user@example.com","password", "2024-10-22", 6, false}, // Should be ID 6 + {"ReuseOpenID", "new_user", "middle","last", "new_user@example.com", "password","2024-10-22", 6, false}, // Should reuse ID 6 // TODO: The functionality for this test need to be implemented // {"AddInvalidEmail", "invalid_email_user","middle","last", "invalidemail.com", "test_pass7", "2024-10-22", 0, true}, // TODO: The functionality for this test need to be implemented From 90c9fb8d8e8fdf906df253918ac4d75285b692ee Mon Sep 17 00:00:00 2001 From: Ali Shah <89487837+Alishah634@users.noreply.github.com> Date: Sat, 20 Jul 2024 14:39:29 -0400 Subject: [PATCH 6/8] Added email check to prevent duplicate tests still being added to the db and incrementing the id counter. --- internal/database/mysql_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/database/mysql_test.go b/internal/database/mysql_test.go index c026df6..e7627c9 100644 --- a/internal/database/mysql_test.go +++ b/internal/database/mysql_test.go @@ -207,6 +207,8 @@ func TestAddUser(t *testing.T) { _, err = probe.Exec("DELETE FROM users WHERE id = ?", id) if err != nil { t.Fatalf("Failed to delete user - %v for case: %s", err, tc.name) + } else { + t.Log("Deleted user for case: ", tc.name) } } } From a28204d8bb87101d185cac7df444dc29bb270fa2 Mon Sep 17 00:00:00 2001 From: Ali Shah <89487837+Alishah634@users.noreply.github.com> Date: Sat, 20 Jul 2024 14:40:27 -0400 Subject: [PATCH 7/8] Added email check to prevent duplicate tests still being added to the db and incrementing the id counter. --- internal/database/mysql.go | 87 ++++++++++++++++++++++----------- internal/database/mysql_test.go | 8 +-- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/internal/database/mysql.go b/internal/database/mysql.go index 8195bc3..7b9a812 100644 --- a/internal/database/mysql.go +++ b/internal/database/mysql.go @@ -131,55 +131,77 @@ func (db *MySQLDatabase) EmailExists(email string) (bool, error) { return exists, nil } -func (db *MySQLDatabase) getOpenID() (id uint64, err error) { - err = db.underlyingDB.QueryRow("SELECT id FROM openid LIMIT 1").Scan(&id) +func (db *MySQLDatabase) getOpenID(tx *sql.Tx) (uint64, error) { + var id uint64 + err := tx.QueryRow("SELECT id FROM openid LIMIT 1").Scan(&id) if err != nil && !errors.Is(err, sql.ErrNoRows) { log.Println("Failed to query for an openid -", err) return 0, errors.New("invalid openid") } - return + return id, nil } -func (db *MySQLDatabase) AddUser(firstName, middleName, lastName, email, password, dateOfBirth string) ( - id uint64, err error) { +func (db *MySQLDatabase) AddUser(firstName, middleName, lastName, email, password, dateOfBirth string) (id uint64, err error) { if len(firstName) == 0 || len(lastName) == 0 || len(email) == 0 || len(password) == 0 || len(dateOfBirth) == 0 { return 0, errors.New("empty fields") } - // Check if email already exists - + //Check if email already exists if emailExists, err := db.EmailExists(email); err != nil { return 0, errors.New("internal server error") } else if emailExists { return 0, errors.New("email already exists") - } - - query := "INSERT INTO users (first_name, middle_name, last_name, email, password, date_of_birth" - if openID, err := db.getOpenID(); err != nil && !errors.Is(err, sql.ErrNoRows) { - log.Println("Error getting open id -", err) - return id, errors.New("internal server error") - } else if openID == 0 { - query += `) VALUES (?, ?, ?, ?, ?, ?)` - } else { - log.Println("Re-using open id:", openID, "for {"+email+"}") - id = openID - query += `, id) VALUES (?, ?, ?, ?, ?, ?, ` + strconv.FormatUint(id, 10) + `)` - if _, err = db.underlyingDB.Exec("DELETE FROM openid WHERE id = ?", id); err != nil { - log.Println("Error deleting open id -", err) - return id, errors.New("internal server error") - } - } + } + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { log.Println("Error hashing password -", err) return id, errors.New("internal server error") } - - result, err := db.underlyingDB.Exec(query, firstName, middleName, lastName, email, hashedPassword, dateOfBirth) + + // Start transaction + tx, err := db.underlyingDB.Begin() + if err != nil { + log.Println("Error starting transaction -", err) + return 0, errors.New("internal server error") + } + defer func() { + if err != nil { + tx.Rollback() + } + }() + + // Get open ID + openID, err := db.getOpenID(tx) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + log.Println("Error getting open ID -", err) + return 0, errors.New("internal server error") + } + + // Build query + var query string + if openID == 0 { + query = "INSERT INTO users (first_name, middle_name, last_name, email, password, date_of_birth) VALUES (?, ?, ?, ?, ?, ?)" + } else { + log.Println("Re-using open ID:", openID, "for {"+email+"}") + query = "INSERT INTO users (first_name, middle_name, last_name, email, password, date_of_birth, id) VALUES (?, ?, ?, ?, ?, ?, " + strconv.FormatUint(openID, 10) + ")" + if _, err = tx.Exec("DELETE FROM openid WHERE id = ?", openID); err != nil { + log.Println("Error deleting open ID -", err) + return 0, errors.New("internal server error") + } + } + + // Execute query + result, err := tx.Exec(query, firstName, middleName, lastName, email, string(hashedPassword), dateOfBirth) if err != nil { log.Println("Error adding user -", err) + if openID != 0 { + tx.Exec("INSERT INTO openid (id) VALUES (?)", openID) + } return 0, errors.New("internal server error") } - if id == 0 { + + // Get inserted user ID + if openID == 0 { userid, err := result.LastInsertId() if err != nil { log.Println("Error getting user ID -", err) @@ -187,8 +209,17 @@ func (db *MySQLDatabase) AddUser(firstName, middleName, lastName, email, passwor } id = uint64(userid) log.Println("All existing IDs in use, assigning new ID:", id, "to {"+email+"}") + } else { + id = openID } - return + + // Commit transaction + if err := tx.Commit(); err != nil { + log.Println("Error committing transaction -", err) + return 0, errors.New("internal server error") + } + + return id, nil } func (db *MySQLDatabase) GetUserID(email string) (id uint64) { diff --git a/internal/database/mysql_test.go b/internal/database/mysql_test.go index e7627c9..53a6dd6 100644 --- a/internal/database/mysql_test.go +++ b/internal/database/mysql_test.go @@ -174,16 +174,16 @@ func TestAddUser(t *testing.T) { {"AddEmptyPassword", "empty", "pass", "user","empty_pass_user@example.com", "", "2021-07-22", 0, true}, {"AddEmptyDateOfBirth", "empty", "", "DOB", "empty_dob@example.com","test_pass8", "", 0, true}, - // WIP: This test case is not implemented yet!!!: {"MultipleFailures1", "user_fail_1","middle","last", "user_fail@example.com","","2024-10-22", 0, true}, // Should fail {"MultipleFailures2", "user_fail","middle","last", "","password","2024-10-22", 0, true}, // Should fail {"MultipleFailures3", "","middle","last", "user_fail@example.com","password", "2024-10-22", 0, true}, // Should fail {"ValidAfterFailures", "valid_user", "middle","last", "valid_user@example.com","password", "2024-10-22", 5, false}, // Should be ID 5 after failures - {"CreateAndReuseOpenID1", "temp_user", "middle","last", "temp_user@example.com","password", "2024-10-22", 6, false}, // Should be ID 6 - {"ReuseOpenID", "new_user", "middle","last", "new_user@example.com", "password","2024-10-22", 6, false}, // Should reuse ID 6 + + // WIP: These test cases to Re-use open id are not implemented yet!!! This DOES NOT WORK ATM!!!: + //{"CreateAndReuseOpenID1", "temp_user", "middle","last", "temp_user@example.com","password", "2024-10-22", 6, false}, // Should be ID 6 + //{"ReuseOpenID", "new_user", "middle","last", "new_user@example.com", "password","2024-10-22", 6, false}, // Should reuse ID 6 // TODO: The functionality for this test need to be implemented // {"AddInvalidEmail", "invalid_email_user","middle","last", "invalidemail.com", "test_pass7", "2024-10-22", 0, true}, // TODO: The functionality for this test need to be implemented - } for _, tc := range testCases { From 0b07897c36e28dbf83ce40a160ae0214e70904bb Mon Sep 17 00:00:00 2001 From: Alishah634 Date: Wed, 31 Jul 2024 18:23:02 -0400 Subject: [PATCH 8/8] updated changes --- internal/database/mysql.go | 490 ++++++++++++++++++------------------- 1 file changed, 245 insertions(+), 245 deletions(-) diff --git a/internal/database/mysql.go b/internal/database/mysql.go index 7b9a812..ff343b8 100644 --- a/internal/database/mysql.go +++ b/internal/database/mysql.go @@ -1,246 +1,246 @@ -// Copyright (c) 2024 Seoyoung Cho and Carlos Andres Cotera Jurado. - -package database - -import ( - "database/sql" - "errors" - "log" - "os" - "strconv" - "strings" - - _ "github.com/go-sql-driver/mysql" - "github.com/matcha-devs/matcha/internal" - "golang.org/x/crypto/bcrypt" -) - -type MySQLDatabase struct { - rootDSN string - dbName string - underlyingDB *sql.DB -} - -func New(dbName string, username string, password string) (mysql *MySQLDatabase) { - // Initialized struct to be returned. - mysql = &MySQLDatabase{ - rootDSN: username + ":" + password + "@tcp(localhost:3306)/", - dbName: dbName, - underlyingDB: nil, - } - - // Open a separate connection to the root DSN and create the database if it does not exist - initDB, err := sql.Open("mysql", mysql.rootDSN+"?multiStatements=true") - if err != nil { - log.Fatalln("Error opening MySQL root DSN -", err) - } - if err = initDB.Ping(); err != nil { - if err = initDB.Close(); err != nil { - log.Fatalln("Error closing broken MySQL root DSN -", err) - } - log.Fatalln("Error connecting to MySQL root DSN -", err) - } - _, err = initDB.Exec("CREATE DATABASE IF NOT EXISTS " + dbName) - if err != nil { - log.Fatalln("Error creating database -", err) - } - - // Move to the database and run 'init_tables.sql' script. - _, err = initDB.Exec("USE " + dbName) - if err != nil { - log.Fatalln("Error using", dbName, "-", err) - } - initScript, err := os.ReadFile("internal/database/queries/init_tables.sql") - if err != nil { - log.Fatalln("Error reading init_tables.sql file -", err) - } - if _, err = initDB.Exec(string(initScript)); err != nil { - log.Fatalln("Error executing init_tables.sql -", err) - } - if err := initDB.Close(); err != nil { - log.Fatalln("Error closing init DB -", err) - } - return -} - -func (db *MySQLDatabase) Open() (err error) { - if db.underlyingDB, err = sql.Open("mysql", db.rootDSN+db.dbName+"?parseTime=true"); err != nil { - log.Println("Error opening database -", err) - return - } - log.Println("MySQL Database connecting to", db.rootDSN[strings.Index(db.rootDSN, "@"):]+db.dbName, "🫡") - if err = db.underlyingDB.Ping(); err != nil { - if err = db.underlyingDB.Close(); err != nil { - log.Println("Error closing broken database -", err) - } - log.Println("Error connecting to database -", err) - } - return -} - -func (db *MySQLDatabase) Close() (err error) { - if err = db.underlyingDB.Close(); err != nil { - log.Println("underlying database close failure -", err) - } else { - log.Println("MySQL database has closed 👋🏽") - } - return -} - -func (db *MySQLDatabase) AuthenticateLogin(email, password string) (id uint64, err error) { - var hash []byte - err = db.underlyingDB.QueryRow("SELECT id, password FROM users WHERE BINARY email = ?", email).Scan( - &id, &hash, - ) - if errors.Is(err, sql.ErrNoRows) { - return 0, errors.New("invalid email") - } - if err = bcrypt.CompareHashAndPassword(hash, []byte(password)); err != nil { - return 0, errors.New("invalid password") - } - return -} - -func (db *MySQLDatabase) GetUser(id uint64) (user *internal.User) { - user = &internal.User{} - err := db.underlyingDB.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan( - &user.ID, &user.FirstName, &user.MiddleName, &user.LastName, &user.Email, &user.Password, &user.DateOfBirth, - &user.CreatedOn) - if errors.Is(err, sql.ErrNoRows) { - log.Println("No user with ID:", id, "-", err) - return nil - } else if err != nil { - log.Println("Failed to query users for ID:", id, "-", err) - return nil - } else if !user.IsValid() { - log.Println("Malformed user with ID:", id, "-", user) - return nil - } - return -} - -// EmailExists checks if an email already exists in the users table. -func (db *MySQLDatabase) EmailExists(email string) (bool, error) { - var exists bool - query := "SELECT EXISTS (SELECT 1 FROM users WHERE email = ?)" - err := db.underlyingDB.QueryRow(query, email).Scan(&exists) - if err != nil { - log.Println("Error checking email existence -", err) - return false, err - } - return exists, nil -} - -func (db *MySQLDatabase) getOpenID(tx *sql.Tx) (uint64, error) { - var id uint64 - err := tx.QueryRow("SELECT id FROM openid LIMIT 1").Scan(&id) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - log.Println("Failed to query for an openid -", err) - return 0, errors.New("invalid openid") - } - return id, nil -} - -func (db *MySQLDatabase) AddUser(firstName, middleName, lastName, email, password, dateOfBirth string) (id uint64, err error) { - if len(firstName) == 0 || len(lastName) == 0 || len(email) == 0 || len(password) == 0 || len(dateOfBirth) == 0 { - return 0, errors.New("empty fields") - } - //Check if email already exists - if emailExists, err := db.EmailExists(email); err != nil { - return 0, errors.New("internal server error") - } else if emailExists { - return 0, errors.New("email already exists") - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - log.Println("Error hashing password -", err) - return id, errors.New("internal server error") - } - - // Start transaction - tx, err := db.underlyingDB.Begin() - if err != nil { - log.Println("Error starting transaction -", err) - return 0, errors.New("internal server error") - } - defer func() { - if err != nil { - tx.Rollback() - } - }() - - // Get open ID - openID, err := db.getOpenID(tx) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - log.Println("Error getting open ID -", err) - return 0, errors.New("internal server error") - } - - // Build query - var query string - if openID == 0 { - query = "INSERT INTO users (first_name, middle_name, last_name, email, password, date_of_birth) VALUES (?, ?, ?, ?, ?, ?)" - } else { - log.Println("Re-using open ID:", openID, "for {"+email+"}") - query = "INSERT INTO users (first_name, middle_name, last_name, email, password, date_of_birth, id) VALUES (?, ?, ?, ?, ?, ?, " + strconv.FormatUint(openID, 10) + ")" - if _, err = tx.Exec("DELETE FROM openid WHERE id = ?", openID); err != nil { - log.Println("Error deleting open ID -", err) - return 0, errors.New("internal server error") - } - } - - // Execute query - result, err := tx.Exec(query, firstName, middleName, lastName, email, string(hashedPassword), dateOfBirth) - if err != nil { - log.Println("Error adding user -", err) - if openID != 0 { - tx.Exec("INSERT INTO openid (id) VALUES (?)", openID) - } - return 0, errors.New("internal server error") - } - - // Get inserted user ID - if openID == 0 { - userid, err := result.LastInsertId() - if err != nil { - log.Println("Error getting user ID -", err) - return 0, errors.New("internal server error") - } - id = uint64(userid) - log.Println("All existing IDs in use, assigning new ID:", id, "to {"+email+"}") - } else { - id = openID - } - - // Commit transaction - if err := tx.Commit(); err != nil { - log.Println("Error committing transaction -", err) - return 0, errors.New("internal server error") - } - - return id, nil -} - -func (db *MySQLDatabase) GetUserID(email string) (id uint64) { - // TODO(@seoyoungcho213): might not use this anymore cuz of cookie - if err := db.underlyingDB.QueryRow( - "SELECT id FROM users WHERE BINARY email = ?", email, - ).Scan(&id); err != nil && !errors.Is(err, sql.ErrNoRows) { - log.Println("Error querying users for email:"+email, "-", err) - return 0 - } - return id -} - -func (db *MySQLDatabase) DeleteUser(id uint64) (err error) { - if _, err = db.underlyingDB.Exec("INSERT INTO openid (id) VALUES(?)", id); err != nil { - log.Println("Error inserting openID", id, " to the table -", err) - return errors.New("internal server error") - } - if _, err = db.underlyingDB.Exec("DELETE FROM users WHERE BINARY id = ?", id); err != nil { - log.Println("Error deleting the user id", id, " -", err) - return errors.New("internal server error") - } - return err +// Copyright (c) 2024 Seoyoung Cho and Carlos Andres Cotera Jurado. + +package database + +import ( + "database/sql" + "errors" + "log" + "os" + "strconv" + "strings" + + _ "github.com/go-sql-driver/mysql" + "github.com/matcha-devs/matcha/internal" + "golang.org/x/crypto/bcrypt" +) + +type MySQLDatabase struct { + rootDSN string + dbName string + underlyingDB *sql.DB +} + +func New(dbName string, username string, password string) (mysql *MySQLDatabase) { + // Initialized struct to be returned. + mysql = &MySQLDatabase{ + rootDSN: username + ":" + password + "@tcp(localhost:3306)/", + dbName: dbName, + underlyingDB: nil, + } + + // Open a separate connection to the root DSN and create the database if it does not exist + initDB, err := sql.Open("mysql", mysql.rootDSN+"?multiStatements=true") + if err != nil { + log.Fatalln("Error opening MySQL root DSN -", err) + } + if err = initDB.Ping(); err != nil { + if err = initDB.Close(); err != nil { + log.Fatalln("Error closing broken MySQL root DSN -", err) + } + log.Fatalln("Error connecting to MySQL root DSN -", err) + } + _, err = initDB.Exec("CREATE DATABASE IF NOT EXISTS " + dbName) + if err != nil { + log.Fatalln("Error creating database -", err) + } + + // Move to the database and run 'init_tables.sql' script. + _, err = initDB.Exec("USE " + dbName) + if err != nil { + log.Fatalln("Error using", dbName, "-", err) + } + initScript, err := os.ReadFile("internal/database/queries/init_tables.sql") + if err != nil { + log.Fatalln("Error reading init_tables.sql file -", err) + } + if _, err = initDB.Exec(string(initScript)); err != nil { + log.Fatalln("Error executing init_tables.sql -", err) + } + if err := initDB.Close(); err != nil { + log.Fatalln("Error closing init DB -", err) + } + return +} + +func (db *MySQLDatabase) Open() (err error) { + if db.underlyingDB, err = sql.Open("mysql", db.rootDSN+db.dbName+"?parseTime=true"); err != nil { + log.Println("Error opening database -", err) + return + } + log.Println("MySQL Database connecting to", db.rootDSN[strings.Index(db.rootDSN, "@"):]+db.dbName, "🫡") + if err = db.underlyingDB.Ping(); err != nil { + if err = db.underlyingDB.Close(); err != nil { + log.Println("Error closing broken database -", err) + } + log.Println("Error connecting to database -", err) + } + return +} + +func (db *MySQLDatabase) Close() (err error) { + if err = db.underlyingDB.Close(); err != nil { + log.Println("underlying database close failure -", err) + } else { + log.Println("MySQL database has closed 👋🏽") + } + return +} + +func (db *MySQLDatabase) AuthenticateLogin(email, password string) (id uint64, err error) { + var hash []byte + err = db.underlyingDB.QueryRow("SELECT id, password FROM users WHERE BINARY email = ?", email).Scan( + &id, &hash, + ) + if errors.Is(err, sql.ErrNoRows) { + return 0, errors.New("invalid email") + } + if err = bcrypt.CompareHashAndPassword(hash, []byte(password)); err != nil { + return 0, errors.New("invalid password") + } + return +} + +func (db *MySQLDatabase) GetUser(id uint64) (user *internal.User) { + user = &internal.User{} + err := db.underlyingDB.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan( + &user.ID, &user.FirstName, &user.MiddleName, &user.LastName, &user.Email, &user.Password, &user.DateOfBirth, + &user.CreatedOn) + if errors.Is(err, sql.ErrNoRows) { + log.Println("No user with ID:", id, "-", err) + return nil + } else if err != nil { + log.Println("Failed to query users for ID:", id, "-", err) + return nil + } else if !user.IsValid() { + log.Println("Malformed user with ID:", id, "-", user) + return nil + } + return +} + +// EmailExists checks if an email already exists in the users table. +func (db *MySQLDatabase) EmailExists(email string) (bool, error) { + var exists bool + query := "SELECT EXISTS (SELECT 1 FROM users WHERE email = ?)" + err := db.underlyingDB.QueryRow(query, email).Scan(&exists) + if err != nil { + log.Println("Error checking email existence -", err) + return false, err + } + return exists, nil +} + +func (db *MySQLDatabase) getOpenID(tx *sql.Tx) (uint64, error) { + var id uint64 + err := tx.QueryRow("SELECT id FROM openid LIMIT 1").Scan(&id) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + log.Println("Failed to query for an openid -", err) + return 0, errors.New("invalid openid") + } + return id, nil +} + +func (db *MySQLDatabase) AddUser(firstName, middleName, lastName, email, password, dateOfBirth string) (id uint64, err error) { + if len(firstName) == 0 || len(lastName) == 0 || len(email) == 0 || len(password) == 0 || len(dateOfBirth) == 0 { + return 0, errors.New("empty fields") + } + //Check if email already exists + if emailExists, err := db.EmailExists(email); err != nil { + return 0, errors.New("internal server error") + } else if emailExists { + return 0, errors.New("email already exists") + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + log.Println("Error hashing password -", err) + return id, errors.New("internal server error") + } + + // Start transaction + tx, err := db.underlyingDB.Begin() + if err != nil { + log.Println("Error starting transaction -", err) + return 0, errors.New("internal server error") + } + defer func() { + if err != nil { + tx.Rollback() + } + }() + + // Get open ID + openID, err := db.getOpenID(tx) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + log.Println("Error getting open ID -", err) + return 0, errors.New("internal server error") + } + + // Build query + var query string + if openID == 0 { + query = "INSERT INTO users (first_name, middle_name, last_name, email, password, date_of_birth) VALUES (?, ?, ?, ?, ?, ?)" + } else { + log.Println("Re-using open ID:", openID, "for {"+email+"}") + query = "INSERT INTO users (first_name, middle_name, last_name, email, password, date_of_birth, id) VALUES (?, ?, ?, ?, ?, ?, " + strconv.FormatUint(openID, 10) + ")" + if _, err = tx.Exec("DELETE FROM openid WHERE id = ?", openID); err != nil { + log.Println("Error deleting open ID -", err) + return 0, errors.New("internal server error") + } + } + + // Execute query + result, err := tx.Exec(query, firstName, middleName, lastName, email, string(hashedPassword), dateOfBirth) + if err != nil { + log.Println("Error adding user -", err) + if openID != 0 { + tx.Exec("INSERT INTO openid (id) VALUES (?)", openID) + } + return 0, errors.New("internal server error") + } + + // Get inserted user ID + if openID == 0 { + userid, err := result.LastInsertId() + if err != nil { + log.Println("Error getting user ID -", err) + return 0, errors.New("internal server error") + } + id = uint64(userid) + log.Println("All existing IDs in use, assigning new ID:", id, "to {"+email+"}") + } else { + id = openID + } + + // Commit transaction + if err := tx.Commit(); err != nil { + log.Println("Error committing transaction -", err) + return 0, errors.New("internal server error") + } + + return id, nil +} + +func (db *MySQLDatabase) GetUserID(email string) (id uint64) { + // TODO(@seoyoungcho213): might not use this anymore cuz of cookie + if err := db.underlyingDB.QueryRow( + "SELECT id FROM users WHERE BINARY email = ?", email, + ).Scan(&id); err != nil && !errors.Is(err, sql.ErrNoRows) { + log.Println("Error querying users for email:"+email, "-", err) + return 0 + } + return id +} + +func (db *MySQLDatabase) DeleteUser(id uint64) (err error) { + if _, err = db.underlyingDB.Exec("INSERT INTO openid (id) VALUES(?)", id); err != nil { + log.Println("Error inserting openID", id, " to the table -", err) + return errors.New("internal server error") + } + if _, err = db.underlyingDB.Exec("DELETE FROM users WHERE BINARY id = ?", id); err != nil { + log.Println("Error deleting the user id", id, " -", err) + return errors.New("internal server error") + } + return err } \ No newline at end of file