diff --git a/backend/README.md b/backend/README.md index 87d63e5..1efdc57 100644 --- a/backend/README.md +++ b/backend/README.md @@ -22,66 +22,66 @@ Some terminology: ### Structure -userRouter := router.Group("/user"){ +```text +. +├── backend +| |── httpd +| | |── actions # Contains the routes for a registered user, actions include: enrollment, post creation, replies, etc. +| | | └── actionsRoutes.go +| | |── auth +| | | └── authRoutes.go # Contains the authRouter functions Login, and Register +| | |── middleware +| | | |── instructorMiddleware.go # Contains middleware for authenticating instructors and their privileges +| | | └── studentMiddleware.go # Contains middleware for authentication the student is registered, and the actions are valid +| | └── main.go +| | +.................. +``` - userRouter.POST("/register", handleUserRegister) - // The request respondst to the url matching "/user/register" and should include - // information such as first_name, last_name, email. How is uid being generated? will it be given to us in the request or is is - // it made automatically in the database and sent to the front end in a different call +### Auth - userRouter.POST("/login", handleUserLogin) - // The request responds to the url matching "/user/login" and should include - // parameters we require for authentication (this will be done later), we can send the uid to the front end as a response here +File: authRoutes.go - Should we add a reset password for a user route when we add authentication ? (Y) +Functions: - userRouter.POST("/resetPassword", handleResetPassword) - // The request responds to the url matching "/user/resetPassword" and should include the users email - // in the request +- Login +- Register -} +Methods: -The following are all routes for a specific user based on their _uid_. Certain routes should only be accessible to professors, e.g. course creation, and archive course. +- POST -specificUserRouter := router.Group("/user/:uid"){ +The _authRoutes_ file contains the functions for login and registering users. The required parameters and JSON format are +provided in the comments of the functions. - **uid is a prerequiste for all requests below** +The functions all correspond to urls of the form /auth/*type, where *type is either login or register. - specificUserRouter.POST("/enroll", handleEnrollment) - // The request responds to url matching: "/user/:uid/enroll?course=:cid" +### Actions - specificUserRouter.POST("/:cid/createPost", handleCreatePost) - // The request responds to url matching "/user/:uid/:cid/createPost" +File: actionsRoutes.go - specificUserRouter.POST("/:cid/replyToPost", handleReplyToPost) - // This route can be used for a user replying to posts made, - // the request should include information about the post that can be used to find it (tid?) +Functions: - specificUserRouter.POST("/:cid/:tid/createComment", handleComment) - // The request responds to the url matching: "/user/:uid/:cid/:tid/createComment" - // Since each comment is specific to a thread in a specific course we will require cid, and tid for comments along with the prerequiste uid as well +- Enroll +- CreatePost +- Reply +- Comment +- DeletePost +- Upvote +- CreateCourse +- ArchiveCourse - specificUserRouter.DELETE("/:cid/:tid", handleDeletePost) - // The request responds to the url matching "/users/:uid/:cid/:tid/deletePost" - // Middleware will be required to make sure only the author of the post, or an instructor is able to delete the post - // for author authentication we can check the uid supplied with the author entry of the thread +Methods: - specificUserRouter.PATCH("/:cid/:tid/:comid/upvote", handleUpvote) - // The request responds to the url matching "/users/:uid/:cid/:tid/:comid/upvote" - // This is an update method that allows users to upvote comments on a given post. +- PATCH +- POST +- DELETE - Do we make some middleware for the routes below to make sure the user here is a professor and is allowed to create/archive courses? +The _actionRoutes_ file contains the functions for user actions. The required parameters and JSON format are +provided in the comments of the functions. The functions above have authentication middleware to determine whether certain users +have permisson to commit certain actions. - specificUserRouter.POST("/createCourse", handleCourseCreation) - // This request responds to url matching "/user/:uid/createCourse" - // This can be used by a professor to create a course, we require cid ? (how is this generated), cname, ccode, semid, and isarc - // for the db entry - - specificUserRouter.POST("/:cid/archiveCourse", handleArchiveCourse) - // This request responds to the url matching "/user/:uid/:cid/archiveCourse" - // and is used for archiving a course in the db, we require the cid for looking up the course in the db, and we can just update the isarc value to be true - -} +The functions all correspond to urls of the form /user/:uid, where uid is the user id. ### CORS diff --git a/backend/httpd/actions/actionsRoutes.go b/backend/httpd/actions/actionsRoutes.go new file mode 100644 index 0000000..324921d --- /dev/null +++ b/backend/httpd/actions/actionsRoutes.go @@ -0,0 +1,156 @@ +package actions + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// Course struct for binding JSON to +// JSON: {cname: "course name", ccode: "CSC4000", cpass: "course password", semid: semesterID, isarc: false} +type Course struct { + CourseName string `json:"cname" binding:"required"` + CourseCode string `json:"ccode" binding:"required"` + CoursePassword string `json:"cpass" binding:"required"` + SemesterID int `json:"semid" binding:"required"` + IsArc bool `json:"isarc" binding:"required"` +} + +// Enroll Handler Function +// Method: POST +// url: /user/:uid/enroll/:cid" +// Requirements: (can be changed later, this is just the skeleton) +// - Param: uid, +// - Param: cid, +func Enroll(c *gin.Context) { + userID := c.Param("uid") + courseID := c.Param("cid") + + // TODO: + // 1. Add user to enrolment table with cid and position type student + + c.JSON(http.StatusOK, gin.H{"status": "You have been enrolled", "user": userID, "course": courseID}) +} + +// CreatePost Handler Function +// Method: POST +// url: /user/:uid/:cid/createPost" +// Requirements: (can be changed later, this is just the skeleton) +// - Param: uid, +// - Param: cid, +func CreatePost(c *gin.Context) { + userID := c.Param("uid") + courseID := c.Param("cid") + + // TODO: + // 1. Create Thread with user as author + + c.JSON(http.StatusOK, gin.H{"status": "You have created a Post!", "user": userID, "course": courseID}) +} + +// Reply Handler Function +// Method: POST +// url: /user/:uid/:cid/replyToPost/:tid" +// Requirements: (can be changed later, this is just the skeleton) +// - Param: uid, +// - Param: cid, +// - Query: tid, +func Reply(c *gin.Context) { + userID := c.Param("uid") + courseID := c.Param("cid") + threadID := c.Param("tid") + // TODO: + // 1. Add reply to thread with user as author to reply + + c.JSON(http.StatusOK, gin.H{"status": "You have created a Post!", "user": userID, "course": courseID, "threadID": threadID}) +} + +// Comment Handler Function +// Method: POST +// url: /user/:uid/:cid/:tid/comment" +// Requirements: (can be changed later, this is just the skeleton) +// - Param: uid, +// - Param: cid, +// - Param: tid, +func Comment(c *gin.Context) { + userID := c.Param("uid") + courseID := c.Param("cid") + threadID := c.Param("tid") + // TODO: + // 1. Find thread/post + // 2. Add comment to post with user as author + + c.JSON(http.StatusOK, gin.H{"status": "You have created a Post!", "user": userID, "course": courseID, "threadID": threadID}) +} + +// DeletePost Handler Function +// Method: DELETE +// url: /user/:uid/:cid/:tid/deletePost" +// Requirements: (can be changed later, this is just the skeleton) +// - Param: uid, +// - Param: cid, +// - Param: tid, +func DeletePost(c *gin.Context) { + userID := c.Param("uid") + courseID := c.Param("cid") + threadID := c.Param("tid") + // TODO: + // 1. Find post + // 2. Delete Post from database + + c.JSON(http.StatusOK, gin.H{"status": "You have created a Post!", "user": userID, "course": courseID, "threadID": threadID}) +} + +// Upvote Handler Function +// Method: PATCH +// url: /user/:uid/:cid/:tid/:comid/upvote" +// Requirements: (can be changed later, this is just the skeleton) +// - Param: uid, +// - Param: cid, +// - Param: tid, +// - Param: comid, +func Upvote(c *gin.Context) { + userID := c.Param("uid") + courseID := c.Param("cid") + threadID := c.Param("tid") + commentID := c.Param("comid") + // TODO: + // 1. Add upvote to the comment, and update anything related to the comment + + c.JSON(http.StatusOK, gin.H{"status": "You have created a Post!", "user": userID, "course": courseID, "threadID": threadID, "commentID": commentID}) +} + +// CreateCourse Handler Function +// Method: POST +// url: /user/:uid/createCourse" +// Requirements: (can be changed later, this is just the skeleton) +// - Param: uid +// - We require course information in json format +// JSON: {cname: "course name", ccode: "CSC4000", cpass: "course password", close: "closing at", semid: semesterID, isarc: false} +func CreateCourse(c *gin.Context) { + userID := c.Param("uid") + var course Course + if err := c.ShouldBindJSON(&course); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + // TODO: + // 1. Create Course from model + // 2. Add to db + c.JSON(http.StatusOK, gin.H{"status": "You have created a Post!", "user": userID}) +} + +// ArchiveCourse Handler Function +// Method: PATCH +// url: /user/:uid/:cid/archiveCourse" +// Requirements: (can be changed later, this is just the skeleton) +// - Param: uid +// - We require course information in json format +func ArchiveCourse(c *gin.Context) { + userID := c.Param("uid") + courseID := c.Param("cid") + // TODO: + // 1. Find course with cid + // 2. Update its is_archived parameter to be false + c.JSON(http.StatusOK, gin.H{"status": "You have created a Post!", "user": userID, "course": courseID}) +} diff --git a/backend/httpd/auth/authRoutes.go b/backend/httpd/auth/authRoutes.go new file mode 100644 index 0000000..0313810 --- /dev/null +++ b/backend/httpd/auth/authRoutes.go @@ -0,0 +1,63 @@ +package auth + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// ExistingUser Binding from JSON for Login +type ExistingUser struct { + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// NewUser Binding from JSON for Register +type NewUser struct { + Firstname string `json:"firstname" binding:"required"` + Lastname string `json:"lastname" binding:"required"` + Email string `json:"email" binding:"required"` +} + +// Login Handler Function +// Method: POST +// url: /auth/login +// Requirements: (can be changed later, this is just the skeleton) +// - email +// - password +// In JSON format: ({"email": "email@mail.com", "password": "password"}) +func Login(c *gin.Context) { + var user ExistingUser + if err := c.ShouldBindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + // TODO: + // 1. Authenticate the user based on their info + // 2. "Log them in" if successful, i.e., return relevant information to frontend + // else, we return error message such as "User does not exist" + c.JSON(http.StatusOK, gin.H{"status": "Logged In", "email": user.Email}) +} + +// Register Handler Function +// Method: POST +// url: /auth/register +// RequirementsL +// - firstname +// - lastname +// - email +// - password (This will be added later on, once password field is added to DB, and updated in the user model) +// in JSON format: ({"firstname":"first", "lastname":"last", "email":"email"}) +func Register(c *gin.Context) { + var user NewUser + if err := c.ShouldBindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + // TODO: + // 1. Create the user model (use the model in backend/storage) + // 2. Add user to db using GORM + // 3. Log in the user automatically, ie., send the frontend all the relevant information + // you would when a user logs in. + c.JSON(http.StatusOK, gin.H{"status": "You have Registered!", "email": user.Email}) +} diff --git a/backend/httpd/main.go b/backend/httpd/main.go index 98a2506..5677254 100644 --- a/backend/httpd/main.go +++ b/backend/httpd/main.go @@ -5,6 +5,10 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + + Actions "pizza/httpd/actions" + Auth "pizza/httpd/auth" + Middleware "pizza/httpd/middleware" ) func main() { @@ -36,6 +40,37 @@ func main() { }) }) + // This is the router group for matching all urls with /auth + // The functions are all contained inside the auth directory's routes.go + authRouter := r.Group("/auth") + { + // Matches url /auth/login and uses the Login function to handle the request + authRouter.POST("/login", Auth.Login) + // Matches url /auth/register and uses the Register function to handle the request + authRouter.POST("/register", Auth.Register) + } + + // This is the router group for matching all urls with /user/:uid + userRouter := r.Group("/user/:uid") + { + // TODO: Authentication Middleware for uid and cid + userRouter.POST("/enroll/:cid", Middleware.IsStudent(), Actions.Enroll) + userRouter.POST("/:cid/createPost", Middleware.IsStudent(), Actions.CreatePost) + // TODO: Authentication Middleware for uid, cid, and tid: Required + userRouter.POST("/:cid/replyToPost/:tid", Middleware.IsStudent(), Middleware.AbleToReply(), Actions.Reply) + userRouter.POST("/:cid/:tid/comment", Actions.Comment) + userRouter.DELETE("/:cid/:tid/deletePost", Actions.DeletePost) + // TODO: Authentication Middleware for uid, cid, tid, comid: Required + userRouter.PATCH("/:cid/:tid/:comid/upvote", Actions.Upvote) + + // These two are special routes available only to instructors for creating courses and archiving them. + // TODO: Authentication Middleware to check uid belongs to instructor thus they have "create course" privileges + userRouter.POST("/createCourse", Middleware.IsInstructor(), Actions.CreateCourse) + // TODO: Authentication Middleware to check uid belongs to instructor thus they have "archive course" privileges, and + // check course with cid actually exists, check if the instructor is the instructor of course with id cid + userRouter.PATCH("/:cid/archiveCourse", Middleware.IsInstructor(), Middleware.InstructorIsAuthor(), Actions.ArchiveCourse) + } + err := r.Run() // listen and serve on 0.0.0.0:3001 (for windows "localhost:3001") if err != nil { panic("Failed to start server") diff --git a/backend/httpd/middleware/instructorMiddleware.go b/backend/httpd/middleware/instructorMiddleware.go new file mode 100644 index 0000000..d2d52c2 --- /dev/null +++ b/backend/httpd/middleware/instructorMiddleware.go @@ -0,0 +1,26 @@ +package middleware + +// make sure to import the model for the db +import "github.com/gin-gonic/gin" + +// IsInstructor handler +// Type: Middleware +// url: /user/:uid/createCourse +func IsInstructor() gin.HandlerFunc { + return func(c *gin.Context) { + // userID := c.Param("uid") use this uid for checking + // TODO: + // 1. Check uid belongs to an instructor. Not sure how we determine this yet + } +} + +// InstructorIsAuthor handler +// Type: Middleware +// url: /user/:uid/:cid/archiveCourse +func InstructorIsAuthor() gin.HandlerFunc { + return func(c *gin.Context) { + // TODO: + // 1. Check if the instructor is the author for the course with id cid + // 2. If Step 1 passes, return + } +} diff --git a/backend/httpd/middleware/studentMiddleware.go b/backend/httpd/middleware/studentMiddleware.go new file mode 100644 index 0000000..973df5a --- /dev/null +++ b/backend/httpd/middleware/studentMiddleware.go @@ -0,0 +1,30 @@ +package middleware + +// make sure to import the model for the db +import "github.com/gin-gonic/gin" + +// IsStudent handler +// Type: Middleware +// url: /user/:uid/enroll/:cid +func IsStudent() gin.HandlerFunc { + return func(c *gin.Context) { + // userID := c.Param("uid") use this uid for checking + // courseID := c.Param("cid") + // TODO: + // 1. Check uid belongs to a registered student + // 2. Check cid belongs to existing course + } +} + +// AbleToReply handler +// Type: Middleware +// url: /user/:uid/:cid/replyToPost/:tid +func AbleToReply() gin.HandlerFunc { + return func(c *gin.Context) { + // userID := c.Param("uid") use this uid for checking + // courseID := c.Param("cid") + // threadID := c.Param("tid") + // TODO: + // 1. Check tid belongs to existing thread + } +}