fixed some login and booking bugs

This commit is contained in:
tumillanino
2025-10-28 13:36:05 +11:00
parent dad3ec655f
commit 5d7f0a8eb3
11 changed files with 402 additions and 141 deletions

View File

@@ -5,67 +5,136 @@ import (
"encoding/json"
"html/template"
"net/http"
"strconv"
"regexp"
"strings"
"time"
"decor-by-hannahs/internal/auth"
ory "github.com/ory/client-go"
"decor-by-hannahs/internal/db"
)
func BookingPageHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
func validateAustralianAddress(address string) (bool, string) {
if len(strings.TrimSpace(address)) < 10 {
return false, "Address is too short. Please provide a complete address."
}
australianStates := []string{"NSW", "VIC", "QLD", "SA", "WA", "TAS", "NT", "ACT"}
hasState := false
upperAddress := strings.ToUpper(address)
for _, state := range australianStates {
if strings.Contains(upperAddress, state) {
hasState = true
break
}
}
postcodePattern := regexp.MustCompile(`\b\d{4}\b`)
hasPostcode := postcodePattern.MatchString(address)
if !hasState && !hasPostcode {
return false, "Please include an Australian state (NSW, VIC, QLD, etc.) and postcode in your address."
}
if !hasState {
return false, "Please include an Australian state (NSW, VIC, QLD, SA, WA, TAS, NT, or ACT) in your address."
}
if !hasPostcode {
return false, "Please include a 4-digit postcode in your address."
}
return true, ""
}
func BookingPageHandler(q *db.Queries, tmpl *template.Template, oryClient *ory.APIClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type data struct {
Title string
ActivePage string
Title string
ActivePage string
Authenticated bool
OryLoginURL string
}
if err := tmpl.ExecuteTemplate(w, "booking.tmpl", data{
Title: "Book a Service",
ActivePage: "booking",
Authenticated: isAuthenticated(r, oryClient),
OryLoginURL: "/login",
}); err != nil {
http.Error(w, "Failed to render page", http.StatusInternalServerError)
return
}
_ = tmpl.ExecuteTemplate(w, "booking.tmpl", data{Title: "Book a Service", ActivePage: "booking"})
}
}
func BookingAPIHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
func BookingAPIHandler(q *db.Queries, tmpl *template.Template, oryClient *ory.APIClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method is not allowed", http.StatusMethodNotAllowed)
return
}
user, err := auth.GetUser(r.Context())
if err != nil {
cookies := r.Header.Get("Cookie")
session, _, err := oryClient.FrontendAPI.ToSession(r.Context()).Cookie(cookies).Execute()
if err != nil || session == nil || session.Active == nil || !*session.Active {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
email := getEmailFromSession(session)
if email == "" {
http.Error(w, "No email in session", http.StatusBadRequest)
return
}
oryID := sql.NullString{String: session.Identity.Id, Valid: true}
user, err := q.GetUserByOryID(r.Context(), oryID)
if err != nil {
user, err = q.CreateUser(r.Context(), db.CreateUserParams{
Email: email,
OryIdentityID: oryID,
})
if err != nil {
http.Error(w, "Failed to get or create user", http.StatusInternalServerError)
return
}
}
var in struct {
ServiceID string `json:"service_id"`
EventDate string `json:"event_date"`
Address string `json:"address"`
Notes string `json:"notes"`
ServiceOption string `json:"service_option"`
EventType string `json:"event_type"`
EventDate string `json:"event_date"`
Address string `json:"address"`
Notes string `json:"notes"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
if valid, msg := validateAustralianAddress(in.Address); !valid {
http.Error(w, msg, http.StatusBadRequest)
return
}
eventTime, err := time.Parse(time.RFC3339, in.EventDate)
if err != nil {
http.Error(w, "Invalid date", http.StatusBadRequest)
return
}
svc, err := strconv.ParseInt(in.ServiceID, 10, 64)
if err != nil {
http.Error(w, "Invalid Service ID", http.StatusBadRequest)
return
}
svcID := sql.NullInt64{Int64: svc, Valid: true}
userID := sql.NullInt64{Int64: user.ID, Valid: true}
booking, err := q.CreateBooking(r.Context(), db.CreateBookingParams{
UserID: userID,
ServiceID: svcID,
EventDate: eventTime,
Address: sqlNullString(in.Address),
Notes: sqlNullString(in.Notes),
Status: sql.NullString{String: "pending", Valid: true},
UserID: userID,
ServiceID: sql.NullInt64{},
EventDate: eventTime,
Address: sqlNullString(in.Address),
Notes: sqlNullString(in.Notes),
Status: sql.NullString{String: "pending", Valid: true},
ServiceOption: sqlNullString(in.ServiceOption),
EventType: sqlNullString(in.EventType),
})
if err != nil {
http.Error(w, "Failed to create booking", http.StatusInternalServerError)

View File

@@ -4,6 +4,9 @@ import (
"database/sql"
"encoding/json"
"net/http"
"os"
ory "github.com/ory/client-go"
)
func sqlNullString(s string) sql.NullString {
@@ -13,6 +16,13 @@ func sqlNullString(s string) sql.NullString {
return sql.NullString{String: s, Valid: true}
}
func nullStringToString(ns sql.NullString) string {
if ns.Valid {
return ns.String
}
return ""
}
func sqlNullUUID(s string) sql.NullString {
if s == "" {
return sql.NullString{}
@@ -24,3 +34,24 @@ func writeJSON(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
func getOryAPIURL() string {
oryAPIURL := os.Getenv("ORY_API_URL")
if oryAPIURL == "" {
tunnelPort := os.Getenv("TUNNEL_PORT")
if tunnelPort == "" {
tunnelPort = "4000"
}
oryAPIURL = "http://localhost:" + tunnelPort
}
return oryAPIURL
}
func isAuthenticated(r *http.Request, oryClient *ory.APIClient) bool {
cookies := r.Header.Get("Cookie")
session, _, err := oryClient.FrontendAPI.ToSession(r.Context()).Cookie(cookies).Execute()
if err != nil {
return false
}
return session != nil && session.Active != nil && *session.Active
}

View File

@@ -8,10 +8,12 @@ import (
"path/filepath"
"sort"
ory "github.com/ory/client-go"
"decor-by-hannahs/internal/db"
)
func HomeHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
func HomeHandler(q *db.Queries, tmpl *template.Template, oryClient *ory.APIClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("HomeHandler called: method=%s path=%s", r.Method, r.URL.Path)
if r.URL.Path != "/" {
@@ -42,11 +44,19 @@ func HomeHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
})
type data struct {
Title string
Photos []Photo
ActivePage string
Title string
Photos []Photo
ActivePage string
Authenticated bool
OryLoginURL string
}
if err := tmpl.ExecuteTemplate(w, "home.tmpl", data{Title: "Home", Photos: photos, ActivePage: "home"}); err != nil {
if err := tmpl.ExecuteTemplate(w, "home.tmpl", data{
Title: "Home",
Photos: photos,
ActivePage: "home",
Authenticated: isAuthenticated(r, oryClient),
OryLoginURL: "/login",
}); err != nil {
log.Printf("Error executing home.tmpl: %v", err)
http.Error(w, "Failed to render page", http.StatusInternalServerError)
}

View File

@@ -1,77 +1,99 @@
package handlers
import (
"database/sql"
"html/template"
"log"
"net/http"
"os"
"strconv"
"decor-by-hannahs/internal/auth"
ory "github.com/ory/client-go"
"decor-by-hannahs/internal/db"
)
type BookingWithService struct {
ID int64
ServiceName string
ServiceDescription string
ServicePriceCents int32
EventDate string
Address string
Notes string
Status string
CreatedAt string
ID int64
ServiceName string
ServiceDescription string
ServicePriceCents int32
ServiceOption string
EventType string
EventDate string
Address string
Notes string
Status string
CreatedAt string
}
type ProfileData struct {
Authenticated bool
Email string
DisplayName string
ID string
ActivePage string
Bookings []BookingWithService
OrySettingsURL string
Authenticated bool
Email string
DisplayName string
ID string
ActivePage string
Bookings []BookingWithService
OrySettingsURL string
OryLoginURL string
}
func ProfileHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
func ProfileHandler(q *db.Queries, tmpl *template.Template, oryClient *ory.APIClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
pd := ProfileData{ActivePage: "profile"}
oryAPIURL := getOryAPIURL()
pd := ProfileData{
ActivePage: "profile",
OryLoginURL: "/login",
OrySettingsURL: oryAPIURL + "/ui/settings",
}
user, err := auth.GetUser(r.Context())
if err == nil {
pd.Authenticated = true
pd.Email = user.Email
pd.ID = strconv.FormatInt(user.ID, 10)
pd.DisplayName = user.Email
bookings, err := q.ListBookingsWithServiceByUser(r.Context(), user.ID)
if err != nil {
log.Printf("Error fetching bookings: %v", err)
} else {
for _, b := range bookings {
pd.Bookings = append(pd.Bookings, BookingWithService{
ID: b.ID,
ServiceName: b.ServiceName,
ServiceDescription: nullStringToString(b.ServiceDescription),
ServicePriceCents: b.ServicePriceCents,
EventDate: b.EventDate.Format("January 2, 2006"),
Address: nullStringToString(b.Address),
Notes: nullStringToString(b.Notes),
Status: b.Status,
CreatedAt: b.CreatedAt.Time.Format("Jan 2, 2006"),
cookies := r.Header.Get("Cookie")
session, _, err := oryClient.FrontendAPI.ToSession(r.Context()).Cookie(cookies).Execute()
if err == nil && session != nil && session.Active != nil && *session.Active {
email := getEmailFromSession(session)
if email != "" {
oryID := sql.NullString{String: session.Identity.Id, Valid: true}
user, err := q.GetUserByOryID(r.Context(), oryID)
if err != nil {
user, err = q.CreateUser(r.Context(), db.CreateUserParams{
Email: email,
OryIdentityID: oryID,
})
if err != nil {
log.Printf("Failed to create user: %v", err)
} else {
log.Printf("Created new user: %s (ID: %d)", email, user.ID)
}
}
}
oryAPIURL := os.Getenv("ORY_API_URL")
if oryAPIURL == "" {
tunnelPort := os.Getenv("TUNNEL_PORT")
if tunnelPort == "" {
tunnelPort = "4000"
if err == nil {
pd.Authenticated = true
pd.Email = user.Email
pd.ID = strconv.FormatInt(user.ID, 10)
pd.DisplayName = user.Email
bookings, err := q.ListBookingsWithServiceByUser(r.Context(), sql.NullInt64{Int64: user.ID, Valid: true})
if err != nil {
log.Printf("Error fetching bookings: %v", err)
} else {
for _, b := range bookings {
pd.Bookings = append(pd.Bookings, BookingWithService{
ID: b.ID,
ServiceName: b.ServiceName,
ServiceDescription: nullStringToString(b.ServiceDescription),
ServicePriceCents: b.ServicePriceCents,
ServiceOption: nullStringToString(b.ServiceOption),
EventType: nullStringToString(b.EventType),
EventDate: b.EventDate.Format("January 2, 2006"),
Address: nullStringToString(b.Address),
Notes: nullStringToString(b.Notes),
Status: nullStringToString(b.Status),
CreatedAt: b.CreatedAt.Time.Format("Jan 2, 2006"),
})
}
}
}
oryAPIURL = "http://localhost:" + tunnelPort
}
pd.OrySettingsURL = oryAPIURL + "/ui/settings"
}
if err := tmpl.ExecuteTemplate(w, "profile.tmpl", pd); err != nil {
@@ -80,3 +102,18 @@ func ProfileHandler(q *db.Queries, tmpl *template.Template) http.HandlerFunc {
}
}
}
func getEmailFromSession(session *ory.Session) string {
if session.Identity.Traits == nil {
return ""
}
traits, ok := session.Identity.Traits.(map[string]interface{})
if !ok {
return ""
}
email, ok := traits["email"].(string)
if !ok {
return ""
}
return email
}