diff --git a/go.mod b/go.mod index baeb86e..2e83309 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( go.mongodb.org/mongo-driver v1.13.1 golang.org/x/crypto v0.14.0 google.golang.org/api v0.128.0 - google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 google.golang.org/grpc v1.60.1 ) @@ -56,6 +55,7 @@ require ( golang.org/x/time v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect google.golang.org/protobuf v1.32.0 // indirect diff --git a/handlers/course/content.go b/handlers/course/content.go new file mode 100644 index 0000000..a85d28b --- /dev/null +++ b/handlers/course/content.go @@ -0,0 +1,152 @@ +/* +MIT License + +Copyright (c) 2024 Praromvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package course + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + + "github.com/praromvik/praromvik/models/course" + perror "github.com/praromvik/praromvik/pkg/error" + + "github.com/go-chi/chi/v5" +) + +type Content struct { + *course.Content +} + +func (c *Content) Create(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&c.Content); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on parsing JSON", err) + return + } + c.CourseRef = chi.URLParam(r, "courseRef") + errCode, err := course.ValidateNameUniqueness(c.Content) + if err != nil { + perror.HandleError(w, errCode, "", err) + return + } + if err := course.Create(c.Content); err != nil { + perror.HandleError(w, http.StatusBadRequest, "failed to create course content data into database", err) + return + } + + if err := course.Sync(&course.Lesson{ + CourseRef: c.Content.CourseRef, + }, "$push", c.LessonRef, "contents", c.ContentID); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on syncing content to lesson", err) + } + + w.WriteHeader(http.StatusOK) +} + +func (c *Content) Get(w http.ResponseWriter, r *http.Request) { + c.Content = &course.Content{ + CourseRef: chi.URLParam(r, "courseRef"), + ContentID: chi.URLParam(r, "id"), + } + // Fetch document from database + document, err := course.Get(c.Content) + if err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on getting course lesson", err) + return + } + + // Check if the document is of the expected type + expectedType := reflect.TypeOf(&course.Content{}) + if !isTypeValid(document, expectedType) { + perror.HandleError(w, http.StatusBadRequest, "", fmt.Errorf("document is not of type %s", expectedType)) + return + } + + // Encode the document to JSON and send the response + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(document.(*course.Content)); err != nil { + perror.HandleError(w, http.StatusInternalServerError, "Error on encoding JSON response", err) + return + } + w.WriteHeader(http.StatusOK) +} + +func (c *Content) List(w http.ResponseWriter, r *http.Request) { + c.Content = &course.Content{ + CourseRef: chi.URLParam(r, "courseRef"), + ContentID: chi.URLParam(r, "id"), + } + documents, err := course.List(c.Content) + if err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on getting course content list.", err) + } + + // Check if the document is of the expected type + expectedType := reflect.TypeOf(&[]course.Content{}) + if !isTypeValid(documents, expectedType) { + perror.HandleError(w, http.StatusBadRequest, "", fmt.Errorf("document is not of type %s", expectedType)) + return + } + + // Encode the documents to JSON and send the response + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(documents.(*[]course.Content)); err != nil { + perror.HandleError(w, http.StatusInternalServerError, "Error on encoding JSON response", err) + return + } +} + +func (c *Content) Delete(w http.ResponseWriter, r *http.Request) { + c.Content = &course.Content{ + CourseRef: chi.URLParam(r, "courseRef"), + ContentID: chi.URLParam(r, "id"), + } + // Fetch document from database + document, err := course.Get(c.Content) + if err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on getting course lesson", err) + return + } + + // Check if the document is of the expected type + expectedType := reflect.TypeOf(&course.Content{}) + if !isTypeValid(document, expectedType) { + perror.HandleError(w, http.StatusBadRequest, "", fmt.Errorf("document is not of type %s", expectedType)) + return + } + c.Content.LessonRef = document.(*course.Content).LessonRef + + if err := course.Delete(c.Content); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on deleting content.", err) + } + + if err := course.Sync(&course.Lesson{ + CourseRef: c.Content.CourseRef, + }, "$pull", c.Content.LessonRef, "contents", c.ContentID); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on syncing content to lesson", err) + } + + w.WriteHeader(http.StatusOK) +} diff --git a/handlers/course/course.go b/handlers/course/course.go index 4d550ff..e90d507 100644 --- a/handlers/course/course.go +++ b/handlers/course/course.go @@ -1,7 +1,7 @@ /* MIT License -# Copyright (c) 2024 Praromvik +Copyright (c) 2024 Praromvik Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -25,32 +25,125 @@ SOFTWARE. package course import ( + "encoding/json" "fmt" "net/http" + "reflect" + "slices" "github.com/praromvik/praromvik/models/course" + "github.com/praromvik/praromvik/pkg/auth" + perror "github.com/praromvik/praromvik/pkg/error" + + "github.com/go-chi/chi/v5" ) type Course struct { *course.Course } -func (o *Course) Create(w http.ResponseWriter, r *http.Request) { - fmt.Println("Create an Course") +func (c *Course) Create(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&c.Course); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on parsing JSON", err) + return + } + errCode, err := course.ValidateNameUniqueness(c.Course) + if err != nil { + perror.HandleError(w, errCode, "", err) + return + } + info, err := auth.GetUserInfoFromSession(r) + if err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on getting session", err) + return + } + if !slices.Contains(c.Instructors, info.Name) { + c.Instructors = append(c.Instructors, info.Name) + } + if err := course.Create(c.Course); err != nil { + perror.HandleError(w, http.StatusBadRequest, "failed to course data into database", err) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (c *Course) List(w http.ResponseWriter, r *http.Request) { + // Initialize Course instance + c.Course = &course.Course{} + // Fetch documents from the database + documents, err := course.List(c.Course) + if err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on getting course list.", err) + return + } + + // Check if the document is of the expected type + expectedType := reflect.TypeOf(&[]course.Course{}) + if !isTypeValid(documents, expectedType) { + perror.HandleError(w, http.StatusBadRequest, "", fmt.Errorf("document is not of type %s", expectedType)) + return + } + + // Encode the documents to JSON and send the response + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(documents.(*[]course.Course)); err != nil { + perror.HandleError(w, http.StatusInternalServerError, "Error on encoding JSON response", err) + return + } } -func (o *Course) List(w http.ResponseWriter, r *http.Request) { - fmt.Println("List all Courses") +func (c *Course) Get(w http.ResponseWriter, r *http.Request) { + // Initialize Course instance + c.Course = &course.Course{} + c.CourseId = chi.URLParam(r, "id") + // Fetch document from database + document, err := course.Get(c.Course) + if err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on getting course.", err) + return + } + + // Check if the document is of the expected type + expectedType := reflect.TypeOf(&course.Course{}) + if !isTypeValid(document, expectedType) { + perror.HandleError(w, http.StatusBadRequest, "", fmt.Errorf("document is not of type %s", expectedType)) + return + } + + // Encode the document to JSON and send the response + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(document.(*course.Course)); err != nil { + perror.HandleError(w, http.StatusInternalServerError, "Error on encoding JSON response", err) + return + } + w.WriteHeader(http.StatusOK) } -func (o *Course) GetByID(w http.ResponseWriter, r *http.Request) { - fmt.Println("Get an Course by ID") +func (c *Course) Update(w http.ResponseWriter, r *http.Request) { + c.Course = &course.Course{} + c.CourseId = chi.URLParam(r, "id") + if err := json.NewDecoder(r.Body).Decode(&c.Course); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on parsing JSON", err) + return + } + if err := course.Update(c.Course); err != nil { + fmt.Println(err) + perror.HandleError(w, http.StatusBadRequest, "Error on updating course", err) + return + } } -func (o *Course) UpdateByID(w http.ResponseWriter, r *http.Request) { - fmt.Println("Update an Course by ID") +func (c *Course) Delete(w http.ResponseWriter, r *http.Request) { + c.Course = &course.Course{} + c.CourseId = chi.URLParam(r, "id") + if err := course.Delete(c.Course); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on deleting course.", err) + } + w.WriteHeader(http.StatusOK) } -func (o *Course) DeleteByID(w http.ResponseWriter, r *http.Request) { - fmt.Println("Delete an Course by ID") +func isTypeValid(document interface{}, expectedType reflect.Type) bool { + docValue := reflect.ValueOf(document) + return docValue.Kind() == reflect.Ptr && docValue.Type() == expectedType } diff --git a/handlers/course/lesson.go b/handlers/course/lesson.go new file mode 100644 index 0000000..e00ce29 --- /dev/null +++ b/handlers/course/lesson.go @@ -0,0 +1,124 @@ +/* +MIT License + +Copyright (c) 2024 Praromvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package course + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + + "github.com/praromvik/praromvik/models/course" + perror "github.com/praromvik/praromvik/pkg/error" + + "github.com/go-chi/chi/v5" +) + +type Lesson struct { + *course.Lesson +} + +func (l *Lesson) Create(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&l.Lesson); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on parsing JSON", err) + return + } + l.CourseRef = chi.URLParam(r, "courseRef") + errCode, err := course.ValidateNameUniqueness(l.Lesson) + if err != nil { + perror.HandleError(w, errCode, "", err) + return + } + if err := course.Create(l.Lesson); err != nil { + perror.HandleError(w, http.StatusBadRequest, "failed to create course lesson data into database", err) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (l *Lesson) Get(w http.ResponseWriter, r *http.Request) { + l.Lesson = &course.Lesson{ + CourseRef: chi.URLParam(r, "courseRef"), + LessonID: chi.URLParam(r, "id"), + } + // Fetch document from database + document, err := course.Get(l.Lesson) + if err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on getting course lesson", err) + return + } + + // Check if the document is of the expected type + expectedType := reflect.TypeOf(&course.Lesson{}) + if !isTypeValid(document, expectedType) { + perror.HandleError(w, http.StatusBadRequest, "", fmt.Errorf("document is not of type %s", expectedType)) + return + } + + // Encode the document to JSON and send the response + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(document.(*course.Lesson)); err != nil { + perror.HandleError(w, http.StatusInternalServerError, "Error on encoding JSON response", err) + return + } + w.WriteHeader(http.StatusOK) +} + +func (l *Lesson) List(w http.ResponseWriter, r *http.Request) { + l.Lesson = &course.Lesson{ + CourseRef: chi.URLParam(r, "courseRef"), + LessonID: chi.URLParam(r, "id"), + } + documents, err := course.List(l.Lesson) + if err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on getting course lesson list.", err) + } + + // Check if the document is of the expected type + expectedType := reflect.TypeOf(&[]course.Lesson{}) + if !isTypeValid(documents, expectedType) { + perror.HandleError(w, http.StatusBadRequest, "", fmt.Errorf("document is not of type %s", expectedType)) + return + } + + // Encode the documents to JSON and send the response + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(documents.(*[]course.Lesson)); err != nil { + perror.HandleError(w, http.StatusInternalServerError, "Error on encoding JSON response", err) + return + } +} + +func (l *Lesson) Delete(w http.ResponseWriter, r *http.Request) { + l.Lesson = &course.Lesson{ + CourseRef: chi.URLParam(r, "courseRef"), + LessonID: chi.URLParam(r, "id"), + } + if err := course.Delete(l.Lesson); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on deleting course lesson.", err) + } + w.WriteHeader(http.StatusOK) +} diff --git a/handlers/user/users.go b/handlers/user/users.go index 76ce7d2..39417fc 100644 --- a/handlers/user/users.go +++ b/handlers/user/users.go @@ -28,11 +28,13 @@ import ( "encoding/json" "net/http" - "github.com/praromvik/praromvik/models" + hutils "github.com/praromvik/praromvik/handlers/utils" "github.com/praromvik/praromvik/models/user" + mutils "github.com/praromvik/praromvik/models/utils" "github.com/praromvik/praromvik/pkg/auth" perror "github.com/praromvik/praromvik/pkg/error" + "github.com/go-chi/chi/v5" "github.com/google/uuid" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -58,10 +60,10 @@ func (u User) SignUp(w http.ResponseWriter, r *http.Request) { if err := u.HashPassword(); err != nil { perror.HandleError(w, http.StatusBadRequest, "Failed to hash password", err) } - if u.Email == models.AdminEmail { - u.Role = string(models.Admin) + if u.Email == mutils.AdminEmail { + u.Role = string(mutils.Admin) } else { - u.Role = string(models.Student) + u.Role = string(mutils.Student) } if err := u.User.AddUserDataToDB(); err != nil { @@ -98,28 +100,52 @@ func (u User) SignIn(w http.ResponseWriter, r *http.Request) { } func (u User) SignOut(w http.ResponseWriter, r *http.Request) { - if err := auth.StoreAuthenticated(w, r, u.User, false); err != nil { - perror.HandleError(w, http.StatusInternalServerError, "failed to store session token", err) - return + if r.Method == http.MethodPost { + if err := auth.StoreAuthenticated(w, r, u.User, false); err != nil { + perror.HandleError(w, http.StatusInternalServerError, "failed to store session token", err) + return + } + } else { + w.WriteHeader(http.StatusBadRequest) } } func (u User) ProvideRoleToUser(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPost { - if err := json.NewDecoder(r.Body).Decode(&u.User); err != nil { - perror.HandleError(w, http.StatusBadRequest, "Error on parsing JSON", err) + if err := json.NewDecoder(r.Body).Decode(&u.User); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on parsing JSON", err) + return + } + + if err := u.User.FetchAndSetUUIDFromDB(); err != nil { + perror.HandleError(w, http.StatusBadRequest, "", err) + return + } + + if err := u.UpdateUserDataToDB(); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on update user", err) + return + } + w.WriteHeader(http.StatusOK) +} + +func (u User) Get(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + u.UserName = chi.URLParam(r, "userName") + if err := u.GetFromMongo(); err != nil { + perror.HandleError(w, http.StatusBadRequest, "Error on getting user", err) return } - - if err := u.User.FetchAndSetUUIDFromDB(); err != nil { - perror.HandleError(w, http.StatusBadRequest, "", err) + data, err := hutils.RemovedUUID(u) + if err != nil { + perror.HandleError(w, http.StatusInternalServerError, "", err) return } - if err := u.UpdateUserDataToDB(); err != nil { - perror.HandleError(w, http.StatusBadRequest, "Error on Update User", err) - return + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(data); err != nil { + perror.HandleError(w, http.StatusInternalServerError, "Error on encoding JSON response", err) } + w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusBadRequest) diff --git a/handlers/utils/utils.go b/handlers/utils/utils.go new file mode 100644 index 0000000..96863ca --- /dev/null +++ b/handlers/utils/utils.go @@ -0,0 +1,42 @@ +/* +MIT License + +Copyright (c) 2024 Praromvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package utils + +import ( + "encoding/json" +) + +func RemovedUUID(v interface{}) (map[string]interface{}, error) { + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + var m map[string]interface{} + if err := json.Unmarshal(data, &m); err != nil { + return nil, err + } + delete(m, "uuid") + return m, nil +} diff --git a/http-req/content.http b/http-req/content.http new file mode 100644 index 0000000..464cca4 --- /dev/null +++ b/http-req/content.http @@ -0,0 +1,60 @@ +### +# SignIn endpoint +POST http://localhost:3030/signin +Content-Type: application/json + +{ + "userName": "admin", + "password": "itiswhatitis" +} + +### +# Create Content 1 +POST http://localhost:3030/course/advanced-golang/content +Authorization: Bearer {{PRAROMVIK}} +Content-Type: application/json + +{ + "_id": "course-introduction", + "courseRef":"advanced-golang", + "lessonRef": "introduction", + "title":"Introduction of Golang", + "type": "video" +} + + + +### +# Create Content 2 +POST http://localhost:3030/course/advanced-golang/content +Authorization: Bearer {{PRAROMVIK}} +Content-Type: application/json + +{ + "_id": "refresher-lab-01", + "courseRef":"advanced-golang", + "lessonRef": "introduction", + "title":"Refreshment Lab 01", + "type": "Lab" +} + + +### +# Get +GET http://localhost:3030/course/advanced-golang/content/course-introduction +Authorization: Bearer {{PRAROMVIK}} +Content-Type: application/json + + +### +# List +GET http://localhost:3030/course/advanced-golang/content/list +Authorization: Bearer {{PRAROMVIK}} +Content-Type: application/json + + +### +# Get +DELETE http://localhost:3030/course/advanced-golang/content/refresher-lab-01 +Authorization: Bearer {{PRAROMVIK}} +Content-Type: application/json diff --git a/http-req/course.http b/http-req/course.http new file mode 100644 index 0000000..5bd4774 --- /dev/null +++ b/http-req/course.http @@ -0,0 +1,64 @@ +### +# SignIn endpoint +POST http://localhost:3030/signin +Content-Type: application/json + +{ + "userName": "admin", + "password": "itiswhatitis" +} + +> {% client.global.set("PRAROMVIK", response.body.json.token); %} + +### +# Create Course 1 +POST http://localhost:3030/course +Authorization: Bearer {{PRAROMVIK}} +Content-Type: application/json + +{ + "_id": "advanced-golang", + "title": "Go: The Complete Developer's Guide (Golang)", + "description": "Master the fundamentals and advanced features of the Go Programming Language (Golang)" +} + +### +# Create Course 2 +POST http://localhost:3030/course +Authorization: Bearer {{PRAROMVIK}} +Content-Type: application/json + +{ + "_id": "prometheus-certified-associate-pca", + "title": "Prometheus Certified Associate (PCA)", + "description": "Prometheus is an open-source monitoring & alerting solution that collects metrics data and stores it in a time-series database." +} + +### +# Get Course +GET http://localhost:3030/course/prometheus-certified-associate-pca +Authorization: Bearer {{PRAROMVIK}} + +### +# Update Course +PUT http://localhost:3030/course/prometheus-certified-associate-pca +Authorization: Bearer {{PRAROMVIK}} +Content-Type: application/json + +{ + "price": 700 +} + + +### +# Delete Course +DELETE http://localhost:3030/course/advanced-golang +Authorization: Bearer {{PRAROMVIK}} + +### +# List Course +GET http://localhost:3030/course/list +Authorization: Bearer {{PRAROMVIK}} + + +GET http://localhost:3030/course/advanced-golang/introduction/quiz-1/ \ No newline at end of file diff --git a/http-req/lesson.http b/http-req/lesson.http new file mode 100644 index 0000000..344d50b --- /dev/null +++ b/http-req/lesson.http @@ -0,0 +1,42 @@ +### +# SignIn endpoint +POST http://localhost:3030/signin +Content-Type: application/json + +{ + "userName": "admin", + "password": "itiswhatitis" +} + + +### +# Create Lesson 1 +POST http://localhost:3030/course/advanced-golang/lesson +Authorization: Bearer {{PRAROMVIK}} +Content-Type: application/json + +{ + "_id": "introduction" +} + +### +# Create Lesson 2 +POST http://localhost:3030/course/advanced-golang/lesson +Authorization: Bearer {{PRAROMVIK}} +Content-Type: application/json + +{ + "_id": "concurrency" +} + + +### +# Get Lesson 1 +GET http://localhost:3030/course/advanced-golang/lesson/concurrency +Authorization: Bearer {{PRAROMVIK}} + + +### +# List +GET http://localhost:3030/course/advanced-golang/lesson/list +Authorization: Bearer {{PRAROMVIK}} diff --git a/req.http b/http-req/user.http similarity index 54% rename from req.http rename to http-req/user.http index 92924dd..1f168e7 100644 --- a/req.http +++ b/http-req/user.http @@ -8,16 +8,24 @@ Content-Type: application/json "phone": "+8801540179777", "email": "sunny.cse7575@gmail.com" } + ### # SignIn endpoint POST http://localhost:3030/signin Content-Type: application/json { - "userName": "student-1", - "password": "123" + "userName": "admin", + "password": "itiswhatitis" } +> {% client.global.set("PRAROMVIK", response.body.json.token); %} + + ### -# +POST http://localhost:3030/user/upload +Content-Type: multipart/form-data +{ +'image=@"/home/anisur/Downloads/100daysleetcode.png"' +} diff --git a/models/course/api.go b/models/course/api.go index 7229853..db355df 100644 --- a/models/course/api.go +++ b/models/course/api.go @@ -24,38 +24,33 @@ SOFTWARE. package course -import ( - "google.golang.org/genproto/googleapis/type/date" -) - type Course struct { - CourseId string `json:"courseId"` - Title string `json:"title"` - Description string `json:"description"` - Instructors []string `json:"instructors"` - StartDate date.Date `json:"startDate"` - EndDate date.Date `json:"endDate"` - Duration int `json:"duration"` // Duration in week - EnrollmentCapacity int `json:"enrollmentCapacity"` - EnrollmentStudents []string `json:"enrollmentStudents"` - Lessons []Lesson `json:"lessons"` + CourseId string `json:"_id" bson:"_id"` + Title string `json:"title" bson:"title"` + Description string `json:"description" bson:"description"` + Instructors []string `json:"instructors" bson:"instructors"` + Moderators []string `json:"moderators" bson:"moderators"` + StartDate string `json:"startDate" bson:"startDate"` + EndDate string `json:"endDate" bson:"endDate"` + Duration int `json:"duration" bson:"duration"` // Duration in week + Capacity int `json:"capacity" bson:"capacity"` + Students []string `json:"students" bson:"students"` + Price int `json:"price" bson:"price"` + Image []byte `json:"image" bson:"-"` } type Lesson struct { - LessonId string `json:"lessonId"` - Title string `json:"title"` - Content string `json:"content"` - Quizzes []Quiz `json:"quizzes"` -} - -type Quiz struct { - QuizId string `json:"quizId"` - Questions []Question `json:"questions"` + LessonID string `json:"_id" bson:"_id"` + CourseRef string `json:"courseRef" bson:"courseRef"` + Title string `json:"title" bson:"title"` + Contents []string `json:"contents" bson:"contents"` } -type Question struct { - QuestionId string `json:"QuestionId"` - Ques string `json:"ques"` - Options []string `json:"options"` - CorrectOption string `json:"correctOption"` +type Content struct { + ContentID string `json:"_id" bson:"_id"` + CourseRef string `json:"courseRef" bson:"courseRef"` + LessonRef string `json:"lessonRef" bson:"lessonRef"` + Title string `json:"title" bson:"title"` + Type string `json:"type" bson:"type"` // video, resource, quiz, lab + Data []byte `json:"data" bson:"data"` } diff --git a/models/course/db.go b/models/course/db.go index 01e6d3b..527cdc7 100644 --- a/models/course/db.go +++ b/models/course/db.go @@ -23,3 +23,118 @@ SOFTWARE. */ package course + +import ( + "context" + "fmt" + "go/types" + "net/http" + "reflect" + + "github.com/praromvik/praromvik/models/db" + "go.mongodb.org/mongo-driver/bson" +) + +func Get(document Document) (any, error) { + mongoDB := db.Mongo{Namespaces: []db.Namespace{document.GetNamespace()}} + result, err := mongoDB.GetDocument(bson.D{{Key: "_id", Value: document.GetID()}}) + if err != nil { + return nil, err + } + + // Create a new instance of the same type as document to decode into + documentType := reflect.TypeOf(document).Elem() + newDoc := reflect.New(documentType).Interface() + + if err := result.Decode(newDoc); err != nil { + return nil, err + } + return newDoc, nil + + // ***Another Approach*** + //// We can't decode the document hence its only interface, therefore we need another method like 'SetDocument' to original struct + //// Set the decoded document back to the original document + //if err := document.SetDocument(newDoc); err != nil { + // return err + //} +} + +func Create(document Document) error { + mongoDB := db.Mongo{Namespaces: []db.Namespace{document.GetNamespace()}} + _, err := mongoDB.AddDocument(document) + return err +} + +func Delete(document Document) error { + mongoDB := db.Mongo{Namespaces: []db.Namespace{document.GetNamespace()}} + if _, err := mongoDB.DeleteDocument(bson.D{{Key: "_id", Value: document.GetID()}}); err != nil { + return err + } + return nil +} + +func Update(document Document) error { + filter := bson.D{{Key: "_id", Value: document.GetID()}} + mongoDB := db.Mongo{Namespaces: []db.Namespace{document.GetNamespace()}} + result, err := mongoDB.GetDocument(filter) + if err != nil { + return err + } + + // Create a new instance of the same type as document to decode into + existingDocType := reflect.TypeOf(document).Elem() + existingDoc := reflect.New(existingDocType).Interface() + + // Decode the existing document + if err := result.Decode(existingDoc); err != nil { + return err + } + + // Merge the updated fields into the existing document + db.MergeStruct(existingDoc, document) + + // Update the document in the database + _, err = mongoDB.UpdateDocument(filter, existingDoc) + return err +} + +func List(document Document) (any, error) { + mongoDB := db.Mongo{Namespaces: []db.Namespace{document.GetNamespace()}} + cursor, err := mongoDB.ListDocuments(types.Interface{}) + if err != nil { + return nil, err + } + + // Create a slice of the correct type + documentType := reflect.TypeOf(document).Elem() + sliceType := reflect.SliceOf(documentType) + documents := reflect.New(sliceType).Interface() + + // Decode all documents into the slice + if err := cursor.All(context.Background(), documents); err != nil { + return nil, err + } + return documents, nil +} + +func Sync(document Document, query string, id string, bsonName string, elementID string) error { + mongoDB := db.Mongo{Namespaces: []db.Namespace{document.GetNamespace()}} + if err := mongoDB.Sync(query, id, bsonName, elementID); err != nil { + return err + } + return nil +} + +func ValidateNameUniqueness(document Document) (int, error) { + filter := bson.D{{Key: "_id", Value: document.GetID()}} + mongoDB := db.Mongo{Namespaces: []db.Namespace{document.GetNamespace()}} + count, err := mongoDB.CountDocuments(filter) + if err != nil { + return http.StatusBadRequest, err + } + + if count != 0 { + return http.StatusBadRequest, fmt.Errorf("course id '%s' already exists", document.GetID()) + } + return http.StatusOK, nil +} diff --git a/models/course/helpers.go b/models/course/helpers.go new file mode 100644 index 0000000..0efe49f --- /dev/null +++ b/models/course/helpers.go @@ -0,0 +1,77 @@ +/* +MIT License + +Copyright (c) 2024 Praromvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package course + +import ( + "github.com/praromvik/praromvik/models/db" +) + +type Document interface { + GetID() interface{} + GetNamespace() db.Namespace +} + +func (c *Course) GetNamespace() db.Namespace { + return db.Namespace{ + Database: "praromvik", + Collection: "courses", + } +} + +func (l *Lesson) GetNamespace() db.Namespace { + return db.Namespace{ + Database: l.CourseRef, + Collection: "lessons", + } +} + +func (c *Content) GetNamespace() db.Namespace { + return db.Namespace{ + Database: c.CourseRef, + Collection: "contents", + } +} + +func (l *Lesson) GetID() interface{} { + return l.LessonID +} + +func (c *Course) GetID() interface{} { + return c.CourseId +} + +func (c *Content) GetID() interface{} { + return c.ContentID +} + +// Another Approach to set Value to pointer. +//func (c *Course) SetDocument(document interface{}) error { +// doc, ok := document.(*Course) +// if !ok { +// return fmt.Errorf("document is not of type %s", reflect.TypeOf(c)) +// } +// *c = *doc +// return nil +//} diff --git a/models/db/db_test.go b/models/db/db_test.go new file mode 100644 index 0000000..a082f75 --- /dev/null +++ b/models/db/db_test.go @@ -0,0 +1,261 @@ +/* +MIT License + +Copyright (c) 2024 Praromvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package db + +import ( + "reflect" + "testing" +) + +func TestMergeStruct(t *testing.T) { + type embeddedUnexported struct { + Field3 string + } + type EmbeddedExported struct { + Field3 string + } + tests := []struct { + name string + oldStruct interface{} + newStruct interface{} + expected interface{} + }{ + { + name: "ReplaceAllFields", + oldStruct: &struct { + Name string + Age int + }{"Anisur", 25}, + newStruct: &struct { + Name string + Age int + }{"Arnob", 28}, + expected: &struct { + Name string + Age int + }{"Arnob", 28}, + }, + { + name: "SetEmptyFields", + oldStruct: &struct { + Name string + Age int + }{}, + newStruct: &struct { + Name string + Age int + }{"Arnob", 28}, + expected: &struct { + Name string + Age int + }{"Arnob", 28}, + }, + { + name: "IgnoreUnexportedFields", + oldStruct: &struct { + Name string + Id int64 + salary int64 + }{"Anisur", 12345, 130000}, + newStruct: &struct { + Name string + Id int64 + salary int64 + }{Name: "Arnob", Id: 123456}, + expected: &struct { + Name string + Id int64 + salary int64 + }{"Arnob", 123456, 130000}, + }, + { + name: "NestedStructUpdate", + oldStruct: &struct { + Name string + Bio struct { + Address string + PostalCode int64 + } + }{Name: "Anisur", Bio: struct { + Address string + PostalCode int64 + }{Address: "Dewliabari, Konabari, Gazipur", PostalCode: 1270}}, + + newStruct: &struct { + Name string + Bio struct { + Address string + PostalCode int64 + } + }{Name: "Anisur", Bio: struct { + Address string + PostalCode int64 + }{Address: "Secotr 10, Uttara, Dhaka", PostalCode: 1210}}, + expected: &struct { + Name string + Bio struct { + Address string + PostalCode int64 + } + }{Name: "Anisur", Bio: struct { + Address string + PostalCode int64 + }{Address: "Secotr 10, Uttara, Dhaka", PostalCode: 1210}}, + }, + { + name: "PartialUpdate", + oldStruct: &struct { + Name string + Age int + Email string + }{"Anisur", 25, "anisur@example.com"}, + newStruct: &struct { + Name string + Age int + Email string + }{Age: 30}, + expected: &struct { + Name string + Age int + Email string + }{"Anisur", 30, "anisur@example.com"}, + }, + { + name: "NestedStructPartialUpdate", + oldStruct: &struct { + Name string + Bio struct { + Address string + Phone string + } + }{Name: "Anisur", Bio: struct { + Address string + Phone string + }{Address: "Old Address", Phone: "1234567890"}}, + + newStruct: &struct { + Name string + Bio struct { + Address string + Phone string + } + }{Bio: struct { + Address string + Phone string + }{Address: "New Address"}}, + expected: &struct { + Name string + Bio struct { + Address string + Phone string + } + }{Name: "Anisur", Bio: struct { + Address string + Phone string + }{Address: "New Address", Phone: "1234567890"}}, + }, + { + name: "EmptyNewStruct", + oldStruct: &struct { + Name string + Age int + Email string + }{"Anisur", 25, "anisur@example.com"}, + newStruct: &struct { + Name string + Age int + Email string + }{}, + expected: &struct { + Name string + Age int + Email string + }{"Anisur", 25, "anisur@example.com"}, + }, + { + name: "EmbeddedStructUnexported", + oldStruct: &struct { + Field1 string + Field2 string + embeddedUnexported + }{Field1: "Field1", Field2: "Field2", embeddedUnexported: embeddedUnexported{Field3: "Field3"}}, + newStruct: &struct { + Field1 string + Field2 string + embeddedUnexported + }{Field1: "NewField1", Field2: "NewField2", embeddedUnexported: embeddedUnexported{Field3: "NewField3"}}, + expected: &struct { + Field1 string + Field2 string + embeddedUnexported + }{Field1: "NewField1", Field2: "NewField2", embeddedUnexported: embeddedUnexported{Field3: "Field3"}}, + }, + { + name: "EmbeddedStructExported", + oldStruct: &struct { + Field1 string + Field2 string + EmbeddedExported + }{Field1: "Field1", Field2: "Field2", EmbeddedExported: EmbeddedExported{Field3: "Field3"}}, + newStruct: &struct { + Field1 string + Field2 string + EmbeddedExported + }{Field1: "NewField1", Field2: "NewField2", EmbeddedExported: EmbeddedExported{Field3: "NewField3"}}, + expected: &struct { + Field1 string + Field2 string + EmbeddedExported + }{Field1: "NewField1", Field2: "NewField2", EmbeddedExported: EmbeddedExported{Field3: "NewField3"}}, + }, + { + name: "EmbeddedStructExportedWithPointer", + oldStruct: &struct { + Field1 string + Field2 string + *EmbeddedExported + }{Field1: "Field1", Field2: "Field2", EmbeddedExported: &EmbeddedExported{Field3: "Field3"}}, + newStruct: &struct { + Field1 string + Field2 string + *EmbeddedExported + }{Field1: "NewField1", Field2: "NewField2", EmbeddedExported: &EmbeddedExported{Field3: "NewField3"}}, + expected: &struct { + Field1 string + Field2 string + *EmbeddedExported + }{Field1: "NewField1", Field2: "NewField2", EmbeddedExported: &EmbeddedExported{Field3: "NewField3"}}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + MergeStruct(test.oldStruct, test.newStruct) + if !reflect.DeepEqual(test.oldStruct, test.expected) { + t.Fatalf("expected %v, got %v", test.expected, test.oldStruct) + } + }) + } +} diff --git a/models/db/helpers.go b/models/db/helpers.go index 2fbd6f2..7318405 100644 --- a/models/db/helpers.go +++ b/models/db/helpers.go @@ -35,14 +35,22 @@ type Namespace struct { Collection string } +// Here, You must have to provide Structure kind instead of ptr. + func MergeStruct(oldStruct interface{}, newStruct interface{}) { - oldValue, newValue := reflect.ValueOf(oldStruct).Elem(), reflect.ValueOf(newStruct) + // We're taking pointer as parameter, but we need struct value instead of pointer + // therefore [Value.Elem()] gives us struct instead of pointer. + oldValue, newValue := reflect.ValueOf(oldStruct).Elem(), reflect.ValueOf(newStruct).Elem() for i := 0; i < oldValue.NumField(); i++ { oldFieldValue, newFieldValue := oldValue.Field(i), newValue.Field(i) - if oldFieldValue.Kind() == reflect.Struct { - MergeStruct(oldFieldValue.Addr().Interface(), newFieldValue.Interface()) - } else if !reflect.DeepEqual(newFieldValue.Interface(), reflect.Zero(newFieldValue.Type()).Interface()) { - oldFieldValue.Set(newFieldValue) + if oldFieldValue.CanSet() { + if oldFieldValue.Kind() == reflect.Struct { + // At this point we've to pass pointer as parameter, so we convert this struct to pointer. + MergeStruct(oldFieldValue.Addr().Interface(), newFieldValue.Addr().Interface()) + } else if !reflect.DeepEqual(newFieldValue.Interface(), reflect.Zero(newFieldValue.Type()).Interface()) { + // [Value.Set] can set value only if field is addressable. we can ensure it by calling [Value.CanSet] + oldFieldValue.Set(newFieldValue) + } } } } diff --git a/models/db/mongo.go b/models/db/mongo.go index bded38f..f2ffd91 100644 --- a/models/db/mongo.go +++ b/models/db/mongo.go @@ -29,7 +29,9 @@ import ( "fmt" "github.com/praromvik/praromvik/models/db/client" + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) type Mongo struct { @@ -41,35 +43,72 @@ func (m Mongo) GetDocument(filter interface{}) (*mongo.SingleResult, error) { if err != nil { return nil, err } - result := collection.FindOne(context.TODO(), filter) - return result, nil + return collection.FindOne(context.TODO(), filter), nil } -func (m Mongo) AddDocument(data interface{}) error { +func (m Mongo) AddDocument(data interface{}) (*mongo.InsertOneResult, error) { collection, err := m.getDBCollection() if err != nil { - return err + return nil, err } - result, err := collection.InsertOne(context.TODO(), data) + return collection.InsertOne(context.TODO(), data) +} + +func (m Mongo) UpdateDocument(filter interface{}, newData interface{}) (*mongo.UpdateResult, error) { + collection, err := m.getDBCollection() if err != nil { - return err + return nil, err } - fmt.Printf("Inserted document with _id: %v\n", result.InsertedID) - return nil + return collection.ReplaceOne(context.TODO(), filter, newData) } -func (m Mongo) UpdateDocument(filter interface{}, newData interface{}) error { +func (m Mongo) DeleteDocument(filter interface{}) (*mongo.DeleteResult, error) { collection, err := m.getDBCollection() if err != nil { - return err + return nil, err + } + return collection.DeleteOne(context.TODO(), filter) +} + +func (m Mongo) ListDocuments(filter interface{}) (*mongo.Cursor, error) { + collection, err := m.getDBCollection() + if err != nil { + return nil, err + } + return collection.Find(context.Background(), filter) +} + +func (m Mongo) CountDocuments(filter interface{}) (int64, error) { + collection, err := m.getDBCollection() + if err != nil { + return 0, err + } + return collection.CountDocuments(context.Background(), filter) +} + +func (m Mongo) BulkWrite(models []mongo.WriteModel) (*mongo.BulkWriteResult, error) { + collection, err := m.getDBCollection() + if err != nil { + return nil, err } - result, err := collection.ReplaceOne(context.TODO(), filter, newData) + opts := options.BulkWrite().SetOrdered(false) + return collection.BulkWrite(context.Background(), models, opts) +} + +func (m Mongo) FindDistinct(field string, filter interface{}) ([]interface{}, error) { + collection, err := m.getDBCollection() if err != nil { - return fmt.Errorf("failed to update document: %v", err) + return nil, err } - fmt.Printf("Update Document. Result. MatchedCount:"+ - " %d, UpsertedCount: %d, ModifiedCount: %d.\n", result.MatchedCount, result.UpsertedCount, result.ModifiedCount) - return nil + return collection.Distinct(context.Background(), field, filter) +} + +func (m Mongo) Aggregate(pipeline interface{}) (*mongo.Cursor, error) { + collection, err := m.getDBCollection() + if err != nil { + return nil, err + } + return collection.Aggregate(context.Background(), pipeline) } func (m Mongo) getDBCollection() (*mongo.Collection, error) { @@ -83,3 +122,39 @@ func (m Mongo) getDBCollection() (*mongo.Collection, error) { } return collection, nil } + +func (m Mongo) Sync(query string, id string, bsonName string, elementId string) error { + collection, err := m.getDBCollection() + if err != nil { + return err + } + // Check if the field is null (unset), and if so, initialize it as an empty array + var doc bson.M + err = collection.FindOne(context.TODO(), bson.M{"_id": id}).Decode(&doc) + if err != nil { + return fmt.Errorf("failed to find document: %v", err) + } + + if doc[bsonName] == nil { + // Initialize the field as an empty array + _, err = collection.UpdateOne( + context.TODO(), + bson.M{"_id": id}, + bson.M{"$set": bson.M{bsonName: []string{elementId}}}, + ) + if err != nil { + return fmt.Errorf("failed to initialize field: %v", err) + } + } else { + // Field is not null, proceed with pushing the element + _, err = collection.UpdateOne( + context.TODO(), + bson.M{"_id": id}, + bson.M{query: bson.M{bsonName: elementId}}, + ) + if err != nil { + return fmt.Errorf("failed to sync document: %v", err) + } + } + return err +} diff --git a/models/user/api.go b/models/user/api.go index f459e50..c27ae30 100644 --- a/models/user/api.go +++ b/models/user/api.go @@ -24,11 +24,16 @@ SOFTWARE. package user +import "github.com/praromvik/praromvik/models/utils" + type User struct { - UserName string `json:"userName" bson:"userName"` - Email string `json:"email" bson:"email"` - Password string `json:"password" bson:"password"` - Phone string `json:"phone" bson:"phone"` - Role string `json:"role" bson:"role"` - UUID string `json:"uuid" bson:"uuid"` + UserName string `json:"userName" bson:"userName"` + Certificates []string `json:"certificates" bson:"certificates"` + EnrolledCourses []utils.Info `json:"enrolledCourses" bson:"enrolledCourses"` + ParticipateExams []utils.Info `json:"participateExams" bson:"participateExams"` + Email string `json:"email" bson:"email"` + Password string `json:"password" bson:"password"` + Phone string `json:"phone" bson:"phone"` + Role string `json:"role" bson:"role"` + UUID string `json:"uuid" bson:"uuid"` } diff --git a/models/user/db.go b/models/user/db.go index 6424565..6c33eb5 100644 --- a/models/user/db.go +++ b/models/user/db.go @@ -29,8 +29,8 @@ import ( "fmt" "net/http" - "github.com/praromvik/praromvik/models" "github.com/praromvik/praromvik/models/db" + "github.com/praromvik/praromvik/models/utils" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "golang.org/x/crypto/bcrypt" @@ -87,7 +87,7 @@ func (u *User) ValidateForm() (int, error) { func (u *User) UpdateUserDataToMongo() error { user := User{} - filter := bson.D{{Key: models.UUID, Value: u.UUID}} + filter := bson.D{{Key: utils.UUID, Value: u.UUID}} mongoDB := db.Mongo{Namespaces: []db.Namespace{userMongoNamespace}} result, err := mongoDB.GetDocument(filter) if err != nil { @@ -97,13 +97,13 @@ func (u *User) UpdateUserDataToMongo() error { return err } db.MergeStruct(&user, *u) - err = mongoDB.UpdateDocument(filter, user) + _, err = mongoDB.UpdateDocument(filter, user) return err } func (u *User) AddUserDataToMongo() error { mongoDB := db.Mongo{Namespaces: []db.Namespace{userMongoNamespace}} - err := mongoDB.AddDocument(u) + _, err := mongoDB.AddDocument(u) return err } @@ -124,9 +124,6 @@ func (u *User) UpdateUserAuthDataToFirestore() error { } db.MergeStruct(&user, *u) authData := getAuthData(user) - if err != nil { - return err - } err = db.Firestore{}.UpdateDocument("users", u.UserName, authData) return err } @@ -153,6 +150,18 @@ func (u *User) FetchAndSetUUIDFromDB() error { return nil } +func (u *User) GetFromMongo() error { + mongoDB := db.Mongo{Namespaces: []db.Namespace{userMongoNamespace}} + result, err := mongoDB.GetDocument(bson.D{{Key: "userName", Value: u.UserName}}) + if err != nil { + return err + } + if err := result.Decode(&u); err != nil { + return err + } + return nil +} + func checkFieldAvailability(field string, value string) error { mongoDB := db.Mongo{Namespaces: []db.Namespace{userMongoNamespace}} result, err := mongoDB.GetDocument(bson.D{{Key: field, Value: value}}) diff --git a/models/constant.go b/models/utils/constant.go similarity index 99% rename from models/constant.go rename to models/utils/constant.go index 42328d4..a92afa5 100644 --- a/models/constant.go +++ b/models/utils/constant.go @@ -22,7 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package models +package utils type RoleType string diff --git a/models/utils/utils.go b/models/utils/utils.go new file mode 100644 index 0000000..c8cf56e --- /dev/null +++ b/models/utils/utils.go @@ -0,0 +1,30 @@ +/* +MIT License + +Copyright (c) 2024 Praromvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package utils + +type Info struct { + Name string `json:"name" bson:"name"` + UUID string `json:"uuid" bson:"uuid"` +} diff --git a/pkg/auth/session.go b/pkg/auth/session.go index 929f0ed..294a5f2 100644 --- a/pkg/auth/session.go +++ b/pkg/auth/session.go @@ -31,9 +31,9 @@ import ( "os" "strings" - "github.com/praromvik/praromvik/models" "github.com/praromvik/praromvik/models/db/client" "github.com/praromvik/praromvik/models/user" + "github.com/praromvik/praromvik/models/utils" rstore "github.com/rbcervilla/redisstore/v8" ) @@ -49,7 +49,7 @@ func init() { if err != nil { log.Fatal("failed to create redis store: ", err) } - redisStore.KeyPrefix(os.Getenv(models.SessionKey)) + redisStore.KeyPrefix(os.Getenv(utils.SessionKey)) } func StoreAuthenticated(w http.ResponseWriter, r *http.Request, u *user.User, valid bool) error { @@ -57,13 +57,14 @@ func StoreAuthenticated(w http.ResponseWriter, r *http.Request, u *user.User, va if err != nil { return err } - session.Values[models.Authenticated] = true + session.Values[utils.Authenticated] = true if u != nil { - session.Values[models.Role] = u.Role - session.Values[models.UserName] = u.UserName + session.Values[utils.UUID] = u.UUID + session.Values[utils.Role] = u.Role + session.Values[utils.UserName] = u.UserName } - session.Values[models.UserIP] = getIpAddress(r) - session.Values[models.UserAgent] = r.UserAgent() + session.Values[utils.UserIP] = getIpAddress(r) + session.Values[utils.UserAgent] = r.UserAgent() if !valid { session.Options.MaxAge = -1 } @@ -75,20 +76,20 @@ func IsAuthenticated(r *http.Request) (bool, error) { if err != nil { return false, err } - authValue := session.Values[models.Authenticated].(bool) + authValue := session.Values[utils.Authenticated].(bool) return authValue, nil } -func GetSessionRole(r *http.Request) (models.RoleType, error) { +func GetSessionRole(r *http.Request) (utils.RoleType, error) { session, err := redisStore.Get(r, sessionTokenName) if err != nil { return "", err } - role, ok := session.Values[models.Role] + role, ok := session.Values[utils.Role] if !ok || role == nil { - return models.None, nil + return utils.None, nil } - return models.RoleType(role.(string)), nil + return utils.RoleType(role.(string)), nil } func SessionValid(r *http.Request) (bool, error) { @@ -97,8 +98,16 @@ func SessionValid(r *http.Request) (bool, error) { return false, err } return !session.IsNew && - session.Values[models.UserIP] == getIpAddress(r) && - session.Values[models.UserAgent] == r.UserAgent(), nil + session.Values[utils.UserIP] == getIpAddress(r) && + session.Values[utils.UserAgent] == r.UserAgent(), nil +} + +func GetUserInfoFromSession(r *http.Request) (*utils.Info, error) { + session, err := redisStore.Get(r, sessionTokenName) + if err != nil { + return nil, err + } + return &utils.Info{Name: session.Values[utils.UserName].(string), UUID: session.Values[utils.UUID].(string)}, nil } func getIpAddress(r *http.Request) string { diff --git a/pkg/middileware/middleware.go b/pkg/middileware/middleware.go index caf0c42..515e5fa 100644 --- a/pkg/middileware/middleware.go +++ b/pkg/middileware/middleware.go @@ -27,7 +27,7 @@ package middleware import ( "net/http" - "github.com/praromvik/praromvik/models" + "github.com/praromvik/praromvik/models/utils" "github.com/praromvik/praromvik/pkg/auth" perror "github.com/praromvik/praromvik/pkg/error" @@ -69,23 +69,23 @@ func SecurityMiddleware(next http.Handler) http.Handler { func AdminAccess(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - serveHTTPIfRoleMatched(next, writer, request, []models.RoleType{models.Admin}) + serveHTTPIfRoleMatched(next, writer, request, []utils.RoleType{utils.Admin}) }) } func AdminOrModeratorAccess(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - serveHTTPIfRoleMatched(next, writer, request, []models.RoleType{models.Moderator, models.Admin}) + serveHTTPIfRoleMatched(next, writer, request, []utils.RoleType{utils.Moderator, utils.Admin}) }) } func ModeratorAccess(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - serveHTTPIfRoleMatched(next, writer, request, []models.RoleType{models.Moderator}) + serveHTTPIfRoleMatched(next, writer, request, []utils.RoleType{utils.Moderator}) }) } -func serveHTTPIfRoleMatched(next http.Handler, writer http.ResponseWriter, request *http.Request, roles []models.RoleType) { +func serveHTTPIfRoleMatched(next http.Handler, writer http.ResponseWriter, request *http.Request, roles []utils.RoleType) { role, err := auth.GetSessionRole(request) if err != nil { perror.HandleError(writer, http.StatusUnauthorized, "Failed to retrieve role from session", err) @@ -103,3 +103,26 @@ func serveHTTPIfRoleMatched(next http.Handler, writer http.ResponseWriter, reque } next.ServeHTTP(writer, request) } + +//func AddCourseIDToCtx(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// id := chi.URLParam(r, "id") +// // Add UUID to request context +// ctx := context.WithValue(r.Context(), "uuid", uuid) +// next.ServeHTTP(w, r.WithContext(ctx)) +// }) +//} + +//func AddLessonUUIDToCtx(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// name := chi.URLParam(r, "name") +// uuid, err := course.GetLessonUUID(name) +// if err != nil { +// perror.HandleError(w, http.StatusNotFound, "", err) +// return +// } +// // Add UUID to request context +// ctx := context.WithValue(r.Context(), "uuid", uuid) +// next.ServeHTTP(w, r.WithContext(ctx)) +// }) +//} diff --git a/routers/routes.go b/routers/routes.go index 37ccce7..7326340 100644 --- a/routers/routes.go +++ b/routers/routes.go @@ -55,20 +55,51 @@ func LoadRoutes() *chi.Mux { router.Route("/course", loadCourseRoutes) return router } - func loadUserAuthRoutes(r chi.Router) { userHandler := &user.User{} - r.HandleFunc("/signup", userHandler.SignUp) - r.HandleFunc("/signin", userHandler.SignIn) - r.HandleFunc("/signout", userHandler.SignOut) + r.Post("/signup", userHandler.SignUp) + r.Post("/signin", userHandler.SignIn) + r.Delete("/signout", userHandler.SignOut) + r.With(middleware.SecurityMiddleware).Get("/user/{userName}", userHandler.Get) } func loadCourseRoutes(r chi.Router) { - courseHandler := &course.Course{} r.Use(middleware.SecurityMiddleware) - r.Get("/", courseHandler.List) - r.Get("/{id}", courseHandler.GetByID) - r.With(middleware.AdminOrModeratorAccess).Post("/", courseHandler.Create) - r.With(middleware.AdminOrModeratorAccess).Put("/{id}", courseHandler.UpdateByID) - r.With(middleware.AdminAccess).Delete("/{id}", courseHandler.DeleteByID) + r.Route("/{courseRef}/lesson", loadLessonRoutes) + r.Route("/{courseRef}/content", loadContentRoutes) + + handler := &course.Course{} + r.Get("/list", handler.List) + r.Get("/{id}", handler.Get) + r.Group(func(r chi.Router) { + r.Use(middleware.AdminOrModeratorAccess) + r.Post("/", handler.Create) + r.Put("/{id}", handler.Update) + }) + //Require admin access + r.With(middleware.AdminAccess).Delete("/{id}", handler.Delete) +} + +func loadLessonRoutes(r chi.Router) { + handler := &course.Lesson{} + r.Get("/list", handler.List) + r.Get("/{id}", handler.Get) + r.Group(func(r chi.Router) { + r.Use(middleware.AdminOrModeratorAccess) + r.Post("/", handler.Create) + }) + //Require admin access + r.With(middleware.AdminAccess).Delete("/{id}", handler.Delete) +} + +func loadContentRoutes(r chi.Router) { + handler := &course.Content{} + r.Get("/list", handler.List) + r.Get("/{id}", handler.Get) + r.Group(func(r chi.Router) { + r.Use(middleware.AdminOrModeratorAccess) + r.Post("/", handler.Create) + }) + //Require admin access + r.With(middleware.AdminAccess).Delete("/{id}", handler.Delete) }