Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/style/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.required {
color: red;
margin-left: 3px;
}
28 changes: 14 additions & 14 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <form action="/loginauth"> 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)
Expand All @@ -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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
54 changes: 54 additions & 0 deletions internal/auth/dashboard.go
Original file line number Diff line number Diff line change
@@ -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)
}
107 changes: 63 additions & 44 deletions internal/auth/login.go
Original file line number Diff line number Diff line change
@@ -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,
})
}
55 changes: 55 additions & 0 deletions internal/auth/rateLimit.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading