From de0e20f785019ae4d01907d674102818e2110962 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 17 Jun 2024 19:44:26 +0200 Subject: [PATCH 1/3] feat: auth logic introduced with backend session management --- occupi-backend/.dev.env.gpg | Bin 594 -> 0 bytes occupi-backend/.env.gpg | Bin 559 -> 0 bytes occupi-backend/.prod.env.gpg | Bin 594 -> 0 bytes occupi-backend/configs/config.go | 36 ++---- occupi-backend/go.mod | 1 + occupi-backend/go.sum | 2 + occupi-backend/pkg/authenticator/auth.go | 69 +++++------ occupi-backend/pkg/database/database.go | 14 +++ occupi-backend/pkg/handlers/api_handlers.go | 17 ++- occupi-backend/pkg/handlers/auth_handlers.go | 107 +++++++++--------- .../pkg/handlers/callback_handlers.go | 53 --------- occupi-backend/pkg/middleware/middleware.go | 64 ++++++++--- occupi-backend/pkg/models/app.go | 9 +- occupi-backend/pkg/router/router.go | 43 +++---- 14 files changed, 191 insertions(+), 224 deletions(-) delete mode 100644 occupi-backend/.dev.env.gpg delete mode 100644 occupi-backend/.env.gpg delete mode 100644 occupi-backend/.prod.env.gpg delete mode 100644 occupi-backend/pkg/handlers/callback_handlers.go diff --git a/occupi-backend/.dev.env.gpg b/occupi-backend/.dev.env.gpg deleted file mode 100644 index 52f36a9f03c2b14590c5a33b6bf38c32d54e091f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 594 zcmV-Y0?i+B(+)U9RJek0lT~D!^mbXYU_OEpI+o1WuztpPh12R&iVXW zetq{ZTK3QL3ctJ-#S*Lr5)r1FC$)hM)7#Z5MebUMtYYkn63dv~CrA^dpuz_o?ZiZ& z;(UvA4`6c3`v*!N?j=L<+imO%UdM5WEVPtGvM7=m$(bpGl^1u-v?nY~_jrr(SG{kU z=t}S}HiLC`Wul0p((JIjID40YMpj`hH^Utl^&42SAf}_Fcs~9?h0Z0Tu}f>izj%zV zC%0KwV^2ag^WGqSrJI8|QZ284)i2O8US2MSspb)j3yMv+* z`b%P1gz<@dGvZfRHJxw_J8rJfK9wE9)rL$YCSntjzdtc}K!RvSyQ@W`Y#!I!jXExd ze3e4@@y2EjcDVbgSW@g;NVoN=j9c9C;B;Abm!&$=?{)~?B!ph1gj}U93;Igf^aQ3u z)Jk7l-`X~gHUcA>xd7A%aijxD(-odC=U!10x@FdB`R)IXulzabyLBJOCDI%un^pjR zrySu0*l+6A8G-N;@GaZCCKPrJrwU&VZzdTC4nUN!7U!S&h0wR9F85GRmv|sU>|q8D zH!5%>@`~WIycD!n$mylIKYnn8+{1{Hjlj?KnQHk{RP zz5~(MLYwyLOv$It+*8^gZ~E}!*{+)dfJdWUTW|)eUk&Qtk)qXA=QPyk0<^SK$AuUw zOh;&_!Shu`L-BE=k_#{+=Syez)2smJx8nIPyiVWDs?Cb;f-YK*7i%d1?3ucufdk1xcOUG7 zViM`BXJ26=+n;*BLaNhF{Ul9-?MhI9m*z_5IrcF<5c6LMewUK&I-0kG8|`p-yHHHt zlUTxH86@5#@9)2?r=mg?!;%OX$+*s%egfP;Y!)sJu;%HDW{sd-unsVQOfe3c^Xao) zKmqc|E!o098@uoee#5Fkwt)Y-MY0w=-&$-MxnokTtw83120}5Ak}$mdL;rb@y2mbI zOX5++9}hfCc}7=5^Hsb-5>NLlUGbOsy`P7rH9^=Syjs1ef)LP-FdE7zw{Jg()Bpqv xM9Z>b(kpO}lk~bTKfB2Q9cI|U^M`h6N7;Tm1CQVzZriMK0Ep$i%*b@>LYWl$9^3!` diff --git a/occupi-backend/.prod.env.gpg b/occupi-backend/.prod.env.gpg deleted file mode 100644 index 8d81fe4fb518cb68cd8a7ee605a18c438bc3f5ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 594 zcmV-Y0`nh>-zEv;Ig6UDSjDZUs&Q3=LfC+ z8)*vXQIil@KFj|i3dQG}_SRO9s%Owg0CNNGL+IFT-ShKi_NkUA78Cdaff!fMY#SyGInXRpLAAXq;5%6}0V zERd~5H-(BS&{XwlJnJ>Rh5(@>nOCT?wPF_vr&-OtD@5xF&3#bI1Z}#(3m&YXZta=@E`v zBBn;^L%B#Hf=`*M1$258T~Z}e%?GmL%Rv~V4&KsdCF5tAF;Dg~0h19ob{GbNI%bI$ zsJibqWP7N4POCcUeNwu^ZHJqWk?v{!-GBsq+luGI*}L}}+KKBUTmrmfxk*NlbY^XU zHC%3vvQw|kvMqH@B)$duHJk=ny9g$vTcF3;R6QqYKd^Rj1(h5Yq=FQY#?N?Ks}8lm zR&v!;XT1br7Pb}d1QY|EFOfnypXez1K?eg_g}H_T^Zhfh55%)w0O%%v;oOV!zMVX# z3vx{zAFJKMG}>|Z!d^?gKt~l$xm($muN*x5>VPJ;kYU2jMx(yWEZ1Zf6HopK1VqhH gKW04F^@4v;AWXb9I5#SHt_zNHu-VLe;Xr$|$0$P>jsO4v diff --git a/occupi-backend/configs/config.go b/occupi-backend/configs/config.go index 810e6902..18ec3a50 100644 --- a/occupi-backend/configs/config.go +++ b/occupi-backend/configs/config.go @@ -150,34 +150,18 @@ func GetTrustedProxies() []string { return []string{""} } -func GetAuth0Domain() string { - auth0Domain := os.Getenv("AUTH0_DOMAIN") - if auth0Domain == "" { - auth0Domain = "" +func GetJWTSecret() string { + secret := os.Getenv("JWT_SECRET") + if secret == "" { + secret = "JWT_SECRET" } - return auth0Domain + return secret } -func GetAuth0ClientID() string { - auth0ClientID := os.Getenv("AUTH0_CLIENT_ID") - if auth0ClientID == "" { - auth0ClientID = "" +func GetSessionSecret() string { + secret := os.Getenv("SESSION_SECRET") + if secret == "" { + secret = "SESSION_SECRET" } - return auth0ClientID -} - -func GetAuth0ClientSecret() string { - auth0ClientSecret := os.Getenv("AUTH0_CLIENT_SECRET") - if auth0ClientSecret == "" { - auth0ClientSecret = "" - } - return auth0ClientSecret -} - -func GetAuth0CallbackURL() string { - auth0CallbackURL := os.Getenv("AUTH0_CALLBACK_URL") - if auth0CallbackURL == "" { - auth0CallbackURL = "" - } - return auth0CallbackURL + return secret } diff --git a/occupi-backend/go.mod b/occupi-backend/go.mod index d17156b1..025af0b8 100644 --- a/occupi-backend/go.mod +++ b/occupi-backend/go.mod @@ -26,6 +26,7 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/gabriel-vasile/mimetype v1.4.4 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect diff --git a/occupi-backend/go.sum b/occupi-backend/go.sum index 7aad25aa..c6d6d94c 100644 --- a/occupi-backend/go.sum +++ b/occupi-backend/go.sum @@ -16,6 +16,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI= diff --git a/occupi-backend/pkg/authenticator/auth.go b/occupi-backend/pkg/authenticator/auth.go index 0794cf8a..94dbfba4 100644 --- a/occupi-backend/pkg/authenticator/auth.go +++ b/occupi-backend/pkg/authenticator/auth.go @@ -1,55 +1,58 @@ package authenticator import ( - "context" "errors" + "time" - "github.com/coreos/go-oidc/v3/oidc" - "golang.org/x/oauth2" + "github.com/dgrijalva/jwt-go" + "github.com/sirupsen/logrus" "github.com/COS301-SE-2024/occupi/occupi-backend/configs" ) -// Authenticator is used to authenticate our users. -type Authenticator struct { - *oidc.Provider - oauth2.Config +type Claims struct { + Email string `json:"email"` + Role string `json:"role"` + jwt.StandardClaims } -// New instantiates the *Authenticator. -func New() (*Authenticator, error) { - provider, err := oidc.NewProvider( - context.Background(), - "https://"+configs.GetAuth0Domain()+"/", - ) - if err != nil { - return nil, err +// GenerateToken generates a JWT token for the user +func GenerateToken(email string, role string) (string, time.Time, error) { + expirationTime := time.Now().Add(5 * time.Minute) + claims := &Claims{ + Email: email, + Role: role, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expirationTime.Unix(), + }, } - conf := oauth2.Config{ - ClientID: configs.GetAuth0ClientID(), - ClientSecret: configs.GetAuth0ClientSecret(), - RedirectURL: configs.GetAuth0CallbackURL(), - Endpoint: provider.Endpoint(), - Scopes: []string{oidc.ScopeOpenID, "profile"}, + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(configs.GetJWTSecret())) + if err != nil { + logrus.Error("Error generating token: ", err) + return "", expirationTime, errors.New("Error generating token") } - return &Authenticator{ - Provider: provider, - Config: conf, - }, nil + return tokenString, expirationTime, nil } -// VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken. -func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) { - rawIDToken, ok := token.Extra("id_token").(string) - if !ok { - return nil, errors.New("no id_token field in oauth2 token") +// ValidateToken validates the JWT token +func ValidateToken(tokenString string) (*Claims, error) { + claims := &Claims{} + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return []byte(configs.GetJWTSecret()), nil + }) + + if err != nil { + logrus.Error("Error validating token: ", err) + return nil, errors.New("Error validating token") } - oidcConfig := &oidc.Config{ - ClientID: a.ClientID, + if !token.Valid { + logrus.Error("Token is invalid") + return nil, errors.New("Token is invalid") } - return a.Verifier(oidcConfig).Verify(ctx, rawIDToken) + return claims, nil } diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index fa6e9c6f..06651763 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -416,3 +416,17 @@ func GetAllRooms(ctx *gin.Context, db *mongo.Client, floorNo int) ([]models.Room return rooms, nil } + +// Checks if a user is an admin +func CheckIfUserIsAdmin(ctx *gin.Context, db *mongo.Client, email string) (bool, error) { + // Check if the user is an admin + collection := db.Database("Occupi").Collection("Users") + filter := bson.M{"email": email} + var user models.User + err := collection.FindOne(ctx, filter).Decode(&user) + if err != nil { + logrus.Error(err) + return false, err + } + return user.Role == "admin", nil +} diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index b61094a2..6711cd0b 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -16,26 +16,23 @@ import ( "github.com/gin-gonic/gin" ) +// PingHandler is a simple handler for testing if the server is up and running +func PingHandler(ctx *gin.Context) { + ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "pong -> I am alive and kicking", 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..dc4772e6 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 == "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 { @@ -122,21 +143,31 @@ func Login(ctx *gin.Context, appsession *models.AppSession) { return } - state, err := utils.GenerateRandomState() + // generate a jwt token for the user + var token string + var expirationTime time.Time + if role == "admin" { + token, expirationTime, err = authenticator.GenerateToken(requestUser.Email, "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) - if err := session.Save(); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.InternalServerError()) - logrus.Error(err) - return + session.Set("email", requestUser.Email) + if role == "admin" { + session.Set("role", "admin") + } else { + session.Set("role", "basic") } + session.Save() + ctx.SetCookie("token", token, int(expirationTime.Unix()), "/", "", false, true) ctx.JSON(http.StatusOK, utils.SuccessResponse( http.StatusOK, @@ -144,9 +175,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 +256,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) @@ -374,31 +385,15 @@ 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()) - logrus.Error(err) - return - } - - parameters := url.Values{} - parameters.Add("returnTo", returnTo.String()) - parameters.Add("client_id", configs.GetAuth0ClientID()) - logoutURL.RawQuery = parameters.Encode() +// handler for logging out a user on occupi /auth/logout TODO: complete implementation +func Logout(ctx *gin.Context) { + session := sessions.Default(ctx) + session.Clear() + session.Save() - 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..49733b3a 100644 --- a/occupi-backend/pkg/middleware/middleware.go +++ b/occupi-backend/pkg/middleware/middleware.go @@ -3,47 +3,85 @@ package middleware import ( "net/http" - "github.com/gin-contrib/sessions" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator" "github.com/gin-gonic/gin" "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 + tokenStr, err := ctx.Cookie("token") + if err != nil { ctx.JSON(http.StatusUnauthorized, gin.H{ "status": http.StatusUnauthorized, "message": "Bad Request", - "error": "User not authenticated", + "error": "User not authorized", }) - // Add the following so that the next() doesn't get called ctx.Abort() return - } else { - ctx.Next() } + + claims, err := authenticator.ValidateToken(tokenStr) + + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "status": http.StatusUnauthorized, + "message": "Bad Request", + "error": "User not authorized", + }) + ctx.Abort() + return + } + + session := sessions.Default(ctx) + session.Set("email", claims.Email) + session.Set("role", claims.Role) + session.Save() + 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 + tokenStr, err := ctx.Cookie("token") + if err == nil { + _, err := authenticator.ValidateToken(tokenStr) + + if err == nil { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "status": http.StatusUnauthorized, + "message": "Bad Request", + "error": "User already authenticated", + }) + 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 != "admin" { ctx.JSON(http.StatusUnauthorized, gin.H{ "status": http.StatusUnauthorized, "message": "Bad Request", - "error": "User already authenticated", + "error": "User not authorized to access admin route", }) - // Add the following so that the next() doesn't get called 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..fda1425c 100644 --- a/occupi-backend/pkg/router/router.go +++ b/occupi-backend/pkg/router/router.go @@ -1,13 +1,11 @@ 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/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 +14,35 @@ 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{}{}) - - store := cookie.NewStore([]byte("secret")) - router.Use(sessions.Sessions("auth-session", store)) + appsession := models.New(db) - 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) }) } 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, "basic") }) + auth.POST("/login-admin", middleware.UnProtectedRoute, func(ctx *gin.Context) { handlers.Login(ctx, appsession, "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) }) } } From 479b824a5a88bdf9d504aced2c179fc57a4f64ed Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 17 Jun 2024 23:27:37 +0200 Subject: [PATCH 2/3] chore: Add ping handlers for authenticated and admin routes and additional unit tests for middleware and jwt generation --- occupi-backend/.dev.env.gpg | Bin 0 -> 608 bytes occupi-backend/.env.gpg | Bin 0 -> 568 bytes occupi-backend/.prod.env.gpg | Bin 0 -> 606 bytes occupi-backend/pkg/handlers/api_handlers.go | 10 ++ occupi-backend/pkg/router/router.go | 8 + occupi-backend/pkg/utils/utils.go | 5 - occupi-backend/tests/authenticator_test.go | 61 ++++++++ occupi-backend/tests/handlers_test.go | 48 +----- occupi-backend/tests/middleware_test.go | 156 ++++++++++++++++++++ 9 files changed, 238 insertions(+), 50 deletions(-) create mode 100644 occupi-backend/.dev.env.gpg create mode 100644 occupi-backend/.env.gpg create mode 100644 occupi-backend/.prod.env.gpg create mode 100644 occupi-backend/tests/authenticator_test.go create mode 100644 occupi-backend/tests/middleware_test.go diff --git a/occupi-backend/.dev.env.gpg b/occupi-backend/.dev.env.gpg new file mode 100644 index 0000000000000000000000000000000000000000..0c4d9bb3aa15067f29b51ac20a50f3f05cb7be2b GIT binary patch literal 608 zcmV-m0-ybi4Fm}T0)di(RFeO@3;)vT0id+$g)@<*y)~2Jbm@`ari1ZPrYK@{wXkuQ zu%B&(XTAWvDulJ$w#|!r<7^QW@OQCLkA9pNmSP`G+eu-s^07s7mUb54FnB*yqJ*qq zGI}Lus^JHyutsDNpnqJwxIEVA`_;xicJ`$r;Sy;bU-c9qAkK7rhNzzDjcj^KoMyz7 zym!>tmH_m;t0Irb=Jxf;=pc2Dna*{S((w{VXD?-6;uid~VZbSUr=)A!VJepDJ&u5b z*3v;0&U^vpUxq#rZWYN+SVj4X69yAPRHXU2u4RXH00aon6J67aQ)|c=RHy6vkpmh> ztTRmCLT}E+UnZ#;4phTM+<^#ZcfEhSFBYT;2_VbiN9H#$*ZnE*m&vQo=Os0W(<=0~ zC0NZ9sIeF0_+OVku)h@epgzn)KSF)=EgOn|H2Y5Pdg*!4<+YtLVBe%}l%CZ{43adH zSM1rNi-Iv4Pt)3K7em(@s?38=U8Y)yNBZk}X_=jfz0$e0wVcZay$0Yl9+V5vt6lF9 zs9o}rwn_8$Q?=+u!;ar@fse@}PF7+te>wO{P|tx}`pPAm|MguM+U0DY9}JWD495tE zA7@WMv@^*|8<<2VORDfvM@?FA4)NRH*&lBQqFuC|;HXt+pLMNKpgYE4(FPMCBPMh+e`Z2IUGXDv3Xg8t=$`>9*`SJXw^RO~TzfQA~6 u1%%ezQ|TIZ-bHw>ht$B-^l>iRmVoJUTezF>`3mF8v`HgJ>~`=zQ0FVMPB;+& literal 0 HcmV?d00001 diff --git a/occupi-backend/.env.gpg b/occupi-backend/.env.gpg new file mode 100644 index 0000000000000000000000000000000000000000..a81501feaf95abf29c64d28671ae18bd1725cd0b GIT binary patch literal 568 zcmV-80>}M~4Fm}T0)%Y-X*xYeh5yp&0W}0d4=rTay=B=kTK^T77ar0>=7t*;tIw4? zJ}=>48%ZgTCdY4*1M6ar1B7v|9amFxG!hk*|b)N*jDm=`-6@==5BEN;uka=>-|L4SGG zi0z!`(6mw9vF!UT{iSg=rwYl`*~7MUv&n zu=^g%7VPb1%N)6$Gv7nb_`qonaK&yVc3R)GHiBm&ooW?|3&r4%l+^caR-aO~BPzuA z*)OR+_k0Ymc;f-Z$hG<&ChAMd<(lM+ag+-u-CV4c>>5CH3bKp!rYOoU_)m!$6Y-Qs G9<;~M%^dLn literal 0 HcmV?d00001 diff --git a/occupi-backend/.prod.env.gpg b/occupi-backend/.prod.env.gpg new file mode 100644 index 0000000000000000000000000000000000000000..777eb2b20fecc194c858c30c143cecf17d1f3e37 GIT binary patch literal 606 zcmV-k0-^nk4Fm}T0&WMoQbzSRnE%r00Y>HIMN3}a8gcSREek#k6vJA&{$_g${l(9# zX=&Bz?|Nx%eHG~vYzPwN`;QFND|3=hL->Ox=zk6(RC1W79TjRk;x5V^ZKZNrp$|67 z7yV^2N9Sd4vU7!$b=NwobRCKH>GS-+D`Q8ZqTjkjggIikKy?j&*x|$_v_PLtMu+3( z4bas+J2pd}iqjlRH^*Eny8HIw?M)HYtLBL1#@$l)FXr;J#48BRG7w#%X7iLDP%fA6 ziK$&?9*^w}PSDcKMB;!`Z$q~)RG_b<=YCr)7JYOD@dk9n*;P2(nt7+T{grrojKTQM z|3P&r^rk|ZzEj%f=uchyQTuKYp0H?%S(S_qCiRH|7{wmVA@X7cYU%8~vS%Qz|7T_O z8n3ttB%Oy^c9-&dI@kb%O$1bIcI6wO8xgK$SJHY9D{)Fy04zuyy#%F~;Xy zbfqsx9Y=$AKM%EgE1b^j35{!M&Z%>qs|5#He63AL25RT{cQJ`K$JUyL4Glv+_BIQTBuH(JdHX4!I1`VYWky4B z1>6n{8u|Z?BVZuF{n)jD6P~9I86&LKWF%?xrZHS}el%*MT8{R0=dX!cj`=P;F%K}l zPFWAb&iOaK4? literal 0 HcmV?d00001 diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 6711cd0b..fc6585c4 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -21,6 +21,16 @@ func PingHandler(ctx *gin.Context) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "pong -> 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) diff --git a/occupi-backend/pkg/router/router.go b/occupi-backend/pkg/router/router.go index fda1425c..8f9a84f6 100644 --- a/occupi-backend/pkg/router/router.go +++ b/occupi-backend/pkg/router/router.go @@ -24,6 +24,14 @@ func OccupiRouter(router *gin.Engine, db *mongo.Client) { { 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") { // resource-auth serves as an example for adding authentication to a route, remove when not needed 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..bf579a26 --- /dev/null +++ b/occupi-backend/tests/authenticator_test.go @@ -0,0 +1,61 @@ +package tests + +import ( + "testing" + "time" + + "github.com/joho/godotenv" + + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator" + "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 := "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 := "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..e40575e6 --- /dev/null +++ b/occupi-backend/tests/middleware_test.go @@ -0,0 +1,156 @@ +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/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", "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", "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\":\"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", "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\":\"User not authorized to access admin route\",\"message\":\"Bad Request\",\"status\":401}", w.Body.String()) +} From 59439a7f14978b3fd00349046c3e5c4a3a562622 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 18 Jun 2024 00:58:55 +0200 Subject: [PATCH 3/3] chore: Add UnAuthorizedCode constant and update test coverage command and updated api docs --- .github/workflows/deploy-golang-develop.yml | 2 +- .github/workflows/deploy-golang-prod.yml | 2 +- .github/workflows/lint-test-build-golang.yml | 2 +- .../pages/api-documentation/api-usage.mdx | 131 +++++++++++++++++- documentation/occupi-docs/theme.config.jsx | 16 ++- occupi-backend/occupi.bat | 2 +- occupi-backend/occupi.sh | 2 +- occupi-backend/pkg/authenticator/auth.go | 6 +- occupi-backend/pkg/constants/constants.go | 3 + occupi-backend/pkg/database/database.go | 5 +- occupi-backend/pkg/handlers/auth_handlers.go | 28 ++-- occupi-backend/pkg/middleware/middleware.go | 60 +++++--- occupi-backend/pkg/router/router.go | 5 +- occupi-backend/tests/authenticator_test.go | 5 +- occupi-backend/tests/middleware_test.go | 11 +- 15 files changed, 220 insertions(+), 60 deletions(-) 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: ( <>