diff --git a/.env b/.env index 43bf5fd412feeda40517d5f5e722333b92271442..568639cff029d91346e8d347f2f5cbab64407d2e 100644 --- a/.env +++ b/.env @@ -4,6 +4,4 @@ PORT=8080 LOGTAIL_TOKEN= HTTP_TIMEOUT_SEC=2 LOG_FLUSH_INTERVAL_MS=1000 -SMTP_USERNAME="noreply@ocw.id" -SMTP_PORT=1025 FE_BASE_URL="http://localhost:3000" diff --git a/.gitignore b/.gitignore index d3eeb070587bb63e0a455b491181f8fc408c8df1..c18416209344fed18ffcb6096897cbbef2ff3dd8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ bin/ tmp/ wire_gen.go __debug_bin* -.env.local +.env.local* diff --git a/.scannerwork/.sonar_lock b/.scannerwork/.sonar_lock new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.scannerwork/report-task.txt b/.scannerwork/report-task.txt new file mode 100644 index 0000000000000000000000000000000000000000..5254c69f7309289db73b05aa53239cf41b055c97 --- /dev/null +++ b/.scannerwork/report-task.txt @@ -0,0 +1,6 @@ +projectKey=ocw-backend +serverUrl=http://localhost:9000 +serverVersion=10.0.0.68432 +dashboardUrl=http://localhost:9000/dashboard?id=ocw-backend +ceTaskId=AYeyCUSQt5Mtvce1YZdB +ceTaskUrl=http://localhost:9000/api/ce/task?id=AYeyCUSQt5Mtvce1YZdB diff --git a/Dockerfile b/Dockerfile index 7673b438cedcef2e1e5775bdc6b25c6049c8d090..fb0d84339ffdb25a43bb456c86cc1262af5ef6bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,13 @@ FROM golang:1.20.0-alpine3.17 AS build RUN apk add --update make +RUN apk add --update git COPY . /app WORKDIR /app RUN go get -RUN go install github.com/swaggo/swag/cmd/swag@latest +RUN go install github.com/swaggo/swag/cmd/swag@v1.8.10 RUN go install github.com/google/wire/cmd/wire@latest RUN make build @@ -17,6 +18,8 @@ RUN mkdir /app WORKDIR /app COPY --from=build /app/bin/server.app /app +RUN touch /app/.env +RUN touch /app/.env.local STOPSIGNAL SIGKILL ENTRYPOINT [ "/app/server.app" ] diff --git a/Dockerfile.dev b/Dockerfile.dev index 03526f3e75455e51850f7f1186b2d2b05f140392..6f6f47c0283f6de5b2139a540fd21e919298a0ba 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,9 +1,10 @@ FROM cosmtrek/air RUN apt install -y make +RUN apt install -y git RUN go install github.com/google/wire/cmd/wire@latest -RUN go install github.com/swaggo/swag/cmd/swag@latest +RUN go install github.com/swaggo/swag/cmd/swag@v1.8.10 COPY . /app diff --git a/Dockerfile.test b/Dockerfile.test index 7b6d3bf4ffb8bbfdc1eed6dfa334889e3d2ec072..d61a7100118f3f92eccfbf3f3b7b3de8948fd4f3 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -1,12 +1,13 @@ FROM golang:1.20.0-alpine3.17 AS build RUN apk add --update make +RUN apk add --update git COPY . /app WORKDIR /app RUN go get -RUN go install github.com/swaggo/swag/cmd/swag@latest +RUN go install github.com/swaggo/swag/cmd/swag@v1.8.10 RUN go install github.com/google/wire/cmd/wire@latest RUN make test-dependency diff --git a/docker-compose.yml b/docker-compose.yml index 3eccf485e7a93a9172dcdfca800f8bcf2b732655..5194e4f7085bd96c5ae3182c99c6a8e0cb195294 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,11 +14,12 @@ services: networks: - api_network backend: - build: + build: context: . - dockerfile: Dockerfile.dev + dockerfile: Dockerfile volumes: - - .:/app + - .env.local:/app/.env.local + - .env:/app/.env ports: - 8888:8080 env_file: .env.docker diff --git a/docs/docs.go b/docs/docs.go index 49b75f3556829fa1845dddc0737e82c9ab85bc01..f8ef591bbbb23cab393505ca805d22079e34632e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1488,6 +1488,540 @@ const docTemplate = `{ } } }, + "/lesson": { + "put": { + "description": "Add a new lesson with the given details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Add a new lesson", + "parameters": [ + { + "type": "string", + "description": "AddLessonToken", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Add Lesson payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/lesson.AddLessonRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/lesson/course/{id}": { + "get": { + "description": "Retrieve lesson data by course ID", + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Get lesson by course ID", + "parameters": [ + { + "type": "string", + "description": "Course ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Invalid ID provided in request path", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/lesson/material": { + "put": { + "description": "Add a new lesson material with the given details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Add a new lesson material", + "parameters": [ + { + "type": "string", + "description": "AddLessonMaterialsToken", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Add Lesson Material payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/materials.AddLessonMaterialsRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/lesson/material/lesson/{id}": { + "get": { + "description": "Retrieve lesson materials data by lesson ID", + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Get lesson materials by lesson ID", + "parameters": [ + { + "type": "string", + "description": "Lesson ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Invalid UUID provided in request path", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/lesson/material/{id}": { + "get": { + "description": "Retrieve lesson data by UUID", + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Get lesson by ID", + "parameters": [ + { + "type": "string", + "description": "Lesson ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Invalid UUID provided in request path", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "delete": { + "description": "Delete a lesson material with the specified ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Delete lesson material by id", + "parameters": [ + { + "type": "string", + "description": "Lesson Material ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "DeleteLessonMaterialToken", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/lesson/{id}": { + "get": { + "description": "Retrieve lesson data by UUID", + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Get lesson by ID", + "parameters": [ + { + "type": "string", + "description": "Lesson ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Invalid UUID provided in request path", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "delete": { + "description": "Delete a lesson with the specified ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Delete lesson by ID", + "parameters": [ + { + "type": "string", + "description": "Lesson ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "DeleteLessonToken", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "patch": { + "description": "Update a lesson material with the given ID", + "tags": [ + "lesson" + ], + "summary": "Update a lesson material", + "parameters": [ + { + "type": "string", + "description": "Lesson Material ID (UUID)", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update Lesson Materials Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/materials.UpdateLessonMaterialsRequestPayload" + } + }, + { + "type": "string", + "description": "UpdateLessonMaterialsToken", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, "/material/{id}": { "get": { "description": "Get material detail", @@ -1531,9 +2065,210 @@ const docTemplate = `{ } } } - }, - "post": { - "description": "Add content of material", + }, + "post": { + "description": "Add content of material", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "content" + ], + "summary": "Add Content", + "parameters": [ + { + "type": "string", + "description": "Access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Add content request", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/material.NewContentRequest" + } + }, + { + "type": "string", + "format": "uuid", + "description": "Material id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/material.NewContentResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "delete": { + "description": "Delete material", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "content" + ], + "summary": "Delete material", + "parameters": [ + { + "type": "string", + "description": "Access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Material id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/material/{id}/content/{content-id}": { + "delete": { + "description": "Delete content of material", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "content" + ], + "summary": "Delete Content", + "parameters": [ + { + "type": "string", + "description": "Access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Material id", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Content id", + "name": "content-id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/quiz": { + "put": { + "description": "New Quiz", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "quiz" + ], + "summary": "New Quiz", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Quiz id", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Add Quiz payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/quiz.AddQuizRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/quiz/{id}": { + "get": { + "description": "Get Quiz Detail", "consumes": [ "application/json" ], @@ -1541,30 +2276,14 @@ const docTemplate = `{ "application/json" ], "tags": [ - "content" + "quiz" ], - "summary": "Add Content", + "summary": "Get Quiz Detail", "parameters": [ - { - "type": "string", - "description": "Access token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "Add content request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/material.NewContentRequest" - } - }, { "type": "string", "format": "uuid", - "description": "Material id", + "description": "Quiz id", "name": "id", "in": "path", "required": true @@ -1582,29 +2301,17 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/material.NewContentResponse" + "$ref": "#/definitions/quiz.Quiz" } } } ] } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/web.BaseResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/web.BaseResponse" - } } } }, "delete": { - "description": "Delete material", + "description": "Delete Quiz", "consumes": [ "application/json" ], @@ -1612,21 +2319,14 @@ const docTemplate = `{ "application/json" ], "tags": [ - "content" + "quiz" ], - "summary": "Delete material", + "summary": "Delete Quiz", "parameters": [ - { - "type": "string", - "description": "Access token", - "name": "Authorization", - "in": "header", - "required": true - }, { "type": "string", "format": "uuid", - "description": "Material id", + "description": "Quiz id", "name": "id", "in": "path", "required": true @@ -1640,11 +2340,9 @@ const docTemplate = `{ } } } - } - }, - "/material/{id}/content/{content-id}": { - "delete": { - "description": "Delete content of material", + }, + "patch": { + "description": "Update Quiz", "consumes": [ "application/json" ], @@ -1652,32 +2350,26 @@ const docTemplate = `{ "application/json" ], "tags": [ - "content" + "quiz" ], - "summary": "Delete Content", + "summary": "Update Quiz", "parameters": [ - { - "type": "string", - "description": "Access token", - "name": "Authorization", - "in": "header", - "required": true - }, { "type": "string", "format": "uuid", - "description": "Material id", + "description": "Quiz id", "name": "id", "in": "path", "required": true }, { - "type": "string", - "format": "uuid", - "description": "Content id", - "name": "content-id", - "in": "path", - "required": true + "description": "Update Quiz payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/quiz.UpdateQuizRequestPayload" + } } ], "responses": { @@ -1690,9 +2382,9 @@ const docTemplate = `{ } } }, - "/quiz/{id}": { - "get": { - "description": "Get Quiz Detail", + "/quiz/{id}/finish": { + "post": { + "description": "Finish quiz session and get the score", "consumes": [ "application/json" ], @@ -1702,8 +2394,24 @@ const docTemplate = `{ "tags": [ "quiz" ], - "summary": "Get Quiz Detail", + "summary": "Finish Quiz", "parameters": [ + { + "type": "string", + "description": "Authenticate User (any role)", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Quiz Finish payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/quiz.FinishQuizPayload" + } + }, { "type": "string", "format": "uuid", @@ -1725,7 +2433,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/quiz.Quiz" + "$ref": "#/definitions/quiz.QuizDetail" } } } @@ -1789,7 +2497,7 @@ const docTemplate = `{ }, "/quiz/{id}/take": { "post": { - "description": "Finish quiz session and get the score", + "description": "Take a quiz", "consumes": [ "application/json" ], @@ -1799,7 +2507,7 @@ const docTemplate = `{ "tags": [ "quiz" ], - "summary": "Finish Quiz", + "summary": "Take Quiz", "parameters": [ { "type": "string", @@ -1808,15 +2516,6 @@ const docTemplate = `{ "in": "header", "required": true }, - { - "description": "Quiz Finish payload", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/quiz.FinishQuizPayload" - } - }, { "type": "string", "format": "uuid", @@ -2026,7 +2725,6 @@ const docTemplate = `{ "description": "Information that should be available when you add a course", "type": "object", "required": [ - "abbreviation", "email", "id", "name" @@ -2053,6 +2751,10 @@ const docTemplate = `{ "description": "Course ID", "type": "string" }, + "lecturer": { + "description": "Course Lecturer Name", + "type": "string" + }, "majabbr": { "description": "Course Major Abbreviation", "type": "string" @@ -2071,9 +2773,7 @@ const docTemplate = `{ "description": "Information that should be available when you add a course", "type": "object", "required": [ - "abbreviation", "email", - "lecturer", "name" ], "properties": { @@ -2090,12 +2790,8 @@ const docTemplate = `{ "type": "string", "example": "someone@example.com" }, - "id": { - "description": "Course ID, Provided by query", - "type": "string" - }, "lecturer": { - "description": "Course Lecturer", + "description": "Course Lecturer Name", "type": "string" }, "majabbr": { @@ -2150,10 +2846,6 @@ const docTemplate = `{ "description": "Faculty Name Abbreviation", "type": "string" }, - "id": { - "description": "Faculty ID, Provided by Query", - "type": "string" - }, "name": { "description": "Faculty Name", "type": "string" @@ -2164,6 +2856,68 @@ const docTemplate = `{ } } }, + "lesson.AddLessonRequestPayload": { + "description": "Information that should be available when you add a lesson", + "type": "object", + "required": [ + "course_id", + "name", + "order" + ], + "properties": { + "addLessonToken": { + "description": "Web Token that was appended to the link", + "type": "string" + }, + "course_id": { + "description": "Course ID", + "type": "string" + }, + "description": { + "description": "Lesson Description (Can be left empty)", + "type": "string" + }, + "name": { + "description": "Lesson Name", + "type": "string" + }, + "order": { + "description": "Lesson Order", + "type": "integer" + } + } + }, + "lesson.UpdateLessonRequestPayload": { + "description": "Information that should be available when you update a lesson", + "type": "object", + "required": [ + "course_id", + "name", + "order" + ], + "properties": { + "course_id": { + "description": "Course ID", + "type": "string" + }, + "description": { + "description": "Lesson Description (Can be left empty)", + "type": "string" + }, + "name": { + "description": "Lesson Name", + "type": "string" + }, + "order": { + "description": "Lesson Order", + "type": "integer" + }, + "updateLessonToken": { + "description": "Web Token that was appended to the link", + "type": "string" + } + } + }, "login.LoginRequestPayload": { "description": "Information that should be available when do a login process", "type": "object", @@ -2248,10 +3002,6 @@ const docTemplate = `{ "description": "Faculty Id, will be set by the server", "type": "string" }, - "id": { - "description": "Major ID, provided by query", - "type": "string" - }, "name": { "description": "Major Name", "type": "string" @@ -2282,11 +3032,15 @@ const docTemplate = `{ "material.CreateMaterialRequest": { "type": "object", "required": [ - "name" + "name", + "week" ], "properties": { "name": { "type": "string" + }, + "week": { + "type": "integer" } } }, @@ -2359,6 +3113,90 @@ const docTemplate = `{ } } }, + "materials.AddLessonMaterialsRequestPayload": { + "description": "Information that should be available when you add a lesson material", + "type": "object", + "required": [ + "contents", + "lesson_id", + "order" + ], + "properties": { + "addLessonMaterialsToken": { + "description": "Web Token that was appended to the link", + "type": "string" + }, + "contents": { + "description": "Lesson Contents", + "type": "string" + }, + "lesson_id": { + "description": "Lesson ID", + "type": "string" + }, + "material_id": { + "description": "Lesson Material ID, optional", + "type": "string" + }, + "order": { + "description": "Lesson Material Order", + "type": "integer" + } + } + }, + "materials.UpdateLessonMaterialsRequestPayload": { + "description": "Information that should be available when you update a lesson material", + "type": "object", + "required": [ + "contents", + "lesson_id", + "order" + ], + "properties": { + "contents": { + "description": "Lesson Contents", + "type": "string" + }, + "lesson_id": { + "description": "Lesson ID", + "type": "string" + }, + "material_id": { + "description": "Lesson Material ID, optional", + "type": "string" + }, + "order": { + "description": "Lesson Material Order", + "type": "integer" + }, + "updateLessonMaterialsToken": { + "description": "Web Token that was appended to the link", + "type": "string" + } + } + }, + "quiz.AddQuizRequestPayload": { + "description": "Information that should be available when you add a quiz", + "type": "object", + "required": [ + "course_id", + "name" + ], + "properties": { + "addQuizToken": { + "description": "Web Token that was appended to the link", + "type": "string" + }, + "course_id": { + "description": "Course ID", + "type": "string" + }, + "name": { + "description": "Quiz Name", + "type": "string" + } + } + }, "quiz.AnswerOption": { "type": "object", "properties": { @@ -2487,6 +3325,20 @@ const docTemplate = `{ } } }, + "quiz.UpdateQuizRequestPayload": { + "description": "Information that should be available when you update a quiz", + "type": "object", + "properties": { + "id": { + "description": "Quiz ID, Set by param", + "type": "string" + }, + "updateQuizToken": { + "description": "Web Token that was appended to the link", + "type": "string" + } + } + }, "refresh.RefreshResponsePayload": { "description": "Refresh endpoint response when process success", "type": "object", diff --git a/docs/swagger.json b/docs/swagger.json index 883d3d799d994fab91eb3dbf9e1aee4d8483c36f..ce06490cd179815074f2a344dc3bddfa5b7a454e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1480,6 +1480,540 @@ } } }, + "/lesson": { + "put": { + "description": "Add a new lesson with the given details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Add a new lesson", + "parameters": [ + { + "type": "string", + "description": "AddLessonToken", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Add Lesson payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/lesson.AddLessonRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/lesson/course/{id}": { + "get": { + "description": "Retrieve lesson data by course ID", + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Get lesson by course ID", + "parameters": [ + { + "type": "string", + "description": "Course ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Invalid ID provided in request path", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/lesson/material": { + "put": { + "description": "Add a new lesson material with the given details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Add a new lesson material", + "parameters": [ + { + "type": "string", + "description": "AddLessonMaterialsToken", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Add Lesson Material payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/materials.AddLessonMaterialsRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/lesson/material/lesson/{id}": { + "get": { + "description": "Retrieve lesson materials data by lesson ID", + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Get lesson materials by lesson ID", + "parameters": [ + { + "type": "string", + "description": "Lesson ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Invalid UUID provided in request path", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/lesson/material/{id}": { + "get": { + "description": "Retrieve lesson data by UUID", + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Get lesson by ID", + "parameters": [ + { + "type": "string", + "description": "Lesson ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Invalid UUID provided in request path", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "delete": { + "description": "Delete a lesson material with the specified ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Delete lesson material by id", + "parameters": [ + { + "type": "string", + "description": "Lesson Material ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "DeleteLessonMaterialToken", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/lesson/{id}": { + "get": { + "description": "Retrieve lesson data by UUID", + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Get lesson by ID", + "parameters": [ + { + "type": "string", + "description": "Lesson ID (UUID)", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Invalid UUID provided in request path", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "delete": { + "description": "Delete a lesson with the specified ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "lesson" + ], + "summary": "Delete lesson by ID", + "parameters": [ + { + "type": "string", + "description": "Lesson ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "DeleteLessonToken", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "patch": { + "description": "Update a lesson material with the given ID", + "tags": [ + "lesson" + ], + "summary": "Update a lesson material", + "parameters": [ + { + "type": "string", + "description": "Lesson Material ID (UUID)", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update Lesson Materials Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/materials.UpdateLessonMaterialsRequestPayload" + } + }, + { + "type": "string", + "description": "UpdateLessonMaterialsToken", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, "/material/{id}": { "get": { "description": "Get material detail", @@ -1523,9 +2057,210 @@ } } } - }, - "post": { - "description": "Add content of material", + }, + "post": { + "description": "Add content of material", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "content" + ], + "summary": "Add Content", + "parameters": [ + { + "type": "string", + "description": "Access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Add content request", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/material.NewContentRequest" + } + }, + { + "type": "string", + "format": "uuid", + "description": "Material id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/material.NewContentResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + }, + "delete": { + "description": "Delete material", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "content" + ], + "summary": "Delete material", + "parameters": [ + { + "type": "string", + "description": "Access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Material id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/material/{id}/content/{content-id}": { + "delete": { + "description": "Delete content of material", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "content" + ], + "summary": "Delete Content", + "parameters": [ + { + "type": "string", + "description": "Access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Material id", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Content id", + "name": "content-id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/quiz": { + "put": { + "description": "New Quiz", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "quiz" + ], + "summary": "New Quiz", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Quiz id", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Add Quiz payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/quiz.AddQuizRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/quiz/{id}": { + "get": { + "description": "Get Quiz Detail", "consumes": [ "application/json" ], @@ -1533,30 +2268,14 @@ "application/json" ], "tags": [ - "content" + "quiz" ], - "summary": "Add Content", + "summary": "Get Quiz Detail", "parameters": [ - { - "type": "string", - "description": "Access token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "Add content request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/material.NewContentRequest" - } - }, { "type": "string", "format": "uuid", - "description": "Material id", + "description": "Quiz id", "name": "id", "in": "path", "required": true @@ -1574,29 +2293,17 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/material.NewContentResponse" + "$ref": "#/definitions/quiz.Quiz" } } } ] } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/web.BaseResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/web.BaseResponse" - } } } }, "delete": { - "description": "Delete material", + "description": "Delete Quiz", "consumes": [ "application/json" ], @@ -1604,21 +2311,14 @@ "application/json" ], "tags": [ - "content" + "quiz" ], - "summary": "Delete material", + "summary": "Delete Quiz", "parameters": [ - { - "type": "string", - "description": "Access token", - "name": "Authorization", - "in": "header", - "required": true - }, { "type": "string", "format": "uuid", - "description": "Material id", + "description": "Quiz id", "name": "id", "in": "path", "required": true @@ -1632,11 +2332,9 @@ } } } - } - }, - "/material/{id}/content/{content-id}": { - "delete": { - "description": "Delete content of material", + }, + "patch": { + "description": "Update Quiz", "consumes": [ "application/json" ], @@ -1644,32 +2342,26 @@ "application/json" ], "tags": [ - "content" + "quiz" ], - "summary": "Delete Content", + "summary": "Update Quiz", "parameters": [ - { - "type": "string", - "description": "Access token", - "name": "Authorization", - "in": "header", - "required": true - }, { "type": "string", "format": "uuid", - "description": "Material id", + "description": "Quiz id", "name": "id", "in": "path", "required": true }, { - "type": "string", - "format": "uuid", - "description": "Content id", - "name": "content-id", - "in": "path", - "required": true + "description": "Update Quiz payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/quiz.UpdateQuizRequestPayload" + } } ], "responses": { @@ -1682,9 +2374,9 @@ } } }, - "/quiz/{id}": { - "get": { - "description": "Get Quiz Detail", + "/quiz/{id}/finish": { + "post": { + "description": "Finish quiz session and get the score", "consumes": [ "application/json" ], @@ -1694,8 +2386,24 @@ "tags": [ "quiz" ], - "summary": "Get Quiz Detail", + "summary": "Finish Quiz", "parameters": [ + { + "type": "string", + "description": "Authenticate User (any role)", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Quiz Finish payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/quiz.FinishQuizPayload" + } + }, { "type": "string", "format": "uuid", @@ -1717,7 +2425,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/quiz.Quiz" + "$ref": "#/definitions/quiz.QuizDetail" } } } @@ -1781,7 +2489,7 @@ }, "/quiz/{id}/take": { "post": { - "description": "Finish quiz session and get the score", + "description": "Take a quiz", "consumes": [ "application/json" ], @@ -1791,7 +2499,7 @@ "tags": [ "quiz" ], - "summary": "Finish Quiz", + "summary": "Take Quiz", "parameters": [ { "type": "string", @@ -1800,15 +2508,6 @@ "in": "header", "required": true }, - { - "description": "Quiz Finish payload", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/quiz.FinishQuizPayload" - } - }, { "type": "string", "format": "uuid", @@ -2018,7 +2717,6 @@ "description": "Information that should be available when you add a course", "type": "object", "required": [ - "abbreviation", "email", "id", "name" @@ -2045,6 +2743,10 @@ "description": "Course ID", "type": "string" }, + "lecturer": { + "description": "Course Lecturer Name", + "type": "string" + }, "majabbr": { "description": "Course Major Abbreviation", "type": "string" @@ -2063,9 +2765,7 @@ "description": "Information that should be available when you add a course", "type": "object", "required": [ - "abbreviation", "email", - "lecturer", "name" ], "properties": { @@ -2082,12 +2782,8 @@ "type": "string", "example": "someone@example.com" }, - "id": { - "description": "Course ID, Provided by query", - "type": "string" - }, "lecturer": { - "description": "Course Lecturer", + "description": "Course Lecturer Name", "type": "string" }, "majabbr": { @@ -2142,10 +2838,6 @@ "description": "Faculty Name Abbreviation", "type": "string" }, - "id": { - "description": "Faculty ID, Provided by Query", - "type": "string" - }, "name": { "description": "Faculty Name", "type": "string" @@ -2156,6 +2848,68 @@ } } }, + "lesson.AddLessonRequestPayload": { + "description": "Information that should be available when you add a lesson", + "type": "object", + "required": [ + "course_id", + "name", + "order" + ], + "properties": { + "addLessonToken": { + "description": "Web Token that was appended to the link", + "type": "string" + }, + "course_id": { + "description": "Course ID", + "type": "string" + }, + "description": { + "description": "Lesson Description (Can be left empty)", + "type": "string" + }, + "name": { + "description": "Lesson Name", + "type": "string" + }, + "order": { + "description": "Lesson Order", + "type": "integer" + } + } + }, + "lesson.UpdateLessonRequestPayload": { + "description": "Information that should be available when you update a lesson", + "type": "object", + "required": [ + "course_id", + "name", + "order" + ], + "properties": { + "course_id": { + "description": "Course ID", + "type": "string" + }, + "description": { + "description": "Lesson Description (Can be left empty)", + "type": "string" + }, + "name": { + "description": "Lesson Name", + "type": "string" + }, + "order": { + "description": "Lesson Order", + "type": "integer" + }, + "updateLessonToken": { + "description": "Web Token that was appended to the link", + "type": "string" + } + } + }, "login.LoginRequestPayload": { "description": "Information that should be available when do a login process", "type": "object", @@ -2240,10 +2994,6 @@ "description": "Faculty Id, will be set by the server", "type": "string" }, - "id": { - "description": "Major ID, provided by query", - "type": "string" - }, "name": { "description": "Major Name", "type": "string" @@ -2274,11 +3024,15 @@ "material.CreateMaterialRequest": { "type": "object", "required": [ - "name" + "name", + "week" ], "properties": { "name": { "type": "string" + }, + "week": { + "type": "integer" } } }, @@ -2351,6 +3105,90 @@ } } }, + "materials.AddLessonMaterialsRequestPayload": { + "description": "Information that should be available when you add a lesson material", + "type": "object", + "required": [ + "contents", + "lesson_id", + "order" + ], + "properties": { + "addLessonMaterialsToken": { + "description": "Web Token that was appended to the link", + "type": "string" + }, + "contents": { + "description": "Lesson Contents", + "type": "string" + }, + "lesson_id": { + "description": "Lesson ID", + "type": "string" + }, + "material_id": { + "description": "Lesson Material ID, optional", + "type": "string" + }, + "order": { + "description": "Lesson Material Order", + "type": "integer" + } + } + }, + "materials.UpdateLessonMaterialsRequestPayload": { + "description": "Information that should be available when you update a lesson material", + "type": "object", + "required": [ + "contents", + "lesson_id", + "order" + ], + "properties": { + "contents": { + "description": "Lesson Contents", + "type": "string" + }, + "lesson_id": { + "description": "Lesson ID", + "type": "string" + }, + "material_id": { + "description": "Lesson Material ID, optional", + "type": "string" + }, + "order": { + "description": "Lesson Material Order", + "type": "integer" + }, + "updateLessonMaterialsToken": { + "description": "Web Token that was appended to the link", + "type": "string" + } + } + }, + "quiz.AddQuizRequestPayload": { + "description": "Information that should be available when you add a quiz", + "type": "object", + "required": [ + "course_id", + "name" + ], + "properties": { + "addQuizToken": { + "description": "Web Token that was appended to the link", + "type": "string" + }, + "course_id": { + "description": "Course ID", + "type": "string" + }, + "name": { + "description": "Quiz Name", + "type": "string" + } + } + }, "quiz.AnswerOption": { "type": "object", "properties": { @@ -2479,6 +3317,20 @@ } } }, + "quiz.UpdateQuizRequestPayload": { + "description": "Information that should be available when you update a quiz", + "type": "object", + "properties": { + "id": { + "description": "Quiz ID, Set by param", + "type": "string" + }, + "updateQuizToken": { + "description": "Web Token that was appended to the link", + "type": "string" + } + } + }, "refresh.RefreshResponsePayload": { "description": "Refresh endpoint response when process success", "type": "object", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 58ea9348fb473426c5890cff161bd98360f62bf0..56b3a2e0d692c706ebedfbf508561859fb68dee3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -75,6 +75,9 @@ definitions: id: description: Course ID type: string + lecturer: + description: Course Lecturer Name + type: string majabbr: description: Course Major Abbreviation type: string @@ -85,7 +88,6 @@ definitions: description: Course Name type: string required: - - abbreviation - email - id - name @@ -103,11 +105,8 @@ definitions: description: Contributor Email example: someone@example.com type: string - id: - description: Course ID, Provided by query - type: string lecturer: - description: Course Lecturer + description: Course Lecturer Name type: string majabbr: description: Course Major Abbreviation @@ -122,9 +121,7 @@ definitions: description: Web Token that was appended to the link type: string required: - - abbreviation - email - - lecturer - name type: object faculty.AddFacultyRequestPayload: @@ -149,9 +146,6 @@ definitions: abbreviation: description: Faculty Name Abbreviation type: string - id: - description: Faculty ID, Provided by Query - type: string name: description: Faculty Name type: string @@ -162,6 +156,52 @@ definitions: - abbreviation - name type: object + lesson.AddLessonRequestPayload: + description: Information that should be available when you add a lesson + properties: + addLessonToken: + description: Web Token that was appended to the link + type: string + course_id: + description: Course ID + type: string + description: + description: Lesson Description (Can be left empty) + type: string + name: + description: Lesson Name + type: string + order: + description: Lesson Order + type: integer + required: + - course_id + - name + - order + type: object + lesson.UpdateLessonRequestPayload: + description: Information that should be available when you update a lesson + properties: + course_id: + description: Course ID + type: string + description: + description: Lesson Description (Can be left empty) + type: string + name: + description: Lesson Name + type: string + order: + description: Lesson Order + type: integer + updateLessonToken: + description: Web Token that was appended to the link + type: string + required: + - course_id + - name + - order + type: object login.LoginRequestPayload: description: Information that should be available when do a login process properties: @@ -221,9 +261,6 @@ definitions: faculty_id: description: Faculty Id, will be set by the server type: string - id: - description: Major ID, provided by query - type: string name: description: Major Name type: string @@ -249,8 +286,11 @@ definitions: properties: name: type: string + week: + type: integer required: - name + - week type: object material.CreateMaterialResponse: properties: @@ -298,6 +338,68 @@ definitions: upload_link: type: string type: object + materials.AddLessonMaterialsRequestPayload: + description: Information that should be available when you add a lesson material + properties: + addLessonMaterialsToken: + description: Web Token that was appended to the link + type: string + contents: + description: Lesson Contents + type: string + lesson_id: + description: Lesson ID + type: string + material_id: + description: Lesson Material ID, optional + type: string + order: + description: Lesson Material Order + type: integer + required: + - contents + - lesson_id + - order + type: object + materials.UpdateLessonMaterialsRequestPayload: + description: Information that should be available when you update a lesson material + properties: + contents: + description: Lesson Contents + type: string + lesson_id: + description: Lesson ID + type: string + material_id: + description: Lesson Material ID, optional + type: string + order: + description: Lesson Material Order + type: integer + updateLessonMaterialsToken: + description: Web Token that was appended to the link + type: string + required: + - contents + - lesson_id + - order + type: object + quiz.AddQuizRequestPayload: + description: Information that should be available when you add a quiz + properties: + addQuizToken: + description: Web Token that was appended to the link + type: string + course_id: + description: Course ID + type: string + name: + description: Quiz Name + type: string + required: + - course_id + - name + type: object quiz.AnswerOption: properties: answer: @@ -381,6 +483,16 @@ definitions: problem_id: type: string type: object + quiz.UpdateQuizRequestPayload: + description: Information that should be available when you update a quiz + properties: + id: + description: Quiz ID, Set by param + type: string + updateQuizToken: + description: Web Token that was appended to the link + type: string + type: object refresh.RefreshResponsePayload: description: Refresh endpoint response when process success properties: @@ -1417,6 +1529,361 @@ paths: summary: Get courses by major tags: - course + /lesson: + put: + consumes: + - application/json + description: Add a new lesson with the given details + parameters: + - description: AddLessonToken + in: header + name: Authorization + required: true + type: string + - description: Add Lesson payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/lesson.AddLessonRequestPayload' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/web.BaseResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.BaseResponse' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Add a new lesson + tags: + - lesson + /lesson/{id}: + delete: + consumes: + - application/json + description: Delete a lesson with the specified ID + parameters: + - description: Lesson ID + in: path + name: id + required: true + type: string + - description: DeleteLessonToken + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/web.BaseResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.BaseResponse' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Delete lesson by ID + tags: + - lesson + get: + description: Retrieve lesson data by UUID + parameters: + - description: Lesson ID (UUID) + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Invalid UUID provided in request path + schema: + $ref: '#/definitions/web.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Get lesson by ID + tags: + - lesson + patch: + description: Update a lesson material with the given ID + parameters: + - description: Lesson Material ID (UUID) + in: path + name: id + required: true + type: string + - description: Update Lesson Materials Payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/materials.UpdateLessonMaterialsRequestPayload' + - description: UpdateLessonMaterialsToken + in: header + name: Authorization + required: true + type: string + responses: + "200": + description: Success + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/web.BaseResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.BaseResponse' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Update a lesson material + tags: + - lesson + /lesson/course/{id}: + get: + description: Retrieve lesson data by course ID + parameters: + - description: Course ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Invalid ID provided in request path + schema: + $ref: '#/definitions/web.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Get lesson by course ID + tags: + - lesson + /lesson/material: + put: + consumes: + - application/json + description: Add a new lesson material with the given details + parameters: + - description: AddLessonMaterialsToken + in: header + name: Authorization + required: true + type: string + - description: Add Lesson Material payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/materials.AddLessonMaterialsRequestPayload' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/web.BaseResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.BaseResponse' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Add a new lesson material + tags: + - lesson + /lesson/material/{id}: + delete: + consumes: + - application/json + description: Delete a lesson material with the specified ID + parameters: + - description: Lesson Material ID + in: path + name: id + required: true + type: string + - description: DeleteLessonMaterialToken + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/web.BaseResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.BaseResponse' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Delete lesson material by id + tags: + - lesson + get: + description: Retrieve lesson data by UUID + parameters: + - description: Lesson ID (UUID) + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Invalid UUID provided in request path + schema: + $ref: '#/definitions/web.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Get lesson by ID + tags: + - lesson + /lesson/material/lesson/{id}: + get: + description: Retrieve lesson materials data by lesson ID + parameters: + - description: Lesson ID (UUID) + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Invalid UUID provided in request path + schema: + $ref: '#/definitions/web.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Get lesson materials by lesson ID + tags: + - lesson /material/{id}: delete: consumes: @@ -1548,7 +2015,56 @@ paths: summary: Delete Content tags: - content + /quiz: + put: + consumes: + - application/json + description: New Quiz + parameters: + - description: Quiz id + format: uuid + in: path + name: id + required: true + type: string + - description: Add Quiz payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/quiz.AddQuizRequestPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + summary: New Quiz + tags: + - quiz /quiz/{id}: + delete: + consumes: + - application/json + description: Delete Quiz + parameters: + - description: Quiz id + format: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Delete Quiz + tags: + - quiz get: consumes: - application/json @@ -1575,6 +2091,71 @@ paths: summary: Get Quiz Detail tags: - quiz + patch: + consumes: + - application/json + description: Update Quiz + parameters: + - description: Quiz id + format: uuid + in: path + name: id + required: true + type: string + - description: Update Quiz payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/quiz.UpdateQuizRequestPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Update Quiz + tags: + - quiz + /quiz/{id}/finish: + post: + consumes: + - application/json + description: Finish quiz session and get the score + parameters: + - description: Authenticate User (any role) + in: header + name: Authorization + required: true + type: string + - description: Quiz Finish payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/quiz.FinishQuizPayload' + - description: Quiz id + format: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/web.BaseResponse' + - properties: + data: + $ref: '#/definitions/quiz.QuizDetail' + type: object + summary: Finish Quiz + tags: + - quiz /quiz/{id}/solution: get: consumes: @@ -1611,19 +2192,13 @@ paths: post: consumes: - application/json - description: Finish quiz session and get the score + description: Take a quiz parameters: - description: Authenticate User (any role) in: header name: Authorization required: true type: string - - description: Quiz Finish payload - in: body - name: data - required: true - schema: - $ref: '#/definitions/quiz.FinishQuizPayload' - description: Quiz id format: uuid in: path @@ -1642,7 +2217,7 @@ paths: data: $ref: '#/definitions/quiz.QuizDetail' type: object - summary: Finish Quiz + summary: Take Quiz tags: - quiz /reset/confirm: diff --git a/handler/course/updateCourse.go b/handler/course/updateCourse.go index 4e2ded686a6281dd2e6baebd5b8868fdcf92c150..597bc92d4c8e3e20b43a008b94aea420a883f2e9 100644 --- a/handler/course/updateCourse.go +++ b/handler/course/updateCourse.go @@ -30,6 +30,7 @@ import ( // @Router /course/{id} [patch] func (c CourseHandlerImpl) UpdateCourse(w http.ResponseWriter, r *http.Request) { payload := course.UpdateCourseRequestPayload{} + payload.ID = chi.URLParam(r, "id") validate := validator.New() // Validate payload @@ -82,7 +83,6 @@ func (c CourseHandlerImpl) UpdateCourse(w http.ResponseWriter, r *http.Request) } payload.UpdateCourseToken = token[1] - payload.ID = chi.URLParam(r, "id") err := c.CourseService.UpdateCourse(payload) if err != nil { diff --git a/handler/di.go b/handler/di.go index 399ef81ecbb23c9d70b0431c6129610c92d5dadc..aefdce321930ee174f0b93d23d3eadc8f3988adc 100644 --- a/handler/di.go +++ b/handler/di.go @@ -6,6 +6,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/handler/auth" "gitlab.informatika.org/ocw/ocw-backend/handler/common" "gitlab.informatika.org/ocw/ocw-backend/handler/course" + "gitlab.informatika.org/ocw/ocw-backend/handler/lesson" "gitlab.informatika.org/ocw/ocw-backend/handler/material" "gitlab.informatika.org/ocw/ocw-backend/handler/quiz" "gitlab.informatika.org/ocw/ocw-backend/handler/reset" @@ -37,6 +38,10 @@ var HandlerSet = wire.NewSet( wire.Struct(new(course.CourseHandlerImpl), "*"), wire.Bind(new(course.CourseHandler), new(*course.CourseHandlerImpl)), + // Lesson + wire.Struct(new(lesson.LessonHandlerImpl), "*"), + wire.Bind(new(lesson.LessonHandler), new(*lesson.LessonHandlerImpl)), + // Material wire.Struct(new(material.MaterialHandlerImpl), "*"), wire.Bind(new(material.MaterialHandler), new(*material.MaterialHandlerImpl)), diff --git a/handler/lesson/addLesson.go b/handler/lesson/addLesson.go new file mode 100644 index 0000000000000000000000000000000000000000..162892b3af78a0b9d12b1c463139004e67c589e7 --- /dev/null +++ b/handler/lesson/addLesson.go @@ -0,0 +1,100 @@ +package lesson + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-playground/validator/v10" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson" +) + +// Index godoc +// +// @Summary Add a new lesson +// @Description Add a new lesson with the given details +// @Tags lesson +// @Accept json +// @Produce json +// @Param Authorization header string true "AddLessonToken" +// @Param data body lesson.AddLessonRequestPayload true "Add Lesson payload" +// @Success 200 {object} web.BaseResponse "Success" +// @Failure 400 {object} web.BaseResponse "Bad Request" +// @Failure 401 {object} web.BaseResponse "Unauthorized" +// @Failure 403 {object} web.BaseResponse "Forbidden" +// @Failure 422 {object} web.BaseResponse "Unprocessable Entity" +// @Failure 500 {object} web.BaseResponse "Internal Server Error" +// @Router /lesson [put] +func (l LessonHandlerImpl) AddLesson(w http.ResponseWriter, r *http.Request) { + payload := lesson.AddLessonRequestPayload{} + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := l.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + l.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := l.HttpUtil.ParseJson(r, &payload); err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + l.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + validate := validator.New() + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := l.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + if validateTokenHeader == "" { + payload := l.WrapperUtil.ErrorResponseWrap("token is required", nil) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + if len(token) != 2 { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + if token[0] != "Bearer" { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + payload.AddLessonToken = token[1] + err := l.LessonService.AddLesson(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + l.Logger.Error( + fmt.Sprintf("[LESSON] some error happened when validating URL: %s", err.Error()), + ) + payload := l.WrapperUtil.ErrorResponseWrap("internal server error", nil) + l.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := l.WrapperUtil.SuccessResponseWrap(nil) + l.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/lesson/addLessonMaterial.go b/handler/lesson/addLessonMaterial.go new file mode 100644 index 0000000000000000000000000000000000000000..bd549025b68e6fcc72e63dbfc18b0964d4dc9281 --- /dev/null +++ b/handler/lesson/addLessonMaterial.go @@ -0,0 +1,102 @@ +package lesson + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-playground/validator/v10" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson/materials" +) + +// Index godoc +// +// @Summary Add a new lesson material +// @Description Add a new lesson material with the given details +// @Tags lesson +// @Accept json +// @Produce json +// @Param Authorization header string true "AddLessonMaterialsToken" +// @Param data body materials.AddLessonMaterialsRequestPayload true "Add Lesson Material payload" +// @Success 200 {object} web.BaseResponse "Success" +// @Failure 400 {object} web.BaseResponse "Bad Request" +// @Failure 401 {object} web.BaseResponse "Unauthorized" +// @Failure 403 {object} web.BaseResponse "Forbidden" +// @Failure 422 {object} web.BaseResponse "Unprocessable Entity" +// @Failure 500 {object} web.BaseResponse "Internal Server Error" +// @Router /lesson/material [put] +func (l LessonHandlerImpl) AddLessonMaterial(w http.ResponseWriter, r *http.Request) { + payload := materials.AddLessonMaterialsRequestPayload{} + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := l.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + l.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := l.HttpUtil.ParseJson(r, &payload); err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + l.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + validate := validator.New() + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := l.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := l.WrapperUtil.ErrorResponseWrap("token is required", nil) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + if token[0] != "Bearer" { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + payload.AddLessonMaterialsToken = token[1] + err := l.LessonService.AddLessonMaterial(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + l.Logger.Error( + fmt.Sprintf("[LESSON] some error happened when validating URL: %s", err.Error()), + ) + payload := l.WrapperUtil.ErrorResponseWrap("internal server error", nil) + l.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := l.WrapperUtil.SuccessResponseWrap(nil) + l.HttpUtil.WriteSuccessJson(w, responsePayload) +} diff --git a/handler/lesson/deleteLesson.go b/handler/lesson/deleteLesson.go new file mode 100644 index 0000000000000000000000000000000000000000..e00f057f39a8a931716c2f24cb6d3a0781ad8b3a --- /dev/null +++ b/handler/lesson/deleteLesson.go @@ -0,0 +1,84 @@ +package lesson + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson" +) + +// Index godoc +// +// @Summary Delete lesson by ID +// @Description Delete a lesson with the specified ID +// @Tags lesson +// @Accept json +// @Produce json +// @Param id path string true "Lesson ID" +// @Param Authorization header string true "DeleteLessonToken" +// @Success 200 {object} web.BaseResponse "Success" +// @Failure 400 {object} web.BaseResponse "Bad Request" +// @Failure 401 {object} web.BaseResponse "Unauthorized" +// @Failure 403 {object} web.BaseResponse "Forbidden" +// @Failure 422 {object} web.BaseResponse "Unprocessable Entity" +// @Failure 500 {object} web.BaseResponse "Internal Server Error" +// @Router /lesson/{id} [delete] +func (l LessonHandlerImpl) DeleteLesson(w http.ResponseWriter, r *http.Request) { + payload := lesson.DeleteByUUIDRequestPayload{} + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := l.WrapperUtil.ErrorResponseWrap("token is required", nil) + l.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + id, err := uuid.Parse(chi.URLParam(r, "id")) + + if err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid id", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.DeleteLessonToken = token[1] + payload.ID = id + err = l.LessonService.DeleteLesson(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + l.Logger.Error( + fmt.Sprintf("[LESSON] some error happened when validating URL: %s", err.Error()), + ) + + payload := l.WrapperUtil.ErrorResponseWrap("internal server error", nil) + l.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := l.WrapperUtil.SuccessResponseWrap(nil) + l.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/lesson/deleteLessonMaterial.go b/handler/lesson/deleteLessonMaterial.go new file mode 100644 index 0000000000000000000000000000000000000000..ee814d240edca4162133a8ad6515e53b83d5d51d --- /dev/null +++ b/handler/lesson/deleteLessonMaterial.go @@ -0,0 +1,85 @@ +package lesson + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson/materials" +) + +// Index godoc +// +// @Summary Delete lesson material by id +// @Description Delete a lesson material with the specified ID +// @Tags lesson +// @Accept json +// @Produce json +// @Param id path string true "Lesson Material ID" +// @Param Authorization header string true "DeleteLessonMaterialToken" +// @Success 200 {object} web.BaseResponse "Success" +// @Failure 400 {object} web.BaseResponse "Bad Request" +// @Failure 401 {object} web.BaseResponse "Unauthorized" +// @Failure 403 {object} web.BaseResponse "Forbidden" +// @Failure 422 {object} web.BaseResponse "Unprocessable Entity" +// @Failure 500 {object} web.BaseResponse "Internal Server Error" +// @Router /lesson/material/{id} [delete] +func (l LessonHandlerImpl) DeleteLessonMaterial(w http.ResponseWriter, r *http.Request) { + payload := materials.DeleteByUUIDRequestPayload{} + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := l.WrapperUtil.ErrorResponseWrap("token is required", nil) + l.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + id, err := uuid.Parse(chi.URLParam(r, "id")) + + if err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid id", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + + payload.DeleteLessonMaterialsToken = token[1] + payload.ID = id + err = l.LessonService.DeleteLessonMaterial(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + l.Logger.Error( + fmt.Sprintf("[LESSON] some error happened when validating URL: %s", err.Error()), + ) + + payload := l.WrapperUtil.ErrorResponseWrap("internal server error", nil) + l.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := l.WrapperUtil.SuccessResponseWrap(nil) + l.HttpUtil.WriteSuccessJson(w, responsePayload) +} diff --git a/handler/lesson/getLesson.go b/handler/lesson/getLesson.go new file mode 100644 index 0000000000000000000000000000000000000000..6fe56562324ca2a8e58330ceb1fb593eb09f2301 --- /dev/null +++ b/handler/lesson/getLesson.go @@ -0,0 +1,55 @@ +package lesson + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson" +) + +// Index godoc +// +// @Summary Get lesson by ID +// @Description Retrieve lesson data by UUID +// @Tags lesson +// @Produce json +// @Param id path string true "Lesson ID (UUID)" +// @Success 200 {object} web.BaseResponse "Success" +// @Failure 400 {object} web.BaseResponse "Invalid UUID provided in request path" +// @Failure 401 {object} web.BaseResponse "Unauthorized" +// @Failure 500 {object} web.BaseResponse "Internal Server Error" +// @Router /lesson/{id} [get] +func (l LessonHandlerImpl) GetLesson(w http.ResponseWriter, r *http.Request) { + payload := lesson.GetByUUIDRequestPayload{} + id, err := uuid.Parse(chi.URLParam(r, "id")) + + if err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid id", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.ID = id + packet, err := l.LessonService.GetLesson(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + l.Logger.Error( + fmt.Sprintf("[LESSON] some error happened when validating URL: %s", err.Error()), + ) + payload := l.WrapperUtil.ErrorResponseWrap("internal server error", nil) + l.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := l.WrapperUtil.SuccessResponseWrap(packet) + l.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/lesson/getLessonMaterial.go b/handler/lesson/getLessonMaterial.go new file mode 100644 index 0000000000000000000000000000000000000000..f6449cd6b95a01cdf6418f47b72849a553cadd12 --- /dev/null +++ b/handler/lesson/getLessonMaterial.go @@ -0,0 +1,55 @@ +package lesson + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson/materials" +) + +// Index godoc +// +// @Summary Get lesson by ID +// @Description Retrieve lesson data by UUID +// @Tags lesson +// @Produce json +// @Param id path string true "Lesson ID (UUID)" +// @Success 200 {object} web.BaseResponse "Success" +// @Failure 400 {object} web.BaseResponse "Invalid UUID provided in request path" +// @Failure 401 {object} web.BaseResponse "Unauthorized" +// @Failure 500 {object} web.BaseResponse "Internal Server Error" +// @Router /lesson/material/{id} [get] +func (l LessonHandlerImpl) GetLessonMaterial(w http.ResponseWriter, r *http.Request) { + payload := materials.GetByUUIDRequestPayload{} + id, err := uuid.Parse(chi.URLParam(r, "id")) + + if err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid id", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.ID = id + packet, err := l.LessonService.GetLessonMaterial(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + l.Logger.Error( + fmt.Sprintf("[LESSON] some error happened when validating URL: %s", err.Error()), + ) + payload := l.WrapperUtil.ErrorResponseWrap("internal server error", nil) + l.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := l.WrapperUtil.SuccessResponseWrap(packet) + l.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/lesson/getLessonMaterialsByLesson.go b/handler/lesson/getLessonMaterialsByLesson.go new file mode 100644 index 0000000000000000000000000000000000000000..dcb37eb5dfee8fd45a338d9a480b17b20c77218b --- /dev/null +++ b/handler/lesson/getLessonMaterialsByLesson.go @@ -0,0 +1,55 @@ +package lesson + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson/materials" +) + +// Index godoc +// +// @Summary Get lesson materials by lesson ID +// @Description Retrieve lesson materials data by lesson ID +// @Tags lesson +// @Produce json +// @Param id path string true "Lesson ID (UUID)" +// @Success 200 {object} web.BaseResponse "Success" +// @Failure 400 {object} web.BaseResponse "Invalid UUID provided in request path" +// @Failure 401 {object} web.BaseResponse "Unauthorized" +// @Failure 500 {object} web.BaseResponse "Internal Server Error" +// @Router /lesson/material/lesson/{id} [get] +func (l LessonHandlerImpl) GetLessonMaterialsByLesson(w http.ResponseWriter, r *http.Request) { + payload := materials.GetByUUIDRequestPayload{} + id, err := uuid.Parse(chi.URLParam(r, "id")) + + if err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid id", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.ID = id + packet, err := l.LessonService.GetLessonMaterials(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + l.Logger.Error( + fmt.Sprintf("[LESSON] some error happened when validating URL: %s", err.Error()), + ) + payload := l.WrapperUtil.ErrorResponseWrap("internal server error", nil) + l.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := l.WrapperUtil.SuccessResponseWrap(packet) + l.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/lesson/getLessonsByCourse.go b/handler/lesson/getLessonsByCourse.go new file mode 100644 index 0000000000000000000000000000000000000000..fd2956b7f2cd1a90fb428dcd1d7945783c2dfd50 --- /dev/null +++ b/handler/lesson/getLessonsByCourse.go @@ -0,0 +1,47 @@ +package lesson + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson" +) + +// Index godoc +// +// @Summary Get lesson by course ID +// @Description Retrieve lesson data by course ID +// @Tags lesson +// @Produce json +// @Param id path string true "Course ID" +// @Success 200 {object} web.BaseResponse "Success" +// @Failure 400 {object} web.BaseResponse "Invalid ID provided in request path" +// @Failure 401 {object} web.BaseResponse "Unauthorized" +// @Failure 500 {object} web.BaseResponse "Internal Server Error" +// @Router /lesson/course/{id} [get] +func (l LessonHandlerImpl) GetLessonsByCourse(w http.ResponseWriter, r *http.Request) { + payload := lesson.GetByStringRequestPayload{} + + payload.ID = chi.URLParam(r, "id") + packet, err := l.LessonService.GetLessons(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + l.Logger.Error( + fmt.Sprintf("[LESSON] some error happened when validating URL: %s", err.Error()), + ) + payload := l.WrapperUtil.ErrorResponseWrap("internal server error", nil) + l.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := l.WrapperUtil.SuccessResponseWrap(packet) + l.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/lesson/handler.go b/handler/lesson/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..874c8ea2727ebf3075a106c7b57eb6dad29ca08e --- /dev/null +++ b/handler/lesson/handler.go @@ -0,0 +1,17 @@ +package lesson + +import ( + r "gitlab.informatika.org/ocw/ocw-backend/repository/lesson" + "gitlab.informatika.org/ocw/ocw-backend/service/lesson" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" + "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" +) + +type LessonHandlerImpl struct { + r.LessonRepository + lesson.LessonService + httputil.HttpUtil + wrapper.WrapperUtil + logger.Logger +} \ No newline at end of file diff --git a/handler/lesson/types.go b/handler/lesson/types.go new file mode 100644 index 0000000000000000000000000000000000000000..f04fef9df33be6d8ab41685f97aec022cbb27e07 --- /dev/null +++ b/handler/lesson/types.go @@ -0,0 +1,23 @@ +package lesson + +import "net/http" + +type LessonHandler interface { + // Get + GetLesson(w http.ResponseWriter, r *http.Request) + GetLessonsByCourse(w http.ResponseWriter, r *http.Request) + GetLessonMaterial(w http.ResponseWriter, r *http.Request) + GetLessonMaterialsByLesson(w http.ResponseWriter, r *http.Request) + + // Add (Put) + AddLesson(w http.ResponseWriter, r *http.Request) + AddLessonMaterial(w http.ResponseWriter, r *http.Request) + + // Update + UpdateLesson(w http.ResponseWriter, r *http.Request) + UpdateLessonMaterial(w http.ResponseWriter, r *http.Request) + + // Delete + DeleteLesson(w http.ResponseWriter, r *http.Request) + DeleteLessonMaterial(w http.ResponseWriter, r *http.Request) +} diff --git a/handler/lesson/updateLesson.go b/handler/lesson/updateLesson.go new file mode 100644 index 0000000000000000000000000000000000000000..5638be670ce3be0b077dd6b2a9a6fbcee91b6381 --- /dev/null +++ b/handler/lesson/updateLesson.go @@ -0,0 +1,114 @@ +package lesson + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson" +) + +// Index godoc +// +// @Summary Update a lesson +// @Description Update a lesson with the given ID +// @Tags lesson +// @Param id path string true "Lesson ID (UUID)" +// @Param payload body lesson.UpdateLessonRequestPayload true "Update Lesson Payload" +// @Param Authorization header string true "UpdateLessonToken" +// @Success 200 {object} web.BaseResponse "Success" +// @Failure 400 {object} web.BaseResponse "Bad Request" +// @Failure 401 {object} web.BaseResponse "Unauthorized" +// @Failure 403 {object} web.BaseResponse "Forbidden" +// @Failure 422 {object} web.BaseResponse "Unprocessable Entity" +// @Failure 500 {object} web.BaseResponse "Internal Server Error" +// @Router /lesson/{id} [patch] +func (l LessonHandlerImpl) UpdateLesson(w http.ResponseWriter, r *http.Request) { + payload := lesson.UpdateLessonRequestPayload{} + validate := validator.New() + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := l.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + l.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := l.HttpUtil.ParseJson(r, &payload); err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + l.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := l.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := l.WrapperUtil.ErrorResponseWrap("token is required", nil) + l.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + id, err := uuid.Parse(chi.URLParam(r, "id")) + + if err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid id", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.UpdateLessonToken = token[1] + payload.ID = id + err = l.LessonService.UpdateLesson(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + l.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := l.WrapperUtil.ErrorResponseWrap("internal server error", nil) + l.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := l.WrapperUtil.SuccessResponseWrap(nil) + l.HttpUtil.WriteSuccessJson(w, responsePayload) +} + + \ No newline at end of file diff --git a/handler/lesson/updateLessonMaterial.go b/handler/lesson/updateLessonMaterial.go new file mode 100644 index 0000000000000000000000000000000000000000..bb3689db902d9918de59840e38d76777d02b9543 --- /dev/null +++ b/handler/lesson/updateLessonMaterial.go @@ -0,0 +1,114 @@ +package lesson + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson/materials" +) + +// Index godoc +// +// @Summary Update a lesson material +// @Description Update a lesson material with the given ID +// @Tags lesson +// @Param id path string true "Lesson Material ID (UUID)" +// @Param payload body materials.UpdateLessonMaterialsRequestPayload true "Update Lesson Materials Payload" +// @Param Authorization header string true "UpdateLessonMaterialsToken" +// @Success 200 {object} web.BaseResponse "Success" +// @Failure 400 {object} web.BaseResponse "Bad Request" +// @Failure 401 {object} web.BaseResponse "Unauthorized" +// @Failure 403 {object} web.BaseResponse "Forbidden" +// @Failure 422 {object} web.BaseResponse "Unprocessable Entity" +// @Failure 500 {object} web.BaseResponse "Internal Server Error" +// @Router /lesson/{id} [patch] +func (l LessonHandlerImpl) UpdateLessonMaterial(w http.ResponseWriter, r *http.Request) { + payload := materials.UpdateLessonMaterialsRequestPayload{} + validate := validator.New() + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := l.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + l.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := l.HttpUtil.ParseJson(r, &payload); err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + l.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := l.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := l.WrapperUtil.ErrorResponseWrap("token is required", nil) + l.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := l.WrapperUtil.ErrorResponseWrap("invalid token", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + id, err := uuid.Parse(chi.URLParam(r, "id")) + + if err != nil { + payload := l.WrapperUtil.ErrorResponseWrap("invalid id", nil) + l.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.UpdateLessonMaterialsToken = token[1] + payload.ID = id + err = l.LessonService.UpdateLessonMaterial(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := l.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + l.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + l.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := l.WrapperUtil.ErrorResponseWrap("internal server error", nil) + l.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := l.WrapperUtil.SuccessResponseWrap(nil) + l.HttpUtil.WriteSuccessJson(w, responsePayload) +} + + \ No newline at end of file diff --git a/handler/material/create_material.go b/handler/material/create_material.go index 55565566908a318265a647ba142e3047d875a003..d0c0e83768c76dbb309ade648ad685ab9c09605b 100644 --- a/handler/material/create_material.go +++ b/handler/material/create_material.go @@ -82,6 +82,7 @@ func (m MaterialHandlerImpl) CreateMaterial(w http.ResponseWriter, r *http.Reque courseId, user.Email, payload.Name, + payload.Week, ) if err != nil { diff --git a/handler/quiz/delete.go b/handler/quiz/delete.go new file mode 100644 index 0000000000000000000000000000000000000000..642e1661fa478b38a3b591b74c616acd0e4d6b26 --- /dev/null +++ b/handler/quiz/delete.go @@ -0,0 +1,86 @@ +package quiz + +import ( + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web/quiz" + "gitlab.informatika.org/ocw/ocw-backend/model/web" +) + +// Index godoc +// +// @Tags quiz +// @Summary Delete Quiz +// @Description Delete Quiz +// @Produce json +// @Accept json +// @Param id path string true "Quiz id" Format(uuid) +// @Success 200 {object} web.BaseResponse +// @Router /quiz/{id} [delete] +func (m QuizHandlerImpl) DeleteQuiz(w http.ResponseWriter, r *http.Request) { + payload := quiz.DeleteRequestPayload{} + quizId := chi.URLParam(r, "id") + + if quizId == "" { + payload := m.WrapperUtil.ErrorResponseWrap("quiz id is required", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + id, err := uuid.Parse(quizId) + + if err != nil { + // invalid uuid + payload := m.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := m.WrapperUtil.ErrorResponseWrap("token is required", nil) + m.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := m.WrapperUtil.ErrorResponseWrap("invalid token", nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := m.WrapperUtil.ErrorResponseWrap("invalid token", nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.DeleteToken = token[1] + payload.ID = id + + err = m.QuizService.DeleteQuiz(payload) + + if err != nil { + respErr, ok := err.(web.ResponseError) + if ok { + payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr) + + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil) + m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := m.WrapperUtil.SuccessResponseWrap(nil) + m.HttpUtil.WriteSuccessJson(w, responsePayload) + +} \ No newline at end of file diff --git a/handler/quiz/get.go b/handler/quiz/get.go index 799ca822a991240a0c6febccb29640720f1b5e67..45918bc40487c4c9c1cdaa091c5a90052e8c8bfb 100644 --- a/handler/quiz/get.go +++ b/handler/quiz/get.go @@ -2,10 +2,12 @@ package quiz import ( "net/http" + "strings" "github.com/go-chi/chi/v5" "github.com/google/uuid" "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/quiz" ) // Index godoc @@ -55,3 +57,77 @@ func (m QuizHandlerImpl) GetQuizDetail(w http.ResponseWriter, r *http.Request) { m.HttpUtil.WriteSuccessJson(w, responsePayload) } + +// Index godoc +// +// @Tags quiz +// @Summary Get Quiz Link +// @Description Get Quiz Link +// @Produce json +// @Accept json +// @Param id path string true "Quiz id" Format(uuid) +// @Success 200 {object} web.BaseResponse +// @Router /quiz/link/{id} [get] + +func (m QuizHandlerImpl) GetQuizLink(w http.ResponseWriter, r *http.Request) { + payload := quiz.GetRequestPayload{} + quizId := chi.URLParam(r, "id") + + if quizId == "" { + payload := m.WrapperUtil.ErrorResponseWrap("quiz id is required", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + id, err := uuid.Parse(quizId) + + if err != nil { + // invalid uuid + payload := m.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := m.WrapperUtil.ErrorResponseWrap("token is required", nil) + m.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := m.WrapperUtil.ErrorResponseWrap("invalid token", nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := m.WrapperUtil.ErrorResponseWrap("invalid token", nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.GetToken = token[1] + payload.ID = id + response, err := m.QuizService.GetQuizLink(payload) + + if err != nil { + respErr, ok := err.(web.ResponseError) + if ok { + payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr) + + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil) + m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := m.WrapperUtil.SuccessResponseWrap(response) + m.HttpUtil.WriteSuccessJson(w, responsePayload) +} diff --git a/handler/quiz/management.go b/handler/quiz/management.go new file mode 100644 index 0000000000000000000000000000000000000000..c53a7312c94af88b20b73d4d32a1bd3be8c9ac99 --- /dev/null +++ b/handler/quiz/management.go @@ -0,0 +1,171 @@ +package quiz + +import ( + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/quiz" +) + +// Index godoc +// +// @Tags quiz +// @Summary New Quiz +// @Description New Quiz +// @Produce json +// @Accept json +// @Param id path string true "Quiz id" Format(uuid) +// @Param data body quiz.AddQuizRequestPayload true "Add Quiz payload" +// @Success 200 {object} web.BaseResponse +// @Router /quiz [put] +func (m QuizHandlerImpl) NewQuiz(w http.ResponseWriter, r *http.Request) { + payload := quiz.AddQuizRequestPayload{} + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := m.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := m.HttpUtil.ParseJson(r, &payload); err != nil { + payload := m.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + m.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + validate := validator.New() + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := m.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := m.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := m.WrapperUtil.ErrorResponseWrap("token is required", nil) + m.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := m.WrapperUtil.ErrorResponseWrap("invalid token", nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := m.WrapperUtil.ErrorResponseWrap("invalid token", nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.AddQuizToken = token[1] + + response, err := m.QuizService.NewQuiz(payload) + + if err != nil { + respErr, ok := err.(web.ResponseError) + if ok { + payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr) + + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil) + m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := m.WrapperUtil.SuccessResponseWrap(response) + m.HttpUtil.WriteSuccessJson(w, responsePayload) +} + +// Index godoc +// +// @Tags quiz +// @Summary Update Quiz +// @Description Update Quiz +// @Produce json +// @Accept json +// @Param id path string true "Quiz id" Format(uuid) +// @Param data body quiz.UpdateQuizRequestPayload true "Update Quiz payload" +// @Success 200 {object} web.BaseResponse +// @Router /quiz/{id} [patch] +func (m QuizHandlerImpl) UpdateQuiz(w http.ResponseWriter, r *http.Request) { + payload := quiz.UpdateQuizRequestPayload{} + quizId := chi.URLParam(r, "id") + + if quizId == "" { + payload := m.WrapperUtil.ErrorResponseWrap("quiz id is required", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + id, err := uuid.Parse(quizId) + + if err != nil { + // invalid uuid + payload := m.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := m.WrapperUtil.ErrorResponseWrap("token is required", nil) + m.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := m.WrapperUtil.ErrorResponseWrap("invalid token", nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := m.WrapperUtil.ErrorResponseWrap("invalid token", nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.UpdateQuizToken = token[1] + payload.ID = id + response, err := m.QuizService.UpdateQuiz(payload) + + if err != nil { + respErr, ok := err.(web.ResponseError) + if ok { + payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr) + + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil) + m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := m.WrapperUtil.SuccessResponseWrap(response) + m.HttpUtil.WriteSuccessJson(w, responsePayload) +} diff --git a/handler/quiz/take.go b/handler/quiz/take.go index 530f4f77823001b1a5027b76d610b8c9ccafcaad..553c6ab6377efbf9ce218a5bd0e9fac1a2d9bc62 100644 --- a/handler/quiz/take.go +++ b/handler/quiz/take.go @@ -104,7 +104,7 @@ func (m QuizHandlerImpl) GetQuizSolution(w http.ResponseWriter, r *http.Request) return } - detail, err := m.GetSolutionQuiz(r.Context(), quizId, user.Email) + detail, err := m.GetSolutionQuiz(r.Context(), quizId, user) if err != nil { respErr, ok := err.(web.ResponseError) @@ -138,7 +138,7 @@ func (m QuizHandlerImpl) GetQuizSolution(w http.ResponseWriter, r *http.Request) // @Param data body quiz.FinishQuizPayload true "Quiz Finish payload" // @Param id path string true "Quiz id" Format(uuid) // @Success 200 {object} web.BaseResponse{data=quiz.QuizDetail} -// @Router /quiz/{id}/take [post] +// @Router /quiz/{id}/finish [post] func (m QuizHandlerImpl) FinishQuiz(w http.ResponseWriter, r *http.Request) { payload := quiz.FinishQuizPayload{} diff --git a/handler/quiz/type.go b/handler/quiz/type.go index 8c3bf6f5567cf7f04be9e42b67558279a0f6159d..4d0e1dc6c900d3d431a1e686de459047b1da188f 100644 --- a/handler/quiz/type.go +++ b/handler/quiz/type.go @@ -9,4 +9,8 @@ type QuizHandler interface { TakeQuiz(w http.ResponseWriter, r *http.Request) GetQuizSolution(w http.ResponseWriter, r *http.Request) FinishQuiz(w http.ResponseWriter, r *http.Request) + NewQuiz(w http.ResponseWriter, r *http.Request) + GetQuizLink(w http.ResponseWriter, r *http.Request) + UpdateQuiz(w http.ResponseWriter, r *http.Request) + DeleteQuiz(w http.ResponseWriter, r *http.Request) } diff --git a/model/domain/course/course.go b/model/domain/course/course.go index 17d592f2301d2bd0097a7fb465e5b8474b46cc09..f57db686acab2cc98221a5bc9cebf2ae85161843 100644 --- a/model/domain/course/course.go +++ b/model/domain/course/course.go @@ -13,7 +13,7 @@ type Major struct { ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"` Name string `json:"name"` Fac_id uuid.UUID `json:"fac_id" gorm:"type:uuid"` - Faculty *Faculty `json:"faculty" gorm:"foreignKey:Fac_id"` + Faculty *Faculty `json:"-" gorm:"foreignKey:Fac_id"` Abbreviation string `json:"abbreviation"` } @@ -21,7 +21,7 @@ type Course struct { ID string `json:"id" gorm:"primaryKey"` Name string `json:"name"` Major_id uuid.UUID `json:"major_id" gorm:"type:uuid"` - Major *Major `json:"major" gorm:"foreignKey:Major_id"` + Major *Major `json:"-" gorm:"foreignKey:Major_id"` Description string `json:"description"` Email string `json:"email"` Abbreviation string `json:"abbreviation"` diff --git a/model/domain/lesson/lesson.go b/model/domain/lesson/lesson.go new file mode 100644 index 0000000000000000000000000000000000000000..621295e7dd5bbf7116654a5fef038cca7981e6ab --- /dev/null +++ b/model/domain/lesson/lesson.go @@ -0,0 +1,34 @@ +package lesson + +import ( + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/course" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/material" +) + +type Lesson struct { + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"` + Name string `json:"name"` + CourseID string `json:"course_id"` + Course *course.Course `json:"-" gorm:"foreignKey:CourseID"` + Order int `json:"order"` + Description string `json:"description"` +} + +type LessonMaterials struct { + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"` + LessonID uuid.UUID `json:"lesson_id"` + Lesson *Lesson `json:"-" gorm:"foreignKey:LessonID"` + Order int `json:"order"` + MaterialID uuid.UUID `json:"material_id"` + Material *material.Material `json:"-" gorm:"foreignKey:MaterialID"` + Contents string `json:"contents"` +} + +func (Lesson) TableName() string { + return "lesson" +} + +func (LessonMaterials) TableName() string { + return "lesson_materials" +} diff --git a/model/web/course/faculty/request.go b/model/web/course/faculty/request.go index 1c08bddf6fc2410cc7bdec8542c9edc17f5a95e9..9267a5f25f5d83b303ddc548547a0cf6c8c8cfc4 100644 --- a/model/web/course/faculty/request.go +++ b/model/web/course/faculty/request.go @@ -22,7 +22,7 @@ type UpdateFacultyRequestPayload struct { UpdateFacultyToken string // Faculty ID, Provided by Query - ID uuid.UUID + ID uuid.UUID `json:"-"` // Faculty Name Name string `json:"name" validate:"required"` diff --git a/model/web/course/major/request.go b/model/web/course/major/request.go index 53bc733584b8c437905b7bf7ef9d3a636866262b..497db3bfb12e13762480c7a0df312f98f7d32875 100644 --- a/model/web/course/major/request.go +++ b/model/web/course/major/request.go @@ -29,7 +29,7 @@ type UpdateMajorRequestPayload struct { UpdateMajorToken string // Major ID, provided by query - ID uuid.UUID + ID uuid.UUID `json:"-"` // Major Name Name string `json:"name" validate:"required"` diff --git a/model/web/course/request.go b/model/web/course/request.go index 0fad2befd53b36b1722fa4d295123606277841e3..e1f84ca34394ce1fadf7f0c45a4f35cf3dbc8724 100644 --- a/model/web/course/request.go +++ b/model/web/course/request.go @@ -27,7 +27,10 @@ type AddCourseRequestPayload struct { Email string `json:"email" validate:"required,email" example:"someone@example.com"` // Course Name Abbreviation - Abbreviation string `json:"abbreviation" validate:"required"` + Abbreviation string `json:"abbreviation"` + + // Course Lecturer Name + Lecturer string `json:"lecturer"` } // DeleteCourse Request Payload @@ -37,22 +40,21 @@ type DeleteByStringRequestPayload struct { DeleteCourseToken string // Course ID, provided by query - ID string + ID string `json:"-" validate:"required"` } - // GetID Request Payload // @Description Information that should be available when you get using course id (string) type GetByStringRequestPayload struct { // Course ID, provided by query - ID string + ID string `json:"-" validate:"required"` } // GetUUID Request Payload // @Description Information that should be available when you get using major/faculty id (string) type GetByUUIDRequestPayload struct { // Major/Faculty ID, provided by query - ID uuid.UUID + ID uuid.UUID `json:"-" validate:"required"` } // UpdateCourse Request Payload @@ -62,7 +64,7 @@ type UpdateCourseRequestPayload struct { UpdateCourseToken string // Course ID, Provided by query - ID string `json:"id"` + ID string `json:"-"` // Course Name Name string `json:"name" validate:"required"` @@ -80,10 +82,8 @@ type UpdateCourseRequestPayload struct { Email string `json:"email" validate:"required,email" example:"someone@example.com"` // Course Name Abbreviation - Abbreviation string `json:"abbreviation" validate:"required"` + Abbreviation string `json:"abbreviation"` - // Course Lecturer - Lecturer string `json:"lecturer" validate:"required"` + // Course Lecturer Name + Lecturer string `json:"lecturer"` } - - diff --git a/model/web/error_code.go b/model/web/error_code.go index 9b29b2d14e9caac4d013f7474592e25ff70b66d5..f150f3f1bf7d5211713144d3c5bfaaac3a11c618 100644 --- a/model/web/error_code.go +++ b/model/web/error_code.go @@ -12,7 +12,10 @@ const ( FacultyNotExist string = "FACULTY_NOT_EXIST" MajorNotExist string = "MAJOR_NOT_EXIST" CourseNotExist string = "COURSE_NOT_EXIST" + LessonNotExist string = "LESSON_NOT_EXIST" + LessonMaterialNotExist string = "LESSON_MATERIAL_NOT_EXIST" IDExists string = "ID_ALREADY_EXISTS" + NotExist string = "NOT_EXIST" TokenError string = "TOKEN_ERROR" ) diff --git a/model/web/lesson/materials/request.go b/model/web/lesson/materials/request.go new file mode 100644 index 0000000000000000000000000000000000000000..b20dcbb537886cd4704dfa401ffd695da77196ef --- /dev/null +++ b/model/web/lesson/materials/request.go @@ -0,0 +1,63 @@ +package materials + +import ( + "github.com/google/uuid" +) + +// AddLessonMaterials Request Payload +// @Description Information that should be available when you add a lesson material +type AddLessonMaterialsRequestPayload struct { + // Web Token that was appended to the link + AddLessonMaterialsToken string + + // Lesson ID + LessonID uuid.UUID `json:"lesson_id" validate:"required"` + + // Lesson Material Order + Order int `json:"order" validate:"required"` + + // Lesson Material ID, optional + MaterialID uuid.UUID `json:"material_id"` + + // Lesson Contents + Contents string `json:"contents" validate:"required"` +} + +// DeleteLessonMaterials Request Payload +// @Description Information that should be available when you delete using lesson material id (uuid) +type DeleteByUUIDRequestPayload struct { + // Web Token that was appended to the link + DeleteLessonMaterialsToken string + + // Lesson Material ID, provided by query + ID uuid.UUID `json:"-" validate:"required"` +} + +// UpdateLessonMaterials Request Payload +// @Description Information that should be available when you update a lesson material +type UpdateLessonMaterialsRequestPayload struct { + // Web Token that was appended to the link + UpdateLessonMaterialsToken string + + // Lesson Material ID, provided by query + ID uuid.UUID `json:"-" validate:"required"` + + // Lesson ID + LessonID uuid.UUID `json:"lesson_id" validate:"required"` + + // Lesson Material Order + Order int `json:"order" validate:"required"` + + // Lesson Material ID, optional + MaterialID uuid.UUID `json:"material_id"` + + // Lesson Contents + Contents string `json:"contents" validate:"required"` +} + +// GetUUID Request Payload +// @Description Information that should be available when you get using id or lesson id (uuid) +type GetByUUIDRequestPayload struct { + // Lesson ID, provided by query + ID uuid.UUID `json:"-" validate:"required"` +} \ No newline at end of file diff --git a/model/web/lesson/request.go b/model/web/lesson/request.go new file mode 100644 index 0000000000000000000000000000000000000000..4a999f7d45320e58d51a3bf4bbd18947f9a5c991 --- /dev/null +++ b/model/web/lesson/request.go @@ -0,0 +1,68 @@ +package lesson + +import "github.com/google/uuid" + +// AddLesson Request Payload +// @Description Information that should be available when you add a lesson +type AddLessonRequestPayload struct { + // Web Token that was appended to the link + AddLessonToken string + + // Lesson Name + Name string `json:"name" validate:"required"` + + // Course ID + CourseID string `json:"course_id" validate:"required"` + + // Lesson Order + Order int `json:"order" validate:"required"` + + // Lesson Description (Can be left empty) + Description string `json:"description"` +} + +// DeleteLesson Request Payload +// @Description Information that should be available when you delete using lesson id (uuid) +type DeleteByUUIDRequestPayload struct { + // Web Token that was appended to the link + DeleteLessonToken string + + // Lesson ID, provided by query + ID uuid.UUID `json:"-" validate:"required"` +} + +// UpdateLesson Request Payload +// @Description Information that should be available when you update a lesson +type UpdateLessonRequestPayload struct { + // Web Token that was appended to the link + UpdateLessonToken string + + // Lesson ID, provided by query + ID uuid.UUID `json:"-" validate:"required"` + + // Lesson Name + Name string `json:"name" validate:"required"` + + // Course ID + CourseID string `json:"course_id" validate:"required"` + + // Lesson Order + Order int `json:"order" validate:"required"` + + // Lesson Description (Can be left empty) + Description string `json:"description"` +} + +// GetUUID Request Payload +// @Description Information that should be available when you get using lesson id (uuid) +type GetByUUIDRequestPayload struct { + // Lesson ID, provided by query + ID uuid.UUID `json:"-" validate:"required"` +} + +// GetID Request Payload +// @Description Information that should be available when you get using course id (string) +type GetByStringRequestPayload struct { + // Course ID, provided by query + ID string `json:"-" validate:"required"` +} diff --git a/model/web/material/material.go b/model/web/material/material.go index 98f8c6cdae84025cf0079c2df683a3be1767420b..84da097087fd4201689587c35d8f0e962865eca5 100644 --- a/model/web/material/material.go +++ b/model/web/material/material.go @@ -4,6 +4,7 @@ import "github.com/google/uuid" type CreateMaterialRequest struct { Name string `json:"name" validate:"required"` + Week int `json:"week" validate:"required"` } type CreateMaterialResponse struct { diff --git a/model/web/quiz/request.go b/model/web/quiz/request.go new file mode 100644 index 0000000000000000000000000000000000000000..634967c6121796140dae0868916e93ca6439edbc --- /dev/null +++ b/model/web/quiz/request.go @@ -0,0 +1,66 @@ +package quiz + +import ( + "github.com/google/uuid" +) + +// AddQuiz Request Payload +// +// @Description Information that should be available when you add a quiz +type AddQuizRequestPayload struct { + // Web Token that was appended to the link + AddQuizToken string + + // Quiz Name + Name string `json:"name" validate:"required"` + + // Course ID + CourseID string `json:"course_id" validate:"required"` +} + +// UpdateQuiz Request Payload +// +// @Description Information that should be available when you update a quiz +type UpdateQuizRequestPayload struct { + // Web Token that was appended to the link + UpdateQuizToken string + + // Quiz ID, Set by param + ID uuid.UUID `json:"id"` +} + +// DeleteQuiz Request Payload +// +// @Description Information that should be available when you delete using uuid +type DeleteRequestPayload struct { + // Web Token that was appended to the link + DeleteToken string + + // Quiz ID, Set by param + ID uuid.UUID +} + +// GetUUID Request Payload +// +// @Description Information that should be available when you get using uuid +type GetRequestPayload struct { + // Web Token that was appended to the link + GetToken string + + // Quiz/Problem/Answer ID, provided by query + ID uuid.UUID +} + +// Link Response Payload +// +// @Description Information that you will get upon successful add/update request +type LinkResponse struct { + UploadLink string `json:"upload_link"` +} + +// Path Response Payload +// +// @Description Information that you will get upon successful get request +type PathResponse struct { + Path string `json:"path"` +} \ No newline at end of file diff --git a/provider/mail/smtp/smtp.go b/provider/mail/smtp/smtp.go index cc1d4060a460ed00840c8f52072ee2ee7b20234f..0dacbabc2f84bf2effb400ce67e9ec46ab397e1c 100644 --- a/provider/mail/smtp/smtp.go +++ b/provider/mail/smtp/smtp.go @@ -13,10 +13,20 @@ type SmtpMailProvider struct { } func New(env *env.Environment) *SmtpMailProvider { - auth := smtp.CRAMMD5Auth( - env.SmtpUsername, - env.SmtpPassword, - ) + var auth smtp.Auth + if env.SmtpAuthType == "CRAM" { + auth = smtp.CRAMMD5Auth( + env.SmtpUsername, + env.SmtpPassword, + ) + } else if env.SmtpAuthType == "plain" { + auth = smtp.PlainAuth( + env.SmtpIdentity, + env.SmtpUsername, + env.SmtpPassword, + env.SmtpServer, + ) + } return &SmtpMailProvider{ Environment: env, diff --git a/provider/redis/cache.go b/provider/redis/cache.go index 9d656ae2093191e41cdd8666e7f20f5b2c185a92..729c3e4f82cffd75685eb44160f5754695da93e9 100644 --- a/provider/redis/cache.go +++ b/provider/redis/cache.go @@ -43,23 +43,29 @@ func NewRedisEnv( IdleTimeout: 240 * time.Second, Dial: func() (redis.Conn, error) { defer resolver(log) - conn, err := redis.Dial("tcp", env.RedisConnection+":"+env.RedisPort) - if err != nil { - log.Warning("failed connect to redis server: tcp " + env.RedisConnection + ":" + env.RedisPort) - log.Warning(err.Error()) - - return nil, err + dialOptions := []redis.DialOption{ + redis.DialUseTLS(env.RedisUseTLS), } if env.RedisUseAuth { - if _, err := conn.Do("AUTH", env.RedisUsername, env.RedisPassword); err != nil { - conn.Close() + dialOptions = append(dialOptions, + redis.DialUsername(env.RedisUsername), + redis.DialPassword(env.RedisPassword), + ) + } - log.Warning("failed connect to redis server: authentication failed") + conn, err := redis.Dial( + "tcp", + env.RedisConnection+":"+env.RedisPort, + dialOptions..., + ) - return nil, err - } + if err != nil { + log.Warning("failed connect to redis server: tcp " + env.RedisConnection + ":" + env.RedisPort) + log.Warning(err.Error()) + + return nil, err } return conn, err diff --git a/provider/storage/manager.go b/provider/storage/manager.go index cfeae57144fe4d23729a2382984360253e5d4659..6cf93f7291323ebb26246670dcafd9978aeda76f 100644 --- a/provider/storage/manager.go +++ b/provider/storage/manager.go @@ -1,7 +1,10 @@ package storage import ( + "bufio" + "bytes" "context" + "io" "github.com/minio/minio-go/v7" ) @@ -18,7 +21,15 @@ func (s S3Storage) Get(ctx context.Context, path string) ([]byte, error) { return result, err } - _, err = obj.Read(result) + var buffer bytes.Buffer + writter := bufio.NewWriter(&buffer) + + _, err = io.Copy(writter, obj) + if err != nil { + return result, err + } + + result = buffer.Bytes() return result, err } diff --git a/repository/di.go b/repository/di.go index b564bd5406f7d94427c5c0f80ccc518bc23fd5b2..9a418dca489cf4056d66d76510015e9f79f0b3cd 100644 --- a/repository/di.go +++ b/repository/di.go @@ -5,6 +5,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/repository/cache" "gitlab.informatika.org/ocw/ocw-backend/repository/content" "gitlab.informatika.org/ocw/ocw-backend/repository/course" + "gitlab.informatika.org/ocw/ocw-backend/repository/lesson" "gitlab.informatika.org/ocw/ocw-backend/repository/material" "gitlab.informatika.org/ocw/ocw-backend/repository/quiz" "gitlab.informatika.org/ocw/ocw-backend/repository/transaction" @@ -24,6 +25,13 @@ var RepositoryBasicSet = wire.NewSet( cache.New, wire.Bind(new(cache.CacheRepository), new(*cache.CacheRepositoryImpl)), + // Lesson Repository + lesson.NewLesson, + lesson.NewLessonMaterials, + wire.Bind(new(lesson.LessonRepository), new(*lesson.LessonRepositoryImpl)), + wire.Bind(new(lesson.LessonMaterialsRepository), new(*lesson.LessonMaterialsRepositoryImpl)), + + // Material Repository material.NewMaterial, material.NewMaterialContent, diff --git a/repository/lesson/lesson.go b/repository/lesson/lesson.go new file mode 100644 index 0000000000000000000000000000000000000000..e3e1002320c10381a8bad1386da2f3e78eb04c22 --- /dev/null +++ b/repository/lesson/lesson.go @@ -0,0 +1,52 @@ +package lesson + +import ( + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/lesson" + "gitlab.informatika.org/ocw/ocw-backend/provider/db" + "gorm.io/gorm" +) + +type LessonRepositoryImpl struct { + db *gorm.DB +} + +func NewLesson( + db db.Database, +) *LessonRepositoryImpl { + return &LessonRepositoryImpl{db.Connect()} +} + +func (repo LessonRepositoryImpl) GetLesson(id uuid.UUID) (*lesson.Lesson, error) { + result := &lesson.Lesson{} + err := repo.db.First(result, "id = ?", id).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo LessonRepositoryImpl) GetLessons(courseId string) ([]lesson.Lesson, error) { + var result []lesson.Lesson + err := repo.db.Where("course_id = ?", courseId).Find(&result).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo LessonRepositoryImpl) AddLesson(lesson lesson.Lesson) error { + return repo.db.Create(lesson).Error +} + +func (repo LessonRepositoryImpl) UpdateLesson(lesson lesson.Lesson) error { + return repo.db.Save(lesson).Error +} + +func (repo LessonRepositoryImpl) DeleteLesson(id uuid.UUID) error { + return repo.db.Delete(&lesson.Lesson{}, id).Error +} diff --git a/repository/lesson/materials.go b/repository/lesson/materials.go new file mode 100644 index 0000000000000000000000000000000000000000..d0e5ea70a634f4283a8c164089013f5249c55ff7 --- /dev/null +++ b/repository/lesson/materials.go @@ -0,0 +1,52 @@ +package lesson + +import ( + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/lesson" + "gitlab.informatika.org/ocw/ocw-backend/provider/db" + "gorm.io/gorm" +) + +type LessonMaterialsRepositoryImpl struct { + db *gorm.DB +} + +func NewLessonMaterials( + db db.Database, +) *LessonMaterialsRepositoryImpl { + return &LessonMaterialsRepositoryImpl{db.Connect()} +} + +func (repo LessonMaterialsRepositoryImpl) GetLessonMaterial(id uuid.UUID) (*lesson.LessonMaterials, error) { + result := &lesson.LessonMaterials{} + err := repo.db.First(result, "id = ?", id).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo LessonMaterialsRepositoryImpl) GetLessonMaterials(lessonId uuid.UUID) ([]lesson.LessonMaterials, error) { + var result []lesson.LessonMaterials + err := repo.db.Where("lesson_id = ?", lessonId).Find(&result).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo LessonMaterialsRepositoryImpl) AddLessonMaterial(lessonMaterial lesson.LessonMaterials) error { + return repo.db.Create(lessonMaterial).Error +} + +func (repo LessonMaterialsRepositoryImpl) UpdateLessonMaterial(lessonMaterial lesson.LessonMaterials) error { + return repo.db.Save(lessonMaterial).Error +} + +func (repo LessonMaterialsRepositoryImpl) DeleteLessonMaterial(id uuid.UUID) error { + return repo.db.Delete(&lesson.LessonMaterials{}, id).Error +} diff --git a/repository/lesson/type.go b/repository/lesson/type.go new file mode 100644 index 0000000000000000000000000000000000000000..4925c016cd2ed78e256a31fb5166a2c1c2ab828e --- /dev/null +++ b/repository/lesson/type.go @@ -0,0 +1,22 @@ +package lesson + +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/lesson" + "github.com/google/uuid" +) + +type LessonRepository interface { + GetLesson(id uuid.UUID) (*lesson.Lesson, error) + GetLessons(courseId string) ([]lesson.Lesson, error) + AddLesson(lesson lesson.Lesson) error + UpdateLesson(lesson lesson.Lesson) error + DeleteLesson(id uuid.UUID) error +} + +type LessonMaterialsRepository interface { + GetLessonMaterial(id uuid.UUID) (*lesson.LessonMaterials, error) + GetLessonMaterials(lessonId uuid.UUID) ([]lesson.LessonMaterials, error) + AddLessonMaterial(lessonMaterial lesson.LessonMaterials) error + UpdateLessonMaterial(lessonMaterial lesson.LessonMaterials) error + DeleteLessonMaterial(id uuid.UUID) error +} \ No newline at end of file diff --git a/repository/material/material.go b/repository/material/material.go index 87cbd435c379d505d4be048f8d1d5e878ffba019..3618779997d7381591c1c6f679b40885a8020dc6 100644 --- a/repository/material/material.go +++ b/repository/material/material.go @@ -35,16 +35,17 @@ func (m MaterialRepositoryImpl) IsUserContributor(id uuid.UUID, email string) (b return true, nil } -func (m MaterialRepositoryImpl) New(courseId string, creatorEmail string, name string) (uuid.UUID, error) { - return m.NewWithTransaction(m.builder.Build(), courseId, creatorEmail, name) +func (m MaterialRepositoryImpl) New(courseId string, creatorEmail string, name string, week int) (uuid.UUID, error) { + return m.NewWithTransaction(m.builder.Build(), courseId, creatorEmail, name, week) } -func (m MaterialRepositoryImpl) NewWithTransaction(tx transaction.Transaction, courseId string, creatorEmail string, name string) (uuid.UUID, error) { +func (m MaterialRepositoryImpl) NewWithTransaction(tx transaction.Transaction, courseId string, creatorEmail string, name string, week int) (uuid.UUID, error) { materialData := &material.Material{ ID: uuid.New(), CourseId: courseId, CreatorEmail: creatorEmail, Name: name, + Week: week, } err := tx.GetTransaction().Create(materialData).Error diff --git a/repository/material/type.go b/repository/material/type.go index 4014763d006c08c2eabeb294a8f043ae023d0b17..6e8ddc93441a80c7b16a52cf756a1e75a0cc5b52 100644 --- a/repository/material/type.go +++ b/repository/material/type.go @@ -7,14 +7,14 @@ import ( ) type MaterialRepository interface { - New(courseId string, creatorEmail string, name string) (uuid.UUID, error) + New(courseId string, creatorEmail string, name string, week int) (uuid.UUID, error) Delete(id uuid.UUID) error Get(materialId uuid.UUID) (*material.Material, error) GetAll(courseId string) ([]material.Material, error) IsUserContributor(id uuid.UUID, email string) (bool, error) - NewWithTransaction(tx transaction.Transaction, courseId string, creatorEmail string, name string) (uuid.UUID, error) + NewWithTransaction(tx transaction.Transaction, courseId string, creatorEmail string, name string, week int) (uuid.UUID, error) DeleteWithTransaction(tx transaction.Transaction, id uuid.UUID) error GetAllWithTransaction(tx transaction.Transaction, courseId string) ([]material.Material, error) } diff --git a/repository/quiz/impl.go b/repository/quiz/impl.go index e51495422f1a10911dd47bf00b1764eb541446ab..fdefa4688f0ce895e0f115a42e1f795c38a57672 100644 --- a/repository/quiz/impl.go +++ b/repository/quiz/impl.go @@ -5,6 +5,7 @@ import ( "time" "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/course" "gitlab.informatika.org/ocw/ocw-backend/model/domain/quiz" "gitlab.informatika.org/ocw/ocw-backend/model/web" "gitlab.informatika.org/ocw/ocw-backend/provider/db" @@ -39,12 +40,20 @@ func (q *QuizRepositoryImpl) GetQuizDetail(quizId uuid.UUID) (*quiz.Quiz, error) return result, nil } -func (q *QuizRepositoryImpl) UpdateScore(takeId uuid.UUID, score int) error { - return q.db. - Model(&quiz.QuizTake{}). - Update("score", score). - Update("is_finished", true). - Where("id = ?", takeId).Error +func (q *QuizRepositoryImpl) UpdateScore(email string, quizId uuid.UUID, score int) error { + tx := q.db.Begin() + defer tx.Commit() + + tx.Where("quiz_id = ? AND email = ?", quizId, email).Delete(&quiz.QuizTake{}) + + return tx. + Create(quiz.QuizTake{ + Score: score, + IsFinished: true, + QuizId: quizId, + Email: email, + Id: uuid.New(), + }).Error } func (q *QuizRepositoryImpl) NewTake(quizId uuid.UUID, userEmail string) (uuid.UUID, error) { @@ -63,19 +72,52 @@ func (q *QuizRepositoryImpl) NewTake(quizId uuid.UUID, userEmail string) (uuid.U return id, err } +func (q *QuizRepositoryImpl) IsUserContributor(id string, email string) (bool, error) { + err := q.db.Where("id = ? AND email = ?", id, email).Find(&course.Course{}).Error + + if err != nil { + return false, err + } + + return true, nil +} + +func (q *QuizRepositoryImpl) NewQuiz(quiz quiz.Quiz) error { + return q.db.Create(&quiz).Error +} + +func (q *QuizRepositoryImpl) UpdateQuiz(quiz quiz.Quiz) error { + return q.db.Save(quiz).Error +} + +func (q *QuizRepositoryImpl) GetQuizLink(quizId uuid.UUID) (string, error) { + result := &quiz.Quiz{} + err := q.db.Where("id = ?", quizId).First(result).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", web.NewResponseError("Record not found", "ERR_NOT_FOUND") + } + + return result.QuizPath, nil +} + +func (q *QuizRepositoryImpl) Delete(quizId uuid.UUID) error { + return q.db.Delete(&quiz.Quiz{}, quizId).Error +} + func (q *QuizRepositoryImpl) IsActiveTake(quizId uuid.UUID, userEmail string) (bool, error) { - result := struct{ cnt int }{} + var result int64 = 0 err := q.db. - Select("COUNT(*) as cnt"). + Model(&quiz.QuizTake{}). Where("quiz_id = ? AND email = ? AND is_finished = false", quizId, userEmail). - Find(result). + Count(&result). Error if err != nil { return false, nil } - return result.cnt > 0, nil + return result > 0, nil } func (q *QuizRepositoryImpl) GetAllTake(quizId uuid.UUID, userEmail string) ([]quiz.QuizTake, error) { @@ -93,5 +135,9 @@ func (q *QuizRepositoryImpl) GetLastTake(quizId uuid.UUID, userEmail string) (*q Where("quiz_id = ? AND email = ?", quizId, userEmail). Last(result).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return result, err } diff --git a/repository/quiz/type.go b/repository/quiz/type.go index 1b4e0cbeca391ec136dfc8c696d1c058d49e55e4..362686b7a9cc3295c05f680e074f2920afd94ea3 100644 --- a/repository/quiz/type.go +++ b/repository/quiz/type.go @@ -8,8 +8,13 @@ import ( type QuizRepository interface { GetQuizes(courseId string) ([]quiz.Quiz, error) GetQuizDetail(quizId uuid.UUID) (*quiz.Quiz, error) - UpdateScore(takeId uuid.UUID, score int) error + UpdateScore(email string, quizId uuid.UUID, score int) error NewTake(quizId uuid.UUID, userEmail string) (uuid.UUID, error) + IsUserContributor(id string, email string) (bool, error) + NewQuiz(quiz quiz.Quiz) error + UpdateQuiz(quiz quiz.Quiz) error + GetQuizLink(quizId uuid.UUID) (string, error) + Delete(quizId uuid.UUID) error IsActiveTake(quizId uuid.UUID, userEmail string) (bool, error) GetAllTake(quizId uuid.UUID, userEmail string) ([]quiz.QuizTake, error) GetLastTake(quizId uuid.UUID, userEmail string) (*quiz.QuizTake, error) diff --git a/routes/di.go b/routes/di.go index ddae40f95c5cde473aadc4f6d26a357c98b5f089..d3129d33efdd8101223d47acb66efa1cd5bd5c73 100644 --- a/routes/di.go +++ b/routes/di.go @@ -6,6 +6,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/routes/auth" "gitlab.informatika.org/ocw/ocw-backend/routes/common" "gitlab.informatika.org/ocw/ocw-backend/routes/course" + "gitlab.informatika.org/ocw/ocw-backend/routes/lesson" "gitlab.informatika.org/ocw/ocw-backend/routes/material" "gitlab.informatika.org/ocw/ocw-backend/routes/quiz" "gitlab.informatika.org/ocw/ocw-backend/routes/reset" @@ -19,6 +20,7 @@ var routesCollectionSet = wire.NewSet( wire.Struct(new(admin.AdminRoutes), "*"), wire.Struct(new(reset.ResetRoutes), "*"), wire.Struct(new(course.CourseRoutes), "*"), + wire.Struct(new(lesson.LessonRoutes), "*"), wire.Struct(new(material.MaterialRoutes), "*"), wire.Struct(new(quiz.QuizRoutes), "*"), ) diff --git a/routes/lesson/route.go b/routes/lesson/route.go new file mode 100644 index 0000000000000000000000000000000000000000..7d19720c746d2a88b4dcb5bb70079bed7bced2db --- /dev/null +++ b/routes/lesson/route.go @@ -0,0 +1,28 @@ +package lesson + +import ( + "github.com/go-chi/chi/v5" + "gitlab.informatika.org/ocw/ocw-backend/handler/lesson" +) + +type LessonRoutes struct { + lesson.LessonHandler +} + +func (l LessonRoutes) Register(r chi.Router) { + r.Route("/lesson", func(r chi.Router) { + r.Get("/{id}", l.LessonHandler.GetLesson) + r.Get("/course/{id}", l.LessonHandler.GetLessonsByCourse) + r.Get("/material/{id}", l.LessonHandler.GetLessonMaterial) + r.Get("/material/lesson/{id}", l.LessonHandler.GetLessonMaterialsByLesson) + + r.Put("/", l.LessonHandler.AddLesson) + r.Put("/material", l.LessonHandler.AddLessonMaterial) + + r.Post("/{id}", l.LessonHandler.UpdateLesson) + r.Post("/material/{id}", l.LessonHandler.UpdateLessonMaterial) + + r.Delete("/{id}", l.LessonHandler.DeleteLesson) + r.Delete("/material/{id}", l.LessonHandler.DeleteLessonMaterial) + }) +} diff --git a/routes/quiz/route.go b/routes/quiz/route.go index b89269412c9add8ccc39a190a965f187a13c15b9..d75c278d19c5ba6d8b4fa62238f8081342c5e374 100644 --- a/routes/quiz/route.go +++ b/routes/quiz/route.go @@ -14,7 +14,6 @@ type QuizRoutes struct { func (q QuizRoutes) Register(r chi.Router) { r.Get("/course/{id}/quiz", q.QuizHandler.GetAllQuizes) - r.Get("/quiz/{id}", q.QuizHandler.GetQuizDetail) guard := q.GuardBuilder.Build( user.Student, @@ -36,4 +35,23 @@ func (q QuizRoutes) Register(r chi.Router) { r.Use(guard) r.Get("/", q.QuizHandler.GetQuizSolution) }) + + r.Route("/quiz/{id}", func(r chi.Router) { + r.Get("/", q.QuizHandler.GetQuizDetail) + r.Route("/", func(r chi.Router) { + r.Use(guard) + r.Patch("/", q.QuizHandler.UpdateQuiz) + r.Delete("/", q.QuizHandler.DeleteQuiz) + }) + }) + + r.Route("/quiz", func(r chi.Router) { + r.Use(guard) + r.Put("/", q.QuizHandler.NewQuiz) + }) + + r.Route("/quiz/link/{id}", func(r chi.Router) { + r.Use(guard) + r.Get("/", q.QuizHandler.GetQuizLink) + }) } diff --git a/routes/routes.go b/routes/routes.go index 096f4d5005e2bfe01f82bfe53552edc99b1d0258..34e2036236b737b7b48fd6d1ad026925a4495681 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -5,6 +5,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/routes/auth" "gitlab.informatika.org/ocw/ocw-backend/routes/common" "gitlab.informatika.org/ocw/ocw-backend/routes/course" + "gitlab.informatika.org/ocw/ocw-backend/routes/lesson" "gitlab.informatika.org/ocw/ocw-backend/routes/material" "gitlab.informatika.org/ocw/ocw-backend/routes/quiz" "gitlab.informatika.org/ocw/ocw-backend/routes/reset" @@ -22,6 +23,7 @@ type AppRouter struct { reset.ResetRoutes quiz.QuizRoutes course.CourseRoutes + lesson.LessonRoutes material.MaterialRoutes // Utility diff --git a/service/admin/addUser.go b/service/admin/addUser.go index 93e53b0a0cccb8921ec5a00b742c6d1b00df9c93..8bcf120e393053195ff37f83cbf2486afd256745 100644 --- a/service/admin/addUser.go +++ b/service/admin/addUser.go @@ -10,11 +10,11 @@ func (as AdminServiceImpl) AddUser(payload req.AdminAddUserPayload) error { var role user.UserRole // TODO: move this - if (payload.Role == "admin") { + if payload.Role == "admin" { role = user.Admin - } else if (payload.Role == "contributor") { + } else if payload.Role == "contributor" { role = user.Contributor - } else if (payload.Role == "member") { + } else if payload.Role == "member" { role = user.Student } @@ -26,4 +26,4 @@ func (as AdminServiceImpl) AddUser(payload req.AdminAddUserPayload) error { }) return err -} \ No newline at end of file +} diff --git a/service/auth/impl.go b/service/auth/impl.go index 9ac5f343fb68b92e44a3a51491389a21d5c70c5a..5175a0a02b2b13654f7f1328a131fb97447ea432 100644 --- a/service/auth/impl.go +++ b/service/auth/impl.go @@ -2,6 +2,7 @@ package auth import ( "gitlab.informatika.org/ocw/ocw-backend/repository/user" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" "gitlab.informatika.org/ocw/ocw-backend/service/verification" "gitlab.informatika.org/ocw/ocw-backend/utils/env" "gitlab.informatika.org/ocw/ocw-backend/utils/password" @@ -14,4 +15,5 @@ type AuthServiceImpl struct { *env.Environment token.TokenUtil verification.VerificationService + logger.Logger } diff --git a/service/auth/register.go b/service/auth/register.go index b7941c50ba99c941bd7810ad82f3c3862f9ef26c..5227bd811d8e9631c09d38f19d4cfef9c364fc45 100644 --- a/service/auth/register.go +++ b/service/auth/register.go @@ -1,8 +1,11 @@ package auth import ( + "errors" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/register" + "gorm.io/gorm" ) func (auth AuthServiceImpl) Register(payload register.RegisterRequestPayload) error { @@ -20,8 +23,19 @@ func (auth AuthServiceImpl) Register(payload register.RegisterRequestPayload) er IsActivated: false, }) + if errors.Is(err, gorm.ErrDuplicatedKey) { + err := auth.SendVerifyMail(payload.Email) + if err != nil { + auth.Logger.Warning("Failed to send email: " + err.Error()) + } + return nil + } + if err == nil { - auth.SendVerifyMail(payload.Email) + err := auth.SendVerifyMail(payload.Email) + if err != nil { + auth.Logger.Warning("Failed to send email: " + err.Error()) + } } return err diff --git a/service/course/add.go b/service/course/add.go index a94a59b6e56c8d465adf46559d3877dff50b34e0..bbed9ccc8d3cf5996e5615f92a82e7b45ed62b29 100644 --- a/service/course/add.go +++ b/service/course/add.go @@ -61,6 +61,7 @@ func (c CourseServiceImpl) AddCourse(payload course.AddCourseRequestPayload) err Description: payload.Description, Email: payload.Email, Abbreviation: payload.Abbreviation, + Lecturer: payload.Lecturer, }) if err != nil { diff --git a/service/course/update.go b/service/course/update.go index 76df6c292d130c3595bbd85a1d0061c1a20ff553..757847e81512f580f8c3ebe192aa4d28e4e574e2 100644 --- a/service/course/update.go +++ b/service/course/update.go @@ -7,9 +7,9 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" "gitlab.informatika.org/ocw/ocw-backend/model/web" "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course" "gitlab.informatika.org/ocw/ocw-backend/model/web/course/faculty" "gitlab.informatika.org/ocw/ocw-backend/model/web/course/major" - "gitlab.informatika.org/ocw/ocw-backend/model/web/course" "gorm.io/gorm" ) @@ -45,13 +45,13 @@ func (c CourseServiceImpl) UpdateCourse(payload course.UpdateCourseRequestPayloa } err = c.CourseRepository.UpdateCourse(domCourse.Course{ - ID: payload.ID, - Name: payload.Name, - Major_id: payload.MajorID, - Description: payload.Description, - Email: payload.Email, + ID: payload.ID, + Name: payload.Name, + Major_id: payload.MajorID, + Description: payload.Description, + Email: payload.Email, Abbreviation: payload.Abbreviation, - Lecturer: payload.Lecturer, + Lecturer: payload.Lecturer, }) if err != nil { @@ -66,7 +66,7 @@ func (c CourseServiceImpl) UpdateCourse(payload course.UpdateCourseRequestPayloa } func (c CourseServiceImpl) UpdateMajor(payload major.UpdateMajorRequestPayload) error { - + // Validate Role claim, err := c.TokenUtil.Validate(payload.UpdateMajorToken, token.Access) @@ -94,11 +94,11 @@ func (c CourseServiceImpl) UpdateMajor(payload major.UpdateMajorRequestPayload) payload.FacultyID = faculty.ID } - + err = c.CourseRepository.UpdateMajor(domCourse.Major{ - ID: payload.ID, - Name: payload.Name, - Fac_id: payload.FacultyID, + ID: payload.ID, + Name: payload.Name, + Fac_id: payload.FacultyID, Abbreviation: payload.Abbreviation, }) @@ -126,10 +126,10 @@ func (c CourseServiceImpl) UpdateFaculty(payload faculty.UpdateFacultyRequestPay if claim.Role != user.Admin { return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) } - + err = c.CourseRepository.UpdateFaculty(domCourse.Faculty{ - ID: payload.ID, - Name: payload.Name, + ID: payload.ID, + Name: payload.Name, Abbreviation: payload.Abbreviation, }) diff --git a/service/di.go b/service/di.go index 3a133f5fc0ccdaba5c7260c52fa6a068306cf8a2..88695f660eade6c4efbaedb4ec8e9265ee283848 100644 --- a/service/di.go +++ b/service/di.go @@ -6,6 +6,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/service/auth" "gitlab.informatika.org/ocw/ocw-backend/service/common" "gitlab.informatika.org/ocw/ocw-backend/service/course" + "gitlab.informatika.org/ocw/ocw-backend/service/lesson" "gitlab.informatika.org/ocw/ocw-backend/service/logger" "gitlab.informatika.org/ocw/ocw-backend/service/logger/hooks" "gitlab.informatika.org/ocw/ocw-backend/service/material" @@ -71,6 +72,12 @@ var ServiceTestSet = wire.NewSet( wire.Struct(new(quiz.QuizServiceImpl), "*"), wire.Bind(new(quiz.QuizService), new(*quiz.QuizServiceImpl)), ), + + // Lesson Service + wire.NewSet( + wire.Struct(new(lesson.LessonServiceImpl), "*"), + wire.Bind(new(lesson.LessonService), new(*lesson.LessonServiceImpl)), + ), ) var ServiceSet = wire.NewSet( diff --git a/service/lesson/add.go b/service/lesson/add.go new file mode 100644 index 0000000000000000000000000000000000000000..9258dcb250ce93be1249a0744ceff4268caebb94 --- /dev/null +++ b/service/lesson/add.go @@ -0,0 +1,68 @@ +package lesson + +import ( + domLesson "gitlab.informatika.org/ocw/ocw-backend/model/domain/lesson" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson/materials" +) + +func (l LessonServiceImpl) AddLesson(payload lesson.AddLessonRequestPayload) error { + // Validate Role + claim, err := l.TokenUtil.Validate(payload.AddLessonToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role == user.Student { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + err = l.LessonRepository.AddLesson(domLesson.Lesson{ + Name: payload.Name, + CourseID: payload.CourseID, + Order: payload.Order, + Description: payload.Description, + }) + + if err != nil { + // Some uncaught error + return err + } + + return nil +} + +func (l LessonServiceImpl) AddLessonMaterial(payload materials.AddLessonMaterialsRequestPayload) error { + // Validate Role + claim, err := l.TokenUtil.Validate(payload.AddLessonMaterialsToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role == user.Student { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + err = l.LessonMaterialsRepository.AddLessonMaterial(domLesson.LessonMaterials{ + LessonID: payload.LessonID, + Order: payload.Order, + MaterialID: payload.MaterialID, + Contents: payload.Contents, + }) + + if err != nil { + // Some uncaught error + return err + } + + return nil +} diff --git a/service/lesson/delete.go b/service/lesson/delete.go new file mode 100644 index 0000000000000000000000000000000000000000..187b6d07aa07bf103a83b1e21639e1843066247e --- /dev/null +++ b/service/lesson/delete.go @@ -0,0 +1,57 @@ +package lesson + +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson/materials" +) + +func (l LessonServiceImpl) DeleteLesson(payload lesson.DeleteByUUIDRequestPayload) error { + // Validate Role + claim, err := l.TokenUtil.Validate(payload.DeleteLessonToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role == user.Student { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + err = l.LessonRepository.DeleteLesson(payload.ID) + + if err != nil { + // Uncaught error + return err + } + + return nil +} + +func (l LessonServiceImpl) DeleteLessonMaterial(payload materials.DeleteByUUIDRequestPayload) error { + // Validate Role + claim, err := l.TokenUtil.Validate(payload.DeleteLessonMaterialsToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role == user.Student { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + err = l.LessonMaterialsRepository.DeleteLessonMaterial(payload.ID) + + if err != nil { + // Uncaught error + return err + } + + return nil +} \ No newline at end of file diff --git a/service/lesson/get.go b/service/lesson/get.go new file mode 100644 index 0000000000000000000000000000000000000000..3b3798b2057cb51ec65c7d5a2e297de7fa16d4d5 --- /dev/null +++ b/service/lesson/get.go @@ -0,0 +1,67 @@ +package lesson + +import ( + "errors" + + domLesson "gitlab.informatika.org/ocw/ocw-backend/model/domain/lesson" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson/materials" + "gorm.io/gorm" +) + +func (l LessonServiceImpl) GetLesson(payload lesson.GetByUUIDRequestPayload) (*domLesson.Lesson, error) { + packet, err := l.LessonRepository.GetLesson(payload.ID) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.LessonNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (l LessonServiceImpl) GetLessons(payload lesson.GetByStringRequestPayload) ([]domLesson.Lesson, error) { + packet, err := l.LessonRepository.GetLessons(payload.ID) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.LessonNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (l LessonServiceImpl) GetLessonMaterial(payload materials.GetByUUIDRequestPayload) (*domLesson.LessonMaterials, error) { + packet, err := l.LessonMaterialsRepository.GetLessonMaterial(payload.ID) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.LessonMaterialNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (l LessonServiceImpl) GetLessonMaterials(payload materials.GetByUUIDRequestPayload) ([]domLesson.LessonMaterials, error) { + packet, err := l.LessonMaterialsRepository.GetLessonMaterials(payload.ID) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.LessonMaterialNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} \ No newline at end of file diff --git a/service/lesson/impl.go b/service/lesson/impl.go new file mode 100644 index 0000000000000000000000000000000000000000..835b27ac03d42afce5757b6d1efba11282f9986b --- /dev/null +++ b/service/lesson/impl.go @@ -0,0 +1,16 @@ +package lesson + +import ( + "gitlab.informatika.org/ocw/ocw-backend/repository/lesson" + "gitlab.informatika.org/ocw/ocw-backend/utils/token" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" +) + +type LessonServiceImpl struct { + lesson.LessonRepository + lesson.LessonMaterialsRepository + *env.Environment + token.TokenUtil + logger.Logger +} \ No newline at end of file diff --git a/service/lesson/type.go b/service/lesson/type.go new file mode 100644 index 0000000000000000000000000000000000000000..3cdae1a02a5eb6cf2bf5961bb2a917114736dc57 --- /dev/null +++ b/service/lesson/type.go @@ -0,0 +1,20 @@ +package lesson + +import ( + domLesson "gitlab.informatika.org/ocw/ocw-backend/model/domain/lesson" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson/materials" +) + +type LessonService interface { + GetLesson(payload lesson.GetByUUIDRequestPayload) (*domLesson.Lesson, error) + GetLessons(payload lesson.GetByStringRequestPayload) ([]domLesson.Lesson, error) + AddLesson(payload lesson.AddLessonRequestPayload) error + UpdateLesson(payload lesson.UpdateLessonRequestPayload) error + DeleteLesson(payload lesson.DeleteByUUIDRequestPayload) error + GetLessonMaterial(payload materials.GetByUUIDRequestPayload) (*domLesson.LessonMaterials, error) + GetLessonMaterials(payload materials.GetByUUIDRequestPayload) ([]domLesson.LessonMaterials, error) + AddLessonMaterial(payload materials.AddLessonMaterialsRequestPayload) error + UpdateLessonMaterial(payload materials.UpdateLessonMaterialsRequestPayload) error + DeleteLessonMaterial(payload materials.DeleteByUUIDRequestPayload) error +} \ No newline at end of file diff --git a/service/lesson/update.go b/service/lesson/update.go new file mode 100644 index 0000000000000000000000000000000000000000..4d80a4e10675c9f42a18bd153ec03c4f3487ecd0 --- /dev/null +++ b/service/lesson/update.go @@ -0,0 +1,79 @@ +package lesson + +import ( + "errors" + + domLesson "gitlab.informatika.org/ocw/ocw-backend/model/domain/lesson" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson" + "gitlab.informatika.org/ocw/ocw-backend/model/web/lesson/materials" + "gorm.io/gorm" +) + +func (l LessonServiceImpl) UpdateLesson(payload lesson.UpdateLessonRequestPayload) error { + // Validate Role + claim, err := l.TokenUtil.Validate(payload.UpdateLessonToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role == user.Student { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + err = l.LessonRepository.UpdateLesson(domLesson.Lesson{ + ID: payload.ID, + Name: payload.Name, + CourseID: payload.CourseID, + Order: payload.Order, + Description: payload.Description, + }) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseErrorFromError(err, web.LessonNotExist) + } + // Uncaught error + return err + } + + return nil +} + +func (l LessonServiceImpl) UpdateLessonMaterial(payload materials.UpdateLessonMaterialsRequestPayload) error { + // Validate Role + claim, err := l.TokenUtil.Validate(payload.UpdateLessonMaterialsToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role == user.Student { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + err = l.LessonMaterialsRepository.UpdateLessonMaterial(domLesson.LessonMaterials{ + ID: payload.ID, + LessonID: payload.LessonID, + Order: payload.Order, + MaterialID: payload.MaterialID, + Contents: payload.Contents, + }) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseErrorFromError(err, web.LessonMaterialNotExist) + } + // Uncaught error + return err + } + + return nil +} diff --git a/service/material/impl.go b/service/material/impl.go index c50b67586123fcf70acf3c2c0a4ee7f42b53034c..02a5b425925d957d4d5023cf8e3f1050c83814e5 100644 --- a/service/material/impl.go +++ b/service/material/impl.go @@ -32,14 +32,14 @@ func (m MaterialServiceImpl) GetById(materialId uuid.UUID) (*materialDomain.Mate return material, err } -func (m MaterialServiceImpl) Create(courseId string, email string, name string) (uuid.UUID, error) { +func (m MaterialServiceImpl) Create(courseId string, email string, name string, week int) (uuid.UUID, error) { isSuccess := false tx := m.TransactionBuilder.Build() tx.Begin() defer tx.Auto(&isSuccess) - id, err := m.MaterialRepository.NewWithTransaction(tx, courseId, email, name) + id, err := m.MaterialRepository.NewWithTransaction(tx, courseId, email, name, week) if err != nil { return uuid.Nil, err diff --git a/service/material/type.go b/service/material/type.go index ca617ce6fdce82217b439b2f4b1f0a0b58d3545d..9cc1f19497423d3a25cbe4ce0cee8b085b3b5e9e 100644 --- a/service/material/type.go +++ b/service/material/type.go @@ -6,7 +6,7 @@ import ( ) type MaterialService interface { - Create(courseId string, email string, name string) (uuid.UUID, error) + Create(courseId string, email string, name string, week int) (uuid.UUID, error) Delete(materialId uuid.UUID, email string) error Get(courseId string) ([]material.Material, error) GetById(materialId uuid.UUID) (*material.Material, error) diff --git a/service/quiz/impl.go b/service/quiz/impl.go index 3898cd448978c6604f71f68e7561077c9c396a00..12e0377b14e9a922e9fd1b346dc02c39c94f352a 100644 --- a/service/quiz/impl.go +++ b/service/quiz/impl.go @@ -4,19 +4,33 @@ import ( "bytes" "context" "encoding/json" + "errors" + "fmt" + "strings" "github.com/google/uuid" "gitlab.informatika.org/ocw/ocw-backend/model/domain/quiz" + userDomain "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + model "gitlab.informatika.org/ocw/ocw-backend/model/web/quiz" "gitlab.informatika.org/ocw/ocw-backend/provider/storage" quizRepo "gitlab.informatika.org/ocw/ocw-backend/repository/quiz" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + tokenUtil "gitlab.informatika.org/ocw/ocw-backend/utils/token" + "gorm.io/gorm" ) type QuizServiceImpl struct { quizRepo.QuizRepository storage.Storage + tokenUtil.TokenUtil + logger.Logger + *env.Environment } +// TODO: should be for admins, make ones for users which doesnt expose minio link func (q QuizServiceImpl) ListAllQuiz(courseId string) ([]quiz.Quiz, error) { return q.QuizRepository.GetQuizes(courseId) } @@ -76,26 +90,30 @@ func (q QuizServiceImpl) DoTakeQuiz(ctx context.Context, quizId uuid.UUID, email return result, nil } -func (q QuizServiceImpl) GetSolutionQuiz(ctx context.Context, quizId uuid.UUID, email string) (*quiz.QuizDetail, error) { +func (q QuizServiceImpl) GetSolutionQuiz(ctx context.Context, quizId uuid.UUID, user token.UserClaim) (*quiz.QuizDetail, error) { result, err := q.getQuizDetail(ctx, quizId) if err != nil { return nil, err } - _, err = q.GetLastTake(quizId, email) + last, err := q.GetLastTake(quizId, user.Email) if err != nil { return nil, err } - taken, err := q.IsActiveTake(quizId, email) + if last == nil && user.Role == userDomain.Student { + return nil, web.NewResponseError("user is not allow to access this data", "ERR_NOT_ALLOWED") + } + + taken, err := q.IsActiveTake(quizId, user.Email) if err != nil { return nil, err } - if taken { + if taken && user.Role == userDomain.Student { return nil, web.NewResponseError("user is not allow to access this data", "ERR_NOT_ALLOWED") } @@ -162,7 +180,7 @@ func (q QuizServiceImpl) DoFinishQuiz(ctx context.Context, quizId uuid.UUID, ema return nil, err } - err = q.QuizRepository.UpdateScore(data.Id, int(score)) + err = q.QuizRepository.UpdateScore(email, quizId, int(score)) if err != nil { return nil, err @@ -170,3 +188,172 @@ func (q QuizServiceImpl) DoFinishQuiz(ctx context.Context, quizId uuid.UUID, ema return data, nil } + +func (q QuizServiceImpl) isQuizContributor(courseId string, email string) error { + _, err := q.QuizRepository.IsUserContributor(courseId, email) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseError("course and user combination not found", "NOT_OWNER") + } + + return err + } + + return nil +} + +func (q QuizServiceImpl) NewQuiz(payload model.AddQuizRequestPayload) (*model.LinkResponse, error) { + // Validate Role + claim, err := q.TokenUtil.Validate(payload.AddQuizToken, token.Access) + + // Invalid Token + if err != nil { + return &model.LinkResponse{}, web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role == userDomain.Student { + return &model.LinkResponse{}, web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + // Validate Ownership + if err := q.isQuizContributor(payload.CourseID, claim.Email); err != nil { + return &model.LinkResponse{}, err + } + + path := fmt.Sprintf("%s/%s.json", q.BucketQuizBasePath, strings.ReplaceAll(uuid.New().String(), "-", "")) + uploadLink, err := q.Storage.CreatePutSignedLink(context.Background(), path) + + if err != nil { + q.Logger.Error("Some error happened when generate link") + q.Logger.Error(err.Error()) + return &model.LinkResponse{}, err + } + + err = q.QuizRepository.NewQuiz(quiz.Quiz{ + Id: uuid.New(), + Name: payload.Name, + CourseId: payload.CourseID, + CreatorEmail: claim.Email, + QuizPath: path, + }) + + if err != nil { + q.Logger.Error("Some error happened when insert to repository") + q.Logger.Error(err.Error()) + return &model.LinkResponse{}, err + } + + return &model.LinkResponse{UploadLink: uploadLink}, nil +} + +func (q QuizServiceImpl) UpdateQuiz(payload model.UpdateQuizRequestPayload) (*model.LinkResponse, error) { + // Validate Role + claim, err := q.TokenUtil.Validate(payload.UpdateQuizToken, token.Access) + + // Invalid Token + if err != nil { + return &model.LinkResponse{}, web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role == userDomain.Student { + return &model.LinkResponse{}, web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + // Get Quiz Detail + prev, err := q.QuizRepository.GetQuizDetail(payload.ID) + + if err != nil { + return &model.LinkResponse{}, err + } + + // Validate Ownership + if err := q.isQuizContributor(prev.CourseId, claim.Email); err != nil { + return &model.LinkResponse{}, err + } + + path := fmt.Sprintf("%s/%s.json", q.BucketQuizBasePath, strings.ReplaceAll(uuid.New().String(), "-", "")) + uploadLink, err := q.Storage.CreatePutSignedLink(context.Background(), path) + + if err != nil { + q.Logger.Error("Some error happened when generate link") + q.Logger.Error(err.Error()) + return &model.LinkResponse{}, err + } + + err = q.QuizRepository.UpdateQuiz(quiz.Quiz{ + Id: prev.Id, + Name: prev.Name, + CourseId: prev.CourseId, + CreatorEmail: prev.CreatorEmail, + QuizPath: path, + }) + + if err != nil { + q.Logger.Error("Some error happened when inserting new link") + q.Logger.Error(err.Error()) + return &model.LinkResponse{}, err + } + + return &model.LinkResponse{UploadLink: uploadLink}, nil +} + +func (q QuizServiceImpl) GetQuizLink(payload model.GetRequestPayload) (*model.PathResponse, error) { + // Validate Role + claim, err := q.TokenUtil.Validate(payload.GetToken, token.Access) + + // Invalid Token + if err != nil { + return &model.PathResponse{}, web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role == userDomain.Student { + return &model.PathResponse{}, web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + // Get Quiz Detail + quiz, err := q.QuizRepository.GetQuizDetail(payload.ID) + + if err != nil { + return &model.PathResponse{}, err + } + + // Validate Ownership + if err := q.isQuizContributor(quiz.CourseId, claim.Email); err != nil { + return &model.PathResponse{}, err + } + + return &model.PathResponse{Path: quiz.QuizPath}, nil +} + +func (q QuizServiceImpl) DeleteQuiz(payload model.DeleteRequestPayload) error { + // Validate Role + claim, err := q.TokenUtil.Validate(payload.DeleteToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role == userDomain.Student { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + // Get Quiz Detail + quiz, err := q.QuizRepository.GetQuizDetail(payload.ID) + + if err != nil { + return err + } + + // Validate Ownership + if err := q.isQuizContributor(quiz.CourseId, claim.Email); err != nil { + return err + } + + return q.QuizRepository.Delete(payload.ID) +} diff --git a/service/quiz/type.go b/service/quiz/type.go index 7c97e4965500fb25f75c19a4117c536559fcf9a8..f06407ac5bd9363b170144c46e06ce6fae85f7d0 100644 --- a/service/quiz/type.go +++ b/service/quiz/type.go @@ -5,6 +5,8 @@ import ( "github.com/google/uuid" "gitlab.informatika.org/ocw/ocw-backend/model/domain/quiz" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + model "gitlab.informatika.org/ocw/ocw-backend/model/web/quiz" ) type QuizService interface { @@ -13,5 +15,10 @@ type QuizService interface { DoTakeQuiz(ctx context.Context, quizId uuid.UUID, email string) (*quiz.QuizDetail, error) DoFinishQuiz(ctx context.Context, quizId uuid.UUID, email string, studentAnswer []quiz.Response) (*quiz.QuizTake, error) - GetSolutionQuiz(ctx context.Context, quizId uuid.UUID, email string) (*quiz.QuizDetail, error) + GetSolutionQuiz(ctx context.Context, quizId uuid.UUID, user token.UserClaim) (*quiz.QuizDetail, error) + isQuizContributor(courseId string, email string) error + NewQuiz(payload model.AddQuizRequestPayload) (*model.LinkResponse, error) + UpdateQuiz(payload model.UpdateQuizRequestPayload) (*model.LinkResponse, error) + GetQuizLink(payload model.GetRequestPayload) (*model.PathResponse, error) + DeleteQuiz(payload model.DeleteRequestPayload) error } diff --git a/test/mail/mail_test.go b/test/mail/mail_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1f1f48e7d346dbb06ca453c27e48d24364e8e7e0 --- /dev/null +++ b/test/mail/mail_test.go @@ -0,0 +1,24 @@ +package mail + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.informatika.org/ocw/ocw-backend/provider/mail/smtp" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" +) + +func IgnoreTestSendMail(t *testing.T) { + smtpClient := smtp.New(&env.Environment{ + SmtpUsername: "", + SmtpPassword: "", + SmtpIdentity: "", + SmtpServer: "", + SmtpPort: 22, + SmtpAuthType: "", + }) + + err := smtpClient.Send([]string{"bayusamudra.55.02.com@gmail.com"}, "Testing", "Ini test") + + assert.Nil(t, err) +} diff --git a/test/middleware/cors_test.go b/test/middleware/cors_test.go index 74615aa27d9b04bcb60edf4173e0bf9f83b9eddf..b793862aed471c20d8332eab34efdf0bc1dc2e53 100644 --- a/test/middleware/cors_test.go +++ b/test/middleware/cors_test.go @@ -16,14 +16,14 @@ func TestPreflight(t *testing.T) { Headers: map[string]string{ "Access-Control-Request-Method": "GET", "Access-Control-Request-Headers": "accept, origin, authorization, content-type, referer", - "Origin": "https://inkubatorit.com", + "Origin": "https://test.com", }, }) assert.Nil(t, err) assert.Equal(t, res.StatusCode, http.StatusOK) - assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://inkubatorit.com") + assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://test.com") assert.Contains(t, res.Header.Get("Access-Control-Allow-Methods"), "GET") }) @@ -34,14 +34,14 @@ func TestPreflight(t *testing.T) { Headers: map[string]string{ "Access-Control-Request-Method": "GET", "Access-Control-Request-Headers": "accept, origin, authorization, content-type, referer", - "Origin": "https://inkubatorit.com", + "Origin": "https://test.com", }, }) assert.Nil(t, err) assert.Equal(t, res.StatusCode, http.StatusOK) - assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://inkubatorit.com") + assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://test.com") assert.Contains(t, res.Header.Get("Access-Control-Allow-Methods"), "GET") }) @@ -52,14 +52,14 @@ func TestPreflight(t *testing.T) { Headers: map[string]string{ "Access-Control-Request-Method": "PATCH", "Access-Control-Request-Headers": "accept, origin, authorization, content-type, referer", - "Origin": "https://inkubatorit.com", + "Origin": "https://test.com", }, }) assert.Nil(t, err) assert.Equal(t, res.StatusCode, http.StatusOK) - assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://inkubatorit.com") + assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://test.com") assert.Equal(t, res.Header.Get("Access-Control-Allow-Methods"), "PATCH") }) } diff --git a/utils/env/env.go b/utils/env/env.go index ff09c00b0ff32c969ddd850d791ca8e35ad8fc9d..50c73820d065affd17ec2d93bf420f994281bdcd 100644 --- a/utils/env/env.go +++ b/utils/env/env.go @@ -36,6 +36,7 @@ type Environment struct { SmtpPassword string `env:"SMTP_PASSWORD"` SmtpServer string `env:"SMTP_SERVER"` SmtpPort int `env:"SMTP_PORT" envDefault:"25"` + SmtpAuthType string `env:"SMTP_TYPE" envDefault:"CRAM"` FrontendBaseURL string `env:"FE_BASE_URL"` ResetPasswordPath string `env:"RESET_PASSWORD_PATH" envDefault:"/resetPassword"` @@ -51,6 +52,7 @@ type Environment struct { RedisPassword string `env:"REDIS_PASSWORD"` RedisUseAuth bool `env:"REDIS_USE_AUTH" envDefault:"false"` RedisPrefixKey string `env:"REDIS_PREFIX_KEY" envDefault:"app:"` + RedisUseTLS bool `env:"REDIS_USE_TLS" envDefault:"false"` BucketEndpoint string `env:"BUCKET_ENDPOINT"` BucketSecretKey string `env:"BUCKET_SECRET_KEY"` @@ -63,6 +65,7 @@ type Environment struct { BucketSignedGetDuration int64 `env:"BUCKET_SIGNED_GET_DURATION_S" envDefault:"1800"` BucketMaterialBasePath string `env:"BUCKET_MATERIAL_BASE_PATH" envDefault:"materials"` + BucketQuizBasePath string `env:"BUCKET_MATERIAL_BASE_PATH" envDefault:"quiz"` UseBucket bool `env:"USE_BUCKET" envDefault:"true"` }