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 54f4aa1..1578869 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -67,22 +67,20 @@ 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) - mux.HandleFunc("GET /admin/addcard", adminHanlder.AddCardsView) - mux.HandleFunc("GET /admin/deactivatecard", adminHanlder.DeactivateView) + // POST Request: JSON API endpoints + mux.HandleFunc("POST /api/v1/loginauth", authHandler.LoginAuthHandler) // Login authentication endpoint + mux.HandleFunc("POST /api/v1/signupauth", authHandler.SignupHandler) - // 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) + // 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) - // Dashboard - mux.HandleFunc("/dashboard", 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) // Start Server fmt.Println("Server started on: http://" + serverAddress + port) @@ -96,3 +94,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 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 2f05c9f..d9aa36f 100644 --- a/internal/auth/login.go +++ b/internal/auth/login.go @@ -1,71 +1,90 @@ package authentication import ( - "fmt" + "encoding/json" + "log" "net/http" - message "unicard-go/internal/pkg" + jsonwrite "unicard-go/internal/pkg/handler" "golang.org/x/crypto/bcrypt" ) +// LoginRequest represents the JSON request for login +type LoginRequest struct { + Username string `json:"username"` // username, email, or full_name + Password string `json:"password"` +} + // View Handler (GET) -// This function checks the URL for errors (e.g., ?error=invalid) -// and displays the red text if needed. +// Serves the login page template 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 - - // Determine the message based on the error code - switch errCode { - case "invalid": - msg = "Wrong password" - case "notfound": - msg = "User not found" - } - - // Render the template with the message - h.Tpl.ExecuteTemplate(w, "login.html", message.MessageData{Error: msg}) + log.Println("Login view is running...") + 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...") + log.Println("LoginAuth is running...") - r.ParseForm() - credential := r.PostFormValue("username") // This can be username, email, or full_name - password := r.PostFormValue("password") + // Parse JSON request body + var loginReq LoginRequest // Define a struct to hold the login request data + 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) - var hash 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 = ?" + // Validate input + 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 + } + } - err := h.DB.QueryRow(stmt, credential, credential, credential).Scan(&hash) + // Query to check if credential matches username, email, or full_name + var hash string + var userID string + 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) - // Redirect with ?user=notfound - http.Redirect(w, r, "/login?user=notfound", http.StatusSeeOther) + log.Printf("User not found or DB error: %v", err) + jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid credentials", + }) return } - fmt.Println("Hash found, verifying...") - err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - - // SUCCESS - if err == nil { - fmt.Println("Login success") - http.Redirect(w, r, "/dashboard", http.StatusSeeOther) + // 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 mismatch") - - // Redirect with ?user=invalid - http.Redirect(w, r, "/login?user=invalid", http.StatusSeeOther) + // 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/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 b378688..56792ce 100644 --- a/internal/auth/signup.go +++ b/internal/auth/signup.go @@ -1,445 +1,66 @@ package authentication import ( - "crypto/rand" - "database/sql" + "encoding/json" // Added for JSON support "fmt" - "math/big" + "log" "net/http" "strings" - "time" - structMessage "unicard-go/internal/pkg" - "unicard-go/internal/pkg/account" + jsonwrite "unicard-go/internal/pkg/handler" ) -// User struct to hold signup data -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) -// 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 func (h *Handler) SignupHandler(w http.ResponseWriter, r *http.Request) { - fmt.Println("Signup is running...") - - if err := r.ParseForm(); err != nil { - http.Redirect(w, r, "/signup?error=parseform", http.StatusSeeOther) - 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) - return - } - - // Now we know the data is clean and present - user := User{ - Usertype: "Regular", - Username: firstName, // Using First Name as Username - Fullname: firstName + " " + lastName, - CardNumber: cardNum, - Password: password, - Email: email, - Phone: phone, - } - - // Check if username exists using helper function - generatedUsername, err := h.GenerateUniqueUsername() - if err != nil { - fmt.Printf("Error generating username: %v\n", err) - http.Redirect(w, r, "/signup?error=genusername", http.StatusSeeOther) - 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 - 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) - 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) - 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) - return - } - // Success: Proceed - fmt.Printf("Card %s is Inactive (Valid). Proceeding...\n", user.CardNumber) + fmt.Println("Signup API is running...") - // 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) + // Decode incoming JSON + var req SignupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Printf("Error decoding signup JSON: %v", err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{Success: false, Message: "Failed to parse JSON request"}) return } - // Generate Hash for 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) - return - } - fmt.Println("raw password is:", user.Password) - user.Password = hashedPassword // Store the hashed password - fmt.Printf("hashed password is: %v\n", user.Password) - - // Check if Email Exists (Using our helper method) - 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."}) - return - } - fmt.Printf("Email %s is available.\n", user.Email) - - // Check if Phone Exists (Using our helper method) - 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) - return - } - fmt.Printf("Phone %s is available.\n", user.Phone) - user.Phone = phone - - // Generate unique UserID (12 digits) - generateUserId, err := h.GenerateUserID() - if err != nil { - fmt.Printf("Error generating UserID: %v", err) - http.Redirect(w, r, "/signup?error=userid", http.StatusSeeOther) - 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) - return - } - user.CardID = generateCardID - fmt.Printf("Generated CardID: %s\n", user.CardID) - - // Time of account creation - 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) - return + // 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 + 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 + } } - 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, - ) - 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) + // 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 } - // Update card status from "Inactive" to "Active" - updateQuery := "UPDATE cards SET status = 'Active' WHERE card_number = ?" - _, err = h.DB.Exec(updateQuery, user.CardNumber) + // 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 { - fmt.Printf("ERROR: Failed to update card status to Active: %v\n", err) - http.Redirect(w, r, "/signup?error=cardupdate", http.StatusSeeOther) + // 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 } - 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) -} - -// ---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... - fmt.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 { - fmt.Println("Phone number check error:", 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 - fmt.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 - } - fmt.Println("Collision detected! Retrying...", 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 { - 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 { - return "", err - } - time.Local = loc - // Format the current time - return time.Now().Format("2006-01-02 03:04:05"), nil + // 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!"}) } 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...") + } +} 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/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 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 f957a5c..f267a26 100644 --- a/templates/login.html +++ b/templates/login.html @@ -4,25 +4,85 @@ + Login Sample

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..9348615 100644 --- a/templates/signup.html +++ b/templates/signup.html @@ -4,38 +4,103 @@ - Signup Page + + Sign Up - -

Signup Page

- -
- * - - * -

- - - * - - - * -

- - * -

- * -

- - - {{if .Error}} -

{{.Error}}

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

{{.Success}}

- {{end}} +

Create an Account

+ + + +

+ + +

+ + +

+ + +

+ + +

+ + +

+ +

Already have an account? Login here


+ + + + +
+ + \ No newline at end of file