diff --git a/.github/workflows/deploy-golang-develop.yml b/.github/workflows/deploy-golang-develop.yml
index 91e977ad..d9d628b7 100644
--- a/.github/workflows/deploy-golang-develop.yml
+++ b/.github/workflows/deploy-golang-develop.yml
@@ -69,7 +69,7 @@ jobs:
- name: Run tests
run: |
- go test -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/handlers ./tests/... -coverprofile=coverage.out
+ go test -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
diff --git a/.github/workflows/deploy-golang-prod.yml b/.github/workflows/deploy-golang-prod.yml
index d130de75..9cfa7e78 100644
--- a/.github/workflows/deploy-golang-prod.yml
+++ b/.github/workflows/deploy-golang-prod.yml
@@ -61,7 +61,7 @@ jobs:
- name: Run tests
run: |
- go test -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/handlers ./tests/... -coverprofile=coverage.out
+ go test -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml
index c03916f5..ec411998 100644
--- a/.github/workflows/lint-test-build-golang.yml
+++ b/.github/workflows/lint-test-build-golang.yml
@@ -67,7 +67,7 @@ jobs:
- name: Run tests
run: |
- go test -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/handlers ./tests/... -coverprofile=coverage.out
+ go test -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
diff --git a/documentation/occupi-docs/pages/api-documentation/api-usage.mdx b/documentation/occupi-docs/pages/api-documentation/api-usage.mdx
index f1ed70ce..b79e54c1 100644
--- a/documentation/occupi-docs/pages/api-documentation/api-usage.mdx
+++ b/documentation/occupi-docs/pages/api-documentation/api-usage.mdx
@@ -11,10 +11,15 @@ The API also allows you to retrieve information about these resources.
- [Table of Contents](#table-of-contents)
- [Base URL](#base-url)
- [Ping](#ping)
+ - [Ping](#ping-1)
+ - [Ping-Auth](#ping-auth)
+ - [Ping-Admin](#ping-admin)
- [Authentication](#authentication)
- [Register](#register)
- [Login](#login)
+ - [Login-Admin](#login-admin)
- [Verify OTP](#verify-otp)
+ - [Logout](#logout)
- [Api](#api)
- [Resources](#resources)
- [BookRoom](#BookRoom)
@@ -26,10 +31,12 @@ The API also allows you to retrieve information about these resources.
## Base URL
-The base URL for the Occupi API is `https://occupi.tech` or `https://localhost:8080` if you are in develop mode.
+The base URL for the Occupi API is `https://occupi.tech`, `https://dev.occupi.tech` or `https://localhost:8080` if you are in develop mode.
## Ping
+### Ping
+
The ping endpoint is used to check if the API is up and running.
- **URL**
@@ -43,7 +50,7 @@ The ping endpoint is used to check if the API is up and running.
- **Success Response**
- **Code:** 200
- - **Content:** `{ "message": "pong -> I am alive and kicking" }`
+ - **Content:** `{ "status": 200, "message": "pong -> I am alive and kicking", "data": {}, }`
- **Error Response**
- **Code:** 404
@@ -55,9 +62,65 @@ The ping endpoint is used to check if the API is up and running.
{}
```
+### Ping-Auth
+
+The ping-auth endpoint is used to check if the API is up and running and also to check if the user is authenticated.
+
+- **URL**
+
+ `/ping-auth`
+
+- **Method**
+
+ `GET`
+
+- **Success Response**
+
+ - **Code:** 200
+ - **Content:** `{ "status": 200, "message": "pong -> I am alive and kicking and you are auth'd", "data": {}, }`
+
+- **Error Response**
+
+ - **Code:** 401
+ - **Content:** `{\"error\":{\"code\":\"INVALID_AUTH\",\"details\":null,\"message\":\"User not authorized\"},\"message\":\"Bad Request\",\"status\":401}`
+
+**_Example json to send:_**
+
+ ```json copy
+ {}
+ ```
+
+### Ping-Admin
+
+The ping-admin endpoint is used to check if the API is up and running and also to check if the user is an admin.
+
+- **URL**
+
+ `/ping-admin`
+
+- **Method**
+
+ `GET`
+
+- **Success Response**
+
+ - **Code:** 200
+ - **Content:** `{ "status": 200, "message": "pong -> I am alive and kicking and you are an admin", "data": {}, }`
+
+- **Error Response**
+
+ - **Code:** 401
+ - **Content:** `{\"error\":{\"code\":\"INVALID_AUTH\",\"details\":null,\"message\":\"User not authorized to access admin route\"},\"message\":\"Bad Request\",\"status\":401}`
+
+**_Example json to send:_**
+
+ ```json copy
+ {}
+ ```
+
## Authentication
-The authentication endpoints are used to register, login, and verify users. Only POST requests are used for these endpoints.
+The authentication endpoints are used to register, login, login-admin, logout, and verify users. Only POST requests are used for these endpoints.
### Register
@@ -125,6 +188,39 @@ The authentication endpoints are used to register, login, and verify users. Only
}
```
+### Login-Admin
+
+- **URL**
+
+ `/auth/login-admin`
+
+- **Method**
+
+ `POST`
+
+- **Success Response**
+
+ - **Code:** 200
+ - **Content:** `{ "status": 200, "message": "Successful login!", "data": {}, }`
+
+- **Error Response**
+
+ - **Code:** 400
+ - **Content:** `{"status": 400, "message": "Invalid email address": {"code": "INVALID_REQUEST_PAYLOAD","message": "Expected a valid format for email address": {}}}`
+
+- **Error Response**
+ - **Code:** 500
+ - **Content:** `{"status": 500, "message": "Internal Server Error","error": {"code": "INTERNAL_SERVER_ERROR","message": "Internal Server Error","details": {}}}`
+
+**_Example json to send:_**
+
+```json copy
+{
+ "email": "abcd@gmail.com",
+ "password": "123456"
+}
+```
+
### Verify OTP
- **URL**
@@ -158,6 +254,35 @@ The authentication endpoints are used to register, login, and verify users. Only
}
```
+### Logout
+
+- **URL**
+
+ `/auth/logout`
+
+- **Method**
+
+ `POST`
+
+- **Success Response**
+
+ - **Code:** 200
+ - **Content:** `{ "status": 200, "message": "Logout successful!", "data": {}, }`
+
+- **Error Response**
+
+ - **Code:** 400
+ - **Content:** `{\"error\":{\"code\":\"INVALID_AUTH\",\"details\":null,\"message\":\"Authorized user can't access this route\"},\"message\":\"Bad Request\",\"status\":401}`
+
+- **Error Response**
+ - **Code:** 500
+ - **Content:** `{"status": 500, "message": "Internal Server Error","error": {"code": "INTERNAL_SERVER_ERROR","message": "Internal Server Error","details": {}}}`
+
+**_Example json to send:_**
+```json copy
+{}
+```
+
## Api
The API endpoints are used to interact with the Occupi platform. Mainly GET, POST, PUT, DELETE requests are used.
diff --git a/documentation/occupi-docs/theme.config.jsx b/documentation/occupi-docs/theme.config.jsx
index cec5a5bf..38e2bd6f 100644
--- a/documentation/occupi-docs/theme.config.jsx
+++ b/documentation/occupi-docs/theme.config.jsx
@@ -1,7 +1,13 @@
export default {
- title: 'Occupi',
- favicon: 'https://raw.githubusercontent.com/COS301-SE-2024/occupi/5f614e7d881c9d4f65ec2cf6ea60bf5542eb77a7/presentation/Occupi/image_2024-05-21_213821107.svg',
- description: 'This is occupi-s documentation site',
+ head: (
+ <>
+
+
+
+
+
+ >
+ ),
logo: (
<>
I am alive and kicking", nil))
+}
+
+// PingHanlderAuth is a simple handler for testing if the server is up and running but requires authentication
+func PingHandlerAuth(ctx *gin.Context) {
+ ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "pong -> I am alive and kicking and you are auth'd", nil))
+}
+
+// PingHandlerAdmin is a simple handler for testing if the server is up and running but requires admin authentication
+func PingHandlerAdmin(ctx *gin.Context) {
+ ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "pong -> I am alive and kicking and you are an admin", nil))
+}
+
// handler for fetching test resource from /api/resource. Formats and returns json response
func FetchResource(ctx *gin.Context, appsession *models.AppSession) {
data := database.GetAllData(appsession.DB)
- ctx.JSON(http.StatusOK, gin.H{
- "status": http.StatusOK,
- "message": "Data fetched successfully",
- "data": data,
- })
+ ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Data fetched successfully", data))
}
// handler for fetching test resource from /api/resource. Formats and returns json response
func FetchResourceAuth(ctx *gin.Context, appsession *models.AppSession) {
data := database.GetAllData(appsession.DB)
- ctx.JSON(http.StatusOK, gin.H{
- "status": http.StatusOK,
- "message": "Data fetched successfully and authenticated",
- "data": data,
- })
+ ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Data fetched successfully", data))
}
// BookRoom handles booking a room and sends a confirmation email
diff --git a/occupi-backend/pkg/handlers/auth_handlers.go b/occupi-backend/pkg/handlers/auth_handlers.go
index 159a299f..004cf145 100644
--- a/occupi-backend/pkg/handlers/auth_handlers.go
+++ b/occupi-backend/pkg/handlers/auth_handlers.go
@@ -2,21 +2,22 @@ package handlers
import (
"net/http"
- "net/url"
+ "time"
- "github.com/COS301-SE-2024/occupi/occupi-backend/configs"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/mail"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils"
+
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// handler for logging a new user on occupi /auth/login
-func Login(ctx *gin.Context, appsession *models.AppSession) {
+func Login(ctx *gin.Context, appsession *models.AppSession, role string) {
var requestUser models.RequestUser
if err := ctx.ShouldBindBodyWithJSON(&requestUser); err != nil {
ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(
@@ -109,6 +110,26 @@ func Login(ctx *gin.Context, appsession *models.AppSession) {
return
}
+ // check if the user is an admin
+ if role == constants.Admin {
+ isAdmin, err := database.CheckIfUserIsAdmin(ctx, appsession.DB, requestUser.Email)
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
+ logrus.Error(err)
+ return
+ }
+
+ if !isAdmin {
+ ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(
+ http.StatusBadRequest,
+ "Not an admin",
+ constants.InvalidAuthCode,
+ "Only admins can access this route",
+ nil))
+ return
+ }
+ }
+
// check if the next verification date is due
due, err := database.CheckIfNextVerificationDateIsDue(ctx, appsession.DB, requestUser.Email)
if err != nil {
@@ -118,25 +139,39 @@ func Login(ctx *gin.Context, appsession *models.AppSession) {
}
if due {
- ReverifyUsersEmail(ctx, appsession, requestUser.Email)
+ reverifyUsersEmail(ctx, appsession, requestUser.Email)
return
}
- state, err := utils.GenerateRandomState()
+ // generate a jwt token for the user
+ var token string
+ var expirationTime time.Time
+ if role == constants.Admin {
+ token, expirationTime, err = authenticator.GenerateToken(requestUser.Email, constants.Admin)
+ } else {
+ token, expirationTime, err = authenticator.GenerateToken(requestUser.Email, "user")
+ }
+
if err != nil {
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
logrus.Error(err)
return
}
- // Save the state inside the session.
+ // set the jwt token in the cookie
session := sessions.Default(ctx)
- session.Set("state", state)
+ session.Set("email", requestUser.Email)
+ if role == constants.Admin {
+ session.Set("role", constants.Admin)
+ } else {
+ session.Set("role", constants.Basic)
+ }
if err := session.Save(); err != nil {
ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
logrus.Error(err)
return
}
+ ctx.SetCookie("token", token, int(expirationTime.Unix()), "/", "", false, true)
ctx.JSON(http.StatusOK, utils.SuccessResponse(
http.StatusOK,
@@ -144,9 +179,6 @@ func Login(ctx *gin.Context, appsession *models.AppSession) {
nil))
}
-// redirect to the Auth0 login page -> social auth stuff here
-// ctx.Redirect(http.StatusTemporaryRedirect, appsession.Authenticator.AuthCodeURL(state))
-
// handler for registering a new user on occupi /auth/register
func Register(ctx *gin.Context, appsession *models.AppSession) {
var requestUser models.RequestUser
@@ -228,23 +260,6 @@ func Register(ctx *gin.Context, appsession *models.AppSession) {
return
}
- // generete auth0 session and token
- state, err := utils.GenerateRandomState()
- if err != nil {
- ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
- logrus.Error(err)
- return
- }
-
- // Save the state inside the session.
- session := sessions.Default(ctx)
- session.Set("state", state)
- if err := session.Save(); err != nil {
- ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
- logrus.Error(err)
- return
- }
-
subject := "Email Verification - Your One-Time Password (OTP)"
body := mail.FormatEmailVerificationBody(otp, requestUser.Email)
@@ -338,7 +353,7 @@ func VerifyOTP(ctx *gin.Context, appsession *models.AppSession) {
}
// handler for reverifying a users email address
-func ReverifyUsersEmail(ctx *gin.Context, appsession *models.AppSession, email string) {
+func reverifyUsersEmail(ctx *gin.Context, appsession *models.AppSession, email string) {
// generate a random otp for the user and send email
otp, err := utils.GenerateOTP()
if err != nil {
@@ -374,31 +389,19 @@ func ResetPassword(ctx *gin.Context, appsession *models.AppSession) {
// this will contain reset password logic
}
-// handler for logging out a user on occupi /auth/logout
-func Logout(c *gin.Context) {
- logoutURL, err := url.Parse("https://" + configs.GetAuth0Domain() + "/v2/logout")
- if err != nil {
- c.JSON(http.StatusInternalServerError, utils.InternalServerError())
- logrus.Error(err)
- return
- }
-
- scheme := "http"
- if c.Request.TLS != nil {
- scheme = "https"
- }
-
- returnTo, err := url.Parse(scheme + "://" + c.Request.Host)
- if err != nil {
- c.JSON(http.StatusInternalServerError, utils.InternalServerError())
+// handler for logging out a user on occupi /auth/logout TODO: complete implementation
+func Logout(ctx *gin.Context) {
+ session := sessions.Default(ctx)
+ session.Clear()
+ if err := session.Save(); err != nil {
+ ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
logrus.Error(err)
return
}
- parameters := url.Values{}
- parameters.Add("returnTo", returnTo.String())
- parameters.Add("client_id", configs.GetAuth0ClientID())
- logoutURL.RawQuery = parameters.Encode()
-
- c.Redirect(http.StatusTemporaryRedirect, logoutURL.String())
+ ctx.SetCookie("token", "", -1, "/", "localhost", false, true)
+ ctx.JSON(http.StatusOK, utils.SuccessResponse(
+ http.StatusOK,
+ "Logged out successfully!",
+ nil))
}
diff --git a/occupi-backend/pkg/handlers/callback_handlers.go b/occupi-backend/pkg/handlers/callback_handlers.go
deleted file mode 100644
index 1a95cec8..00000000
--- a/occupi-backend/pkg/handlers/callback_handlers.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package handlers
-
-import (
- "net/http"
-
- "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models"
- "github.com/gin-contrib/sessions"
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
-)
-
-// Handler for our callback.
-func CallbackHandler(c *gin.Context, appsession *models.AppSession) {
- session := sessions.Default(c)
- if c.Query("state") != session.Get("state") {
- c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
- logrus.Error("Invalid state parameter.")
- return
- }
-
- // Exchange an authorization code for a token.
- token, err := appsession.Authenticator.Exchange(c.Request.Context(), c.Query("code"))
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
- logrus.Error(err)
- return
- }
-
- idToken, err := appsession.Authenticator.VerifyIDToken(c.Request.Context(), token)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify ID Token."})
- logrus.Error(err)
- return
- }
-
- var profile map[string]interface{}
- if err := idToken.Claims(&profile); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unmarshal ID Token."})
- logrus.Error(err)
- return
- }
-
- session.Set("access_token", token.AccessToken)
- session.Set("profile", profile)
- if err := session.Save(); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
- logrus.Error(err)
- return
- }
-
- // Redirect to logged in page.
- c.Redirect(http.StatusTemporaryRedirect, "/api/resource-auth")
-}
diff --git a/occupi-backend/pkg/middleware/middleware.go b/occupi-backend/pkg/middleware/middleware.go
index 3b3b2536..b1c0f905 100644
--- a/occupi-backend/pkg/middleware/middleware.go
+++ b/occupi-backend/pkg/middleware/middleware.go
@@ -3,47 +3,101 @@ package middleware
import (
"net/http"
- "github.com/gin-contrib/sessions"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils"
"github.com/gin-gonic/gin"
+ "github.com/sirupsen/logrus"
"github.com/ulule/limiter/v3"
mgin "github.com/ulule/limiter/v3/drivers/middleware/gin"
"github.com/ulule/limiter/v3/drivers/store/memory"
+
+ "github.com/gin-contrib/sessions"
)
// ProtectedRoute is a middleware that checks if
// the user has already been authenticated previously.
func ProtectedRoute(ctx *gin.Context) {
- if sessions.Default(ctx).Get("profile") == nil {
- // If the user is not authenticated, return a 401 Unauthorized response
- ctx.JSON(http.StatusUnauthorized, gin.H{
- "status": http.StatusUnauthorized,
- "message": "Bad Request",
- "error": "User not authenticated",
- })
- // Add the following so that the next() doesn't get called
+ tokenStr, err := ctx.Cookie("token")
+ if err != nil {
+ ctx.JSON(http.StatusUnauthorized,
+ utils.ErrorResponse(
+ http.StatusUnauthorized,
+ "Bad Request",
+ constants.InvalidAuthCode,
+ "User not authorized",
+ nil))
+ ctx.Abort()
+ return
+ }
+
+ claims, err := authenticator.ValidateToken(tokenStr)
+
+ if err != nil {
+ ctx.JSON(http.StatusUnauthorized,
+ utils.ErrorResponse(
+ http.StatusUnauthorized,
+ "Bad Request",
+ constants.InvalidAuthCode,
+ "User not authorized",
+ nil))
+ ctx.Abort()
+ return
+ }
+
+ session := sessions.Default(ctx)
+ session.Set("email", claims.Email)
+ session.Set("role", claims.Role)
+ if err := session.Save(); err != nil {
+ ctx.JSON(http.StatusInternalServerError, utils.InternalServerError())
+ logrus.Error(err)
ctx.Abort()
return
- } else {
- ctx.Next()
}
+ ctx.Next()
}
// ProtectedRoute is a middleware that checks if
// the user has not been authenticated previously.
func UnProtectedRoute(ctx *gin.Context) {
- if sessions.Default(ctx).Get("profile") != nil {
- // If the user is authenticated, return a 401 Unauthorized response
- ctx.JSON(http.StatusUnauthorized, gin.H{
- "status": http.StatusUnauthorized,
- "message": "Bad Request",
- "error": "User already authenticated",
- })
- // Add the following so that the next() doesn't get called
+ tokenStr, err := ctx.Cookie("token")
+ if err == nil {
+ _, err := authenticator.ValidateToken(tokenStr)
+
+ if err == nil {
+ ctx.JSON(http.StatusUnauthorized,
+ utils.ErrorResponse(
+ http.StatusUnauthorized,
+ "Bad Request",
+ constants.InvalidAuthCode,
+ "User already authorized",
+ nil))
+ ctx.Abort()
+ return
+ }
+ }
+
+ ctx.Next()
+}
+
+// AdminRoute is a middleware that checks if
+// the user has the admin role.
+func AdminRoute(ctx *gin.Context) {
+ session := sessions.Default(ctx)
+ role := session.Get("role")
+ if role != constants.Admin {
+ ctx.JSON(http.StatusUnauthorized,
+ utils.ErrorResponse(
+ http.StatusUnauthorized,
+ "Bad Request",
+ constants.InvalidAuthCode,
+ "User not authorized to access admin route",
+ nil))
ctx.Abort()
return
- } else {
- ctx.Next()
}
+
+ ctx.Next()
}
// AttachRateLimitMiddleware attaches the rate limit middleware to the router.
diff --git a/occupi-backend/pkg/models/app.go b/occupi-backend/pkg/models/app.go
index dd50a757..a44047b1 100644
--- a/occupi-backend/pkg/models/app.go
+++ b/occupi-backend/pkg/models/app.go
@@ -1,20 +1,17 @@
package models
import (
- "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator"
"go.mongodb.org/mongo-driver/mongo"
)
// state management for the web app during runtime
type AppSession struct {
- Authenticator *authenticator.Authenticator
- DB *mongo.Client
+ DB *mongo.Client
}
// constructor for app session
-func New(authenticator *authenticator.Authenticator, db *mongo.Client) *AppSession {
+func New(db *mongo.Client) *AppSession {
return &AppSession{
- Authenticator: authenticator,
- DB: db,
+ DB: db,
}
}
diff --git a/occupi-backend/pkg/router/router.go b/occupi-backend/pkg/router/router.go
index 6882f3d8..f105ff9a 100644
--- a/occupi-backend/pkg/router/router.go
+++ b/occupi-backend/pkg/router/router.go
@@ -1,13 +1,12 @@
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/configs"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/handlers"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models"
+
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
@@ -16,44 +15,43 @@ import (
// creates available endpoints and attaches handlers for each endpoint
func OccupiRouter(router *gin.Engine, db *mongo.Client) {
- authenticator, err := authenticator.New()
- if err != nil {
- panic(err)
- }
-
// creating a new valid session for management of shared variables
- appsession := models.New(authenticator, db)
-
- // To store custom types in our cookies,
- // we must first register them using gob.Register
- gob.Register(map[string]interface{}{})
+ appsession := models.New(db)
- store := cookie.NewStore([]byte("secret"))
- router.Use(sessions.Sessions("auth-session", store))
-
- router.Static("/landing", "./web/landing")
- router.Static("/app/dashboard", "./web/dashboard")
- router.Static("/documentation", "./web/documentation")
+ store := cookie.NewStore([]byte(configs.GetSessionSecret()))
+ router.Use(sessions.Sessions("occupi-sessions-store", store))
ping := router.Group("/ping")
{
- ping.GET("", func(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"message": "pong -> I am alive and kicking"}) })
+ ping.GET("", func(ctx *gin.Context) { handlers.PingHandler(ctx) })
+ }
+ pingAuth := router.Group("/ping-auth")
+ {
+ pingAuth.GET("", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.PingHandlerAuth(ctx) })
+ }
+ pingAdmin := router.Group("/ping-admin")
+ {
+ pingAdmin.GET("", middleware.ProtectedRoute, middleware.AdminRoute, func(ctx *gin.Context) { handlers.PingHandlerAdmin(ctx) })
}
api := router.Group("/api")
{
- api.GET("/resource-auth", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.FetchResourceAuth(ctx, appsession) }) // authenticated
+ // resource-auth serves as an example for adding authentication to a route, remove when not needed
+ api.GET("/resource-auth", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.FetchResourceAuth(ctx, appsession) })
+ // resource-auth-admin serves as an example for adding authentication as well as protecting admin routes, remove when not needed
+ api.GET("/resource-auth-admin", middleware.ProtectedRoute, middleware.AdminRoute, func(ctx *gin.Context) { handlers.FetchResourceAuth(ctx, appsession) })
api.POST("/book-room", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.BookRoom(ctx, appsession) })
api.POST("/check-in", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.CheckIn(ctx, appsession) })
- api.POST("cancel-booking", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.CancelBooking(ctx, appsession) })
- api.GET(("view-bookings"), middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.ViewBookings(ctx, appsession) })
+ api.POST("/cancel-booking", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.CancelBooking(ctx, appsession) })
+ api.GET(("/view-bookings"), middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.ViewBookings(ctx, appsession) })
api.GET("/view-rooms", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.ViewRooms(ctx, appsession) })
}
auth := router.Group("/auth")
{
- auth.POST("/login", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.Login(ctx, appsession) })
+ auth.POST("/login", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.Login(ctx, appsession, constants.Basic) })
+ auth.POST("/login-admin", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.Login(ctx, appsession, constants.Admin) })
auth.POST("/register", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.Register(ctx, appsession) })
auth.POST("/verify-otp", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.VerifyOTP(ctx, appsession) })
- // auth.POST("/logout", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.Logout(ctx) })
- auth.GET("/callback", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.CallbackHandler(ctx, appsession) })
+ auth.POST("/logout", middleware.ProtectedRoute, func(ctx *gin.Context) { handlers.Logout(ctx) })
+ // auth.POST("/reset-password", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.ForgotPassword(ctx) })
}
}
diff --git a/occupi-backend/pkg/utils/utils.go b/occupi-backend/pkg/utils/utils.go
index 814193a8..1f1dff29 100644
--- a/occupi-backend/pkg/utils/utils.go
+++ b/occupi-backend/pkg/utils/utils.go
@@ -153,8 +153,3 @@ func CompareArgon2IDHash(password string, hashedPassword string) (bool, error) {
}
return match, nil
}
-
-func WillRemove() {
- // This function is only here to make sure that the package is not empty
- // and that the linter does not complain about it
-}
diff --git a/occupi-backend/tests/authenticator_test.go b/occupi-backend/tests/authenticator_test.go
new file mode 100644
index 00000000..7a706377
--- /dev/null
+++ b/occupi-backend/tests/authenticator_test.go
@@ -0,0 +1,62 @@
+package tests
+
+import (
+ "testing"
+ "time"
+
+ "github.com/joho/godotenv"
+
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGenerateToken(t *testing.T) {
+ // Load environment variables from .env file
+ if err := godotenv.Load("../.env"); err != nil {
+ t.Fatal("Error loading .env file: ", err)
+ }
+
+ email := "test@example.com"
+ role := constants.Admin
+ tokenString, expirationTime, err := authenticator.GenerateToken(email, role)
+
+ require.NoError(t, err)
+ require.NotEmpty(t, tokenString)
+ require.WithinDuration(t, time.Now().Add(5*time.Minute), expirationTime, time.Second)
+
+ // Validate the token
+ claims, err := authenticator.ValidateToken(tokenString)
+ require.NoError(t, err)
+ require.NotNil(t, claims)
+ assert.Equal(t, email, claims.Email)
+ assert.Equal(t, role, claims.Role)
+}
+
+func TestValidateToken(t *testing.T) {
+ // Load environment variables from .env file
+ if err := godotenv.Load("../.env"); err != nil {
+ t.Fatal("Error loading .env file: ", err)
+ }
+
+ email := "test@example.com"
+ role := constants.Admin
+ tokenString, _, err := authenticator.GenerateToken(email, role)
+
+ require.NoError(t, err)
+ require.NotEmpty(t, tokenString)
+
+ // Validate the token
+ claims, err := authenticator.ValidateToken(tokenString)
+ require.NoError(t, err)
+ require.NotNil(t, claims)
+ assert.Equal(t, email, claims.Email)
+ assert.Equal(t, role, claims.Role)
+
+ // Test with an invalid token
+ invalidTokenString := "invalid_token"
+ claims, err = authenticator.ValidateToken(invalidTokenString)
+ require.Error(t, err)
+ assert.Nil(t, claims)
+}
diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go
index 6423e529..0cb9bf8b 100644
--- a/occupi-backend/tests/handlers_test.go
+++ b/occupi-backend/tests/handlers_test.go
@@ -14,13 +14,10 @@ import (
"github.com/gin-gonic/gin"
- "github.com/COS301-SE-2024/occupi/occupi-backend/configs"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/router"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils"
- // "github.com/joho/godotenv"
- // "github.com/stretchr/testify/assert"
// "github.com/stretchr/testify/mock"
)
@@ -159,7 +156,7 @@ func TestPingRoute(t *testing.T) {
db := database.ConnectToDatabase()
// set gin run mode
- gin.SetMode(configs.GetGinRunMode())
+ gin.SetMode("test")
// Create a Gin router
ginRouter := gin.Default()
@@ -216,7 +213,7 @@ func TestRateLimit(t *testing.T) {
db := database.ConnectToDatabase()
// set gin run mode
- gin.SetMode(configs.GetGinRunMode())
+ gin.SetMode("test")
// Create a Gin router
ginRouter := gin.Default()
@@ -275,7 +272,7 @@ func TestRateLimitWithMultipleIPs(t *testing.T) {
db := database.ConnectToDatabase()
// set gin run mode
- gin.SetMode(configs.GetGinRunMode())
+ gin.SetMode("test")
// Create a Gin router
ginRouter := gin.Default()
@@ -364,42 +361,3 @@ func TestRateLimitWithMultipleIPs(t *testing.T) {
// Assertions for IP2
assert.Equal(t, rateLimitedCountIP2, 0, "There should be no requests from IP2 that are rate limited")
}
-
-func TestGetResource(t *testing.T) {
- // Load environment variables from .env file
- if err := godotenv.Load("../.env"); err != nil {
- t.Fatal("Error loading .env file: ", err)
- }
-
- // setup logger to log all server interactions
- utils.SetupLogger()
-
- // connect to the database
- db := database.ConnectToDatabase()
-
- // set gin run mode
- gin.SetMode(configs.GetGinRunMode())
-
- // Create a Gin router
- r := gin.Default()
-
- // Register the route
- router.OccupiRouter(r, db)
-
- // Create a request to pass to the handler
- req, err := http.NewRequest("GET", "/api/resource", nil)
- if err != nil {
- t.Fatal(err)
- }
-
- // Create a response recorder to record the response
- rr := httptest.NewRecorder()
-
- // Serve the request
- r.ServeHTTP(rr, req)
-
- // Check the status code is what we expect.
- if status := rr.Code; status != http.StatusNotFound {
- t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound)
- }
-}
diff --git a/occupi-backend/tests/middleware_test.go b/occupi-backend/tests/middleware_test.go
new file mode 100644
index 00000000..6e07bf60
--- /dev/null
+++ b/occupi-backend/tests/middleware_test.go
@@ -0,0 +1,157 @@
+package tests
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/joho/godotenv"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/gin-gonic/gin"
+
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/router"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils"
+ // "github.com/stretchr/testify/mock"
+)
+
+func TestProtectedRoute(t *testing.T) {
+ // Load environment variables from .env file
+ if err := godotenv.Load("../.env"); err != nil {
+ t.Fatal("Error loading .env file: ", err)
+ }
+
+ // setup logger to log all server interactions
+ utils.SetupLogger()
+
+ // connect to the database
+ db := database.ConnectToDatabase()
+
+ // set gin run mode
+ gin.SetMode("test")
+
+ // Create a Gin router
+ r := gin.Default()
+
+ // Register the route
+ router.OccupiRouter(r, db)
+
+ token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic)
+
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest("GET", "/ping-auth", nil)
+ req.AddCookie(&http.Cookie{Name: "token", Value: token})
+
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Equal(
+ t,
+ "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}",
+ strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"),
+ )
+}
+
+func TestAdminRoute(t *testing.T) {
+ // Load environment variables from .env file
+ if err := godotenv.Load("../.env"); err != nil {
+ t.Fatal("Error loading .env file: ", err)
+ }
+
+ // setup logger to log all server interactions
+ utils.SetupLogger()
+
+ // connect to the database
+ db := database.ConnectToDatabase()
+
+ // set gin run mode
+ gin.SetMode("test")
+
+ // Create a Gin router
+ r := gin.Default()
+
+ // Register the route
+ router.OccupiRouter(r, db)
+
+ token, _, _ := authenticator.GenerateToken("admin@example.com", constants.Admin)
+
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest("GET", "/ping-admin", nil)
+ req.AddCookie(&http.Cookie{Name: "token", Value: token})
+
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Equal(
+ t,
+ "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are an admin\",\"status\":200}",
+ strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"),
+ )
+}
+
+func TestUnauthorizedAccess(t *testing.T) {
+ // Load environment variables from .env file
+ if err := godotenv.Load("../.env"); err != nil {
+ t.Fatal("Error loading .env file: ", err)
+ }
+
+ // setup logger to log all server interactions
+ utils.SetupLogger()
+
+ // connect to the database
+ db := database.ConnectToDatabase()
+
+ // set gin run mode
+ gin.SetMode("test")
+
+ // Create a Gin router
+ r := gin.Default()
+
+ // Register the route
+ router.OccupiRouter(r, db)
+
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest("GET", "/ping-auth", nil)
+
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+ assert.Equal(t, "{\"error\":{\"code\":\"INVALID_AUTH\",\"details\":null,\"message\":\"User not authorized\"},\"message\":\"Bad Request\",\"status\":401}", w.Body.String())
+}
+
+func TestUnauthorizedAdminAccess(t *testing.T) {
+ // Load environment variables from .env file
+ if err := godotenv.Load("../.env"); err != nil {
+ t.Fatal("Error loading .env file: ", err)
+ }
+
+ // setup logger to log all server interactions
+ utils.SetupLogger()
+
+ // connect to the database
+ db := database.ConnectToDatabase()
+
+ // set gin run mode
+ gin.SetMode("test")
+
+ // Create a Gin router
+ r := gin.Default()
+
+ // Register the route
+ router.OccupiRouter(r, db)
+
+ token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic)
+
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest("GET", "/ping-admin", nil)
+ req.AddCookie(&http.Cookie{Name: "token", Value: token})
+
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+ assert.Equal(t, "{\"error\":{\"code\":\"INVALID_AUTH\",\"details\":null,\"message\":\"User not authorized to access admin route\"},\"message\":\"Bad Request\",\"status\":401}", w.Body.String())
+}