Skip to content

Commit

Permalink
sessions are functional as well as login, signup, and task creation t…
Browse files Browse the repository at this point in the history
…ied to users
  • Loading branch information
ATLIOD committed Feb 19, 2025
1 parent 6d317ee commit 9d7ef47
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 43 deletions.
91 changes: 81 additions & 10 deletions cmd/web/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,22 @@ import (
)

func tasks(w http.ResponseWriter, r *http.Request, db *pgxpool.Pool) {
ID := "c51cc54d-a264-4007-ba6f-bfa3ad7466e4"
st, err := r.Cookie("session_token")
if err != nil || st.Value == "" {
log.Println("unable to retrieve session token")
// return errors.New("unable to retrieve token")
}

// get user id from token
userID, err := getUserIDFromToken(st.Value, db)
if err != nil {
log.Println("error getting user ID from token")
// return err
}

stmt := "SELECT id, title, stage FROM tasks WHERE user_id = $1"
// rows = result of statement
rows, err := db.Query(context.Background(), stmt, ID)
rows, err := db.Query(context.Background(), stmt, userID)
// if row error
if err != nil {
log.Println("error querying tasks")
Expand Down Expand Up @@ -115,7 +127,7 @@ func addTaskHandler(w http.ResponseWriter, r *http.Request, db *pgxpool.Pool) {
task := models.Task{Title: Formtitle, Stage: Formstage}

// Save the task to the database
saveToDatabase(task, db)
saveToDatabase(task, db, r)
fmt.Fprintln(w, "Task added successfully!")
}
}
Expand Down Expand Up @@ -199,15 +211,33 @@ func loginPageHandler(w http.ResponseWriter, r *http.Request, db *pgxpool.Pool)

func loginHandler(w http.ResponseWriter, r *http.Request, db *pgxpool.Pool) {
if r.Method != http.MethodPost {
err := http.StatusMethodNotAllowed
http.Error(w, "Invalid request method", err)
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
email := r.FormValue("username")

email := r.FormValue("email")
password := r.FormValue("password")

// Save the task to the database
loginUser(w, email, password, db)
if email == "" || password == "" {
http.Error(w, "Missing credentials", http.StatusBadRequest)
return
}

err := loginUser(w, email, password, db)
if err != nil {
log.Printf("Login failed: %v", err)
// Don't expose internal errors to the client
if err.Error() == "invalid credentials" {
http.Error(w, "Invalid email or password", http.StatusUnauthorized)
} else {
http.Error(w, "Login failed", http.StatusInternalServerError)
}
return
}

// Successful login
w.Header().Set("HX-Redirect", "/")
w.WriteHeader(http.StatusOK)
}

func signUpHandler(w http.ResponseWriter, r *http.Request, db *pgxpool.Pool) {
Expand All @@ -225,7 +255,7 @@ func signUpHandler(w http.ResponseWriter, r *http.Request, db *pgxpool.Pool) {

func registerUserHandler(w http.ResponseWriter, r *http.Request, db *pgxpool.Pool) {
if r.Method == http.MethodPost {
email := r.FormValue("username")
email := r.FormValue("email")
password := r.FormValue("password")
confirmedPassword := r.FormValue("confirm-password")

Expand All @@ -246,7 +276,48 @@ func registerUserHandler(w http.ResponseWriter, r *http.Request, db *pgxpool.Poo
}
}

func logOutHandler(w http.ResponseWriter, r *http.Request) {
func logOutHandler(w http.ResponseWriter, r *http.Request, db *pgxpool.Pool) {
st, err := r.Cookie("session_token")
if err != nil || st.Value == "" {
log.Println("unable to retrieve session token")
// return errors.New("unable to retrieve token")
}

// get user id from token
userID, err := getUserIDFromToken(st.Value, db)
if err != nil {
log.Println("error getting user ID from token")
// return err
}

// Set cookies with better security parameters
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: "",
HttpOnly: true,
// Secure: true, // Only send over HTTPS
SameSite: http.SameSiteStrictMode,
Path: "/",
MaxAge: 3600 * 24, // 24 hours
})

http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: "",
HttpOnly: false, // Needs to be accessible by JavaScript
Secure: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
MaxAge: 3600 * 24,
})
stmt := "UPDATE users SET sessiontoken = $1, csrftoken = $2 WHERE id = $3 RETURNING id;"

var updatedID string
err = db.QueryRow(context.Background(), stmt, "", "", userID).Scan(&updatedID)
if err != nil {
log.Printf("Failed to delete tokens: %v", err)
}
log.Println("tokens deleted for user: ", updatedID)
}

func timer(w http.ResponseWriter, r *http.Request) {
Expand Down
141 changes: 111 additions & 30 deletions cmd/web/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,45 @@ import (
"donow/models"
"encoding/base64"
"errors"
"fmt"
"log"
"net/http"

"github.com/jackc/pgx"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/crypto/bcrypt"
)

func saveToDatabase(t models.Task, db *pgxpool.Pool) error {
ID := "c51cc54d-a264-4007-ba6f-bfa3ad7466e4"
stmt := "INSERT INTO tasks (user_id, title, stage) VALUES ($1, $2, $3);"
func saveToDatabase(t models.Task, db *pgxpool.Pool, r *http.Request) error {
// authorize
err := authorize(r, db)
if err != nil {
log.Println("Authorization failed:", err)
return err
}

// extra log
log.Println("Authorization successful, proceeding to save task")

_, err := db.Exec(context.Background(), stmt, ID, t.Title, t.Stage)
// get session from token
st, err := r.Cookie("session_token")
if err != nil || st.Value == "" {
return errors.New("unable to retrieve token")
}

// get user id from token
userID, err := getUserIDFromToken(st.Value, db)
if err != nil {
log.Println("Error inserting item:", err)
return err // Return the error for the caller to handle
return err
}

// functionality to search for user in database := user, found
stmt := "INSERT INTO tasks (user_id, title, stage) VALUES ($1, $2, $3);"
_, err = db.Exec(context.Background(), stmt, userID, t.Title, t.Stage)
if err != nil {
log.Println("Error inserting task:", err)
return fmt.Errorf("failed to save task: %w", err)
}
return nil
}

Expand All @@ -46,13 +68,23 @@ func moveTask(taskID string, stage string, db *pgxpool.Pool) error {

func authorize(r *http.Request, db *pgxpool.Pool) error {
st, err := r.Cookie("session_token")
if err != nil || st.Value == "" || !tokenExists(st.Value, db) {
return errors.New("Unauthroized")
if err != nil || st.Value == "" {
return errors.New("unauthorized: missing or empty session token")
}

if !tokenExists(st.Value, db) {
return errors.New("unauthorized: invalid session token")
}

csrf := r.Header.Get("X-CSRF-Token")
if csrf != lookupCSRF(st.Value, db) || csrf == "" {
return errors.New("Unauthroized")
expectedCSRF, err := lookupCSRF(st.Value, db)
if err != nil {
return errors.New("unauthorized: could not fetch csrf token")
}
if csrf == "" || expectedCSRF == "" || csrf != expectedCSRF {
return errors.New("unauthorized: invalid CSRF token")
}

return nil
}

Expand All @@ -76,38 +108,67 @@ func addUser(email string, password string, confirmedPassword string, db *pgxpoo
}

func loginUser(w http.ResponseWriter, email string, password string, db *pgxpool.Pool) error {
stmt := "SELECT password_hash FROM users WHERE email = $1;"
// Add logging for debugging
log.Printf("Login attempt for email: %s", email)

// Get user's password hash
stmt := "SELECT id, password_hash FROM users WHERE email = $1;"
row := db.QueryRow(context.Background(), stmt, email)
var hash string
err := row.Scan(&hash)
// functionality to search for user in database := user, found
if err != nil || checkPasswordHash(password, hash) {
log.Println("loging unsuccesful:", err)
return err
var (
userID string
hash string
)
if err := row.Scan(&userID, &hash); err != nil {
log.Printf("User lookup failed: %v", err)
return fmt.Errorf("invalid credentials")
}
log.Println("loging succesful")

// Verify password
if !checkPasswordHash(password, hash) {
log.Printf("Password verification failed for user: %s", email)
return fmt.Errorf("invalid credentials")
}

// Generate tokens
sessionToken := generateToken(32)
csrfToken := generateToken(32)

// Set cookies with better security parameters
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: sessionToken,
HttpOnly: true,
// Secure: true, // Only send over HTTPS
SameSite: http.SameSiteStrictMode,
Path: "/",
MaxAge: 3600 * 24, // 24 hours
})

http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: csrfToken,
HttpOnly: false,
HttpOnly: false, // Needs to be accessible by JavaScript
Secure: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
MaxAge: 3600 * 24,
})

stmt = "INSERT INTO users (sessionToken, csrfToken) VALUES ($1, $2);"
_, err = db.Exec(context.Background(), stmt, sessionToken, csrfToken)
// Update database with new tokens
stmt = "UPDATE users SET sessiontoken = $1, csrftoken = $2 WHERE email = $3 RETURNING id;"

var updatedID string
err := db.QueryRow(context.Background(), stmt, sessionToken, csrfToken, email).Scan(&updatedID)
if err != nil {
log.Println("error adding session tokens to database:", err)
return err
log.Printf("Failed to update tokens: %v", err)
return fmt.Errorf("login failed: %w", err)
}

if updatedID == "" {
return fmt.Errorf("no user updated")
}

log.Printf("Login successful for user: %s", email)
return nil
}

Expand All @@ -130,17 +191,37 @@ func generateToken(length int) string {
}

func tokenExists(sessionToken string, db *pgxpool.Pool) bool {
var count int
stmt := "SELECT session_token FROM users WHERE session_token = $1;"
err := db.QueryRow(context.Background(), stmt, sessionToken).Scan(&count)
var token string
stmt := "SELECT sessiontoken FROM users WHERE sessiontoken = $1;"
err := db.QueryRow(context.Background(), stmt, sessionToken).Scan(&token)
return err == nil
}

func lookupCSRF(sessionToken string, db *pgxpool.Pool) string {
stmt := "SELECT csrf_token FROM users WHERE session_token = $1;"
func lookupCSRF(sessionToken string, db *pgxpool.Pool) (string, error) {
stmt := "SELECT csrftoken FROM users WHERE sessiontoken = $1;"
row := db.QueryRow(context.Background(), stmt, sessionToken)
var csrfToken string
row.Scan(&csrfToken)
err := row.Scan(&csrfToken)
if err != nil {
if err == pgx.ErrNoRows {
return "", errors.New("no user found with this csrf token")
}
return "", fmt.Errorf("unable to retrieve csrf token: %w", err)
}

return csrfToken
return csrfToken, err
}

func getUserIDFromToken(sessionToken string, db *pgxpool.Pool) (string, error) {
var userID string
getUserIDstmt := "SELECT id FROM users WHERE sessiontoken = $1;"
row := db.QueryRow(context.Background(), getUserIDstmt, sessionToken)
err := row.Scan(&userID)
if err != nil {
if err == pgx.ErrNoRows {
return "", errors.New("no user found with this session token")
}
return "", fmt.Errorf("unable to retrieve user id: %w", err)
}
return userID, nil
}
2 changes: 1 addition & 1 deletion cmd/web/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func main() {
})

mux.HandleFunc("/logOut", func(w http.ResponseWriter, r *http.Request) {
logOutHandler(w, r)
logOutHandler(w, r, dbPool)
})

// Start the server
Expand Down
11 changes: 9 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ module donow
go 1.23

require (
github.com/google/uuid v1.6.0
github.com/jackc/pgx v3.6.2+incompatible
github.com/jackc/pgx/v5 v5.7.2
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.31.0
)

require (
github.com/google/uuid v1.6.0 // indirect
github.com/cockroachdb/apd v1.1.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/crypto v0.31.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
)
Loading

0 comments on commit 9d7ef47

Please sign in to comment.