Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/backend/email functionality #50

Merged
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
26 changes: 24 additions & 2 deletions occupi-backend/pkg/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,30 @@ func SaveBooking(ctx *gin.Context, db *mongo.Client, booking models.Booking) (bo
}
return true, nil
}

// Retrieves bookings associated with a user
func GetUserBookings(ctx *gin.Context, db *mongo.Client, email string) ([]models.Booking, error) {
// Get the bookings for the user
collection := db.Database("Occupi").Collection("RoomBooking")
filter := bson.M{"emails": bson.M{"$elemMatch": bson.M{"$eq": email}}}
cursor, err := collection.Find(ctx, filter)
if err != nil {
logrus.Error(err)
return nil, err
}
defer cursor.Close(ctx)

var bookings []models.Booking
for cursor.Next(ctx) {
var booking models.Booking
if err := cursor.Decode(&booking); err != nil {
logrus.Error(err)
return nil, err
}
bookings = append(bookings, booking)
}
return bookings, nil
}
func ConfirmCheckIn(ctx *gin.Context, db *mongo.Client, checkIn models.CheckIn) (bool, error) {
// Save the check-in to the database
collection := db.Database("Occupi").Collection("RoomBooking")
Expand All @@ -108,10 +132,8 @@ func ConfirmCheckIn(ctx *gin.Context, db *mongo.Client, checkIn models.CheckIn)

// Find the booking
var booking models.Booking
fmt.Println(filter)
err := collection.FindOne(context.TODO(), filter).Decode(&booking)
if err != nil {
fmt.Println(err)
if err == mongo.ErrNoDocuments {
logrus.Error("Booking not found")
return false, errors.New("booking not found")
Expand Down
40 changes: 35 additions & 5 deletions occupi-backend/pkg/handlers/api_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) {

var booking models.Booking
if err := ctx.ShouldBindJSON(&booking); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected RoomID,Slot,Emails ", nil))
return
}

Expand All @@ -53,7 +53,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) {
// Save the booking to the database
_, err := database.SaveBooking(ctx, appsession.DB, booking)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save booking"})
ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to save booking", constants.InternalServerErrorCode, "Failed to save booking", nil))
return
}

Expand All @@ -65,11 +65,29 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) {
emailErrors := utils.SendMultipleEmailsConcurrently(booking.Emails, subject, body)

if len(emailErrors) > 0 {
// avoid letting the user know which emails failed
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send confirmation emails to some addresses"})
ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send confirmation email", constants.InternalServerErrorCode, "Failed to send confirmation email", nil))
return
}
ctx.JSON(http.StatusOK, gin.H{"message": "Booking successful! Confirmation emails sent."})

ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully booked!", nil))
}

// ViewBookings handles the retrieval of all bookings for a user
func ViewBookings(ctx *gin.Context, appsession *models.AppSession) {
var user models.User
if err := ctx.ShouldBindJSON(&user); err != nil {
ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Email Address", nil))
return
}

// Get all bookings for the user
bookings, err := database.GetUserBookings(ctx, appsession.DB, user.Email)
if err != nil {
ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get bookings", constants.InternalServerErrorCode, "Failed to get bookings", nil))
return
}

ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched bookings!", bookings))
}

// Cancel booking handles the cancellation of a booking
Expand All @@ -94,6 +112,18 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) {
return
}

// Prepare the email content
subject := "Booking Cancelled - Occupi"
body := mail.FormatBookingEmailBody(booking.BookingID, booking.RoomID, booking.Slot)

// Send the confirmation email concurrently to all recipients
emailErrors := utils.SendMultipleEmailsConcurrently(booking.Emails, subject, body)

if len(emailErrors) > 0 {
ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send cancellation email", constants.InternalServerErrorCode, "Failed to send cancellation email", nil))
return
}

ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully cancelled booking!", nil))
}

Expand Down
4 changes: 2 additions & 2 deletions occupi-backend/pkg/handlers/auth_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ func Register(ctx *gin.Context, appsession *models.AppSession) {
}

subject := "Email Verification - Your One-Time Password (OTP)"
body := mail.FormatEmailVerificationBody(otp)
body := mail.FormatEmailVerificationBody(otp, requestUser.Email)

