From 0259ca95c3ff5e079dc7b184b85b79f9faf40d17 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sun, 15 Mar 2026 09:11:49 +0800 Subject: [PATCH 1/5] refactor: update files to a JSON approach --- internal/auth/login.go | 95 ++++++++++----- internal/auth/signup.go | 260 ++++++++++++++++------------------------ templates/login.html | 73 +++++++++-- templates/signup.html | 113 ++++++++++++----- 4 files changed, 315 insertions(+), 226 deletions(-) diff --git a/internal/auth/login.go b/internal/auth/login.go index 2f05c9f..efa8fc9 100644 --- a/internal/auth/login.go +++ b/internal/auth/login.go @@ -1,71 +1,100 @@ package authentication import ( + "encoding/json" "fmt" "net/http" - message "unicard-go/internal/pkg" "golang.org/x/crypto/bcrypt" ) -// View Handler (GET) -// This function checks the URL for errors (e.g., ?error=invalid) -// and displays the red text if needed. -func (h *Handler) LoginView(w http.ResponseWriter, r *http.Request) { - fmt.Println("Login view is running...") - // Get the error code from the URL - errCode := r.URL.Query().Get("user") - - var msg string +// LoginRequest represents the JSON request for login +type LoginRequest struct { + Username string `json:"username"` // username, email, or full_name + Password string `json:"password"` +} - // Determine the message based on the error code - switch errCode { - case "invalid": - msg = "Wrong password" - case "notfound": - msg = "User not found" - } +// LoginResponse represents the JSON response for login +type LoginResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + UserID string `json:"user_id,omitempty"` +} - // Render the template with the message - h.Tpl.ExecuteTemplate(w, "login.html", message.MessageData{Error: msg}) +// View Handler (GET) +// Serves the login page template +func (h *Handler) LoginView(w http.ResponseWriter, r *http.Request) { + h.Tpl.ExecuteTemplate(w, "login.html", nil) } // Auth Handler (POST) -// Accepts login credentials: username, email, or full_name +// Accepts JSON login credentials: username, email, or full_name +// Returns JSON response with success status and message func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("loginauth running...") - r.ParseForm() - credential := r.PostFormValue("username") // This can be username, email, or full_name - password := r.PostFormValue("password") + w.Header().Set("Content-Type", "application/json") + + // Parse JSON request body + var loginReq LoginRequest // Define a struct to hold the login request data + err := json.NewDecoder(r.Body).Decode(&loginReq) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(LoginResponse{ + Success: false, + Message: "Invalid request format", + }) + return + } + + // Validate input + if loginReq.Username == "" || loginReq.Password == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(LoginResponse{ + Success: false, + Message: "Credential and password are required", + }) + return + } var hash string + var userID string // Query to check if credential matches username, email, or full_name - stmt := "SELECT password_hash FROM users WHERE username = ? OR email = ? OR full_name = ?" + stmt := "SELECT id, password_hash FROM users WHERE username = ? OR email = ? OR full_name = ?" - err := h.DB.QueryRow(stmt, credential, credential, credential).Scan(&hash) + err = h.DB.QueryRow(stmt, loginReq.Username, loginReq.Username, loginReq.Username).Scan(&userID, &hash) // User not found if err != nil { fmt.Println("User not found or DB error:", err) - // Redirect with ?user=notfound - http.Redirect(w, r, "/login?user=notfound", http.StatusSeeOther) + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(LoginResponse{ + Success: false, + Message: "User not found", + }) return } fmt.Println("Hash found, verifying...") - err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(loginReq.Password)) // SUCCESS if err == nil { fmt.Println("Login success") - http.Redirect(w, r, "/dashboard", http.StatusSeeOther) + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(LoginResponse{ + Success: true, + Message: "Login successful", + UserID: userID, + }) return } // Password mismatch - fmt.Println("Password mismatch") - - // Redirect with ?user=invalid - http.Redirect(w, r, "/login?user=invalid", http.StatusSeeOther) + fmt.Println("Password Incorrect:", err) + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(LoginResponse{ + Success: false, + Message: "Password incorrect", + }) } diff --git a/internal/auth/signup.go b/internal/auth/signup.go index b378688..3dc3168 100644 --- a/internal/auth/signup.go +++ b/internal/auth/signup.go @@ -3,16 +3,32 @@ package authentication import ( "crypto/rand" "database/sql" + "encoding/json" // Added for JSON support "fmt" "math/big" "net/http" "strings" "time" - structMessage "unicard-go/internal/pkg" "unicard-go/internal/pkg/account" ) -// User struct to hold signup data +// 1. Create a struct to catch the incoming JSON from the frontend +type SignupRequest struct { + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + CardNumber string `json:"cardNumber"` + Password string `json:"password"` + Email string `json:"email"` + ContactNumber string `json:"contactNumber"` +} + +// 2. Create a standard API response struct +type APIResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// User struct to hold signup data (Keep your existing one) type User struct { UserID string Username string @@ -28,242 +44,170 @@ type User struct { } // View Handler (GET) -// This function checks the URL for errors (e.g., ?error=invalid) -// and displays the red text if needed. +// You can now simplify this because JS handles the errors! func (h *Handler) SignupView(w http.ResponseWriter, r *http.Request) { fmt.Println("Signup view is running...") - // Get the error code from the URL - errCode := r.URL.Query().Get("error") - - var msg string - // Determine the message based on the error code - switch errCode { - case "cardnotfound": - msg = "Card number not found. Please check your card or contact support." - case "userid": - msg = "System error generating UserID. Please try again." - case "cardnumber": - msg = "System error generating CardNumber. Please try again." - case "invalidcard": - msg = "Invalid Card Number. Please check your card." - case "contactnumber": - msg = "Phone number already registered. Please use a different number." - case "email": - msg = "Email already registered. Please use a different email." - case "emptyfields": - msg = "Please fill in all fields." - case "genusername": - msg = "System error generating username. Please try again." - case "cardstatuscheck": - msg = "System error checking card status. Please try again." - case "cardstatus": - msg = "Card is invalid. Please contact support." - case "passwordshort": - msg = "Password must be at least 8 characters long." - case "gencardid": - msg = "System error generating CardID. Please try again." - case "dbinsert": - msg = "System error creating account. Please try again." - case "parseform": - msg = "Failed to parse form. Please try again." - case "hashpassword": - msg = "System error processing password. Please try again." - case "cardupdate": - msg = "System error activating card. Please contact support." - } - // Render the signup template with error message - h.Tpl.ExecuteTemplate(w, "signup.html", structMessage.MessageData{Error: msg}) + // Just serve the template. No need for the huge switch statement anymore. + h.Tpl.ExecuteTemplate(w, "signup.html", nil) } -// This function processes the signup form submission. -// It validates the input, checks for existing usernames and card numbers, -// hashes the password, and inserts the new user into the database. -// On success, it redirects to the login page. -// On failure, it re-renders the signup page with an error message. -// POST /signupauth +// Auth Handler (POST) - Converted to JSON API func (h *Handler) SignupHandler(w http.ResponseWriter, r *http.Request) { - fmt.Println("Signup is running...") + fmt.Println("Signup API is running...") + w.Header().Set("Content-Type", "application/json") - if err := r.ParseForm(); err != nil { - http.Redirect(w, r, "/signup?error=parseform", http.StatusSeeOther) + // Enforce POST method + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Method not allowed"}) return } - // strings.TrimSpace removes leading/trailing spaces. - // Example: " John " becomes "John". " " becomes "". - firstName := strings.TrimSpace(r.PostFormValue("firstName")) - lastName := strings.TrimSpace(r.PostFormValue("lastName")) - cardNum := strings.TrimSpace(r.PostFormValue("cardNumber")) - password := strings.TrimSpace(r.PostFormValue("password")) - email := strings.TrimSpace(r.PostFormValue("email")) - phone := strings.TrimSpace(r.PostFormValue("contactNumber")) - - // We check individual variables before putting them in the struct - if firstName == "" || lastName == "" || cardNum == "" || password == "" || email == "" || phone == "" { - fmt.Println("Validation Failed: One or more fields are empty.") - http.Redirect(w, r, "/signup?error=emptyfields", http.StatusSeeOther) + // Decode incoming JSON + var req SignupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Failed to parse JSON request"}) return } - // Now we know the data is clean and present + // Clean the inputs + req.FirstName = strings.TrimSpace(req.FirstName) + req.LastName = strings.TrimSpace(req.LastName) + req.CardNumber = strings.TrimSpace(req.CardNumber) + req.Password = strings.TrimSpace(req.Password) + req.Email = strings.TrimSpace(req.Email) + req.ContactNumber = strings.TrimSpace(req.ContactNumber) + + // Validation: Empty Fields + if req.FirstName == "" || req.LastName == "" || req.CardNumber == "" || req.Password == "" || req.Email == "" || req.ContactNumber == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Please fill in all fields."}) + return + } + + // Prepare User struct user := User{ Usertype: "Regular", - Username: firstName, // Using First Name as Username - Fullname: firstName + " " + lastName, - CardNumber: cardNum, - Password: password, - Email: email, - Phone: phone, + Username: req.FirstName, // Using First Name as temporary Username + Fullname: req.FirstName + " " + req.LastName, + CardNumber: req.CardNumber, + Password: req.Password, + Email: req.Email, + Phone: req.ContactNumber, } - // Check if username exists using helper function + // Generate Username generatedUsername, err := h.GenerateUniqueUsername() if err != nil { - fmt.Printf("Error generating username: %v\n", err) - http.Redirect(w, r, "/signup?error=genusername", http.StatusSeeOther) + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error generating username. Please try again."}) return } user.Username = generatedUsername - fmt.Printf("Assigned Username: %s\n", user.Username) - // Check card number if exist - // If exist push thru - // Variable to hold the status from the DB + // Check Card Status var cardStatus string - // Make sure your column name is correct (card_status vs status) based on your table definition query := "SELECT status FROM cards WHERE card_number = ?" err = h.DB.QueryRow(query, user.CardNumber).Scan(&cardStatus) - // Card does not exist if err == sql.ErrNoRows { - fmt.Printf("Validation Failed: Card Number %s does not exist in system.\n", user.CardNumber) - http.Redirect(w, r, "/signup?error=cardnotfound", http.StatusSeeOther) + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Card number not found. Please check your card."}) return - } - // System Error - if err != nil { - fmt.Printf("Validation Error: System error checking card: %v\n", err) - http.Redirect(w, r, "/signup?error=cardstatuscheck", http.StatusSeeOther) + } else if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error checking card status."}) return } - // Handle: Card Exists, but is NOT "Inactive" (e.g., Active, Blocked) - // This blocks 'Active', 'Blocked', 'Lost', 'Expired', etc. + if cardStatus != "Inactive" { - fmt.Printf("Validation Failed: Card %s is currently '%s'.\n", user.CardNumber, cardStatus) - http.Redirect(w, r, "/signup?error=cardstatus", http.StatusSeeOther) + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Card is currently '%s'. Please contact support.", cardStatus)}) return } - // Success: Proceed - fmt.Printf("Card %s is Inactive (Valid). Proceeding...\n", user.CardNumber) - // Password length check + // Password Length Check if len(user.Password) < 8 { - fmt.Printf("Validation Failed: Password too short (%d chars).\n", len(user.Password)) - http.Redirect(w, r, "/signup?error=passwordshort", http.StatusSeeOther) + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Password must be at least 8 characters long."}) return } - // Generate Hash for password + // Hash Password hashedPassword, err := account.HashPassword(user.Password) if err != nil { - fmt.Printf("Error hashing password: %v", err) - http.Redirect(w, r, "/signup?error=hashpassword", http.StatusSeeOther) + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error processing password."}) return } - fmt.Println("raw password is:", user.Password) - user.Password = hashedPassword // Store the hashed password - fmt.Printf("hashed password is: %v\n", user.Password) + user.Password = hashedPassword - // Check if Email Exists (Using our helper method) + // Check Email if exists, _ := account.IsEmailExist(h.DB, user.Email); exists { - fmt.Printf("Validation Failed: Email %s already exists.\n", user.Email) - http.Redirect(w, r, "/signup?error=email", http.StatusSeeOther) - //h.Tpl.ExecuteTemplate(w, "signup.html", structMessage.MessageData{Error: "Email already registered."}) + w.WriteHeader(http.StatusConflict) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Email already registered. Please use a different email."}) return } - fmt.Printf("Email %s is available.\n", user.Email) - // Check if Phone Exists (Using our helper method) + // Check Phone if exists, _ := h.isPhoneExist(user.Phone); exists { - fmt.Printf("Validation Failed: Phone %s already exists.\n", user.Phone) - http.Redirect(w, r, "/signup?error=contactnumber", http.StatusSeeOther) + w.WriteHeader(http.StatusConflict) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Phone number already registered. Please use a different number."}) return } - fmt.Printf("Phone %s is available.\n", user.Phone) - user.Phone = phone - // Generate unique UserID (12 digits) + // Generate IDs generateUserId, err := h.GenerateUserID() if err != nil { - fmt.Printf("Error generating UserID: %v", err) - http.Redirect(w, r, "/signup?error=userid", http.StatusSeeOther) + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error generating UserID."}) return } - fmt.Printf("Generated UserID: %d\n", generateUserId) user.UserID = fmt.Sprintf("%d", generateUserId) - // Generate unique CardID generateCardID, err := h.GenerateCardID() if err != nil { - fmt.Printf("Error generating CardID: %v", err) - http.Redirect(w, r, "/signup?error=gencardid", http.StatusSeeOther) + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error generating CardID."}) return } user.CardID = generateCardID - fmt.Printf("Generated CardID: %s\n", user.CardID) - // Time of account creation + // Get Timestamp & Balance user.CreatedAt, _ = CurrentTimestamp() - - // Check initial balance base on card - // Using helper function from account package user.Balance, err = h.GetInitialBalance(user.CardNumber) if err != nil { - fmt.Printf("Error: Failed to retrieve initial balance for card %s: %v\n", user.CardNumber, err) - http.Redirect(w, r, "/signup?error=invalidcard", http.StatusSeeOther) + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Invalid Card Number."}) return } - fmt.Printf("Initial balance for card %s is %.2f\n", user.CardNumber, user.Balance) - - // Insert to DB - query = "INSERT INTO users (user_id, username, full_name, email, phone, password_hash, card_id, card_number, user_type, balance, created_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - - // Execute - _, err = h.DB.Exec( - query, - user.UserID, - user.Username, - user.Fullname, - user.Email, - user.Phone, - user.Password, - user.CardID, - user.CardNumber, - user.Usertype, - user.Balance, - user.CreatedAt, - ) + + // Insert into DB + insertQuery := "INSERT INTO users (user_id, username, full_name, email, phone, password_hash, card_id, card_number, user_type, balance, created_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + _, err = h.DB.Exec(insertQuery, user.UserID, user.Username, user.Fullname, user.Email, user.Phone, user.Password, user.CardID, user.CardNumber, user.Usertype, user.Balance, user.CreatedAt) if err != nil { - fmt.Printf("CRITICAL ERROR: Failed to insert user into database: %v\n", err) - http.Redirect(w, r, "/signup?error=dbinsert", http.StatusSeeOther) + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error creating account. Please try again."}) return } - // Update card status from "Inactive" to "Active" + // Activate Card updateQuery := "UPDATE cards SET status = 'Active' WHERE card_number = ?" _, err = h.DB.Exec(updateQuery, user.CardNumber) if err != nil { - fmt.Printf("ERROR: Failed to update card status to Active: %v\n", err) - http.Redirect(w, r, "/signup?error=cardupdate", http.StatusSeeOther) + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error activating card."}) return } - fmt.Printf("Card %s status updated to Active\n", user.CardNumber) - // Succesfully account creation - fmt.Printf("account successfully created!") - http.Redirect(w, r, "/login", http.StatusSeeOther) + // SUCCESS! Send a JSON success message instead of redirecting. + fmt.Println("Account successfully created!") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(APIResponse{Success: true, Message: "Account created successfully!"}) } +// ... [KEEP ALL YOUR HELPER FUNCTIONS DOWN HERE EXACTLY AS THEY WERE] ... // ---Helper Functions--- // GenerateUniqueUsername creates a random unique username diff --git a/templates/login.html b/templates/login.html index f957a5c..1c5839d 100644 --- a/templates/login.html +++ b/templates/login.html @@ -10,19 +10,78 @@

