diff --git a/rest-api/api-test/cancel-registration.http b/rest-api/api-test/cancel-registration.http new file mode 100644 index 0000000..63ea2b0 --- /dev/null +++ b/rest-api/api-test/cancel-registration.http @@ -0,0 +1,2 @@ +DELETE http://localhost:8080/events/1/register +authorization: \ No newline at end of file diff --git a/rest-api/api-test/login.http b/rest-api/api-test/login.http new file mode 100644 index 0000000..c013796 --- /dev/null +++ b/rest-api/api-test/login.http @@ -0,0 +1,7 @@ +POST http://localhost:8080/login +content-type: application/json + +{ + "email": "test3@example.com", + "password": "something" +} \ No newline at end of file diff --git a/rest-api/api-test/register.http b/rest-api/api-test/register.http new file mode 100644 index 0000000..0868f72 --- /dev/null +++ b/rest-api/api-test/register.http @@ -0,0 +1,3 @@ +POST http://localhost:8080/events/1/register +authorization: + diff --git a/rest-api/db/db.go b/rest-api/db/db.go index ba6fddd..2434c63 100644 --- a/rest-api/db/db.go +++ b/rest-api/db/db.go @@ -54,4 +54,20 @@ func createTables() { if err != nil { panic("Could not create table") } + + createRegistrationsTable := ` + CREATE TABLE IF NOT EXISTS registrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event_id INTEGER, + user_id INTEGER, + FOREIGN KEY(event_id) REFERENCES events(id), + FOREIGN KEY(user_id) REFERENCES users(id) + ) + ` + + _, err = DB.Exec(createRegistrationsTable) + + if err != nil { + panic("Could not create table") + } } diff --git a/rest-api/go.mod b/rest-api/go.mod index 9ad9f74..fb15110 100644 --- a/rest-api/go.mod +++ b/rest-api/go.mod @@ -15,6 +15,7 @@ require ( github.com/go-playground/validator/v10 v10.30.3 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/rest-api/go.sum b/rest-api/go.sum index af5a401..b7f5564 100644 --- a/rest-api/go.sum +++ b/rest-api/go.sum @@ -24,6 +24,8 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= diff --git a/rest-api/middlewares/auth.go b/rest-api/middlewares/auth.go new file mode 100644 index 0000000..14459ca --- /dev/null +++ b/rest-api/middlewares/auth.go @@ -0,0 +1,26 @@ +package middlewares + +import ( + "net/http" + + "example.com/rest-api/utils" + "github.com/gin-gonic/gin" +) + +func Authenticate(context *gin.Context) { + token := context.Request.Header.Get("Authorization") + + if token == "" { + context.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Not authorized"}) + return + } + + userId, err := utils.VerifyToken(token) + + if err != nil { + context.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Not authorized"}) + } + + context.Set("userId", userId) + context.Next() +} diff --git a/rest-api/models/event.go b/rest-api/models/event.go index 3bc570e..a34b90e 100644 --- a/rest-api/models/event.go +++ b/rest-api/models/event.go @@ -12,12 +12,12 @@ type Event struct { Description string `binding:"required"` Location string `binding:"required"` DateTime time.Time `binding:"required"` - UserID int + UserID int64 } var events []Event = []Event{} -func (e Event) Save() error { +func (e *Event) Save() error { query := ` INSERT INTO events(name, description, location, dateTime, user_id) VALUES (?, ?, ?, ?, ?)` @@ -105,3 +105,33 @@ func (event Event) Delete() error { return err } + +func (e Event) Register(userId int64) error { + query := "INSERT INTO registrations(event_id, user_id) VALUES (?, ?)" + stmt, err := db.DB.Prepare(query) + + if err != nil { + return err + } + + defer stmt.Close() + + _, err = stmt.Exec(e.ID, userId) + + return err +} + +func (e Event) CancelRegistration(userId int64) error { + query := "DELETE FROM registrations WHERE event_id = ? AND user_id = ?" + stmt, err := db.DB.Prepare(query) + + if err != nil { + return err + } + + defer stmt.Close() + + _, err = stmt.Exec(e.ID, userId) + + return err +} diff --git a/rest-api/models/user.go b/rest-api/models/user.go index 80bd9bf..00e31e3 100644 --- a/rest-api/models/user.go +++ b/rest-api/models/user.go @@ -1,6 +1,8 @@ package models import ( + "errors" + "example.com/rest-api/db" "example.com/rest-api/utils" ) @@ -38,3 +40,23 @@ func (u User) Save() error { u.ID = userId return err } + +func (u *User) ValidateCredentials() error { + query := "SELECT id, password FROM users WHERE email = ?" + row := db.DB.QueryRow(query, u.Email) + + var retrievedPassword string + err := row.Scan(&u.ID, &retrievedPassword) + + if err != nil { + return errors.New("Credentials invalid") + } + + passwordIsValid := utils.CheckPasswordHash(u.Password, retrievedPassword) + + if !passwordIsValid { + return errors.New("Credentials invalid") + } + + return nil +} diff --git a/rest-api/routes/events.go b/rest-api/routes/events.go index b58e0e0..085548f 100644 --- a/rest-api/routes/events.go +++ b/rest-api/routes/events.go @@ -42,8 +42,8 @@ func createEvent(context *gin.Context) { return } - event.ID = 1 - event.UserID = 1 + userId := context.GetInt64("userId") + event.UserID = userId err = event.Save() @@ -61,13 +61,19 @@ func updateEvent(context *gin.Context) { return } - _, err = models.GetEventByID(eventId) + userId := context.GetInt64("userId") + event, err := models.GetEventByID(eventId) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{"message": "Could not fetch the event."}) return } + if event.UserID != userId { + context.JSON(http.StatusUnauthorized, gin.H{"message": "Not authorized to update event"}) + return + } + var updatedEvent models.Event err = context.ShouldBindJSON(&updatedEvent) @@ -92,6 +98,7 @@ func deleteEvent(context *gin.Context) { return } + userId := context.GetInt64("userId") event, err := models.GetEventByID(eventId) if err != nil { @@ -99,6 +106,11 @@ func deleteEvent(context *gin.Context) { return } + if event.UserID != userId { + context.JSON(http.StatusUnauthorized, gin.H{"message": "Not authorized to delete event"}) + return + } + err = event.Delete() if err != nil { diff --git a/rest-api/routes/register.go b/rest-api/routes/register.go new file mode 100644 index 0000000..cda44c0 --- /dev/null +++ b/rest-api/routes/register.go @@ -0,0 +1,51 @@ +package routes + +import ( + "net/http" + "strconv" + + "example.com/rest-api/models" + "github.com/gin-gonic/gin" +) + +func registerForEvent(context *gin.Context) { + userId := context.GetInt64("userId") + eventId, err := strconv.ParseInt(context.Param("id"), 10, 64) + if err != nil { + context.JSON(http.StatusBadRequest, gin.H{"message": "Could not parse event id."}) + return + } + + event, err := models.GetEventByID(eventId) + + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{"message": "Could not fetch event."}) + return + } + + err = event.Register(userId) + + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{"message": "Could not register user for event"}) + return + } + + context.JSON(http.StatusCreated, gin.H{"message": "Registered!"}) +} + +func cancelRegistration(context *gin.Context) { + userId := context.GetInt64("userId") + eventId, err := strconv.ParseInt(context.Param("id"), 10, 64) + + var event models.Event + event.ID = eventId + + err = event.CancelRegistration(userId) + + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{"message": "Could not cancel registration"}) + return + } + + context.JSON(http.StatusOK, gin.H{"message": "Cancelled!"}) +} diff --git a/rest-api/routes/routes.go b/rest-api/routes/routes.go index 4607459..32d2ace 100644 --- a/rest-api/routes/routes.go +++ b/rest-api/routes/routes.go @@ -1,12 +1,22 @@ package routes -import "github.com/gin-gonic/gin" +import ( + "example.com/rest-api/middlewares" + "github.com/gin-gonic/gin" +) func RegisterRoutes(server *gin.Engine) { server.GET("/events", getEvents) server.GET("/events/:id", getEvent) - server.POST("/events", createEvent) - server.PUT("/events/:id", updateEvent) - server.DELETE("/events/:id", deleteEvent) + + authenticated := server.Group("/") + authenticated.Use(middlewares.Authenticate) + authenticated.POST("/events", createEvent) + authenticated.PUT("/events/:id", updateEvent) + authenticated.DELETE("/events/:id", deleteEvent) + authenticated.POST("/events/:id/register", registerForEvent) + authenticated.DELETE("/events/:id/register", cancelRegistration) + server.POST("/signup", signup) + server.POST("/login", login) } diff --git a/rest-api/routes/users.go b/rest-api/routes/users.go index 80f1b09..d62e30f 100644 --- a/rest-api/routes/users.go +++ b/rest-api/routes/users.go @@ -4,6 +4,7 @@ import ( "net/http" "example.com/rest-api/models" + "example.com/rest-api/utils" "github.com/gin-gonic/gin" ) @@ -26,3 +27,29 @@ func signup(context *gin.Context) { context.JSON(http.StatusCreated, gin.H{"message": "User created successfully"}) } + +func login(context *gin.Context) { + var user models.User + + err := context.ShouldBindJSON(&user) + + if err != nil { + context.JSON(http.StatusBadRequest, gin.H{"message": "Could not parse request data."}) + return + } + + err = user.ValidateCredentials() + + if err != nil { + context.JSON(http.StatusUnauthorized, gin.H{"message": "Could not authenticate user."}) + } + + token, err := utils.GenerateToken(user.Email, user.ID) + + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{"message": "Could not authenticate user."}) + return + } + + context.JSON(http.StatusOK, gin.H{"message": "Login successful!", "token": token}) +} diff --git a/rest-api/utils/hash.go b/rest-api/utils/hash.go index 33a59b0..a2a6bde 100644 --- a/rest-api/utils/hash.go +++ b/rest-api/utils/hash.go @@ -6,3 +6,8 @@ func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) return string(bytes), err } + +func CheckPasswordHash(password, hashedPassword string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) + return err == nil +} diff --git a/rest-api/utils/jwt.go b/rest-api/utils/jwt.go new file mode 100644 index 0000000..221407b --- /dev/null +++ b/rest-api/utils/jwt.go @@ -0,0 +1,53 @@ +package utils + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +const secretKey = "supersecret" + +func GenerateToken(email string, userId int64) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "email": "", + "userId": "", + "exp": time.Now().Add(time.Hour * 2).Unix(), + }) + + return token.SignedString([]byte(secretKey)) +} + +func VerifyToken(token string) (int64, error) { + parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + _, ok := token.Method.(*jwt.SigningMethodHMAC) + + if !ok { + return nil, errors.New("Unexpected signing method") + } + + return secretKey, nil + }) + + if err != nil { + return 0, errors.New("Could not parse token.") + } + + tokenIsValid := parsedToken.Valid + + if !tokenIsValid { + return 0, errors.New("Invalid token") + } + + claims, ok := parsedToken.Claims.(jwt.MapClaims) + + if !ok { + return 0, errors.New("Invalid token claims.") + } + + // email := claims["email"].(string) + userId := int64(claims["userId"].(float64)) + + return userId, nil +}