if err := mail.SendMail(requestUser.Email, subject, body); err != nil {
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
Expand Down Expand Up @@ -355,7 +355,7 @@ func ReverifyUsersEmail(ctx *gin.Context, appsession *models.AppSession, email s
}

subject := "Email Verification - Your One-Time Password (OTP)"
body := mail.FormatEmailVerificationBody(otp)
body := mail.FormatEmailVerificationBody(otp, email)

if err := mail.SendMail(email, subject, body); err != nil {
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
Expand Down
108 changes: 98 additions & 10 deletions occupi-backend/pkg/mail/email_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,106 @@ func FormatBookingEmailBody(bookingID int, roomID string, slot int) string {
`
}

// formats verification email body
func FormatEmailVerificationBody(otp string) string {
return `
Thank you for registering with Occupi. To complete your registration, please use the following One-Time Password (OTP) to verify your email address:
// formats booking email body to send person who booked
func FormatBookingEmailBodyForBooker(bookingID int, roomID string, slot int, attendees map[string]string) string {
listOfAttendees := "<ul>"
for _, email := range attendees {
listOfAttendees += "<li>" + email + "</li>"
}
listOfAttendees += "</ul>"

return appendHeader("Booking") + `
<div class="content">
<p>Dear booker,</p>
<p>
You have successfully booked an office space. Here are the booking details:<br><br>
<b>Booking ID:</b> ` + strconv.Itoa(bookingID) + `<br>
<b>Room ID:</b> ` + roomID + `<br>
<b>Slot:</b> ` + strconv.Itoa(slot) + `<br><br>
<b>Attendees:</b>` + listOfAttendees + `<br><br>
Please ensure you arrive on time for your booking.<br><br>
Thank you,<br>
<b>The Occupi Team</b><br>
</p>
</div>` + appendFooter()
}

OTP: ` + otp + `
// formats booking email body to send attendees
func FormatBookingEmailBodyForAttendees(bookingID int, roomID string, slot int, email string) string {
return appendHeader("Booking") + `
<div class="content">
<p>Dear attendees,</p>
<p>
` + email + ` has booked an office space and invited you to join. Here are the booking details:<br><br>
<b>Booking ID:</b> ` + strconv.Itoa(bookingID) + `<br>
<b>Room ID:</b> ` + roomID + `<br>
<b>Slot:</b> ` + strconv.Itoa(slot) + `<br><br>
If you have any questions, feel free to contact us.<br><br>
Thank you,<br>
<b>The Occupi Team</b><br>
</p>
</div>` + appendFooter()
}

This OTP is valid for the next 10 minutes. Please do not share this OTP with anyone for security reasons.
// formats verification email body
func FormatEmailVerificationBody(otp string, email string) string {
return appendHeader("Registration") + `
<div class="content">
<p>Dear ` + email + `,</p>
<p>
Thank you for registering with Occupi. <br><br>
To complete your registration, please use the following One-Time Password (OTP) to verify your email address:<br>
OTP: <b>` + otp + `</b><br>
This OTP is valid for the next <i>10 minutes</i>. Please do not share this OTP with anyone for security reasons.<br><br>
If you did not request this email, please disregard it.<br><br>
Thank you,<br>
<b>The Occupi Team</b><br>
</p>
</div>` + appendFooter()
}

If you did not request this email, please disregard it.
func appendHeader(title string) string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>` + title + `</title>
<style>
/* Inline CSS for better compatibility */
.header {
background-color: #f8f9fa;
padding: 20px;
text-align: center;
font-family: Arial, sans-serif;
}
.content {
padding: 20px;
font-family: Arial, sans-serif;
}
.footer {
padding: 10px;
text-align: center;
font-family: Arial, sans-serif;
font-size: 12px;
color: #888;
}
</style>
</head>
<body>
<div class="header">
<h1>Occupi ` + title + `</h1>
</div>
`
}

Thank you,
The Occupi Team
`
func appendFooter() string {
return `<div class="footer">
<img src="https://raw.githubusercontent.com/COS301-SE-2024/occupi/develop/presentation/Occupi/Occupi-black.png" alt="Business Banner" style="width:100%;">
<p>140 Lunnon Road, Hillcrest, Pretoria. PO Box 14679, Hatfield, 0028</p>
</div>
</body>
</html>
`
}
2 changes: 1 addition & 1 deletion occupi-backend/pkg/mail/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func SendMail(to string, subject string, body string) error {
m.SetHeader("From", from)
m.SetHeader("To", to)
m.SetHeader("Subject", subject)
m.SetBody("text/plain", body)
m.SetBody("text/html", body)

d := gomail.NewDialer(smtpHost, smtpPort, from, password)

Expand Down
14 changes: 7 additions & 7 deletions occupi-backend/pkg/models/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ type User struct {

// structure of booking
type Booking struct {
ID string `json:"_id" bson:"_id,omitempty"`
OccupiID int `json:"occupiId" bson:"occupiId"`
BookingID int `json:"bookingId" bson:"bookingId"`
RoomID string `json:"roomId" bson:"roomId"`
Slot int `json:"slot" bson:"slot"`
Emails map[string]string `json:"emails" bson:"emails"`
CheckedIn bool `json:"checkedIn" bson:"checkedIn"`
ID string `json:"_id" bson:"_id,omitempty"`
OccupiID int `json:"occupiId" bson:"occupiId"`
BookingID int `json:"bookingId" bson:"bookingId"`
RoomID string `json:"roomId" bson:"roomId"`
Slot int `json:"slot" bson:"slot"`
Emails []string `json:"emails" bson:"emails"`
CheckedIn bool `json:"checkedIn" bson:"checkedIn"`
}

// structure of CheckIn
Expand Down
3 changes: 2 additions & 1 deletion occupi-backend/pkg/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package router

import (
"encoding/gob"
"net/http"

"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/handlers"
Expand Down Expand Up @@ -36,7 +37,7 @@ func OccupiRouter(router *gin.Engine, db *mongo.Client) {

ping := router.Group("/ping")
{
ping.GET("", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "pong -> I am alive and kicking"}) })
ping.GET("", func(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"message": "pong -> I am alive and kicking"}) })
}
api := router.Group("/api")
{
Expand Down
2 changes: 1 addition & 1 deletion occupi-backend/pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func GenerateRandomState() (string, error) {
}

// sends multiple emails concurrently
func SendMultipleEmailsConcurrently(emails map[string]string, subject, body string) []string {
func SendMultipleEmailsConcurrently(emails []string, subject, body string) []string {
// Use a WaitGroup to wait for all goroutines to complete
var wg sync.WaitGroup
var emailErrors []string
Expand Down