Login Page

-
+ -

+

-

+

+ Forgot Password? -

dont have account?Sign Up


- {{if .Error}} -

{{.Error}}

- {{end}} +

Don't have an account? Sign Up


+ + +
+ + \ No newline at end of file diff --git a/templates/signup.html b/templates/signup.html index 9cadeab..bb38e59 100644 --- a/templates/signup.html +++ b/templates/signup.html @@ -1,41 +1,98 @@ - - Signup Page + Sign Up - - -

Signup Page

+

Create an Account

+ +
+ +

+ + +

- - * - - * -

+ +

+ +

+ + +

+ + +

- * - +

Already have an account? Login here


- * -

- - * -

- * -

- - - {{if .Error}} -

{{.Error}}

- {{end}} - {{if .Success}} -

{{.Success}}

- {{end}} - + + + +
+ + \ No newline at end of file From 287506fd97e067ee9d8a43fa25c5e0dae208ed14 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sun, 15 Mar 2026 09:15:31 +0800 Subject: [PATCH 2/5] update endpoint to restAPI endpoint --- cmd/app/main.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/cmd/app/main.go b/cmd/app/main.go index 54f4aa1..e72bd57 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -67,19 +67,18 @@ func main() { mux.Handle("/assets/", http.StripPrefix("/assets/", fileServer)) // --- ROUTES --- - // GET Request: Shows the page AND handles the ?error=invalid logic - // We use the function from the auth package now - mux.HandleFunc("GET /login", authHandler.LoginView) - mux.HandleFunc("GET /signup", authHandler.SignupHandler) + // POST Request: JSON API endpoints + mux.HandleFunc("POST /api/v1/loginauth", authHandler.LoginAuthHandler) // Login authentication endpoint + mux.HandleFunc("POST /api/v1/signupauth", authHandler.SignupHandler) + + // GET Request: SSR endpoints (for signup, admin) + mux.HandleFunc("GET /login", authHandler.LoginView) // + mux.HandleFunc("GET /signup", authHandler.SignupView) mux.HandleFunc("GET /admin/addcard", adminHanlder.AddCardsView) mux.HandleFunc("GET /admin/deactivatecard", adminHanlder.DeactivateView) - // POST Request: Processes the form - // This matches
in your HTML - mux.HandleFunc("POST /loginauth", authHandler.LoginAuthHandler) - mux.HandleFunc("POST /signupauth", authHandler.SignupHandler) - mux.HandleFunc("POST /admin/addcardauth", adminHanlder.AddCardHandler) - mux.HandleFunc("POST /admin/deactivatecardauth", adminHanlder.DeactivateCardHanlder) + mux.HandleFunc("POST /api/v1/admin/addcardauth", adminHanlder.AddCardHandler) + mux.HandleFunc("POST /api/v1/admin/deactivatecardauth", adminHanlder.DeactivateCardHanlder) // Dashboard mux.HandleFunc("/dashboard", dashboardHandler) @@ -96,3 +95,5 @@ func dashboardHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("Dashboard is running") tpl.ExecuteTemplate(w, "dashboard.html", nil) } + +// View handler - just serve the template From a805b43f046b969544f3dc2ce7cd15431966f921 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:22:15 +0800 Subject: [PATCH 3/5] feat: Implement dashboard and login features with JSON API support - Added dashboard handler and view with user transaction details. - Enhanced login functionality with JSON response handling. - Introduced rate limiting middleware for login attempts. - Updated signup process to handle JSON requests and responses. - Refactored HTML templates for better user experience and styling. - Created a new JSON write utility for consistent API responses. - Added CSS for required field indicators in forms. --- assets/style/index.css | 4 + cmd/app/main.go | 7 +- go.mod | 1 + go.sum | 2 + internal/auth/dashboard.go | 54 ++++++++ internal/auth/login.go | 86 ++++++------ internal/auth/rateLimit.go | 55 ++++++++ internal/auth/signup.go | 211 +++++++++++++++++------------- internal/pkg/account/services.go | 8 +- internal/pkg/handler/jsonWrite.go | 26 ++++ templates/dashboard.html | 58 +++++++- templates/login.html | 23 ++-- templates/signup.html | 68 +++++----- 13 files changed, 412 insertions(+), 191 deletions(-) create mode 100644 assets/style/index.css create mode 100644 internal/auth/dashboard.go create mode 100644 internal/auth/rateLimit.go create mode 100644 internal/pkg/handler/jsonWrite.go diff --git a/assets/style/index.css b/assets/style/index.css new file mode 100644 index 0000000..40fa6e9 --- /dev/null +++ b/assets/style/index.css @@ -0,0 +1,4 @@ +.required { + color: red; + margin-left: 3px; +} \ No newline at end of file diff --git a/cmd/app/main.go b/cmd/app/main.go index e72bd57..1578869 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -74,15 +74,14 @@ func main() { // GET Request: SSR endpoints (for signup, admin) mux.HandleFunc("GET /login", authHandler.LoginView) // mux.HandleFunc("GET /signup", authHandler.SignupView) + mux.HandleFunc("GET /dashboard", authHandler.DashboardHandler) + + // endpoints for admin mux.HandleFunc("GET /admin/addcard", adminHanlder.AddCardsView) mux.HandleFunc("GET /admin/deactivatecard", adminHanlder.DeactivateView) - mux.HandleFunc("POST /api/v1/admin/addcardauth", adminHanlder.AddCardHandler) mux.HandleFunc("POST /api/v1/admin/deactivatecardauth", adminHanlder.DeactivateCardHanlder) - // Dashboard - mux.HandleFunc("/dashboard", dashboardHandler) - // Start Server fmt.Println("Server started on: http://" + serverAddress + port) if err := http.ListenAndServe(port, mux); err != nil { diff --git a/go.mod b/go.mod index 17b7473..aae0e72 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.45.0 + golang.org/x/time v0.15.0 ) require ( diff --git a/go.sum b/go.sum index b3854f0..bffa25e 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/auth/dashboard.go b/internal/auth/dashboard.go new file mode 100644 index 0000000..2229575 --- /dev/null +++ b/internal/auth/dashboard.go @@ -0,0 +1,54 @@ +package authentication + +import ( + "fmt" + "net/http" +) + +// Transaction struct represents a user's transaction for the dashboard view +type Transaction struct { + Date string `db:"date" json:"date"` + Description string `db:"description" json:"description"` + Type string `db:"transaction_type" json:"type"` + Amount float64 `db:"transaction_amount" json:"amount"` +} + +// DashboardUser info struct for the user dashboard view +type DashboardUser struct { + ID int `db:"id" json:"id,omitempty"` + UserID string `db:"user_id" json:"user_id,omitempty"` + Username string `db:"username" json:"username"` + Name string `db:"name" json:"name"` + Transaction string `db:"transaction" json:"transaction"` + Balance float64 `db:"balance" json:"balance"` + LoyaltyPoints int `db:"loyalty_points" json:"loyalty_points"` + AccountType string `db:"account_type" json:"account_type"` + RecentTransactions []Transaction `json:"recent_transactions"` // Add recent transactions to the dashboard response +} + +func (h *Handler) DashboardView(w http.ResponseWriter, r *http.Request) { + fmt.Println("Dashboard view is running...") + h.Tpl.ExecuteTemplate(w, "dashboard.html", nil) +} + +func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { + fmt.Println("Dashboard handler is running...") + +// For demonstration, we'll create a dummy user with some transactions + dashboardUser := DashboardUser{ + ID: 1, + UserID: "user123", + Username: "johndoe", + Name: "John Doe", + Transaction: "Transaction", + Balance: 150.75, + LoyaltyPoints: 200, + AccountType: "Premium", + RecentTransactions: []Transaction{ + {Date: "2024-06-01", Description: "Grocery Store", Type: "Debit", Amount: 50.25}, + {Date: "2024-06-03", Description: "Salary", Type: "Credit", Amount: 2000.00}, + {Date: "2024-06-05", Description: "Online Shopping", Type: "Debit", Amount: 75.50}, + }, + } + h.Tpl.ExecuteTemplate(w, "dashboard.html", dashboardUser) +} \ No newline at end of file diff --git a/internal/auth/login.go b/internal/auth/login.go index efa8fc9..d9aa36f 100644 --- a/internal/auth/login.go +++ b/internal/auth/login.go @@ -2,8 +2,9 @@ package authentication import ( "encoding/json" - "fmt" + "log" "net/http" + jsonwrite "unicard-go/internal/pkg/handler" "golang.org/x/crypto/bcrypt" ) @@ -11,19 +12,13 @@ import ( // LoginRequest represents the JSON request for login type LoginRequest struct { Username string `json:"username"` // username, email, or full_name - Password string `json:"password"` -} - -// LoginResponse represents the JSON response for login -type LoginResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - UserID string `json:"user_id,omitempty"` + Password string `json:"password"` } // View Handler (GET) // Serves the login page template func (h *Handler) LoginView(w http.ResponseWriter, r *http.Request) { + log.Println("Login view is running...") h.Tpl.ExecuteTemplate(w, "login.html", nil) } @@ -31,70 +26,65 @@ func (h *Handler) LoginView(w http.ResponseWriter, r *http.Request) { // Accepts JSON login credentials: username, email, or full_name // Returns JSON response with success status and message func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { - fmt.Println("loginauth running...") - - w.Header().Set("Content-Type", "application/json") + log.Println("LoginAuth is running...") // Parse JSON request body var loginReq LoginRequest // Define a struct to hold the login request data - err := json.NewDecoder(r.Body).Decode(&loginReq) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(LoginResponse{ + if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil { + log.Printf("Error decoding login JSON: %v", err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ Success: false, Message: "Invalid request format", }) return } + log.Printf("Login attempt for: %s", loginReq.Username) // Validate input - if loginReq.Username == "" || loginReq.Password == "" { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(LoginResponse{ - Success: false, - Message: "Credential and password are required", - }) - return + fields := []string{loginReq.Username, loginReq.Password} + for _, f := range fields { + if f == "" { + log.Println("Validation failed: empty fields") + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Please fill in all fields.", + }) + return + } } + // Query to check if credential matches username, email, or full_name var hash string var userID string - // Query to check if credential matches username, email, or full_name - stmt := "SELECT id, password_hash FROM users WHERE username = ? OR email = ? OR full_name = ?" - - err = h.DB.QueryRow(stmt, loginReq.Username, loginReq.Username, loginReq.Username).Scan(&userID, &hash) + stmt := "SELECT user_id, password_hash FROM users WHERE username = ? OR email = ? OR full_name = ?" + err := h.DB.QueryRow(stmt, loginReq.Username, loginReq.Username, loginReq.Username).Scan(&userID, &hash) // User not found if err != nil { - fmt.Println("User not found or DB error:", err) - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(LoginResponse{ + log.Printf("User not found or DB error: %v", err) + jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ Success: false, - Message: "User not found", + Message: "Invalid credentials", }) return } - fmt.Println("Hash found, verifying...") - err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(loginReq.Password)) - - // SUCCESS - if err == nil { - fmt.Println("Login success") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(LoginResponse{ - Success: true, - Message: "Login successful", - UserID: userID, + // Verify password + log.Println("Hash found, verifying password...") + if err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(loginReq.Password)); err != nil { + log.Printf("Password mismatch for user: %s", loginReq.Username) + jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid credentials", }) return } - // Password mismatch - fmt.Println("Password Incorrect:", err) - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(LoginResponse{ - Success: false, - Message: "Password incorrect", + // SUCCESS + log.Printf("Login success for user: %s", loginReq.Username) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.LoginResponse{ + Success: true, + Message: "Login successful", + UserID: userID, }) } diff --git a/internal/auth/rateLimit.go b/internal/auth/rateLimit.go new file mode 100644 index 0000000..23821d9 --- /dev/null +++ b/internal/auth/rateLimit.go @@ -0,0 +1,55 @@ +package authentication + +import ( + "encoding/json" + "net/http" + "sync" + "time" + + "golang.org/x/time/rate" +) + +// We need a map to store a rate limiter for each individual IP address +var visitors = make(map[string]*rate.Limiter) +var mu sync.Mutex + +// getVisitor checks if an IP already has a limiter. If not, it makes a new one. +func getVisitor(ip string) *rate.Limiter { + mu.Lock() + defer mu.Unlock() + + limiter, exists := visitors[ip] + if !exists { + // Create a new limiter: allow 1 request per second, with a burst maximum of 3 + limiter = rate.NewLimiter(rate.Every(1*time.Second), 2) + visitors[ip] = limiter + } + + return limiter +} + +// RateLimitMiddleware acts as a shield in front of your handlers +func RateLimitMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get the user's IP address (simplified for this example) + ip := r.RemoteAddr + + // Get the rate limiter for this specific IP + limiter := getVisitor(ip) + + // Ask the limiter if we are allowed to proceed + if !limiter.Allow() { + // If they are going too fast, block them! + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusTooManyRequests) // 429 Error code + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": false, + "message": "Too many login attempts. Please wait a moment and try again.", + }) + return + } + + // If they are within the limit, allow the request to pass through to your handler + next.ServeHTTP(w, r) + } +} \ No newline at end of file diff --git a/internal/auth/signup.go b/internal/auth/signup.go index 3dc3168..84a800a 100644 --- a/internal/auth/signup.go +++ b/internal/auth/signup.go @@ -5,14 +5,16 @@ import ( "database/sql" "encoding/json" // Added for JSON support "fmt" + "log" "math/big" "net/http" "strings" "time" "unicard-go/internal/pkg/account" + jsonwrite "unicard-go/internal/pkg/handler" ) -// 1. Create a struct to catch the incoming JSON from the frontend +// Create a struct to catch the incoming JSON from the frontend type SignupRequest struct { FirstName string `json:"firstName"` LastName string `json:"lastName"` @@ -22,7 +24,7 @@ type SignupRequest struct { ContactNumber string `json:"contactNumber"` } -// 2. Create a standard API response struct +// Create a standard API response struct type APIResponse struct { Success bool `json:"success"` Message string `json:"message"` @@ -51,23 +53,14 @@ func (h *Handler) SignupView(w http.ResponseWriter, r *http.Request) { h.Tpl.ExecuteTemplate(w, "signup.html", nil) } -// Auth Handler (POST) - Converted to JSON API func (h *Handler) SignupHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("Signup API is running...") - w.Header().Set("Content-Type", "application/json") - - // Enforce POST method - if r.Method != http.MethodPost { - w.WriteHeader(http.StatusMethodNotAllowed) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Method not allowed"}) - return - } // Decode incoming JSON var req SignupRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Failed to parse JSON request"}) + log.Printf("Error decoding signup JSON: %v", err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Failed to parse JSON request"}) return } @@ -80,134 +73,170 @@ func (h *Handler) SignupHandler(w http.ResponseWriter, r *http.Request) { req.ContactNumber = strings.TrimSpace(req.ContactNumber) // Validation: Empty Fields - if req.FirstName == "" || req.LastName == "" || req.CardNumber == "" || req.Password == "" || req.Email == "" || req.ContactNumber == "" { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Please fill in all fields."}) - return - } - - // Prepare User struct - user := User{ - Usertype: "Regular", - Username: req.FirstName, // Using First Name as temporary Username - Fullname: req.FirstName + " " + req.LastName, - CardNumber: req.CardNumber, - Password: req.Password, - Email: req.Email, - Phone: req.ContactNumber, + fields := []string{req.FirstName, req.LastName, req.CardNumber, req.Password, req.Email, req.ContactNumber} + for _, f := range fields { + if f == "" { + log.Printf("Validation failed: Empty fields") + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Please fill in all fields."}) + return + } } - // Generate Username - generatedUsername, err := h.GenerateUniqueUsername() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error generating username. Please try again."}) + // Validation: Password Length (fail fast before any DB calls) + if len(req.Password) < 8 { + log.Printf("Validation failed: Password must be at least 8 characters long") + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Password must be at least 8 characters long."}) return } - user.Username = generatedUsername // Check Card Status var cardStatus string - query := "SELECT status FROM cards WHERE card_number = ?" - err = h.DB.QueryRow(query, user.CardNumber).Scan(&cardStatus) - + err := h.DB.QueryRow("SELECT status FROM cards WHERE card_number = ?", req.CardNumber).Scan(&cardStatus) if err == sql.ErrNoRows { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Card number not found. Please check your card."}) + log.Printf("Card not found: %v", req.CardNumber) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Card number not found. Please check your card."}) return } else if err != nil { - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error checking card status."}) + log.Printf("Error checking card status: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error checking card status."}) return } if cardStatus != "Inactive" { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: fmt.Sprintf("Card is currently '%s'. Please contact support.", cardStatus)}) + log.Printf("Card is not inactive: %v", cardStatus) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: fmt.Sprintf("Card is currently '%s'. Please contact support.", cardStatus)}) return } - // Password Length Check - if len(user.Password) < 8 { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Password must be at least 8 characters long."}) + // Check Email + exists, err := account.IsEmailExist(h.DB, req.Email) + if err != nil { + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error checking email."}) + return + } + if exists { + jsonwrite.WriteJSON(w, http.StatusConflict, jsonwrite.APIResponse{Success: false, Message: "Email already registered. Please use a different email."}) return } - // Hash Password - hashedPassword, err := account.HashPassword(user.Password) + // Check Phone + exists, err = h.isPhoneExist(req.ContactNumber) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error processing password."}) + fmt.Println("Phone number check error:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error checking phone number."}) + return + } + if exists { + jsonwrite.WriteJSON(w, http.StatusConflict, jsonwrite.APIResponse{Success: false, Message: "Phone number already registered. Please use a different number."}) return } - user.Password = hashedPassword - // Check Email - if exists, _ := account.IsEmailExist(h.DB, user.Email); exists { - w.WriteHeader(http.StatusConflict) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Email already registered. Please use a different email."}) + // Hash Password + hashedPassword, err := account.HashPassword(req.Password) + if err != nil { + log.Printf("Error hashing password: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error processing password."}) return } + log.Printf("Password: %v", req.Password) + log.Printf("Password hashed successfully: %v", hashedPassword) - // Check Phone - if exists, _ := h.isPhoneExist(user.Phone); exists { - w.WriteHeader(http.StatusConflict) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Phone number already registered. Please use a different number."}) + // Generate Username + generatedUsername, err := h.GenerateUniqueUsername() + if err != nil { + fmt.Println("Error generating username:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error generating username. Please try again."}) return } + log.Printf("Generated Username: %v", generatedUsername) // Generate IDs generateUserId, err := h.GenerateUserID() if err != nil { - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error generating UserID."}) + log.Printf("Error generating UserID: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error generating UserID."}) return } - user.UserID = fmt.Sprintf("%d", generateUserId) generateCardID, err := h.GenerateCardID() if err != nil { - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error generating CardID."}) + log.Printf("Error generating CardID: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error generating CardID."}) return } - user.CardID = generateCardID - // Get Timestamp & Balance - user.CreatedAt, _ = CurrentTimestamp() - user.Balance, err = h.GetInitialBalance(user.CardNumber) + // Get Timestamp + createdAt, err := CurrentTimestamp() if err != nil { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "Invalid Card Number."}) + log.Printf("Error getting timestamp: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error getting timestamp."}) return } + fmt.Println("Timestamp:", createdAt) + + // Get Initial Balance + balance, err := h.GetInitialBalance(req.CardNumber) + if err != nil { + log.Printf("Error getting initial balance: %v", err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Invalid Card Number."}) + return + } + + // Build User struct + user := User{ + UserID: fmt.Sprintf("%d", generateUserId), + CardID: generateCardID, + Usertype: "Regular", + Username: generatedUsername, + Fullname: req.FirstName + " " + req.LastName, + CardNumber: req.CardNumber, + Password: hashedPassword, + Email: req.Email, + Phone: req.ContactNumber, + CreatedAt: createdAt, + Balance: balance, + } - // Insert into DB - insertQuery := "INSERT INTO users (user_id, username, full_name, email, phone, password_hash, card_id, card_number, user_type, balance, created_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - _, err = h.DB.Exec(insertQuery, user.UserID, user.Username, user.Fullname, user.Email, user.Phone, user.Password, user.CardID, user.CardNumber, user.Usertype, user.Balance, user.CreatedAt) + // Begin transaction: insert user + activate card atomically + tx, err := h.DB.Begin() if err != nil { - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error creating account. Please try again."}) + log.Printf("Error starting transaction: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error starting transaction."}) return } + defer tx.Rollback() // no-op if tx.Commit() is called + + insertQuery := `INSERT INTO users + (user_id, username, full_name, email, phone, password_hash, card_id, card_number, user_type, balance, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + _, err = tx.Exec(insertQuery, + user.UserID, user.Username, user.Fullname, user.Email, user.Phone, + user.Password, user.CardID, user.CardNumber, user.Usertype, user.Balance, user.CreatedAt, + ) + if err != nil { + log.Printf("Error inserting user: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error creating account. Please try again."}) + return + } + log.Printf("User record inserted: %v", user.UserID) - // Activate Card - updateQuery := "UPDATE cards SET status = 'Active' WHERE card_number = ?" - _, err = h.DB.Exec(updateQuery, user.CardNumber) + _, err = tx.Exec("UPDATE cards SET status = 'Active' WHERE card_number = ?", user.CardNumber) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(APIResponse{Success: false, Message: "System error activating card."}) + log.Printf("Error activating card for card_number %s: %v", user.CardNumber, err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error activating card."}) + return + } + + if err = tx.Commit(); err != nil { + log.Printf("Error committing transaction: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error finalizing account creation."}) return } - // SUCCESS! Send a JSON success message instead of redirecting. - fmt.Println("Account successfully created!") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(APIResponse{Success: true, Message: "Account created successfully!"}) + log.Printf("Account successfully created! UserID: %s", user.UserID) // moved here + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{Success: true, Message: "Account created successfully!"}) } -// ... [KEEP ALL YOUR HELPER FUNCTIONS DOWN HERE EXACTLY AS THEY WERE] ... // ---Helper Functions--- // GenerateUniqueUsername creates a random unique username @@ -253,7 +282,7 @@ func (h *Handler) GenerateUniqueUsername() (string, error) { } // Collision detected, loop runs again... - fmt.Println("Username collision! Retrying...") + log.Println("Username collision! Retrying...") } } @@ -272,7 +301,7 @@ func (h *Handler) isPhoneExist(phone string) (bool, error) { return false, nil } if err != nil { - fmt.Println("Phone number check error:", err) + log.Printf("Phone number check error: %v", err) return false, err } return true, nil @@ -312,7 +341,7 @@ func (h *Handler) GenerateUserID() (int64, error) { return 0, err // Real DB error } // If it exists, loop runs again - fmt.Println("Collision detected! Retrying...") + log.Println("Collision detected! Retrying...") } } @@ -354,7 +383,7 @@ func (h *Handler) GenerateCardID() (string, error) { } else if err != nil { return "", err // DB error } - fmt.Println("Collision detected! Retrying...", cardID) + log.Printf("Collision detected! Retrying... conflicting ID: %s", cardID) } } @@ -368,6 +397,7 @@ func (h *Handler) GetInitialBalance(cardNumber string) (float64, error) { query := "SELECT initial_amount FROM cards WHERE card_number = ?" err := h.DB.QueryRow(query, cardNumber).Scan(&initialBalance) if err != nil { + log.Printf("GetInitialBalance error for card %s: %v", cardNumber, err) return 0, err } return initialBalance, nil @@ -380,6 +410,7 @@ func CurrentTimestamp() (string, error) { // Load Asia/Manila location loc, err := time.LoadLocation("Asia/Manila") if err != nil { + log.Printf("CurrentTimestamp error loading timezone: %v", err) return "", err } time.Local = loc diff --git a/internal/pkg/account/services.go b/internal/pkg/account/services.go index a533877..0de3e89 100644 --- a/internal/pkg/account/services.go +++ b/internal/pkg/account/services.go @@ -8,7 +8,7 @@ package account import ( "database/sql" - "fmt" + "log" "golang.org/x/crypto/bcrypt" ) @@ -42,13 +42,13 @@ func IsEmailExist(db *sql.DB, email string) (bool, error) { query := "SELECT email FROM users WHERE email = ?" err := db.QueryRow(query, email).Scan(&existingEmail) if err == sql.ErrNoRows { - fmt.Println("Email is available.") + log.Println("Email does not exist, can proceed with signup.") return false, nil } if err != nil { - fmt.Println("Email check error:", err) + log.Printf("Email check error: %v", err) return false, err } - fmt.Println("Email already exists.") + log.Println("Email already exists.") return true, nil } diff --git a/internal/pkg/handler/jsonWrite.go b/internal/pkg/handler/jsonWrite.go new file mode 100644 index 0000000..27e4473 --- /dev/null +++ b/internal/pkg/handler/jsonWrite.go @@ -0,0 +1,26 @@ +package jsonwrite + +import ( + "encoding/json" + "net/http" +) + +// Create a standard API response struct +type APIResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// Login specific response — returns user data after login +type LoginResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + UserID string `json:"user_id"` +} + +// Auth Handler (POST) - Converted to JSON API +func WriteJSON(w http.ResponseWriter, status int, resp any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(resp) +} diff --git a/templates/dashboard.html b/templates/dashboard.html index ccf6a68..72dd59f 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -2,11 +2,61 @@ - - Document + Unicard Dashboard + -

Dashboard

-

This is the dashboard page.

+ +
+

Welcome, {{ .Name }}!

+

Username: {{ .Username }} | Account: {{ .AccountType }}

+
+ +
+
+

Current Balance

+

₱{{ printf "%.2f" .Balance }}

+
+
+

Loyalty Points

+

{{ .LoyaltyPoints }} pts

+
+
+ +

Recent Transactions

+ + + + + + + + + + + {{ range .RecentTransactions }} + + + + + + + {{ else }} + + + + {{ end }} + +
DateDescriptionTypeAmount
{{ .Date }}{{ .Description }}{{ .Type }} + ₱{{ printf "%.2f" .Amount }} +
No recent transactions found.
+ \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index 1c5839d..f267a26 100644 --- a/templates/login.html +++ b/templates/login.html @@ -4,6 +4,7 @@ + Login Sample @@ -11,10 +12,10 @@

Login Page

- +

- +

Forgot Password? @@ -26,11 +27,11 @@
+ \ No newline at end of file From dac4c890d0d3ffcd34848d6618775642319b573a Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:33:50 +0800 Subject: [PATCH 4/5] feat: Implement user signup and dashboard features with account management --- internal/auth/repository.go | 151 +++++++++++++ internal/auth/signup.go | 364 +------------------------------- internal/auth/signup_service.go | 108 ++++++++++ internal/auth/types.go | 32 +++ internal/auth/utils.go | 61 ++++++ 5 files changed, 357 insertions(+), 359 deletions(-) create mode 100644 internal/auth/repository.go create mode 100644 internal/auth/signup_service.go create mode 100644 internal/auth/types.go create mode 100644 internal/auth/utils.go diff --git a/internal/auth/repository.go b/internal/auth/repository.go new file mode 100644 index 0000000..e17899e --- /dev/null +++ b/internal/auth/repository.go @@ -0,0 +1,151 @@ +package authentication + +import ( + "crypto/rand" + "database/sql" + "fmt" + "log" + "math/big" + "time" +) + +// isUserIDExist checks if a given user ID already exists in the database. +// It queries the users table and returns true if the ID is found, false otherwise. +func (h *Handler) isUserIDExist(userID int64) (bool, error) { + var tmpId int64 + query := "SELECT user_id FROM users WHERE user_id = ?" + err := h.DB.QueryRow(query, userID).Scan(&tmpId) + + if err == sql.ErrNoRows { + return false, nil // Doesn't exist! + } else if err != nil { + return false, err // Real DB error + } + return true, nil // It exists +} + +// GenerateUniqueUsername creates a random unique username +// Format: user + 12 random lowercase characters/numbers +// Example: user9d8a7c2b3e4f +func (h *Handler) GenerateUniqueUsername() (string, error) { + const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + usernamePrefix := "user" + length := 7 + + for { + // Get the current date in DDYY format (Go uses "010206" as reference) + userDate := time.Now().Format("06") // e.g., "012624" for Jan 26, 2024 + // time .Now().Format("1504") // e.g., "1530" for 3:30 PM + timePart := time.Now().Format("0405") // e.g., "1530" for 3:30 PM + + // Combine date and time to form part of the username + //usernamePrefix = fmt.Sprintf("user%s%s", userDate, timePart) + + // Generate the random suffix + randomPart := "" + for i := 0; i < length; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", err + } + randomPart += string(charset[num.Int64()]) + } + + // Combine prefix + date + time + random part + username := fmt.Sprintf("%s%s%s%s", usernamePrefix, userDate, randomPart, timePart) + + // Check DB for uniqueness + var existing string + query := "SELECT username FROM users WHERE username = ?" + err := h.DB.QueryRow(query, username).Scan(&existing) + + if err == sql.ErrNoRows { + //fmt.Println("Generated unique username:", username) + return username, nil // Found a unique one! + } else if err != nil { + return "", err // Real DB Error + } + + // Collision detected, loop runs again... + log.Println("Username collision! Retrying...") + } +} + +// It generates the unique cardID for every card users +// Checks the database for uniqueness. +// Returns the unique card ID as string or an error if any occurs. +// Example format: CARD-XXXXXXX +func (h *Handler) GenerateCardID() (string, error) { + // Define charset: letters and numbers + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + cardIDPrefix := "CARD-" + randomLength := 7 + + for { + // Get the current date in MMDDYY format (Go uses "010206" as reference) + datePart := time.Now().Format("010206") + + // Generate the 7 random characters + randomPart := "" + for i := 0; i < randomLength; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", err + } + randomPart += string(charset[num.Int64()]) + } + + // Combine them: CARD- + Date + Random + // Example output: CARD-012626Ab7z9X1 + cardID := fmt.Sprintf("%s%s%s", cardIDPrefix, datePart, randomPart) + + // Check database for uniqueness + var tmpCardID string + query := "SELECT card_id FROM users WHERE card_id = ?" + err := h.DB.QueryRow(query, cardID).Scan(&tmpCardID) + + if err == sql.ErrNoRows { + return cardID, nil // Unique ID found + } else if err != nil { + return "", err // DB error + } + log.Printf("Collision detected! Retrying... conflicting ID: %s", cardID) + } +} + +// It check the initial balance based on card number prefix +// Returns the initial balance as float64 or an error if any occurs. +// Gets the initial balance from the "card" table in the database. +// Example: Card Number "1234567890" has initial balance of 100.0 +func (h *Handler) GetInitialBalance(cardNumber string) (float64, error) { + var initialBalance float64 // to hold the initial balance + + query := "SELECT initial_amount FROM cards WHERE card_number = ?" + err := h.DB.QueryRow(query, cardNumber).Scan(&initialBalance) + if err != nil { + log.Printf("GetInitialBalance error for card %s: %v", cardNumber, err) + return 0, err + } + return initialBalance, nil +} + +// This function checks if a given phone number already exists in the database. +// It executes a SQL query to search for the phone number in the users table. +// If the phone number is found, it returns true. If not found, it returns false. +// If an error occurs during the query, it returns the error. +func (h *Handler) isPhoneExist(phone string) (bool, error) { + // Hold the existing phone number + var existingPhone string + + // Check query + query := "SELECT phone FROM users WHERE phone = ?" + err := h.DB.QueryRow(query, phone).Scan(&existingPhone) + if err == sql.ErrNoRows { + return false, nil + } + if err != nil { + log.Printf("Phone number check error: %v", err) + return false, err + } + return true, nil +} diff --git a/internal/auth/signup.go b/internal/auth/signup.go index 84a800a..56792ce 100644 --- a/internal/auth/signup.go +++ b/internal/auth/signup.go @@ -1,50 +1,14 @@ package authentication import ( - "crypto/rand" - "database/sql" "encoding/json" // Added for JSON support "fmt" "log" - "math/big" "net/http" "strings" - "time" - "unicard-go/internal/pkg/account" jsonwrite "unicard-go/internal/pkg/handler" ) -// Create a struct to catch the incoming JSON from the frontend -type SignupRequest struct { - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - CardNumber string `json:"cardNumber"` - Password string `json:"password"` - Email string `json:"email"` - ContactNumber string `json:"contactNumber"` -} - -// Create a standard API response struct -type APIResponse struct { - Success bool `json:"success"` - Message string `json:"message"` -} - -// User struct to hold signup data (Keep your existing one) -type User struct { - UserID string - Username string - Fullname string - Email string - Phone string - CardNumber string - Password string - CardID string - Usertype string - Balance float64 - CreatedAt string -} - // View Handler (GET) // You can now simplify this because JS handles the errors! func (h *Handler) SignupView(w http.ResponseWriter, r *http.Request) { @@ -89,332 +53,14 @@ func (h *Handler) SignupHandler(w http.ResponseWriter, r *http.Request) { return } - // Check Card Status - var cardStatus string - err := h.DB.QueryRow("SELECT status FROM cards WHERE card_number = ?", req.CardNumber).Scan(&cardStatus) - if err == sql.ErrNoRows { - log.Printf("Card not found: %v", req.CardNumber) - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Card number not found. Please check your card."}) - return - } else if err != nil { - log.Printf("Error checking card status: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error checking card status."}) - return - } - - if cardStatus != "Inactive" { - log.Printf("Card is not inactive: %v", cardStatus) - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: fmt.Sprintf("Card is currently '%s'. Please contact support.", cardStatus)}) - return - } - - // Check Email - exists, err := account.IsEmailExist(h.DB, req.Email) - if err != nil { - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error checking email."}) - return - } - if exists { - jsonwrite.WriteJSON(w, http.StatusConflict, jsonwrite.APIResponse{Success: false, Message: "Email already registered. Please use a different email."}) - return - } - - // Check Phone - exists, err = h.isPhoneExist(req.ContactNumber) - if err != nil { - fmt.Println("Phone number check error:", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error checking phone number."}) - return - } - if exists { - jsonwrite.WriteJSON(w, http.StatusConflict, jsonwrite.APIResponse{Success: false, Message: "Phone number already registered. Please use a different number."}) - return - } - - // Hash Password - hashedPassword, err := account.HashPassword(req.Password) - if err != nil { - log.Printf("Error hashing password: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error processing password."}) - return - } - log.Printf("Password: %v", req.Password) - log.Printf("Password hashed successfully: %v", hashedPassword) - - // Generate Username - generatedUsername, err := h.GenerateUniqueUsername() - if err != nil { - fmt.Println("Error generating username:", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error generating username. Please try again."}) - return - } - log.Printf("Generated Username: %v", generatedUsername) - - // Generate IDs - generateUserId, err := h.GenerateUserID() - if err != nil { - log.Printf("Error generating UserID: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error generating UserID."}) - return - } - - generateCardID, err := h.GenerateCardID() - if err != nil { - log.Printf("Error generating CardID: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error generating CardID."}) - return - } - - // Get Timestamp - createdAt, err := CurrentTimestamp() - if err != nil { - log.Printf("Error getting timestamp: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error getting timestamp."}) - return - } - fmt.Println("Timestamp:", createdAt) - - // Get Initial Balance - balance, err := h.GetInitialBalance(req.CardNumber) - if err != nil { - log.Printf("Error getting initial balance: %v", err) - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Invalid Card Number."}) - return - } - - // Build User struct - user := User{ - UserID: fmt.Sprintf("%d", generateUserId), - CardID: generateCardID, - Usertype: "Regular", - Username: generatedUsername, - Fullname: req.FirstName + " " + req.LastName, - CardNumber: req.CardNumber, - Password: hashedPassword, - Email: req.Email, - Phone: req.ContactNumber, - CreatedAt: createdAt, - Balance: balance, - } - - // Begin transaction: insert user + activate card atomically - tx, err := h.DB.Begin() + // We pass the cleaned 'req' to signup_service.go. It will return an error if something goes wrong. + err := h.CreateAccount(req) if err != nil { - log.Printf("Error starting transaction: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error starting transaction."}) + // If the service fails (e.g., "Email already exists"), we send that specific message back to the user + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: err.Error()}) return } - defer tx.Rollback() // no-op if tx.Commit() is called - insertQuery := `INSERT INTO users - (user_id, username, full_name, email, phone, password_hash, card_id, card_number, user_type, balance, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - _, err = tx.Exec(insertQuery, - user.UserID, user.Username, user.Fullname, user.Email, user.Phone, - user.Password, user.CardID, user.CardNumber, user.Usertype, user.Balance, user.CreatedAt, - ) - if err != nil { - log.Printf("Error inserting user: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error creating account. Please try again."}) - return - } - log.Printf("User record inserted: %v", user.UserID) - - _, err = tx.Exec("UPDATE cards SET status = 'Active' WHERE card_number = ?", user.CardNumber) - if err != nil { - log.Printf("Error activating card for card_number %s: %v", user.CardNumber, err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error activating card."}) - return - } - - if err = tx.Commit(); err != nil { - log.Printf("Error committing transaction: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{Success: false, Message: "System error finalizing account creation."}) - return - } - - log.Printf("Account successfully created! UserID: %s", user.UserID) // moved here + // If everything is successful, we send a success message back to the user jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{Success: true, Message: "Account created successfully!"}) } - -// ---Helper Functions--- - -// GenerateUniqueUsername creates a random unique username -// Format: user + 12 random lowercase characters/numbers -// Example: user9d8a7c2b3e4f -func (h *Handler) GenerateUniqueUsername() (string, error) { - const charset = "abcdefghijklmnopqrstuvwxyz0123456789" - usernamePrefix := "user" - length := 7 - - for { - // Get the current date in DDYY format (Go uses "010206" as reference) - userDate := time.Now().Format("06") // e.g., "012624" for Jan 26, 2024 - // time .Now().Format("1504") // e.g., "1530" for 3:30 PM - timePart := time.Now().Format("0405") // e.g., "1530" for 3:30 PM - - // Combine date and time to form part of the username - //usernamePrefix = fmt.Sprintf("user%s%s", userDate, timePart) - - // Generate the random suffix - randomPart := "" - for i := 0; i < length; i++ { - num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - return "", err - } - randomPart += string(charset[num.Int64()]) - } - - // Combine prefix + date + time + random part - username := fmt.Sprintf("%s%s%s%s", usernamePrefix, userDate, randomPart, timePart) - - // Check DB for uniqueness - var existing string - query := "SELECT username FROM users WHERE username = ?" - err := h.DB.QueryRow(query, username).Scan(&existing) - - if err == sql.ErrNoRows { - //fmt.Println("Generated unique username:", username) - return username, nil // Found a unique one! - } else if err != nil { - return "", err // Real DB Error - } - - // Collision detected, loop runs again... - log.Println("Username collision! Retrying...") - } -} - -// This function checks if a given phone number already exists in the database. -// It executes a SQL query to search for the phone number in the users table. -// If the phone number is found, it returns true. If not found, it returns false. -// If an error occurs during the query, it returns the error. -func (h *Handler) isPhoneExist(phone string) (bool, error) { - // Hold the existing phone number - var existingPhone string - - // Check query - query := "SELECT phone FROM users WHERE phone = ?" - err := h.DB.QueryRow(query, phone).Scan(&existingPhone) - if err == sql.ErrNoRows { - return false, nil - } - if err != nil { - log.Printf("Phone number check error: %v", err) - return false, err - } - return true, nil -} - -// It generates random numbers and checks the database for uniqueness. -// If a generated ID already exists, it retries until a unique one is found. -// Returns the unique user ID as int64 or an error if any occurs. -func (h *Handler) GenerateUserID() (int64, error) { - // Generate random 12 digits number - // Range: 100,000,000,000 to 999,999,999,999 - min := int64(100000000000) - max := int64(999999999999) - - for { - // Calculate the range size (max - min + 1) - diff := new(big.Int).Sub(big.NewInt(max), big.NewInt(min)) - diff.Add(diff, big.NewInt(1)) - - number, err := rand.Int(rand.Reader, diff) - if err != nil { - return 0, err - } - - // Add min to get the final ID within the range - userID := number.Int64() + min - - // Check DB directly - var tmpId int64 - query := "SELECT user_id FROM users WHERE user_id = ?" - - err = h.DB.QueryRow(query, userID).Scan(&tmpId) - if err == sql.ErrNoRows { - // fmt.Println("Unique UserID generated:", userID) - return userID, nil // Unique ID found - } else if err != nil { - return 0, err // Real DB error - } - // If it exists, loop runs again - log.Println("Collision detected! Retrying...") - } -} - -// It generates the unique cardID for every card users -// Checks the database for uniqueness. -// Returns the unique card ID as string or an error if any occurs. -// Example format: CARD-XXXXXXX -func (h *Handler) GenerateCardID() (string, error) { - // Define charset: letters and numbers - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - cardIDPrefix := "CARD-" - randomLength := 7 - - for { - // Get the current date in MMDDYY format (Go uses "010206" as reference) - datePart := time.Now().Format("010206") - - // Generate the 7 random characters - randomPart := "" - for i := 0; i < randomLength; i++ { - num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - return "", err - } - randomPart += string(charset[num.Int64()]) - } - - // Combine them: CARD- + Date + Random - // Example output: CARD-012626Ab7z9X1 - cardID := fmt.Sprintf("%s%s%s", cardIDPrefix, datePart, randomPart) - - // Check database for uniqueness - var tmpCardID string - query := "SELECT card_id FROM users WHERE card_id = ?" - err := h.DB.QueryRow(query, cardID).Scan(&tmpCardID) - - if err == sql.ErrNoRows { - return cardID, nil // Unique ID found - } else if err != nil { - return "", err // DB error - } - log.Printf("Collision detected! Retrying... conflicting ID: %s", cardID) - } -} - -// It check the initial balance based on card number prefix -// Returns the initial balance as float64 or an error if any occurs. -// Gets the initial balance from the "card" table in the database. -// Example: Card Number "1234567890" has initial balance of 100.0 -func (h *Handler) GetInitialBalance(cardNumber string) (float64, error) { - var initialBalance float64 // to hold the initial balance - - query := "SELECT initial_amount FROM cards WHERE card_number = ?" - err := h.DB.QueryRow(query, cardNumber).Scan(&initialBalance) - if err != nil { - log.Printf("GetInitialBalance error for card %s: %v", cardNumber, err) - return 0, err - } - return initialBalance, nil -} - -// This function returns the current timestamp formatted as "YYYY-MM-DD HH:MM:SS" -// in the "Asia/Manila" timezone. If there's an error loading the timezone, -// it returns an empty string and the error. -func CurrentTimestamp() (string, error) { - // Load Asia/Manila location - loc, err := time.LoadLocation("Asia/Manila") - if err != nil { - log.Printf("CurrentTimestamp error loading timezone: %v", err) - return "", err - } - time.Local = loc - - // Format the current time - return time.Now().Format("2006-01-02 03:04:05"), nil -} diff --git a/internal/auth/signup_service.go b/internal/auth/signup_service.go new file mode 100644 index 0000000..5a755cb --- /dev/null +++ b/internal/auth/signup_service.go @@ -0,0 +1,108 @@ +package authentication + +import ( + "database/sql" + "fmt" + "log" + "unicard-go/internal/pkg/account" +) + +// CreateAccount handles all the business rules, ID generation, and DB transactions +func (h *Handler) CreateAccount(req SignupRequest) error { + + // 1. Check Card Status + var cardStatus string + err := h.DB.QueryRow("SELECT status FROM cards WHERE card_number = ?", req.CardNumber).Scan(&cardStatus) + if err == sql.ErrNoRows { + return fmt.Errorf("Card number not found. Please check your card") + } else if err != nil { + log.Printf("System error checking card status: %v", err) + return fmt.Errorf("System error checking card status") + } + if cardStatus != "Inactive" { + return fmt.Errorf("Card is currently '%s'. Please contact support", cardStatus) + } + + // 2. Check Email + exists, err := account.IsEmailExist(h.DB, req.Email) + if err != nil { + return fmt.Errorf("System error checking email") + } + if exists { + return fmt.Errorf("Email already registered. Please use a different email") + } + + // 3. Check Phone (Using your new repository function!) + exists, err = h.isPhoneExist(req.ContactNumber) + if err != nil { + return fmt.Errorf("System error checking phone number") + } + if exists { + return fmt.Errorf("Phone number already registered") + } + + // 4. Hash Password + hashedPassword, err := account.HashPassword(req.Password) + if err != nil { + return fmt.Errorf("System error processing password") + } + + // 5. Generate IDs (Using your new utils functions!) + generatedUsername, err := h.GenerateUniqueUsername() + if err != nil { return fmt.Errorf("System error generating username") } + + generateUserId, err := h.GenerateUserID() + if err != nil { return fmt.Errorf("System error generating UserID") } + + generateCardID, err := h.GenerateCardID() + if err != nil { return fmt.Errorf("System error generating CardID") } + + createdAt, err := CurrentTimestamp() + if err != nil { return fmt.Errorf("System error getting timestamp") } + + balance, err := h.GetInitialBalance(req.CardNumber) + if err != nil { return fmt.Errorf("Invalid Card Number") } + + // 6. Build User struct + user := User{ + UserID: fmt.Sprintf("%d", generateUserId), + CardID: generateCardID, + Usertype: "Regular", + Username: generatedUsername, + Fullname: req.FirstName + " " + req.LastName, + CardNumber: req.CardNumber, + Password: hashedPassword, + Email: req.Email, + Phone: req.ContactNumber, + CreatedAt: createdAt, + Balance: balance, + } + + // 7. Begin transaction: insert user + activate card atomically + tx, err := h.DB.Begin() + if err != nil { + return fmt.Errorf("System error starting transaction") + } + defer tx.Rollback() + + insertQuery := `INSERT INTO users (user_id, username, full_name, email, phone, password_hash, card_id, card_number, user_type, balance, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + _, err = tx.Exec(insertQuery, user.UserID, user.Username, user.Fullname, user.Email, user.Phone, user.Password, user.CardID, user.CardNumber, user.Usertype, user.Balance, user.CreatedAt) + if err != nil { + log.Printf("Error inserting user: %v", err) + return fmt.Errorf("System error creating account. Please try again") + } + + _, err = tx.Exec("UPDATE cards SET status = 'Active' WHERE card_number = ?", user.CardNumber) + if err != nil { + log.Printf("Error activating card: %v", err) + return fmt.Errorf("System error activating card") + } + + if err = tx.Commit(); err != nil { + log.Printf("Error committing transaction: %v", err) + return fmt.Errorf("System error finalizing account creation") + } + + log.Printf("Account successfully created! UserID: %s", user.UserID) + return nil // Success! No errors! +} \ No newline at end of file diff --git a/internal/auth/types.go b/internal/auth/types.go new file mode 100644 index 0000000..8df4166 --- /dev/null +++ b/internal/auth/types.go @@ -0,0 +1,32 @@ +package authentication + +// Create a struct to catch the incoming JSON from the frontend +type SignupRequest struct { + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + CardNumber string `json:"cardNumber"` + Password string `json:"password"` + Email string `json:"email"` + ContactNumber string `json:"contactNumber"` +} + +// Create a standard API response struct +type APIResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// User struct to hold signup data (Keep your existing one) +type User struct { + UserID string + Username string + Fullname string + Email string + Phone string + CardNumber string + Password string + CardID string + Usertype string + Balance float64 + CreatedAt string +} \ No newline at end of file diff --git a/internal/auth/utils.go b/internal/auth/utils.go new file mode 100644 index 0000000..eab33ff --- /dev/null +++ b/internal/auth/utils.go @@ -0,0 +1,61 @@ +package authentication + +import ( + "crypto/rand" + "log" + "math/big" + "time" +) + +// This function returns the current timestamp formatted as "YYYY-MM-DD HH:MM:SS" +// in the "Asia/Manila" timezone. If there's an error loading the timezone, +// it returns an empty string and the error. +func CurrentTimestamp() (string, error) { + // Load Asia/Manila location + loc, err := time.LoadLocation("Asia/Manila") + if err != nil { + log.Printf("CurrentTimestamp error loading timezone: %v", err) + return "", err + } + time.Local = loc + + // Format the current time + // Pro-Tip: Use .In(loc) instead of changing the global time.Local! + // Also, use 15:04:05 to get 24-hour time (03:04:05 gives 12-hour time without AM/PM) + return time.Now().In(loc).Format("2006-01-02 03:04:05"), nil +} + +// It generates random numbers and checks the database for uniqueness. +// If a generated ID already exists, it retries until a unique one is found. +// Returns the unique user ID as int64 or an error if any occurs. +func (h *Handler) GenerateUserID() (int64, error) { + // Generate random 12 digits number + // Range: 100,000,000,000 to 999,999,999,999 + min := int64(100000000000) + max := int64(999999999999) + + for { + // Calculate the range size (max - min + 1) + diff := new(big.Int).Sub(big.NewInt(max), big.NewInt(min)) + diff.Add(diff, big.NewInt(1)) + + number, err := rand.Int(rand.Reader, diff) + if err != nil { + return 0, err + } + + // Add min to get the final ID within the range + userID := number.Int64() + min + + // Ask the Repository if it exists! + exists, err := h.isUserIDExist(userID) + if err != nil { + return 0, err // Real DB error + } + if !exists { + return userID, nil // Unique ID found! + } + + log.Println("Collision detected! Retrying...") + } +} From 66edc8335b5a5bcebd01ec09e0c29f341ed29487 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:47:18 +0800 Subject: [PATCH 5/5] try lang --- sample.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 sample.txt diff --git a/sample.txt b/sample.txt new file mode 100644 index 0000000..10342d3 --- /dev/null +++ b/sample.txt @@ -0,0 +1 @@ +try nga natin \ No newline at end of file