diff --git a/.env b/.env index 1f5f0f141e9c1391bcd5b0325fbb516f0fa4bbfb..43bf5fd412feeda40517d5f5e722333b92271442 100644 --- a/.env +++ b/.env @@ -4,7 +4,6 @@ PORT=8080 LOGTAIL_TOKEN= HTTP_TIMEOUT_SEC=2 LOG_FLUSH_INTERVAL_MS=1000 -DB_STRING="host=localhost user=ocw password=ocw dbname=ocw-db port=5433 sslmode=disable TimeZone=Asia/Shanghai" SMTP_USERNAME="noreply@ocw.id" -SMTP_SERVER=localhost -SMTP_PORT=1025 \ No newline at end of file +SMTP_PORT=1025 +FE_BASE_URL="http://localhost:3000" diff --git a/.env.docker b/.env.docker index 11f1edfc100a7fcebca3466da30fcdd21feff42d..2f87fea919b87db0f51f52306a1a8c21e13e3aaf 100644 --- a/.env.docker +++ b/.env.docker @@ -1,3 +1,14 @@ POSTGRES_USER=ocw POSTGRES_PASSWORD=ocw POSTGRES_DB=ocw-db +ENV=DEVELOPMENT +LISTEN_ADDR=0.0.0.0 +PORT=8080 +LOGTAIL_TOKEN= +HTTP_TIMEOUT_SEC=2 +LOG_FLUSH_INTERVAL_MS=1000 +DB_STRING="host=database user=ocw password=ocw dbname=ocw-db port=5432 sslmode=disable TimeZone=Asia/Shanghai" +SMTP_USERNAME="noreply@ocw.id" +SMTP_SERVER=mailhog +SMTP_PORT=1025 +REDIS_STRING="redis" \ No newline at end of file diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000000000000000000000000000000000000..c251113ad3df4ab91c35cd7202eb9b5158ecc145 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,11 @@ +# Sesuaikan dengan environment masing-masing +BUCKET_ENDPOINT="minio:9000" +BUCKET_SECRET_KEY="SUTT" +BUCKET_ACCESS_ID="ACCESS" +BUKET_TOKEN_KEY="" +BUCKET_USE_SSL=false +BUCKET_NAME="ocw" + +SMTP_SERVER=localhost +DB_STRING="host=localhost user=ocw password=ocw dbname=ocw-db port=5433 sslmode=disable TimeZone=Asia/Shanghai" +REDIS_STRING="localhost" diff --git a/.gitignore b/.gitignore index 7e2965e3e35f23ac242d20ffece83456a71de7ef..d3eeb070587bb63e0a455b491181f8fc408c8df1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ bin/ tmp/ wire_gen.go +__debug_bin* +.env.local diff --git a/Dockerfile.dev b/Dockerfile.dev index 8def8850bfd96d51a7f1c72944556ba5823eb648..03526f3e75455e51850f7f1186b2d2b05f140392 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -6,6 +6,9 @@ RUN go install github.com/google/wire/cmd/wire@latest RUN go install github.com/swaggo/swag/cmd/swag@latest COPY . /app + +RUN git config --global --add safe.directory /app + WORKDIR /app EXPOSE 8080 diff --git a/Makefile b/Makefile index 65b15586a8cec0a52bff462637f75ad06310cc3e..961e18808da80a896920915b35bbc96e27f321d0 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,8 @@ run: dependency build: dependency @go build -o=bin/server.app . -watch: - @air --build.cmd="make build" --build.bin="./bin/server.app" --build.exclude_dir="bin,tmp,docs" --build.exclude_file="wire_gen.go" +watch: dependency + @air --build.cmd="make build" --build.bin="./bin/server.app" --build.exclude_dir="bin,tmp,docs" --build.exclude_file="wire_gen.go" --build.kill_delay="0s" test: test-dependency @go test ./test/... -v diff --git a/docker-compose.yml b/docker-compose.yml index 7ccabe6b9f336b3be6a3a55f4c8d11c8c2127306..3eccf485e7a93a9172dcdfca800f8bcf2b732655 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: - .:/app ports: - 8888:8080 - env_file: .env + env_file: .env.docker depends_on: - database - minio diff --git a/docs/docs.go b/docs/docs.go index 6fece1a6822d55bcc98a20a155a4039e5fb1a903..aad76131fffdf4511f3699ffc3dafc59aa1bc6a5 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -56,6 +56,9 @@ const docTemplate = `{ }, "post": { "description": "Add a user to database", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], @@ -63,6 +66,17 @@ const docTemplate = `{ "admin" ], "summary": "Add User", + "parameters": [ + { + "description": "Admin Add User Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.AdminAddUserPayload" + } + } + ], "responses": { "200": { "description": "OK", @@ -71,16 +85,18 @@ const docTemplate = `{ } } } - }, - "delete": { - "description": "Delete a user from database", + } + }, + "/admin/user/{email}": { + "get": { + "description": "Get a user from database", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Delete User By Id", + "summary": "Get User By Email", "responses": { "200": { "description": "OK", @@ -90,15 +106,18 @@ const docTemplate = `{ } } }, - "patch": { - "description": "Update a user from database", + "delete": { + "description": "Delete a user from database", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Update User By Id", + "summary": "Delete User By Id", "responses": { "200": { "description": "OK", @@ -107,18 +126,30 @@ const docTemplate = `{ } } } - } - }, - "/admin/user/{id}": { - "get": { - "description": "Get a user from database", + }, + "patch": { + "description": "Update a user from database", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get User By Email", + "summary": "Update User By Id", + "parameters": [ + { + "description": "Admin Update User Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.AdminUpdateUserPayload" + } + } + ], "responses": { "200": { "description": "OK", @@ -327,9 +358,273 @@ const docTemplate = `{ } } } + }, + "/auth/verify": { + "post": { + "description": "Do Email Verification to user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Do Email Verification", + "parameters": [ + { + "description": "Register Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/verification.VerificationRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/auth/verify/resend": { + "post": { + "description": "Send Email Verification to user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Send Email Verification", + "parameters": [ + { + "description": "Register Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/verification.VerificationSendRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/reset/confirm": { + "put": { + "description": "Do confirmation to reset password", + "produces": [ + "application/json" + ], + "tags": [ + "reset" + ], + "summary": "Confirm Reset Password", + "parameters": [ + { + "type": "string", + "description": "Email validation token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/confirm.ConfirmRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "Login Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/reset/request": { + "post": { + "description": "Send Reset password token to email", + "produces": [ + "application/json" + ], + "tags": [ + "reset" + ], + "summary": "Request Reset Password Token", + "parameters": [ + { + "description": "payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RequestRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "Login Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/reset/validate": { + "get": { + "description": "Send Reset password token to email", + "produces": [ + "application/json" + ], + "tags": [ + "reset" + ], + "summary": "Request Reset Password Token", + "parameters": [ + { + "type": "string", + "description": "Email validation token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Login Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } } }, "definitions": { + "admin.AdminAddUserPayload": { + "type": "object", + "required": [ + "email", + "name", + "role" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + }, + "name": { + "description": "User name", + "type": "string", + "example": "someone" + }, + "role": { + "description": "User Role", + "type": "string", + "example": "admin" + } + } + }, + "admin.AdminUpdateUserPayload": { + "type": "object", + "required": [ + "email", + "name", + "role" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + }, + "name": { + "description": "User name", + "type": "string", + "example": "someone" + }, + "role": { + "description": "User Role", + "type": "string", + "example": "admin" + } + } + }, + "confirm.ConfirmRequestPayload": { + "description": "Information that should be available when you confirm a password reset", + "type": "object", + "required": [ + "password", + "password_validation" + ], + "properties": { + "confirmToken": { + "description": "Web Token that was appended to the link", + "type": "string" + }, + "password": { + "description": "User Password", + "type": "string", + "example": "secret" + }, + "password_validation": { + "description": "User Password Validation, must be same as user", + "type": "string", + "example": "secret" + } + } + }, "login.LoginRequestPayload": { "description": "Information that should be available when do a login process", "type": "object", @@ -406,6 +701,46 @@ const docTemplate = `{ } } }, + "request.RequestRequestPayload": { + "description": "Information that should be available when password reset is requested", + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + } + } + }, + "verification.VerificationRequestPayload": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "example": "6ba7b812-9dad-11d1-80b4-00c04fd430c8" + } + } + }, + "verification.VerificationSendRequestPayload": { + "description": "Information that should be passed when request verify", + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + } + } + }, "web.BaseResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 513bd4b6b3f4b57241fb86bbb6eb81d7ed0bd1b1..5e40a0e52c5c837cf1a9c167f1e4ab064fce87f3 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -48,6 +48,9 @@ }, "post": { "description": "Add a user to database", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], @@ -55,6 +58,17 @@ "admin" ], "summary": "Add User", + "parameters": [ + { + "description": "Admin Add User Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.AdminAddUserPayload" + } + } + ], "responses": { "200": { "description": "OK", @@ -63,16 +77,18 @@ } } } - }, - "delete": { - "description": "Delete a user from database", + } + }, + "/admin/user/{email}": { + "get": { + "description": "Get a user from database", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Delete User By Id", + "summary": "Get User By Email", "responses": { "200": { "description": "OK", @@ -82,15 +98,18 @@ } } }, - "patch": { - "description": "Update a user from database", + "delete": { + "description": "Delete a user from database", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Update User By Id", + "summary": "Delete User By Id", "responses": { "200": { "description": "OK", @@ -99,18 +118,30 @@ } } } - } - }, - "/admin/user/{id}": { - "get": { - "description": "Get a user from database", + }, + "patch": { + "description": "Update a user from database", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get User By Email", + "summary": "Update User By Id", + "parameters": [ + { + "description": "Admin Update User Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.AdminUpdateUserPayload" + } + } + ], "responses": { "200": { "description": "OK", @@ -319,9 +350,273 @@ } } } + }, + "/auth/verify": { + "post": { + "description": "Do Email Verification to user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Do Email Verification", + "parameters": [ + { + "description": "Register Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/verification.VerificationRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/auth/verify/resend": { + "post": { + "description": "Send Email Verification to user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Send Email Verification", + "parameters": [ + { + "description": "Register Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/verification.VerificationSendRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/reset/confirm": { + "put": { + "description": "Do confirmation to reset password", + "produces": [ + "application/json" + ], + "tags": [ + "reset" + ], + "summary": "Confirm Reset Password", + "parameters": [ + { + "type": "string", + "description": "Email validation token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/confirm.ConfirmRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "Login Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/reset/request": { + "post": { + "description": "Send Reset password token to email", + "produces": [ + "application/json" + ], + "tags": [ + "reset" + ], + "summary": "Request Reset Password Token", + "parameters": [ + { + "description": "payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RequestRequestPayload" + } + } + ], + "responses": { + "200": { + "description": "Login Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + }, + "/reset/validate": { + "get": { + "description": "Send Reset password token to email", + "produces": [ + "application/json" + ], + "tags": [ + "reset" + ], + "summary": "Request Reset Password Token", + "parameters": [ + { + "type": "string", + "description": "Email validation token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "Login Success", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } } }, "definitions": { + "admin.AdminAddUserPayload": { + "type": "object", + "required": [ + "email", + "name", + "role" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + }, + "name": { + "description": "User name", + "type": "string", + "example": "someone" + }, + "role": { + "description": "User Role", + "type": "string", + "example": "admin" + } + } + }, + "admin.AdminUpdateUserPayload": { + "type": "object", + "required": [ + "email", + "name", + "role" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + }, + "name": { + "description": "User name", + "type": "string", + "example": "someone" + }, + "role": { + "description": "User Role", + "type": "string", + "example": "admin" + } + } + }, + "confirm.ConfirmRequestPayload": { + "description": "Information that should be available when you confirm a password reset", + "type": "object", + "required": [ + "password", + "password_validation" + ], + "properties": { + "confirmToken": { + "description": "Web Token that was appended to the link", + "type": "string" + }, + "password": { + "description": "User Password", + "type": "string", + "example": "secret" + }, + "password_validation": { + "description": "User Password Validation, must be same as user", + "type": "string", + "example": "secret" + } + } + }, "login.LoginRequestPayload": { "description": "Information that should be available when do a login process", "type": "object", @@ -398,6 +693,46 @@ } } }, + "request.RequestRequestPayload": { + "description": "Information that should be available when password reset is requested", + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + } + } + }, + "verification.VerificationRequestPayload": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "example": "6ba7b812-9dad-11d1-80b4-00c04fd430c8" + } + } + }, + "verification.VerificationSendRequestPayload": { + "description": "Information that should be passed when request verify", + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "description": "User Email", + "type": "string", + "example": "someone@example.com" + } + } + }, "web.BaseResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ee76e9375fe026003fafa69a41451de0804766b5..d2280c2b16106e29102e0ed33e5e14d8ac794bc6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,61 @@ definitions: + admin.AdminAddUserPayload: + properties: + email: + description: User Email + example: someone@example.com + type: string + name: + description: User name + example: someone + type: string + role: + description: User Role + example: admin + type: string + required: + - email + - name + - role + type: object + admin.AdminUpdateUserPayload: + properties: + email: + description: User Email + example: someone@example.com + type: string + name: + description: User name + example: someone + type: string + role: + description: User Role + example: admin + type: string + required: + - email + - name + - role + type: object + confirm.ConfirmRequestPayload: + description: Information that should be available when you confirm a password + reset + properties: + confirmToken: + description: Web Token that was appended to the link + type: string + password: + description: User Password + example: secret + type: string + password_validation: + description: User Password Validation, must be same as user + example: secret + type: string + required: + - password + - password_validation + type: object login.LoginRequestPayload: description: Information that should be available when do a login process properties: @@ -56,6 +113,34 @@ definitions: - password - password_validation type: object + request.RequestRequestPayload: + description: Information that should be available when password reset is requested + properties: + email: + description: User Email + example: someone@example.com + type: string + required: + - email + type: object + verification.VerificationRequestPayload: + properties: + id: + example: 6ba7b812-9dad-11d1-80b4-00c04fd430c8 + type: string + required: + - id + type: object + verification.VerificationSendRequestPayload: + description: Information that should be passed when request verify + properties: + email: + description: User Email + example: someone@example.com + type: string + required: + - email + type: object web.BaseResponse: properties: data: {} @@ -87,8 +172,8 @@ paths: tags: - common /admin/user: - delete: - description: Delete a user from database + get: + description: Get all users from database produces: - application/json responses: @@ -96,11 +181,20 @@ paths: description: OK schema: $ref: '#/definitions/web.BaseResponse' - summary: Delete User By Id + summary: Get All User tags: - admin - get: - description: Get all users from database + post: + consumes: + - application/json + description: Add a user to database + parameters: + - description: Admin Add User Payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/admin.AdminAddUserPayload' produces: - application/json responses: @@ -108,11 +202,14 @@ paths: description: OK schema: $ref: '#/definitions/web.BaseResponse' - summary: Get All User + summary: Add User tags: - admin - patch: - description: Update a user from database + /admin/user/{email}: + delete: + consumes: + - application/json + description: Delete a user from database produces: - application/json responses: @@ -120,11 +217,11 @@ paths: description: OK schema: $ref: '#/definitions/web.BaseResponse' - summary: Update User By Id + summary: Delete User By Id tags: - admin - post: - description: Add a user to database + get: + description: Get a user from database produces: - application/json responses: @@ -132,12 +229,20 @@ paths: description: OK schema: $ref: '#/definitions/web.BaseResponse' - summary: Add User + summary: Get User By Email tags: - admin - /admin/user/{id}: - get: - description: Get a user from database + patch: + consumes: + - application/json + description: Update a user from database + parameters: + - description: Admin Update User Payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/admin.AdminUpdateUserPayload' produces: - application/json responses: @@ -145,7 +250,7 @@ paths: description: OK schema: $ref: '#/definitions/web.BaseResponse' - summary: Get User By Email + summary: Update User By Id tags: - admin /auth/login: @@ -270,4 +375,128 @@ paths: summary: Register New Account tags: - auth + /auth/verify: + post: + consumes: + - application/json + description: Do Email Verification to user + parameters: + - description: Register Payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/verification.VerificationRequestPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Do Email Verification + tags: + - auth + /auth/verify/resend: + post: + consumes: + - application/json + description: Send Email Verification to user + parameters: + - description: Register Payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/verification.VerificationSendRequestPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Send Email Verification + tags: + - auth + /reset/confirm: + put: + description: Do confirmation to reset password + parameters: + - description: Email validation token + in: header + name: Authorization + required: true + type: string + - description: payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/confirm.ConfirmRequestPayload' + produces: + - application/json + responses: + "200": + description: Login Success + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Confirm Reset Password + tags: + - reset + /reset/request: + post: + description: Send Reset password token to email + parameters: + - description: payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.RequestRequestPayload' + produces: + - application/json + responses: + "200": + description: Login Success + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Request Reset Password Token + tags: + - reset + /reset/validate: + get: + description: Send Reset password token to email + parameters: + - description: Email validation token + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "200": + description: Login Success + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Request Reset Password Token + tags: + - reset swagger: "2.0" diff --git a/go.mod b/go.mod index b69d320688f3db0c6bbdcc1ad19eea994fde74ea..838370778ef7bc486acbbfbcdb8116a3988375ba 100644 --- a/go.mod +++ b/go.mod @@ -8,51 +8,56 @@ require ( github.com/go-chi/cors v1.2.1 github.com/go-playground/validator/v10 v10.11.2 github.com/golang-jwt/jwt/v4 v4.4.3 + github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 - github.com/joho/godotenv v1.5.0 + github.com/joho/godotenv v1.5.1 + github.com/minio/minio-go/v7 v7.0.49 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 github.com/swaggo/http-swagger v1.3.3 github.com/swaggo/swag v1.8.10 - golang.org/x/crypto v0.6.0 - gorm.io/driver/postgres v1.4.6 - gorm.io/gorm v1.24.5 + golang.org/x/crypto v0.7.0 + gorm.io/driver/postgres v1.5.0 + gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 ) require ( - github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/rs/xid v1.4.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) + +require ( + github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/KyleBanks/depth v1.2.1 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/cosmtrek/air v1.41.0 // indirect - github.com/creack/pty v1.1.18 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fatih/color v1.14.1 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.8 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/gomodule/redigo v1.8.9 github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.2.0 // indirect + github.com/jackc/pgx/v5 v5.3.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect github.com/swaggo/files v1.0.0 // indirect - golang.org/x/net v0.6.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect - golang.org/x/tools v0.5.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/tools v0.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d771a874f694fecc968e13f8658ba7de1cd1ca6f..4b7ff59fd497a0efd4a6fa5af4f7e7646cb28f7a 100644 --- a/go.sum +++ b/go.sum @@ -2,47 +2,32 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20O github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= -github.com/cosmtrek/air v1.41.0 h1:6ck2LbcVvby6cyuwE8ruia41U2nppMZGWOpq+E/EhoU= -github.com/cosmtrek/air v1.41.0/go.mod h1:+RBGjJt7T2f3I7td8Tvk0XsH/hZ3E1QBLfiWObICO4c= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU= github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -51,64 +36,78 @@ github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyh github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8= -github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk= -github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels= +github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/joho/godotenv v1.5.0 h1:C/Vohk/9L1RCoS/UW2gfyi2N0EElSW3yb9zwi3PjosE= -github.com/joho/godotenv v1.5.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.49 h1:dE5DfOtnXMXCjr/HWI6zN9vCrY6Sv666qhhiwUMvGV4= +github.com/minio/minio-go/v7 v7.0.49/go.mod h1:UI34MvQEiob3Cf/gGExGMmzugkM/tNgbFypNDy5LMVc= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -122,88 +121,69 @@ github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXz github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo= github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= -golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.4.6 h1:1FPESNXqIKG5JmraaH2bfCVlMQ7paLoCreFxDtqzwdc= -gorm.io/driver/postgres v1.4.6/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4= -gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE= -gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= +gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= +gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g= +gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= diff --git a/handler/admin/addUser.go b/handler/admin/addUser.go index 7b53f3d3d4977c157d67048d3079c40e46e446fe..02c631be6d59ed66f628b57dfa4ace8191b05249 100644 --- a/handler/admin/addUser.go +++ b/handler/admin/addUser.go @@ -2,6 +2,7 @@ package admin import ( "net/http" + req "gitlab.informatika.org/ocw/ocw-backend/model/web/admin/addUser" ) // Index godoc @@ -10,9 +11,32 @@ import ( // @Summary Add User // @Description Add a user to database // @Produce json +// @Accept json +// @Param data body req.AdminAddUserPayload true "Admin Add User Payload" // @Success 200 {object} web.BaseResponse // @Router /admin/user [post] func (route AdminHandlerImpl) AddUser(w http.ResponseWriter, r *http.Request){ - payload := route.WrapperUtil.SuccessResponseWrap(route.AdminService.AddUser()) + payload := req.AdminAddUserPayload{} + + if r.Header.Get("Content-Type") != "application/json" { + payload := route.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + route.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := route.HttpUtil.ParseJson(r, &payload); err != nil { + payload := route.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + route.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + err := route.AdminService.AddUser(payload) + + if err != nil { + payload := route.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + route.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + route.HttpUtil.WriteSuccessJson(w, payload) } \ No newline at end of file diff --git a/handler/admin/deleteUser.go b/handler/admin/deleteUser.go index 0be96ccc00451aaf0dd1490d6b29154a1b2d62dd..9979a47a2279814f9de6c9302c3a555531a659aa 100644 --- a/handler/admin/deleteUser.go +++ b/handler/admin/deleteUser.go @@ -2,6 +2,7 @@ package admin import ( "net/http" + "path" ) // Index godoc @@ -10,9 +11,23 @@ import ( // @Summary Delete User By Id // @Description Delete a user from database // @Produce json +// @Accept json // @Success 200 {object} web.BaseResponse -// @Router /admin/user [delete] +// @Router /admin/user/{email} [delete] func (route AdminHandlerImpl) DeleteUser(w http.ResponseWriter, r *http.Request){ - payload := route.WrapperUtil.SuccessResponseWrap(route.AdminService.DeleteUser()) - route.HttpUtil.WriteSuccessJson(w, payload) + email := path.Base(r.URL.Path) + + // get user from database + err := route.AdminService.DeleteUser(email) + + if err != nil { + // error handling + payload := route.WrapperUtil.ErrorResponseWrap("error", err.Error()) + route.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + // return user + result := route.WrapperUtil.SuccessResponseWrap(email) + route.HttpUtil.WriteJson(w, http.StatusOK, result) } \ No newline at end of file diff --git a/handler/admin/getUserByEmail.go b/handler/admin/getUserByEmail.go index 0ad7a59fce2214548fd0aaae8b2443e4b5595ca7..fd21301db325bf6d59eb8df4404f352ba4fd2b29 100644 --- a/handler/admin/getUserByEmail.go +++ b/handler/admin/getUserByEmail.go @@ -2,6 +2,7 @@ package admin import ( "net/http" + "path" ) // Index godoc @@ -11,8 +12,22 @@ import ( // @Description Get a user from database // @Produce json // @Success 200 {object} web.BaseResponse -// @Router /admin/user/{id} [get] +// @Router /admin/user/{email} [get] func (route AdminHandlerImpl) GetUserByEmail(w http.ResponseWriter, r *http.Request) { - payload := route.WrapperUtil.SuccessResponseWrap(route.AdminService.GetUserByEmail()) - route.HttpUtil.WriteSuccessJson(w, payload) + // email := r.URL.Query().Get("email") + email := path.Base(r.URL.Path) + + // get user from database + user, err := route.AdminService.GetUserByEmail(email) + + if err != nil { + // error handling + payload := route.WrapperUtil.ErrorResponseWrap("error", err.Error()) + route.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + // return user + result := route.WrapperUtil.SuccessResponseWrap(user) + route.HttpUtil.WriteJson(w, http.StatusOK, result) } diff --git a/handler/admin/updateUser.go b/handler/admin/updateUser.go index c6531ec04835aabca40eedf74f3f008bc27d017c..877d5c84a3785626e9736ecf1d433d37fdf1df40 100644 --- a/handler/admin/updateUser.go +++ b/handler/admin/updateUser.go @@ -2,6 +2,8 @@ package admin import ( "net/http" + "path" + req "gitlab.informatika.org/ocw/ocw-backend/model/web/admin/updateUser" ) // Index godoc @@ -10,9 +12,35 @@ import ( // @Summary Update User By Id // @Description Update a user from database // @Produce json +// @Accept json +// @Param data body req.AdminUpdateUserPayload true "Admin Update User Payload" // @Success 200 {object} web.BaseResponse -// @Router /admin/user [patch] +// @Router /admin/user/{email} [patch] func (route AdminHandlerImpl) UpdateUser(w http.ResponseWriter, r *http.Request){ - payload := route.WrapperUtil.SuccessResponseWrap(route.AdminService.UpdateUser()) + email := path.Base(r.URL.Path) + // TODO: how to change email + + payload := req.AdminUpdateUserPayload{} + + if r.Header.Get("Content-Type") != "application/json" { + payload := route.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + route.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := route.HttpUtil.ParseJson(r, &payload); err != nil { + payload := route.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + route.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + err := route.AdminService.UpdateUser(email, payload) + + if err != nil { + payload := route.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + route.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + route.HttpUtil.WriteSuccessJson(w, payload) } \ No newline at end of file diff --git a/handler/auth/handler.go b/handler/auth/handler.go index adae847f2baec21f127940794b447cffdb9824b3..0799b93d0b06e23856bf1cc49949511d7f8924bf 100644 --- a/handler/auth/handler.go +++ b/handler/auth/handler.go @@ -3,6 +3,7 @@ package auth import ( "gitlab.informatika.org/ocw/ocw-backend/service/auth" "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/service/verification" "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" ) @@ -12,4 +13,5 @@ type AuthHandlerImpl struct { httputil.HttpUtil wrapper.WrapperUtil logger.Logger + verification.VerificationService } diff --git a/handler/auth/send_verify.go b/handler/auth/send_verify.go new file mode 100644 index 0000000000000000000000000000000000000000..b2b4a2e70eaca89ac5de5f26a76fb4210d894416 --- /dev/null +++ b/handler/auth/send_verify.go @@ -0,0 +1,72 @@ +package auth + +import ( + "fmt" + "net/http" + + "github.com/go-playground/validator/v10" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/verification" +) + +// Index godoc +// +// @Tags auth +// @Summary Send Email Verification +// @Description Send Email Verification to user +// @Produce json +// @Accept json +// @Param data body verification.VerificationSendRequestPayload true "Register Payload" +// @Success 200 {object} web.BaseResponse +// @Failure 400 {object} web.BaseResponse +// @Failure 500 {object} web.BaseResponse +// @Router /auth/verify/resend [post] +func (a AuthHandlerImpl) SendEmailVerify(w http.ResponseWriter, r *http.Request) { + payload := verification.VerificationSendRequestPayload{} + validate := validator.New() + + if r.Header.Get("Content-Type") != "application/json" { + payload := a.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + a.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := a.HttpUtil.ParseJson(r, &payload); err != nil { + payload := a.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + a.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := a.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := a.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + err := a.AuthService.SendVerifyEmail(payload) + + if err != nil { + respErr, ok := err.(web.ResponseError) + if ok { + payload := a.WrapperUtil.ErrorResponseWrap("you have reach limit of resend verification", respErr) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + a.Logger.Error( + fmt.Sprintf("[AUTH] some error happened when do email verification: %s", err.Error()), + ) + payload := a.WrapperUtil.ErrorResponseWrap("internal server error", nil) + a.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := a.WrapperUtil.SuccessResponseWrap(nil) + a.HttpUtil.WriteSuccessJson(w, responsePayload) +} diff --git a/handler/auth/types.go b/handler/auth/types.go index 71b4d1eba14c50a290f9c43565da1dd707e73f29..0848fea6ecd2b79b4f2470f432eeb8cbaf3c3d48 100644 --- a/handler/auth/types.go +++ b/handler/auth/types.go @@ -6,4 +6,6 @@ type AuthHandler interface { Login(w http.ResponseWriter, r *http.Request) Register(w http.ResponseWriter, r *http.Request) Refresh(w http.ResponseWriter, r *http.Request) + EmailVerify(w http.ResponseWriter, r *http.Request) + SendEmailVerify(w http.ResponseWriter, r *http.Request) } diff --git a/handler/auth/verify.go b/handler/auth/verify.go new file mode 100644 index 0000000000000000000000000000000000000000..a118518f67d6d8e4dff329541d93d406f545cbca --- /dev/null +++ b/handler/auth/verify.go @@ -0,0 +1,72 @@ +package auth + +import ( + "fmt" + "net/http" + + "github.com/go-playground/validator/v10" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/verification" +) + +// Index godoc +// +// @Tags auth +// @Summary Do Email Verification +// @Description Do Email Verification to user +// @Produce json +// @Accept json +// @Param data body verification.VerificationRequestPayload true "Register Payload" +// @Success 200 {object} web.BaseResponse +// @Failure 400 {object} web.BaseResponse +// @Failure 500 {object} web.BaseResponse +// @Router /auth/verify [post] +func (a AuthHandlerImpl) EmailVerify(w http.ResponseWriter, r *http.Request) { + payload := verification.VerificationRequestPayload{} + validate := validator.New() + + if r.Header.Get("Content-Type") != "application/json" { + payload := a.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + a.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := a.HttpUtil.ParseJson(r, &payload); err != nil { + payload := a.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + a.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := a.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := a.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + err := a.VerificationService.DoVerification(payload.Id) + + if err != nil { + respErr, ok := err.(web.ResponseError) + if ok { + payload := a.WrapperUtil.ErrorResponseWrap("you have reach limit of resend verification", respErr) + a.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + a.Logger.Error( + fmt.Sprintf("[AUTH] some error happened when do email verification: %s", err.Error()), + ) + payload := a.WrapperUtil.ErrorResponseWrap("internal server error", nil) + a.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := a.WrapperUtil.SuccessResponseWrap(nil) + a.HttpUtil.WriteSuccessJson(w, responsePayload) +} diff --git a/handler/course/addCourse.go b/handler/course/addCourse.go new file mode 100644 index 0000000000000000000000000000000000000000..c07f9ce00468bf9589df78b295922432342e307e --- /dev/null +++ b/handler/course/addCourse.go @@ -0,0 +1,86 @@ +package course + +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/course/add" +) + +func (c CourseHandlerImpl) AddCourse(w http.ResponseWriter, r *http.Request) { + payload := add.AddCourseRequestPayload{} + validate := validator.New() + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := c.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + c.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := c.HttpUtil.ParseJson(r, &payload); err != nil { + payload := c.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + c.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := c.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := c.WrapperUtil.ErrorResponseWrap("token is required", nil) + c.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.AddCourseToken = token[1] + err := c.CourseService.AddCourse(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(nil) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/addFaculty.go b/handler/course/addFaculty.go new file mode 100644 index 0000000000000000000000000000000000000000..a0430da8c4541a971771b86ef0d8cd3a1a002927 --- /dev/null +++ b/handler/course/addFaculty.go @@ -0,0 +1,86 @@ +package course + +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/course/faculty/add" +) + +func (c CourseHandlerImpl) AddFaculty(w http.ResponseWriter, r *http.Request) { + payload := add.AddFacultyRequestPayload{} + validate := validator.New() + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := c.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + c.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := c.HttpUtil.ParseJson(r, &payload); err != nil { + payload := c.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + c.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := c.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := c.WrapperUtil.ErrorResponseWrap("token is required", nil) + c.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.AddFacultyToken = token[1] + err := c.CourseService.AddFaculty(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(nil) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/addMajor.go b/handler/course/addMajor.go new file mode 100644 index 0000000000000000000000000000000000000000..3f3c3ab0faaf177ab6fc12a1da902dac2b8d7ad8 --- /dev/null +++ b/handler/course/addMajor.go @@ -0,0 +1,86 @@ +package course + +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/course/major/add" +) + +func (c CourseHandlerImpl) AddMajor(w http.ResponseWriter, r *http.Request) { + payload := add.AddMajorRequestPayload{} + validate := validator.New() + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := c.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + c.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := c.HttpUtil.ParseJson(r, &payload); err != nil { + payload := c.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + c.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := c.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := c.WrapperUtil.ErrorResponseWrap("token is required", nil) + c.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.AddMajorToken = token[1] + err := c.CourseService.AddMajor(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(nil) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/deleteCourse.go b/handler/course/deleteCourse.go new file mode 100644 index 0000000000000000000000000000000000000000..a06a7a3e41c72742cb6bda478ecb34def0791056 --- /dev/null +++ b/handler/course/deleteCourse.go @@ -0,0 +1,57 @@ +package course + +import ( + "fmt" + "net/http" + "strings" + + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course/delete" +) + +func (c CourseHandlerImpl) DeleteCourse(w http.ResponseWriter, r *http.Request) { + payload := delete.DeleteByStringRequestPayload{} + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := c.WrapperUtil.ErrorResponseWrap("token is required", nil) + c.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.DeleteCourseToken = token[1] + err := c.CourseService.DeleteCourse(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(nil) + c.HttpUtil.WriteSuccessJson(w, responsePayload) + +} \ No newline at end of file diff --git a/handler/course/getCourse.go b/handler/course/getCourse.go new file mode 100644 index 0000000000000000000000000000000000000000..290087fda55ac0219d3ad037f5c3a38415453056 --- /dev/null +++ b/handler/course/getCourse.go @@ -0,0 +1,35 @@ +package course + +import ( + "fmt" + "net/http" + "path" + + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course/get" +) + +func (c CourseHandlerImpl) GetCourse(w http.ResponseWriter, r *http.Request) { + payload := get.GetByStringRequestPayload{} + payload.ID = path.Base(r.URL.Path) + + packet, err := c.CourseService.GetCourse(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(packet) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/getCourses.go b/handler/course/getCourses.go new file mode 100644 index 0000000000000000000000000000000000000000..9280e47c7d96abd7f12d5188cc1c11c6ce927bab --- /dev/null +++ b/handler/course/getCourses.go @@ -0,0 +1,30 @@ +package course + +import ( + "fmt" + "net/http" + + "gitlab.informatika.org/ocw/ocw-backend/model/web" +) + +func (c CourseHandlerImpl) GetCourses(w http.ResponseWriter, r *http.Request) { + packet, err := c.CourseService.GetAllCourse() + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(packet) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/getCoursesByFaculty.go b/handler/course/getCoursesByFaculty.go new file mode 100644 index 0000000000000000000000000000000000000000..a59f6a82342b6f3989c4f7b95585c4e59f1b7ff1 --- /dev/null +++ b/handler/course/getCoursesByFaculty.go @@ -0,0 +1,44 @@ +package course + +import ( + "fmt" + "net/http" + "path" + + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course/get" +) + +func (c CourseHandlerImpl) GetCoursesByFaculty(w http.ResponseWriter, r *http.Request) { + payload := get.GetByUUIDRequestPayload{} + id, err := uuid.Parse(path.Base(r.URL.Path)) + + if err != nil { + // invalid uuid + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.ID = id + packet, err := c.CourseService.GetAllCourseByFaculty(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(packet) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/getCoursesByMajor.go b/handler/course/getCoursesByMajor.go new file mode 100644 index 0000000000000000000000000000000000000000..3f8413871825b2bcc11ad1579f3c82d56b294f58 --- /dev/null +++ b/handler/course/getCoursesByMajor.go @@ -0,0 +1,45 @@ +package course + +import ( + "fmt" + "net/http" + "path" + + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course/get" +) + +func (c CourseHandlerImpl) GetCoursesByMajor(w http.ResponseWriter, r *http.Request) { + payload := get.GetByUUIDRequestPayload{} + id, err := uuid.Parse(path.Base(r.URL.Path)) + + if err != nil { + // invalid uuid + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.ID = id + packet, err := c.CourseService.GetAllCourseByMajor(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(packet) + c.HttpUtil.WriteSuccessJson(w, responsePayload) + +} \ No newline at end of file diff --git a/handler/course/getFaculties.go b/handler/course/getFaculties.go new file mode 100644 index 0000000000000000000000000000000000000000..65473e5b47aa33f10c47f696dd4dd974896773d2 --- /dev/null +++ b/handler/course/getFaculties.go @@ -0,0 +1,31 @@ +package course + +import ( + "fmt" + "net/http" + + "gitlab.informatika.org/ocw/ocw-backend/model/web" +) + +func (c CourseHandlerImpl) GetFaculties(w http.ResponseWriter, r *http.Request) { + packet, err := c.CourseService.GetAllFaculty() + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(packet) + c.HttpUtil.WriteSuccessJson(w, responsePayload) + +} \ No newline at end of file diff --git a/handler/course/getFaculty.go b/handler/course/getFaculty.go new file mode 100644 index 0000000000000000000000000000000000000000..e50d77b17d5ec0482e1b96ee3a9901dc7b58b43b --- /dev/null +++ b/handler/course/getFaculty.go @@ -0,0 +1,44 @@ +package course + +import ( + "fmt" + "net/http" + "path" + + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course/get" +) + +func (c CourseHandlerImpl) GetFaculty(w http.ResponseWriter, r *http.Request) { + payload := get.GetByUUIDRequestPayload{} + id, err := uuid.Parse(path.Base(r.URL.Path)) + + if err != nil { + // invalid uuid + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.ID = id + packet, err := c.CourseService.GetFaculty(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(packet) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/getMajor.go b/handler/course/getMajor.go new file mode 100644 index 0000000000000000000000000000000000000000..8413ce7f474dc9a6e4259ea3a65fed1ee770cd02 --- /dev/null +++ b/handler/course/getMajor.go @@ -0,0 +1,44 @@ +package course + +import ( + "fmt" + "net/http" + "path" + + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course/get" +) + +func (c CourseHandlerImpl) GetMajor(w http.ResponseWriter, r *http.Request) { + payload := get.GetByUUIDRequestPayload{} + id, err := uuid.Parse(path.Base(r.URL.Path)) + + if err != nil { + // invalid uuid + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.ID = id + packet, err := c.CourseService.GetMajor(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(packet) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/getMajors.go b/handler/course/getMajors.go new file mode 100644 index 0000000000000000000000000000000000000000..05466ce40e6e430b284a0e98f905fd0c14fe3869 --- /dev/null +++ b/handler/course/getMajors.go @@ -0,0 +1,31 @@ +package course + +import ( + "fmt" + "net/http" + + "gitlab.informatika.org/ocw/ocw-backend/model/web" +) + +func (c CourseHandlerImpl) GetMajors(w http.ResponseWriter, r *http.Request) { + packet, err := c.CourseService.GetAllMajor() + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(packet) + c.HttpUtil.WriteSuccessJson(w, responsePayload) + +} \ No newline at end of file diff --git a/handler/course/getMajorsByFaculty.go b/handler/course/getMajorsByFaculty.go new file mode 100644 index 0000000000000000000000000000000000000000..abdedb8b72337808f4f092806784ddcf3bdefd5b --- /dev/null +++ b/handler/course/getMajorsByFaculty.go @@ -0,0 +1,44 @@ +package course + +import ( + "fmt" + "net/http" + "path" + + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course/get" +) + +func (c CourseHandlerImpl) GetMajorsByFaculty(w http.ResponseWriter, r *http.Request) { + payload := get.GetByUUIDRequestPayload{} + id, err := uuid.Parse(path.Base(r.URL.Path)) + + if err != nil { + // invalid uuid + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.ID = id + packet, err := c.CourseService.GetAllMajorByFaculty(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(packet) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/handler.go b/handler/course/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..b2251fa09b188ffc5b92ba57c9929c7c9ab98104 --- /dev/null +++ b/handler/course/handler.go @@ -0,0 +1,17 @@ +package course + +import ( + r "gitlab.informatika.org/ocw/ocw-backend/repository/course" + "gitlab.informatika.org/ocw/ocw-backend/service/course" + "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 CourseHandlerImpl struct { + r.CourseRepository + course.CourseService + httputil.HttpUtil + wrapper.WrapperUtil + logger.Logger +} diff --git a/handler/course/types.go b/handler/course/types.go new file mode 100644 index 0000000000000000000000000000000000000000..2398dbaaa5e9c788dbc6ae6b11ac5b1e860280a5 --- /dev/null +++ b/handler/course/types.go @@ -0,0 +1,29 @@ +package course + +import "net/http" + +type CourseHandler interface { + // Get + GetCourses(w http.ResponseWriter, r *http.Request) + GetCourse(w http.ResponseWriter, r *http.Request) + GetMajors(w http.ResponseWriter, r *http.Request) + GetMajor(w http.ResponseWriter, r *http.Request) + GetCoursesByMajor(w http.ResponseWriter, r *http.Request) + GetFaculty(w http.ResponseWriter, r *http.Request) + GetFaculties(w http.ResponseWriter, r *http.Request) + GetCoursesByFaculty(w http.ResponseWriter, r *http.Request) + GetMajorsByFaculty(w http.ResponseWriter, r *http.Request) + + // Add (Put) + AddCourse(w http.ResponseWriter, r *http.Request) + AddMajor(w http.ResponseWriter, r *http.Request) + AddFaculty(w http.ResponseWriter, r *http.Request) + + // Update + UpdateCourse(w http.ResponseWriter, r *http.Request) + UpdateMajor(w http.ResponseWriter, r *http.Request) + UpdateFaculty(w http.ResponseWriter, r *http.Request) + + // Delete + DeleteCourse(w http.ResponseWriter, r *http.Request) +} diff --git a/handler/course/updateCourse.go b/handler/course/updateCourse.go new file mode 100644 index 0000000000000000000000000000000000000000..92dfef946154ecfd72775d44be63a3bb31bdb505 --- /dev/null +++ b/handler/course/updateCourse.go @@ -0,0 +1,86 @@ +package course + +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/course/update" +) + +func (c CourseHandlerImpl) UpdateCourse(w http.ResponseWriter, r *http.Request) { + payload := update.UpdateCourseRequestPayload{} + validate := validator.New() + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := c.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + c.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := c.HttpUtil.ParseJson(r, &payload); err != nil { + payload := c.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + c.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := c.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := c.WrapperUtil.ErrorResponseWrap("token is required", nil) + c.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.UpdateCourseToken = token[1] + err := c.CourseService.UpdateCourse(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(nil) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/updateFaculty.go b/handler/course/updateFaculty.go new file mode 100644 index 0000000000000000000000000000000000000000..e79c7e51b717479b76ed11e0ff17a80a0f0d52ec --- /dev/null +++ b/handler/course/updateFaculty.go @@ -0,0 +1,86 @@ +package course + +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/course/faculty/update" +) + +func (c CourseHandlerImpl) UpdateFaculty(w http.ResponseWriter, r *http.Request) { + payload := update.UpdateFacultyRequestPayload{} + validate := validator.New() + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := c.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + c.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := c.HttpUtil.ParseJson(r, &payload); err != nil { + payload := c.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + c.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := c.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := c.WrapperUtil.ErrorResponseWrap("token is required", nil) + c.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.UpdateFacultyToken = token[1] + err := c.CourseService.UpdateFaculty(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(nil) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/course/updateMajor.go b/handler/course/updateMajor.go new file mode 100644 index 0000000000000000000000000000000000000000..b302b6e638f80206d3756f2af292b86373f2658a --- /dev/null +++ b/handler/course/updateMajor.go @@ -0,0 +1,86 @@ +package course + +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/course/major/update" +) + +func (c CourseHandlerImpl) UpdateMajor(w http.ResponseWriter, r *http.Request) { + payload := update.UpdateMajorRequestPayload{} + validate := validator.New() + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := c.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + c.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := c.HttpUtil.ParseJson(r, &payload); err != nil { + payload := c.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + c.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := c.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := c.WrapperUtil.ErrorResponseWrap("token is required", nil) + c.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := c.WrapperUtil.ErrorResponseWrap("invalid token", nil) + c.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.UpdateMajorToken = token[1] + err := c.CourseService.UpdateMajor(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := c.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + c.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + c.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := c.WrapperUtil.ErrorResponseWrap("internal server error", nil) + c.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := c.WrapperUtil.SuccessResponseWrap(nil) + c.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/di.go b/handler/di.go index 835b134c53081d8bfb44e80ddda91bb03f7f7eeb..0f3eae80a5bd38faa6a1e07e63e13a9c6c41e11e 100644 --- a/handler/di.go +++ b/handler/di.go @@ -5,6 +5,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/handler/admin" "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/reset" "gitlab.informatika.org/ocw/ocw-backend/handler/swagger" ) @@ -29,4 +30,8 @@ var HandlerSet = wire.NewSet( // Reset wire.Struct(new(reset.ResetHandlerImpl), "*"), wire.Bind(new(reset.ResetHandler), new(*reset.ResetHandlerImpl)), + + // Course + wire.Struct(new(course.CourseHandlerImpl), "*"), + wire.Bind(new(course.CourseHandler), new(*course.CourseHandlerImpl)), ) diff --git a/handler/reset/confirm.go b/handler/reset/confirm.go index e68ba0568c58bacb594068f183d79c7d5a20f94b..da9420190d78f8c5938860b8bfca3e9375c90c16 100644 --- a/handler/reset/confirm.go +++ b/handler/reset/confirm.go @@ -10,6 +10,16 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/confirm" ) +// Index godoc +// +// @Tags reset +// @Summary Confirm Reset Password +// @Description Do confirmation to reset password +// @Produce json +// @Param Authorization header string true "Email validation token" +// @Param data body confirm.ConfirmRequestPayload true "payload" +// @Success 200 {object} web.BaseResponse "Login Success" +// @Router /reset/confirm [put] func (rs ResetHandlerImpl) Confirm(w http.ResponseWriter, r *http.Request) { payload := confirm.ConfirmRequestPayload{} validate := validator.New() @@ -88,4 +98,4 @@ func (rs ResetHandlerImpl) Confirm(w http.ResponseWriter, r *http.Request) { responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil) rs.HttpUtil.WriteSuccessJson(w, responsePayload) -} \ No newline at end of file +} diff --git a/handler/reset/request.go b/handler/reset/request.go index f2dc0d450e58ca4c26d33f574a403e3e3269c221..e118a0261b97d7bd0dbec2cde86644293d786313 100644 --- a/handler/reset/request.go +++ b/handler/reset/request.go @@ -10,6 +10,15 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/request" ) +// Index godoc +// +// @Tags reset +// @Summary Request Reset Password Token +// @Description Send Reset password token to email +// @Produce json +// @Param data body request.RequestRequestPayload true "payload" +// @Success 200 {object} web.BaseResponse "Login Success" +// @Router /reset/request [post] func (rs ResetHandlerImpl) Request(w http.ResponseWriter, r *http.Request) { payload := request.RequestRequestPayload{} validate := validator.New() @@ -62,4 +71,4 @@ func (rs ResetHandlerImpl) Request(w http.ResponseWriter, r *http.Request) { responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil) rs.HttpUtil.WriteSuccessJson(w, responsePayload) -} \ No newline at end of file +} diff --git a/handler/reset/validate.go b/handler/reset/validate.go index 19d48dbfdd2e9cf42e5c874779a1f03c8db100f9..1bfa4dec36c2c1f3f2d2ff94110c80070906c3db 100644 --- a/handler/reset/validate.go +++ b/handler/reset/validate.go @@ -9,6 +9,15 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/validate" ) +// Index godoc +// +// @Tags reset +// @Summary Request Reset Password Token +// @Description Send Reset password token to email +// @Produce json +// @Param Authorization header string true "Email validation token" +// @Success 200 {object} web.BaseResponse "Login Success" +// @Router /reset/validate [get] func (rs ResetHandlerImpl) Validate(w http.ResponseWriter, r *http.Request) { payload := validate.ValidateRequestPayload{} validateTokenHeader := r.Header.Get("Authorization") @@ -54,4 +63,4 @@ func (rs ResetHandlerImpl) Validate(w http.ResponseWriter, r *http.Request) { responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil) rs.HttpUtil.WriteSuccessJson(w, responsePayload) -} \ No newline at end of file +} diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go index 7be187fccb50edb243cd4f9d01b6a9041ebdd170..bfd86033b6d3150080088e3e9fc3999f09d2a5b5 100644 --- a/middleware/cors/cors.go +++ b/middleware/cors/cors.go @@ -10,7 +10,7 @@ type CorsMiddleware struct{} var corsHandler = cors.Handler(cors.Options{ AllowedOrigins: []string{"http://*", "https://*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowedHeaders: []string{"*"}, ExposedHeaders: []string{"Link"}, AllowCredentials: false, diff --git a/middleware/di.go b/middleware/di.go index 3e73df4ca24a398c120b42551be10f5b5d1f030c..d83e52f49c9e67f0ad90f29e745108cc6ecb6121 100644 --- a/middleware/di.go +++ b/middleware/di.go @@ -4,6 +4,7 @@ import ( "github.com/google/wire" "gitlab.informatika.org/ocw/ocw-backend/middleware/cleanpath" "gitlab.informatika.org/ocw/ocw-backend/middleware/cors" + "gitlab.informatika.org/ocw/ocw-backend/middleware/guard" "gitlab.informatika.org/ocw/ocw-backend/middleware/log" "gitlab.informatika.org/ocw/ocw-backend/middleware/recoverer" "gitlab.informatika.org/ocw/ocw-backend/middleware/trailslash" @@ -24,6 +25,8 @@ var middlewareCollectionSet = wire.NewSet( // Trailslash wire.Struct(new(trailslash.TrailSlashMiddleware), "*"), + + guard.NewBuilder, ) var MiddlewareSet = wire.NewSet( diff --git a/middleware/guard/builder.go b/middleware/guard/builder.go new file mode 100644 index 0000000000000000000000000000000000000000..75850ebf45a8ef08662c76311156cd2a067a4ff3 --- /dev/null +++ b/middleware/guard/builder.go @@ -0,0 +1,43 @@ +package guard + +import ( + "net/http" + + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/token" + "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" +) + +type GuardBuilder struct { + GuardMiddleware +} + +func NewBuilder( + token token.TokenUtil, + logger logger.Logger, + wrapper wrapper.WrapperUtil, +) *GuardBuilder { + return &GuardBuilder{ + GuardMiddleware{ + Token: token, + Role: []user.UserRole{}, + Logger: logger, + WrapperUtil: wrapper, + }, + } +} + +func (g *GuardBuilder) AddRole(role ...user.UserRole) *GuardBuilder { + g.GuardMiddleware.Role = role + return g +} + +func (g *GuardBuilder) Build() func(http.Handler) http.Handler { + return g.GuardMiddleware.Handle +} + +func (g *GuardBuilder) BuildSimple(role user.UserRole) func(http.Handler) http.Handler { + g.AddRole(role) + return g.Build() +} diff --git a/middleware/guard/guard.go b/middleware/guard/guard.go new file mode 100644 index 0000000000000000000000000000000000000000..3b71984a870188559f7345793d786ccfce0f1e6d --- /dev/null +++ b/middleware/guard/guard.go @@ -0,0 +1,93 @@ +package guard + +import ( + "context" + "encoding/json" + "net/http" + "strings" + + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + authToken "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/token" + "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" +) + +type GuardMiddleware struct { + Token token.TokenUtil + Role []user.UserRole + Logger logger.Logger + wrapper.WrapperUtil +} + +type ContextKey string + +const UserContext ContextKey = "user_claim" + +func (g GuardMiddleware) Handle(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + if len(g.Role) > 0 { + authorization := r.Header.Get("Authorization") + + tokenSplit := strings.Split(authorization, " ") + + if len(tokenSplit) != 2 { + g.Logger.Info("Unauthorized access detected") + + w.WriteHeader(http.StatusBadRequest) + payload := g.WrapperUtil.ErrorResponseWrap("authorization is required", nil) + + parser := json.NewEncoder(w) + parser.Encode(payload) + return + } + + if tokenSplit[0] != "Bearer" { + w.WriteHeader(http.StatusUnprocessableEntity) + payload := g.WrapperUtil.ErrorResponseWrap("authorization must be bearer token", nil) + + parser := json.NewEncoder(w) + parser.Encode(payload) + return + } + + tokenString := tokenSplit[1] + claim, err := g.Token.Validate(tokenString, authToken.Access) + + if err != nil { + g.Logger.Info("Invalid token request") + parser := json.NewEncoder(w) + + w.WriteHeader(http.StatusUnauthorized) + payload := g.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + parser.Encode(payload) + return + } + + isAuthorized := false + + for _, user := range g.Role { + if user == claim.Role { + isAuthorized = true + } + } + + if !isAuthorized { + g.Logger.Info("Unauthorized user access") + parser := json.NewEncoder(w) + + w.WriteHeader(http.StatusForbidden) + payload := g.WrapperUtil.ErrorResponseWrap("current user role is prohibited to access this resources", nil) + parser.Encode(payload) + return + } + + ctx := context.WithValue(r.Context(), UserContext, claim) + next.ServeHTTP(w, r.WithContext(ctx)) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/model/domain/cache/cache.go b/model/domain/cache/cache.go new file mode 100644 index 0000000000000000000000000000000000000000..dc0d3361389b8dc39be807a61dbe54d890fa5577 --- /dev/null +++ b/model/domain/cache/cache.go @@ -0,0 +1,60 @@ +package cache + +import "fmt" + +type String struct { + Key Key + Value string + ExpiryInMinutes int +} + +type Hash struct { + Key Key + Values []Value + ExpiryInMinutes int +} + +type Key struct { + Hash string + Id string +} + +type Value struct { + Field string + Store string +} + +func (c *Hash) AppendValue(value Value) *Hash { + c.Values = append(c.Values, value) + return c +} + +func NewKey(hash string, id string) *Key { + return &Key{hash, id} +} + +func (k Key) String() string { + return fmt.Sprintf("%s:%s", k.Hash, k.Id) +} + +func NewValue(field string, store string) *Value { + return &Value{field, store} +} + +func NewString(key Key, value string, expiryInMinutes int) *String { + return &String{key, value, expiryInMinutes} +} + +func NewHash(key Key, initValue Value, expiryInMinutes int) *Hash { + return &Hash{key, []Value{initValue}, expiryInMinutes} +} + +func (c *Hash) Slice() []interface{} { + slice := make([]interface{}, len(c.Values)*2+1) + slice[0] = c.Key.String() + for i := range make([]int, len(c.Values)) { + slice[i*2+1] = c.Values[i].Field + slice[i*2+2] = c.Values[i].Store + } + return slice +} diff --git a/model/domain/course/course.go b/model/domain/course/course.go new file mode 100644 index 0000000000000000000000000000000000000000..3d11918d8bea39471ab99306e5dfda5f5d9de3a1 --- /dev/null +++ b/model/domain/course/course.go @@ -0,0 +1,41 @@ +package course + +import "github.com/google/uuid" + +// TODO: Abbreviations should be unique constrainted as identifiers +type Faculty struct { + ID uuid.UUID `gorm:"primaryKey"` + Name string + Abbreviation string +} + +type Major struct { + ID uuid.UUID `gorm:"primaryKey;type:uuid"` + Name string + Fac_id uuid.UUID `gorm:"type:uuid"` + Faculty Faculty `gorm:"foreignKey:Fac_id"` + Abbreviation string +} + +type Course struct { + ID string `gorm:"primaryKey"` + Name string + Major_id uuid.UUID `gorm:"type:uuid"` + Major Major `gorm:"foreignKey:Major_id"` + Description string + Email string + Abbreviation string + Lecturer string +} + +func (Faculty) TableName() string { + return "faculty" +} + +func (Major) TableName() string { + return "major" +} + +func (Course) TableName() string { + return "course" +} diff --git a/model/domain/material/content.go b/model/domain/material/content.go new file mode 100644 index 0000000000000000000000000000000000000000..0e181045ba1141a363e07b196bf985c9fc0f4adf --- /dev/null +++ b/model/domain/material/content.go @@ -0,0 +1,14 @@ +package material + +import "github.com/google/uuid" + +type Content struct { + Id uuid.UUID `gorm:"primaryKey"` + Type MaterialType + Link string + MaterialId uuid.UUID +} + +func (Content) TableName() string { + return "material_data" +} diff --git a/model/domain/material/material.go b/model/domain/material/material.go new file mode 100644 index 0000000000000000000000000000000000000000..55d6bfbf7d002af41a7edc6bdb50beed800650a0 --- /dev/null +++ b/model/domain/material/material.go @@ -0,0 +1,20 @@ +package material + +import ( + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/course" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" +) + +type Material struct { + Id uuid.UUID `gorm:"primaryKey"` + CourseId string + CreatorEmail string + Creator user.User `gorm:"foreignKey:CreatorEmail;references:Email"` + Course course.Course `gorm:"foreignKey:CourseId;references:Id"` + Contents []Content `gorm:"foreignKey:MaterialId;references:Id"` +} + +func (Material) TableName() string { + return "material" +} diff --git a/model/domain/material/material_type.go b/model/domain/material/material_type.go new file mode 100644 index 0000000000000000000000000000000000000000..10e8abdc730e658f28f99da007bb755fc6896795 --- /dev/null +++ b/model/domain/material/material_type.go @@ -0,0 +1,71 @@ +package material + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +type MaterialType int + +const ( + Video MaterialType = iota + Handout + External +) + +var roleMapping = map[MaterialType]string{ + Video: "video", + Handout: "handout", + External: "external", +} + +func (ur *MaterialType) Scan(value interface{}) error { + val := value.(string) + + for key, label := range roleMapping { + if label == val { + *ur = key + return nil + } + } + + return fmt.Errorf("invalid user role") +} + +func (u MaterialType) Value() (driver.Value, error) { + value, ok := roleMapping[u] + + if !ok { + return nil, fmt.Errorf("invalid user role") + } + + return value, nil +} + +func (u *MaterialType) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + for key, label := range roleMapping { + if label == s { + *u = key + return nil + } + } + + return fmt.Errorf("unkown role, given %s", s) +} + +func (u MaterialType) MarshalJSON() ([]byte, error) { + s, ok := roleMapping[u] + + if !ok { + return nil, errors.New("unkown user role") + } + + return json.Marshal(s) +} diff --git a/model/domain/quiz/options.go b/model/domain/quiz/options.go new file mode 100644 index 0000000000000000000000000000000000000000..de062c3cc531f34b4572016248e2085a2d1f1d99 --- /dev/null +++ b/model/domain/quiz/options.go @@ -0,0 +1,14 @@ +package quiz + +import "github.com/google/uuid" + +type AnswerOption struct { + Id uuid.UUID `gorm:"primaryKey"` + QuizProblemId uuid.UUID `gorm:"primaryKey"` + Statement string + IsAnswer bool +} + +func (AnswerOption) TableName() string { + return "quiz_choice_answer" +} diff --git a/model/domain/quiz/problem_type.go b/model/domain/quiz/problem_type.go new file mode 100644 index 0000000000000000000000000000000000000000..c03621526c635dc524f1caee1518665931b80d10 --- /dev/null +++ b/model/domain/quiz/problem_type.go @@ -0,0 +1,67 @@ +package quiz + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +type ProblemType int + +const ( + Choice ProblemType = iota +) + +var roleMapping = map[ProblemType]string{ + Choice: "choice", +} + +func (ur *ProblemType) Scan(value interface{}) error { + val := value.(string) + + for key, label := range roleMapping { + if label == val { + *ur = key + return nil + } + } + + return fmt.Errorf("invalid user role") +} + +func (u ProblemType) Value() (driver.Value, error) { + value, ok := roleMapping[u] + + if !ok { + return nil, fmt.Errorf("invalid user role") + } + + return value, nil +} + +func (u *ProblemType) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + for key, label := range roleMapping { + if label == s { + *u = key + return nil + } + } + + return fmt.Errorf("unkown role, given %s", s) +} + +func (u ProblemType) MarshalJSON() ([]byte, error) { + s, ok := roleMapping[u] + + if !ok { + return nil, errors.New("unkown user role") + } + + return json.Marshal(s) +} diff --git a/model/domain/quiz/quiz.go b/model/domain/quiz/quiz.go new file mode 100644 index 0000000000000000000000000000000000000000..6caade417ae857ec2a78fdf8c13b5ef007a8529a --- /dev/null +++ b/model/domain/quiz/quiz.go @@ -0,0 +1,21 @@ +package quiz + +import ( + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/course" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" +) + +type Quiz struct { + Id uuid.UUID `gorm:"primaryKey"` + Name string + CourseId string + CreatorEmail string + Creator user.User `gorm:"foreignKey:CreatorEmail;references:Email"` + Course course.Course `gorm:"foreignKey:CourseId;references:Id"` + Problems []QuizProblem `gorm:"foreignKey:QuizId;references:Id"` +} + +func (Quiz) TableName() string { + return "quiz" +} diff --git a/model/domain/quiz/quiz_problem.go b/model/domain/quiz/quiz_problem.go new file mode 100644 index 0000000000000000000000000000000000000000..3ce4195465d2fd477567f18858296d949c60020c --- /dev/null +++ b/model/domain/quiz/quiz_problem.go @@ -0,0 +1,15 @@ +package quiz + +import "github.com/google/uuid" + +type QuizProblem struct { + Id uuid.UUID `gorm:"primaryKey"` + Statement string + Type ProblemType + QuizId uuid.UUID + Options []AnswerOption `gorm:"foreignKey:QuizProblemId;references:Id"` +} + +func (QuizProblem) TableName() string { + return "quiz_problem" +} diff --git a/model/domain/quiz/take.go b/model/domain/quiz/take.go new file mode 100644 index 0000000000000000000000000000000000000000..c66263f60f1e4009019e5dcd428e7c905784f243 --- /dev/null +++ b/model/domain/quiz/take.go @@ -0,0 +1,24 @@ +package quiz + +import ( + "os/user" + "time" + + "github.com/google/uuid" +) + +type QuizTake struct { + Id uuid.UUID `gorm:"primaryKey"` + QuizId uuid.UUID + Email string + StartTime time.Time + IsFinished bool + Score int + Quiz `gorm:"foreignKey:QuizId;references:Id"` + user.User `gorm:"foreignKey:Email;references:Email"` + ChoiceAnswers []TakeChoiceAnswer `gorm:"foreignKey:QuizTakeId;references:Id"` +} + +func (QuizTake) TableName() string { + return "quiz_take" +} diff --git a/model/domain/quiz/take_choice_answer.go b/model/domain/quiz/take_choice_answer.go new file mode 100644 index 0000000000000000000000000000000000000000..25e76746d8951251d3dbdee694986205ce364d53 --- /dev/null +++ b/model/domain/quiz/take_choice_answer.go @@ -0,0 +1,14 @@ +package quiz + +import "github.com/google/uuid" + +type TakeChoiceAnswer struct { + QuizTakeId uuid.UUID `gorm:"primaryKey"` + AnswerChoice uuid.UUID + QuizProblemId uuid.UUID `gorm:"primaryKey"` + AnswerOption `gorm:"foreignKey:AnswerChoice,QuizProblemId;references:Id,QuizProblemId"` +} + +func (TakeChoiceAnswer) TableName() string { + return "quiz_take_choice_answer" +} diff --git a/model/domain/user/verification.go b/model/domain/user/verification.go new file mode 100644 index 0000000000000000000000000000000000000000..1d0d04925c079c1f18de8fdb125595aa6ee937f7 --- /dev/null +++ b/model/domain/user/verification.go @@ -0,0 +1,7 @@ +package user + +type EmailData struct { + Email string + BaseUrl string + Token string +} diff --git a/model/web/admin/addUser/request.go b/model/web/admin/addUser/request.go new file mode 100644 index 0000000000000000000000000000000000000000..c08f743abc16843b3c0ca60c3725ac24d348db05 --- /dev/null +++ b/model/web/admin/addUser/request.go @@ -0,0 +1,17 @@ +package admin + +// AdminAddUserPayload Request Payload +// @Description Information that should be available when admin add user + +// TODO: find a way to make default password for new user + +type AdminAddUserPayload struct { + // User name + Name string `json:"name" validate:"required" example:"someone"` + + // User Email + Email string `json:"email" validate:"required,email" example:"someone@example.com"` + + // User Role + Role string `json:"role" validate:"required" example:"admin"` +} diff --git a/model/web/admin/updateUser/request.go b/model/web/admin/updateUser/request.go new file mode 100644 index 0000000000000000000000000000000000000000..437d35fa7e7f5f415fea0ab49ec84f09c246ff7e --- /dev/null +++ b/model/web/admin/updateUser/request.go @@ -0,0 +1,15 @@ +package admin + +// AdminUpdateUserPayload Request Payload +// @Description Information that should be available when admin update user + +type AdminUpdateUserPayload struct { + // User name + Name string `json:"name" validate:"required" example:"someone"` + + // User Email + Email string `json:"email" validate:"required,email" example:"someone@example.com"` + + // User Role + Role string `json:"role" validate:"required" example:"admin"` +} diff --git a/model/web/auth/verification/request.go b/model/web/auth/verification/request.go new file mode 100644 index 0000000000000000000000000000000000000000..dfddab4ba7adb0585a2147daa85ecfbfce77b25e --- /dev/null +++ b/model/web/auth/verification/request.go @@ -0,0 +1,12 @@ +package verification + +// Email Verification Request Payload +// @Description Information that should be passed when request verify +type VerificationSendRequestPayload struct { + // User Email + Email string `json:"email" validate:"required,email" example:"someone@example.com"` +} + +type VerificationRequestPayload struct { + Id string `json:"id" validate:"required" example:"6ba7b812-9dad-11d1-80b4-00c04fd430c8"` +} diff --git a/model/web/course/README.md b/model/web/course/README.md new file mode 100644 index 0000000000000000000000000000000000000000..98c617f8e3fc02fe20a2b095382b35135357a15f --- /dev/null +++ b/model/web/course/README.md @@ -0,0 +1,28 @@ +### Routes + +- Getting: +course/major - Get all majors +course/major/{id} - Get major with id +course/major/courses/{id} - Get all courses in major with id +course/faculty - Get all faculties +course/faculty/{id} - Get faculty with id +course/faculty/courses/{id} - Get all courses in faculty with id +course/faculty/majors/{id} - Get all majors in faculty with id +course - get all courses +course/{id} - get course with id + +- Adding: +course/major - add a major +course/faculty - add a faculty +course - add a course + +- Updating: +course/major/{id} - update a major +course/faculty/{id} - update a faculty +course/{id} - update a course + +- Deleting: +course/{id} - delete course with id + +### Cannot Delete Majors/Faculties ### +Notes: ID is interchangeable with abbreviation (has OR relationship) \ No newline at end of file diff --git a/model/web/course/add/request.go b/model/web/course/add/request.go new file mode 100644 index 0000000000000000000000000000000000000000..0b86f3ab6618ef4ec1f7f299b0fd45c81bfb61dc --- /dev/null +++ b/model/web/course/add/request.go @@ -0,0 +1,31 @@ +package add + +import "github.com/google/uuid" + +// AddCourse Request Payload +// @Description Information that should be available when you add a course +type AddCourseRequestPayload struct { + // Web Token that was appended to the link + AddCourseToken string + + // Course ID + ID string `json:"id" validate:"required"` + + // Course Name + Name string `json:"name" validate:"required"` + + // Course Major Abbreviation + MajAbbr string `json:"majabbr" validate:"required_without=MajorID"` + + // Major Id, will be set by the server + MajorID uuid.UUID `json:"major_id"` + + // Course Description (Can be left empty) + Description string `json:"description"` + + // Contributor Email + Email string `json:"email" validate:"required,email" example:"someone@example.com"` + + // Course Name Abbreviation + Abbreviation string `json:"abbreviation" validate:"required"` +} diff --git a/model/web/course/delete/request.go b/model/web/course/delete/request.go new file mode 100644 index 0000000000000000000000000000000000000000..fdfcdbc2809b1e2ad428cae959569dc84703b6d3 --- /dev/null +++ b/model/web/course/delete/request.go @@ -0,0 +1,11 @@ +package delete + +// DeleteCourse Request Payload +// @Description Information that should be available when you delete using course id (string) +type DeleteByStringRequestPayload struct { + // Web Token that was appended to the link + DeleteCourseToken string + + // Course ID, provided by query + ID string +} diff --git a/model/web/course/faculty/add/request.go b/model/web/course/faculty/add/request.go new file mode 100644 index 0000000000000000000000000000000000000000..fb7c2da86ab96c2c326748d03704ef7c62e07d39 --- /dev/null +++ b/model/web/course/faculty/add/request.go @@ -0,0 +1,14 @@ +package add + +// AddFaculty Request Payload +// @Description Information that should be available when you add a faculty +type AddFacultyRequestPayload struct { + // Web Token that was appended to the link + AddFacultyToken string + + // Faculty Name + Name string `json:"name" validate:"required"` + + // Faculty Name Abbreviation + Abbreviation string `json:"abbreviation" validate:"required"` +} diff --git a/model/web/course/faculty/update/request.go b/model/web/course/faculty/update/request.go new file mode 100644 index 0000000000000000000000000000000000000000..38f04860abe184ab7e61182c538c8a4b39003885 --- /dev/null +++ b/model/web/course/faculty/update/request.go @@ -0,0 +1,19 @@ +package update + +import "github.com/google/uuid" + +// UpdateFaculty Request Payload +// @Description Information that should be available when you update a faculty +type UpdateFacultyRequestPayload struct { + // Web Token that was appended to the link + UpdateFacultyToken string + + // Faculty ID, Provided by Query + ID uuid.UUID + + // Faculty Name + Name string `json:"name" validate:"required"` + + // Faculty Name Abbreviation + Abbreviation string `json:"abbreviation" validate:"required"` +} diff --git a/model/web/course/get/request.go b/model/web/course/get/request.go new file mode 100644 index 0000000000000000000000000000000000000000..7f1853ea4ad2051e145d01cb86b45f68c7804f44 --- /dev/null +++ b/model/web/course/get/request.go @@ -0,0 +1,17 @@ +package get + +import "github.com/google/uuid" + +// GetCourse 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 +} + +// GetCourse 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 +} \ No newline at end of file diff --git a/model/web/course/major/add/request.go b/model/web/course/major/add/request.go new file mode 100644 index 0000000000000000000000000000000000000000..fbc207644da74f83915b6ac16456d03b9a8efd1d --- /dev/null +++ b/model/web/course/major/add/request.go @@ -0,0 +1,22 @@ +package add + +import "github.com/google/uuid" + +// AddMajor Request Payload +// @Description Information that should be available when you add a major +type AddMajorRequestPayload struct { + // Web Token that was appended to the link + AddMajorToken string + + // Major Name + Name string `json:"name" validate:"required"` + + // Major Faculty Abbreviation + FacAbbr string `json:"facabbr" validate:"required_without=FacultyID"` + + // Faculty Id, will be set by the server + FacultyID uuid.UUID `json:"faculty_id"` + + // Major Name Abbreviation + Abbreviation string `json:"abbreviation" validate:"required"` +} diff --git a/model/web/course/major/update/request.go b/model/web/course/major/update/request.go new file mode 100644 index 0000000000000000000000000000000000000000..b024427a8fdc6ed1d3851d10be75bec67a40aa71 --- /dev/null +++ b/model/web/course/major/update/request.go @@ -0,0 +1,25 @@ +package update + +import "github.com/google/uuid" + +// UpdateMajor Request Payload +// @Description Information that should be available when you update a major +type UpdateMajorRequestPayload struct { + // Web Token that was appended to the link + UpdateMajorToken string + + // Major ID, provided by query + ID uuid.UUID + + // Major Name + Name string `json:"name" validate:"required"` + + // Major Faculty Abbreviation + FacAbbr string `json:"facabbr" validate:"required_without=FacultyID"` + + // Faculty Id, will be set by the server + FacultyID uuid.UUID `json:"faculty_id"` + + // Major Name Abbreviation + Abbreviation string `json:"abbreviation" validate:"required"` +} diff --git a/model/web/course/update/request.go b/model/web/course/update/request.go new file mode 100644 index 0000000000000000000000000000000000000000..59d2b4c9f91503df99ca15808f7b73d2412e8e07 --- /dev/null +++ b/model/web/course/update/request.go @@ -0,0 +1,34 @@ +package update + +import "github.com/google/uuid" + +// UpdateCourse Request Payload +// @Description Information that should be available when you add a course +type UpdateCourseRequestPayload struct { + // Web Token that was appended to the link + UpdateCourseToken string + + // Course ID, Provided by query + ID string + + // Course Name + Name string `json:"name" validate:"required"` + + // Course Major Abbreviation + MajAbbr string `json:"majabbr" validate:"required_without=MajorID"` + + // Major Id, will be set by the server + MajorID uuid.UUID `json:"major_id"` + + // Course Description (Can be left empty) + Description string `json:"description"` + + // Contributor Email + Email string `json:"email" validate:"required,email" example:"someone@example.com"` + + // Course Name Abbreviation + Abbreviation string `json:"abbreviation" validate:"required"` + + // Course Lecturer + Lecturer string `json:"lecturer" validate:"required"` +} diff --git a/model/web/error_code.go b/model/web/error_code.go index e610d20e8224716d3e4ff4ea6daa8c66948eb093..9b29b2d14e9caac4d013f7474592e25ff70b66d5 100644 --- a/model/web/error_code.go +++ b/model/web/error_code.go @@ -9,6 +9,10 @@ const ( EmailExist string = "EMAIL_EXIST" EmailNotExist string = "EMAIL_NOT_EXIST" LinkNotAvailable string = "LINK_NOT_AVAILABLE" + FacultyNotExist string = "FACULTY_NOT_EXIST" + MajorNotExist string = "MAJOR_NOT_EXIST" + CourseNotExist string = "COURSE_NOT_EXIST" + IDExists string = "ID_ALREADY_EXISTS" TokenError string = "TOKEN_ERROR" ) diff --git a/provider/di.go b/provider/di.go index 51e100b166816bee6b5d9c23147f9470831a1b9f..08cd4bf4d7c13a2bbccf9ebae7582afc13f2ed3c 100644 --- a/provider/di.go +++ b/provider/di.go @@ -5,6 +5,8 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/provider/db" "gitlab.informatika.org/ocw/ocw-backend/provider/mail" "gitlab.informatika.org/ocw/ocw-backend/provider/mail/smtp" + "gitlab.informatika.org/ocw/ocw-backend/provider/redis" + "gitlab.informatika.org/ocw/ocw-backend/provider/storage" ) var ProviderTestSet = wire.NewSet( @@ -14,6 +16,14 @@ var ProviderTestSet = wire.NewSet( wire.Bind(new(mail.MailQueue), new(*mail.MailQueueImpl)), wire.Bind(new(mail.MailProvider), new(*smtp.SmtpMailProvider)), + + // Redis utility + wire.Bind(new(redis.Redis), new(*redis.RedisImpl)), + redis.NewRedisEnv, + + // Storage utility + wire.Bind(new(storage.Storage), new(*storage.S3Storage)), + storage.NewS3, ) var ProviderSet = wire.NewSet( diff --git a/provider/mail/smtp/smtp.go b/provider/mail/smtp/smtp.go index dd2873a17083a7381fa3009679552d7dbd121590..cc1d4060a460ed00840c8f52072ee2ee7b20234f 100644 --- a/provider/mail/smtp/smtp.go +++ b/provider/mail/smtp/smtp.go @@ -13,11 +13,9 @@ type SmtpMailProvider struct { } func New(env *env.Environment) *SmtpMailProvider { - auth := smtp.PlainAuth( - env.SmtpIdentity, + auth := smtp.CRAMMD5Auth( env.SmtpUsername, env.SmtpPassword, - env.SmtpServer, ) return &SmtpMailProvider{ @@ -30,6 +28,7 @@ func (s SmtpMailProvider) Send(to []string, subject string, message string) erro payload := fmt.Sprintf( "To: %s\r\n"+ "Subject: %s\r\n"+ + "Content-Type: text/html; charset=UTF-8\r\n"+ "\r\n%s\r\n", to, subject, message, ) diff --git a/provider/redis/cache.go b/provider/redis/cache.go new file mode 100644 index 0000000000000000000000000000000000000000..9d656ae2093191e41cdd8666e7f20f5b2c185a92 --- /dev/null +++ b/provider/redis/cache.go @@ -0,0 +1,84 @@ +package redis + +import ( + "fmt" + "os" + "runtime/debug" + "strings" + "time" + + "github.com/gomodule/redigo/redis" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" +) + +type RedisImpl struct { + pool *redis.Pool +} + +func resolver(log logger.Logger) { + if rec := recover(); rec != nil { + log.Error("Some panic occured when processing request:") + log.Error(fmt.Sprint(rec)) + log.Error("") + + log.Error("Stack Trace:") + stacks := strings.Split(string(debug.Stack()), "\n") + + for _, val := range stacks { + log.Error(val) + } + + os.Exit(-1) + } +} + +func NewRedisEnv( + env *env.Environment, + log logger.Logger, +) (*RedisImpl, error) { + return &RedisImpl{ + &redis.Pool{ + MaxIdle: 3, + 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 + } + + if env.RedisUseAuth { + if _, err := conn.Do("AUTH", env.RedisUsername, env.RedisPassword); err != nil { + conn.Close() + + log.Warning("failed connect to redis server: authentication failed") + + return nil, err + } + } + + return conn, err + }, + TestOnBorrow: func(c redis.Conn, t time.Time) error { + if time.Since(t) < time.Minute { + return nil + } + _, err := c.Do("PING") + + if err != nil { + log.Warning("redis server is down") + } + + return err + }, + }}, nil +} + +func (r RedisImpl) Pool() *redis.Pool { + return r.pool +} diff --git a/provider/redis/type.go b/provider/redis/type.go new file mode 100644 index 0000000000000000000000000000000000000000..c2b4e2c661a7ce9851e8963e107c7fd063f04c26 --- /dev/null +++ b/provider/redis/type.go @@ -0,0 +1,7 @@ +package redis + +import "github.com/gomodule/redigo/redis" + +type Redis interface { + Pool() *redis.Pool +} \ No newline at end of file diff --git a/provider/storage/manager.go b/provider/storage/manager.go new file mode 100644 index 0000000000000000000000000000000000000000..cc87edd73acda90734470345e5fd3ea1230a1280 --- /dev/null +++ b/provider/storage/manager.go @@ -0,0 +1,11 @@ +package storage + +import ( + "context" + + "github.com/minio/minio-go/v7" +) + +func (s S3Storage) Delete(ctx context.Context, path string) error { + return s.minio.RemoveObject(ctx, s.env.BucketName, path, minio.RemoveObjectOptions{}) +} diff --git a/provider/storage/s3.go b/provider/storage/s3.go new file mode 100644 index 0000000000000000000000000000000000000000..3f746712f3fa34c1ae77ea87fe4491282e4a6609 --- /dev/null +++ b/provider/storage/s3.go @@ -0,0 +1,30 @@ +package storage + +import ( + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" +) + +type S3Storage struct { + env *env.Environment + minio *minio.Client +} + +func NewS3( + env *env.Environment, +) (*S3Storage, error) { + client, err := minio.New(env.BucketEndpoint, &minio.Options{ + Creds: credentials.NewStaticV4(env.BucketAccessKey, env.BucketSecretKey, env.BucketTokenKey), + Secure: env.BucketUseSSL, + }) + + if err != nil { + return nil, err + } + + return &S3Storage{ + minio: client, + env: env, + }, nil +} diff --git a/provider/storage/signedlink.go b/provider/storage/signedlink.go new file mode 100644 index 0000000000000000000000000000000000000000..2ebf9f28b5b59a6b0d6a42c44aeeec46ce6b8ce5 --- /dev/null +++ b/provider/storage/signedlink.go @@ -0,0 +1,38 @@ +package storage + +import ( + "context" + "net/url" + "time" +) + +func (s S3Storage) CreatePutSignedLink(ctx context.Context, path string) (string, error) { + url, err := s.minio.PresignedPutObject( + ctx, + s.env.BucketName, + path, + time.Duration(s.env.BucketSignedPutDuration)*time.Second, + ) + + if err != nil { + return "", err + } + + return url.String(), nil +} + +func (s S3Storage) CreateGetSignedLink(ctx context.Context, path string, reqParam url.Values) (string, error) { + url, err := s.minio.PresignedGetObject( + ctx, + s.env.BucketName, + path, + time.Duration(s.env.BucketSignedGetDuration)*time.Second, + reqParam, + ) + + if err != nil { + return "", err + } + + return url.String(), nil +} diff --git a/provider/storage/type.go b/provider/storage/type.go new file mode 100644 index 0000000000000000000000000000000000000000..dc7cdd2f97597d8760d26aa2b75791989a12ff9f --- /dev/null +++ b/provider/storage/type.go @@ -0,0 +1,12 @@ +package storage + +import ( + "context" + "net/url" +) + +type Storage interface { + CreatePutSignedLink(ctx context.Context, path string) (string, error) + CreateGetSignedLink(ctx context.Context, path string, reqParam url.Values) (string, error) + Delete(ctx context.Context, path string) error +} diff --git a/repository/cache/cache.go b/repository/cache/cache.go new file mode 100644 index 0000000000000000000000000000000000000000..724e4585ec8e0b3c8f60f2be22f0cf625a0d5908 --- /dev/null +++ b/repository/cache/cache.go @@ -0,0 +1,141 @@ +package cache + +import ( + "github.com/gomodule/redigo/redis" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/cache" + rd "gitlab.informatika.org/ocw/ocw-backend/provider/redis" +) + +type CacheRepositoryImpl struct { + pool *redis.Pool +} + +func New( + cache rd.Redis, +) *CacheRepositoryImpl { + return &CacheRepositoryImpl{cache.Pool()} +} + +func (c CacheRepositoryImpl) Get(key cache.Key) (string, error) { + conn := c.pool.Get() + defer conn.Close() + + value, err := redis.String(conn.Do("GET", key)) + + if err != nil { + return "", err + } + + return value, nil +} + +func (c CacheRepositoryImpl) GetInteger(key cache.Key) (int64, error) { + conn := c.pool.Get() + defer conn.Close() + + value, err := redis.Int64(conn.Do("GET", key)) + + if err != nil { + return 0, err + } + + return value, err +} + +func (c CacheRepositoryImpl) Incr(key string, expr int64) error { + conn := c.pool.Get() + defer conn.Close() + + value, err := redis.Int64(conn.Do("INCR", key)) + + if err != nil { + return err + } + + if value == 1 && expr > 0 { + _, err := conn.Do("EXPIRE", key, expr) + + if err != nil { + return err + } + } + + return nil +} + +func (c CacheRepositoryImpl) Delete(key string) error { + conn := c.pool.Get() + defer conn.Close() + + _, err := conn.Do("DEL", key) + return err +} + +func (c CacheRepositoryImpl) Set(str cache.String) error { + conn := c.pool.Get() + defer conn.Close() + + _, err := conn.Do("SET", str.Key, str.Value) + + if err != nil { + return err + } + + if str.ExpiryInMinutes > 0 { + _, err = conn.Do("EXPIRE", str.Key, str.ExpiryInMinutes*60) + + if err != nil { + return err + } + } + + return nil +} + +func (c CacheRepositoryImpl) HGet(cache cache.Hash, field string) (string, error) { + conn := c.pool.Get() + defer conn.Close() + + value, err := redis.String(conn.Do("HGET", cache.Key.String(), field)) + + if err != nil { + return "", err + } + + return value, nil +} + +func (c CacheRepositoryImpl) HGetAll(cache cache.Hash) (map[string]string, error) { + conn := c.pool.Get() + defer conn.Close() + + value, err := redis.StringMap(conn.Do("HGETALL", cache.Key.String())) + + if err != nil { + return nil, err + } + + return value, nil +} + +func (c CacheRepositoryImpl) HSet(cache cache.Hash) error { + conn := c.pool.Get() + defer conn.Close() + + slice := cache.Slice() + _, err := conn.Do("HSET", slice...) + + if err != nil { + return err + } + + if cache.ExpiryInMinutes > 0 { + _, err = conn.Do("EXPIRE", cache.Key, cache.ExpiryInMinutes*60) + + if err != nil { + return err + } + } + + return nil +} diff --git a/repository/cache/type.go b/repository/cache/type.go new file mode 100644 index 0000000000000000000000000000000000000000..d482986d93093b7dc4435cfbd8880af9a8e19b1d --- /dev/null +++ b/repository/cache/type.go @@ -0,0 +1,16 @@ +package cache + +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/cache" +) + +type CacheRepository interface { + Get(key cache.Key) (string, error) + GetInteger(key cache.Key) (int64, error) + Set(str cache.String) error + Delete(key string) error + HGet(cache cache.Hash, field string) (string, error) + HGetAll(cache cache.Hash) (map[string]string, error) + HSet(cache cache.Hash) error + Incr(key string, expr int64) error +} diff --git a/repository/content/impl.go b/repository/content/impl.go new file mode 100644 index 0000000000000000000000000000000000000000..ca06fd4007d71037180debdb94bd1aa6566b0d67 --- /dev/null +++ b/repository/content/impl.go @@ -0,0 +1,27 @@ +package content + +import ( + "context" + "net/url" + + "gitlab.informatika.org/ocw/ocw-backend/provider/storage" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" +) + +type ContentRepositoryImpl struct { + storage.Storage + *env.Environment +} + +func (c ContentRepositoryImpl) GenerateNewLink(ctx context.Context, path string) (string, error) { + return c.Storage.CreatePutSignedLink(ctx, path) +} + +func (c ContentRepositoryImpl) GetLink(ctx context.Context, path string) (string, error) { + res, err := c.Storage.CreateGetSignedLink(ctx, path, url.Values{}) + return res, err +} + +func (c ContentRepositoryImpl) Delete(ctx context.Context, path string) error { + return c.Storage.Delete(ctx, path) +} diff --git a/repository/content/type.go b/repository/content/type.go new file mode 100644 index 0000000000000000000000000000000000000000..84f74a0313cfb98fb9934c42b4c13c38ef38b05f --- /dev/null +++ b/repository/content/type.go @@ -0,0 +1,9 @@ +package content + +import "context" + +type ContentRepository interface { + GenerateNewLink(ctx context.Context, path string) (string, error) + GetLink(ctx context.Context, path string) (string, error) + Delete(ctx context.Context, path string) error +} diff --git a/repository/course/course.go b/repository/course/course.go new file mode 100644 index 0000000000000000000000000000000000000000..6b5d06ee1121b393081d5f4421b41d825614a844 --- /dev/null +++ b/repository/course/course.go @@ -0,0 +1,211 @@ +package course + +import ( + "errors" + + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/course" + "gitlab.informatika.org/ocw/ocw-backend/provider/db" + "gorm.io/gorm" +) + +type CourseRepositoryImpl struct { + db *gorm.DB +} + +func New( + db db.Database, +) *CourseRepositoryImpl { + return &CourseRepositoryImpl{db.Connect()} +} + +func (repo CourseRepositoryImpl) IsCourseExist(id string) (bool, error) { + _, err := repo.GetCourse(id) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + + return false, err + } + + return true, nil +} + +func (repo CourseRepositoryImpl) IsMajorExist(id uuid.UUID) (bool, error) { + _, err := repo.GetMajor(id) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + + return false, err + } + + return true, nil +} + +func (repo CourseRepositoryImpl) IsFacultyExist(id uuid.UUID) (bool, error) { + _, err := repo.GetFaculty(id) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + + return false, err + } + + return true, nil +} + +func (repo CourseRepositoryImpl) AddCourse(course course.Course) error { + return repo.db.Create(&course).Error +} + +func (repo CourseRepositoryImpl) AddMajor(major course.Major) error { + return repo.db.Create(&major).Error +} + +func (repo CourseRepositoryImpl) AddFaculty(faculty course.Faculty) error { + return repo.db.Create(&faculty).Error +} + +func (repo CourseRepositoryImpl) GetCourse(id string) (*course.Course, error) { + result := &course.Course{} + err := repo.db.First(result, "id = ?", id).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo CourseRepositoryImpl) GetMajor(id uuid.UUID) (*course.Major, error) { + result := &course.Major{} + err := repo.db.First(result, "id = ?", id).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo CourseRepositoryImpl) GetFaculty(id uuid.UUID) (*course.Faculty, error) { + result := &course.Faculty{} + err := repo.db.First(result, "id = ?", id).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo CourseRepositoryImpl) GetAllCourse() ([]course.Course, error) { + var result []course.Course + err := repo.db.Find(&result).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo CourseRepositoryImpl) GetAllMajor() ([]course.Major, error) { + var result []course.Major + err := repo.db.Find(&result).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo CourseRepositoryImpl) GetAllFaculty() ([]course.Faculty, error) { + var result []course.Faculty + err := repo.db.Find(&result).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo CourseRepositoryImpl) GetAllCourseByMajor(id uuid.UUID) ([]course.Course, error) { + var result []course.Course + err := repo.db.InnerJoins("Major", repo.db.Where(&course.Major{ID: id})).Find(&result).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo CourseRepositoryImpl) GetAllCourseByFaculty(id uuid.UUID) ([]course.Course, error) { + var result []course.Course + err := repo.db.InnerJoins("Faculty", repo.db.Where(&course.Faculty{ID: id})).InnerJoins("Major").Find(&result).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo CourseRepositoryImpl) GetAllMajorByFaculty(id uuid.UUID) ([]course.Major, error) { + var result []course.Major + err := repo.db.InnerJoins("Faculty", repo.db.Where(&course.Faculty{ID: id})).Find(&result).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo CourseRepositoryImpl) UpdateCourse(course course.Course) error { + return repo.db.Save(course).Error +} + +func (repo CourseRepositoryImpl) UpdateMajor(major course.Major) error { + return repo.db.Save(major).Error +} + +func (repo CourseRepositoryImpl) UpdateFaculty(faculty course.Faculty) error { + return repo.db.Save(faculty).Error +} + +func (repo CourseRepositoryImpl) DeleteCourse(id string) error { + return repo.db.Where("id = ?", id).Delete(&course.Course{}).Error +} + +func (repo CourseRepositoryImpl) GetMajorByAbbr(abbr string) (*course.Major, error) { + result := &course.Major{} + err := repo.db.First(result, "abbreviation = ?", abbr).Error + + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo CourseRepositoryImpl) GetFacultyByAbbr(abbr string) (*course.Faculty, error) { + result := &course.Faculty{} + err := repo.db.First(result, "abbreviation = ?", abbr).Error + + if err != nil { + return nil, err + } + + return result, nil +} \ No newline at end of file diff --git a/repository/course/type.go b/repository/course/type.go new file mode 100644 index 0000000000000000000000000000000000000000..252eb470be01f16794cbbb37dc3f85fee1ecfca0 --- /dev/null +++ b/repository/course/type.go @@ -0,0 +1,33 @@ +package course + +import ( + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/course" +) + +type CourseRepository interface { + AddCourse(course course.Course) error + AddMajor(major course.Major) error + AddFaculty(faculty course.Faculty) error + GetCourse(id string) (*course.Course, error) + GetMajor(id uuid.UUID) (*course.Major, error) + GetFaculty(id uuid.UUID) (*course.Faculty, error) + GetAllCourse() ([]course.Course, error) + GetAllMajor() ([]course.Major, error) + GetAllFaculty() ([]course.Faculty, error) + GetAllCourseByMajor(id uuid.UUID) ([]course.Course, error) + GetAllCourseByFaculty(id uuid.UUID) ([]course.Course, error) + GetAllMajorByFaculty(id uuid.UUID) ([]course.Major, error) + UpdateCourse(course course.Course) error + UpdateMajor(major course.Major) error + UpdateFaculty(faculty course.Faculty) error + DeleteCourse(id string) error + IsCourseExist(id string) (bool, error) + IsMajorExist(id uuid.UUID) (bool, error) + IsFacultyExist(id uuid.UUID) (bool, error) + + // Internal Method Only + + GetMajorByAbbr(abbr string) (*course.Major, error) + GetFacultyByAbbr(abbr string) (*course.Faculty, error) +} diff --git a/repository/di.go b/repository/di.go index ccec9594f14c22bb4d6444b2edbb26f54b8a90f3..9136c031788ba4f56d8d3bfe4c46f2864b641571 100644 --- a/repository/di.go +++ b/repository/di.go @@ -3,12 +3,40 @@ package repository import ( "github.com/google/wire" "gitlab.informatika.org/ocw/ocw-backend/repository/user" + "gitlab.informatika.org/ocw/ocw-backend/repository/course" + "gitlab.informatika.org/ocw/ocw-backend/repository/cache" + "gitlab.informatika.org/ocw/ocw-backend/repository/content" + "gitlab.informatika.org/ocw/ocw-backend/repository/material" + "gitlab.informatika.org/ocw/ocw-backend/repository/transaction" ) var RepositoryBasicSet = wire.NewSet( // User Repository user.New, wire.Bind(new(user.UserRepository), new(*user.UserRepositoryImpl)), + + // Course Repository + course.New, + wire.Bind(new(course.CourseRepository), new(*course.CourseRepositoryImpl)), + + // Cache Repository + cache.New, + wire.Bind(new(cache.CacheRepository), new(*cache.CacheRepositoryImpl)), + + material.NewMaterial, + material.NewMaterialContent, + + wire.Struct(new(content.ContentRepositoryImpl), "*"), + + wire.Bind(new(material.MaterialContentRepository), new(*material.MaterialContentRepositoryImpl)), + wire.Bind(new(material.MaterialRepository), new(*material.MaterialRepositoryImpl)), + + wire.Bind(new(content.ContentRepository), new(*content.ContentRepositoryImpl)), + + transaction.New, + transaction.NewBuilder, + wire.Bind(new(transaction.Transaction), new(*transaction.TransactionRepositoryImpl)), + wire.Bind(new(transaction.TransactionBuilder), new(*transaction.TransactionBuilderImpl)), ) var RepositorySet = wire.NewSet( diff --git a/repository/material/content.go b/repository/material/content.go new file mode 100644 index 0000000000000000000000000000000000000000..0f321a1b58f824fecb3bfeb5b5ea5b26c331b434 --- /dev/null +++ b/repository/material/content.go @@ -0,0 +1,74 @@ +package material + +import ( + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/material" + "gitlab.informatika.org/ocw/ocw-backend/repository/transaction" +) + +type MaterialContentRepositoryImpl struct { + builder transaction.TransactionBuilder +} + +func NewMaterialContent( + builder transaction.TransactionBuilder, +) *MaterialContentRepositoryImpl { + return &MaterialContentRepositoryImpl{builder} +} + +func (m MaterialContentRepositoryImpl) New( + materialId uuid.UUID, + materialType material.MaterialType, + link string, +) (uuid.UUID, error) { + return m.NewWithTransaction( + m.builder.Build(), + materialId, + materialType, + link, + ) +} + +func (m MaterialContentRepositoryImpl) NewWithTransaction( + tx transaction.Transaction, + materialId uuid.UUID, + materialType material.MaterialType, + link string) (uuid.UUID, error) { + contentData := material.Content{ + MaterialId: materialId, + Type: materialType, + Link: link, + } + + if err := tx.GetTransaction().Create(&contentData).Error; err != nil { + return uuid.Nil, err + } + + return contentData.Id, nil +} + +func (m MaterialContentRepositoryImpl) GetAll(materialId uuid.UUID) ([]material.Content, error) { + return m.GetAllWithTransaction(m.builder.Build(), materialId) +} +func (m MaterialContentRepositoryImpl) GetAllWithTransaction(tx transaction.Transaction, materialId uuid.UUID) ([]material.Content, error) { + result := []material.Content{} + err := tx.GetTransaction().Where("material_id = ?", materialId).Find(&result).Error + + return result, err +} + +func (m MaterialContentRepositoryImpl) Delete(contentId uuid.UUID) error { + return m.DeleteWithTransaction(m.builder.Build(), contentId) +} + +func (m MaterialContentRepositoryImpl) DeleteWithTransaction(tx transaction.Transaction, contentId uuid.UUID) error { + return tx.GetTransaction().Where("id = ?", contentId).Delete(&material.Content{}).Error +} + +func (m MaterialContentRepositoryImpl) UpdateLink(contentId uuid.UUID, link string) error { + return m.UpdateLinkWithTransaction(m.builder.Build(), contentId, link) +} + +func (m MaterialContentRepositoryImpl) UpdateLinkWithTransaction(tx transaction.Transaction, contentId uuid.UUID, link string) error { + return tx.GetTransaction().Where("id = ?", contentId).Update("link", link).Error +} diff --git a/repository/material/material.go b/repository/material/material.go new file mode 100644 index 0000000000000000000000000000000000000000..a20281eab9223275befc37993d0a0a3383e9dc86 --- /dev/null +++ b/repository/material/material.go @@ -0,0 +1,55 @@ +package material + +import ( + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/material" + "gitlab.informatika.org/ocw/ocw-backend/repository/transaction" +) + +type MaterialRepositoryImpl struct { + builder transaction.TransactionBuilder +} + +func NewMaterial( + builder transaction.TransactionBuilder, +) *MaterialRepositoryImpl { + return &MaterialRepositoryImpl{builder} +} + +func (m MaterialRepositoryImpl) New(courseId string, creatorEmail string) (uuid.UUID, error) { + return m.NewWithTransaction(m.builder.Build(), courseId, creatorEmail) +} + +func (m MaterialRepositoryImpl) NewWithTransaction(tx transaction.Transaction, courseId string, creatorEmail string) (uuid.UUID, error) { + materialData := &material.Material{ + CourseId: courseId, + CreatorEmail: creatorEmail, + } + + err := tx.GetTransaction().Create(materialData).Error + + if err != nil { + return uuid.Nil, err + } + + return materialData.Id, nil +} + +func (m MaterialRepositoryImpl) Delete(id uuid.UUID) error { + return m.DeleteWithTransaction(m.builder.Build(), id) +} + +func (m MaterialRepositoryImpl) DeleteWithTransaction(tx transaction.Transaction, id uuid.UUID) error { + return tx.GetTransaction().Where("id = ?", id).Delete(&material.Material{}).Error +} + +func (m MaterialRepositoryImpl) GetAll(courseId string) ([]material.Material, error) { + return m.GetAllWithTransaction(m.builder.Build(), courseId) +} + +func (m MaterialRepositoryImpl) GetAllWithTransaction(tx transaction.Transaction, courseId string) ([]material.Material, error) { + result := []material.Material{} + err := tx.GetTransaction().Joins("Contents").Where("CourseId = ?", courseId).Find(&result).Error + + return result, err +} diff --git a/repository/material/type.go b/repository/material/type.go new file mode 100644 index 0000000000000000000000000000000000000000..8343c4df1757d7f4f585efd43fe3cc18e4f325d4 --- /dev/null +++ b/repository/material/type.go @@ -0,0 +1,29 @@ +package material + +import ( + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/material" + "gitlab.informatika.org/ocw/ocw-backend/repository/transaction" +) + +type MaterialRepository interface { + New(courseId string, creatorEmail string) (uuid.UUID, error) + Delete(id uuid.UUID) error + GetAll(courseId string) ([]material.Material, error) + + NewWithTransaction(tx transaction.Transaction, courseId string, creatorEmail string) (uuid.UUID, error) + DeleteWithTransaction(tx transaction.Transaction, id uuid.UUID) error + GetAllWithTransaction(tx transaction.Transaction, courseId string) ([]material.Material, error) +} + +type MaterialContentRepository interface { + New(materialId uuid.UUID, materialType material.MaterialType, link string) (uuid.UUID, error) + GetAll(materialId uuid.UUID) ([]material.Content, error) + Delete(contentId uuid.UUID) error + UpdateLink(contentId uuid.UUID, link string) error + + NewWithTransaction(tx transaction.Transaction, materialId uuid.UUID, materialType material.MaterialType, link string) (uuid.UUID, error) + GetAllWithTransaction(tx transaction.Transaction, materialId uuid.UUID) ([]material.Content, error) + DeleteWithTransaction(tx transaction.Transaction, contentId uuid.UUID) error + UpdateLinkWithTransaction(tx transaction.Transaction, contentId uuid.UUID, link string) error +} diff --git a/repository/transaction/builder.go b/repository/transaction/builder.go new file mode 100644 index 0000000000000000000000000000000000000000..2ec486b728e0861a6af0b9da4c6ad08716791c38 --- /dev/null +++ b/repository/transaction/builder.go @@ -0,0 +1,20 @@ +package transaction + +import ( + "gitlab.informatika.org/ocw/ocw-backend/provider/db" + "gorm.io/gorm" +) + +type TransactionBuilderImpl struct { + db *gorm.DB +} + +func NewBuilder( + db db.Database, +) *TransactionBuilderImpl { + return &TransactionBuilderImpl{db.Connect()} +} + +func (t TransactionBuilderImpl) Build() Transaction { + return New(t.db) +} diff --git a/repository/transaction/impl.go b/repository/transaction/impl.go new file mode 100644 index 0000000000000000000000000000000000000000..ccc27ec1ccb42bc7013516cafdbb4a7b8c83f858 --- /dev/null +++ b/repository/transaction/impl.go @@ -0,0 +1,36 @@ +package transaction + +import "gorm.io/gorm" + +type TransactionRepositoryImpl struct { + db *gorm.DB +} + +func New(db *gorm.DB) *TransactionRepositoryImpl { + return &TransactionRepositoryImpl{db} +} + +func (t *TransactionRepositoryImpl) Begin() { + t.db = t.db.Begin() +} + +func (t *TransactionRepositoryImpl) GetTransaction() *gorm.DB { + return t.db +} + +func (t *TransactionRepositoryImpl) Commit() { + t.db.Commit() +} + +func (t *TransactionRepositoryImpl) Rollback() { + t.db.Rollback() +} + +func (t *TransactionRepositoryImpl) Auto(status *bool) { + if status != nil && *status { + t.Commit() + return + } + + t.Rollback() +} diff --git a/repository/transaction/type.go b/repository/transaction/type.go new file mode 100644 index 0000000000000000000000000000000000000000..de73f66293ff955b7c9191973a6e3d9e91021262 --- /dev/null +++ b/repository/transaction/type.go @@ -0,0 +1,15 @@ +package transaction + +import "gorm.io/gorm" + +type Transaction interface { + Begin() + GetTransaction() *gorm.DB + Commit() + Rollback() + Auto(*bool) +} + +type TransactionBuilder interface { + Build() Transaction +} diff --git a/repository/user/type.go b/repository/user/type.go index 77304dccc4c5ee44ac66d1e77af7d4144970b7ed..c276adc2c9ddb96cbccf32a8aeaf09542dfdbac7 100644 --- a/repository/user/type.go +++ b/repository/user/type.go @@ -2,6 +2,7 @@ package user import ( "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/repository/transaction" ) type UserRepository interface { @@ -11,4 +12,11 @@ type UserRepository interface { Update(user user.User) error Delete(username string) error IsExist(user string) (bool, error) + + AddWithTransaction(tx transaction.Transaction, user user.User) error + GetWithTransaction(tx transaction.Transaction, username string) (*user.User, error) + GetAllWithTransaction(tx transaction.Transaction) ([]user.User, error) + UpdateWithTransaction(tx transaction.Transaction, user user.User) error + DeleteWithTransaction(tx transaction.Transaction, username string) error + IsExistWithTransaction(tx transaction.Transaction, user string) (bool, error) } diff --git a/repository/user/user.go b/repository/user/user.go index f68ccb7032f192d474a767cb485002437151260a..4bcd060691690f314921f9c920bd47993c3abe6d 100644 --- a/repository/user/user.go +++ b/repository/user/user.go @@ -4,21 +4,25 @@ import ( "errors" "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" - "gitlab.informatika.org/ocw/ocw-backend/provider/db" + "gitlab.informatika.org/ocw/ocw-backend/repository/transaction" "gorm.io/gorm" ) type UserRepositoryImpl struct { - db *gorm.DB + builder transaction.TransactionBuilder } func New( - db db.Database, + builder transaction.TransactionBuilder, ) *UserRepositoryImpl { - return &UserRepositoryImpl{db.Connect()} + return &UserRepositoryImpl{builder} } func (repo UserRepositoryImpl) IsExist(email string) (bool, error) { + return repo.IsExistWithTransaction(repo.builder.Build(), email) +} + +func (repo UserRepositoryImpl) IsExistWithTransaction(tx transaction.Transaction, email string) (bool, error) { _, err := repo.Get(email) if err != nil { @@ -33,12 +37,20 @@ func (repo UserRepositoryImpl) IsExist(email string) (bool, error) { } func (repo UserRepositoryImpl) Add(user user.User) error { - return repo.db.Create(&user).Error + return repo.AddWithTransaction(repo.builder.Build(), user) +} + +func (repo UserRepositoryImpl) AddWithTransaction(tx transaction.Transaction, user user.User) error { + return tx.GetTransaction().Create(&user).Error } func (repo UserRepositoryImpl) Get(email string) (*user.User, error) { + return repo.GetWithTransaction(repo.builder.Build(), email) +} + +func (repo UserRepositoryImpl) GetWithTransaction(tx transaction.Transaction, email string) (*user.User, error) { result := &user.User{} - err := repo.db.Where("email = ?", email).First(result).Error + err := tx.GetTransaction().Where("email = ?", email).First(result).Error if err != nil { return nil, err @@ -48,8 +60,11 @@ func (repo UserRepositoryImpl) Get(email string) (*user.User, error) { } func (repo UserRepositoryImpl) GetAll() ([]user.User, error) { + return repo.GetAllWithTransaction(repo.builder.Build()) +} +func (repo UserRepositoryImpl) GetAllWithTransaction(tx transaction.Transaction) ([]user.User, error) { var result []user.User - err := repo.db.Find(&result).Error + err := tx.GetTransaction().Find(&result).Error if err != nil { return nil, err @@ -59,9 +74,17 @@ func (repo UserRepositoryImpl) GetAll() ([]user.User, error) { } func (repo UserRepositoryImpl) Update(user user.User) error { - return repo.db.Save(user).Error + return repo.UpdateWithTransaction(repo.builder.Build(), user) +} + +func (repo UserRepositoryImpl) UpdateWithTransaction(tx transaction.Transaction, user user.User) error { + return tx.GetTransaction().Save(user).Error +} + +func (repo UserRepositoryImpl) Delete(email string) error { + return repo.DeleteWithTransaction(repo.builder.Build(), email) } -func (repo UserRepositoryImpl) Delete(username string) error { - return repo.db.Where("username = ?", username).Delete(&user.User{}).Error +func (repo UserRepositoryImpl) DeleteWithTransaction(tx transaction.Transaction, email string) error { + return tx.GetTransaction().Where("email = ?", email).Delete(&user.User{}).Error } diff --git a/routes/admin/route.go b/routes/admin/route.go index 01f19943524626adb4aea1b73f5a912d4fdee3fa..d1ff589b86faa181e6e671bf7111ab3efed0b239 100644 --- a/routes/admin/route.go +++ b/routes/admin/route.go @@ -3,18 +3,23 @@ package admin import ( "github.com/go-chi/chi/v5" "gitlab.informatika.org/ocw/ocw-backend/handler/admin" + "gitlab.informatika.org/ocw/ocw-backend/middleware/guard" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" ) type AdminRoutes struct { admin.AdminHandler + *guard.GuardBuilder } func (adr AdminRoutes) Register(r chi.Router) { r.Route("/admin", func(r chi.Router) { + r.Use(adr.GuardBuilder.BuildSimple(user.Admin)) + r.Get("/user", adr.AdminHandler.GetAllUser) - r.Get("/user/{id}", adr.AdminHandler.GetUserByEmail) + r.Get("/user/{email}", adr.AdminHandler.GetUserByEmail) r.Post("/user", adr.AdminHandler.AddUser) - r.Patch("/user/{id}", adr.AdminHandler.UpdateUser) - r.Delete("/user/{id}", adr.AdminHandler.DeleteUser) + r.Patch("/user/{email}", adr.AdminHandler.UpdateUser) + r.Delete("/user/{email}", adr.AdminHandler.DeleteUser) }) } diff --git a/routes/auth/route.go b/routes/auth/route.go index d7ea811b69fd604111f9a013b7821a10ef005619..e81bb261069b4abc287c84ec793901ca42a9529e 100644 --- a/routes/auth/route.go +++ b/routes/auth/route.go @@ -14,5 +14,7 @@ func (ar AuthRoutes) Register(r chi.Router) { r.Post("/login", ar.AuthHandler.Login) r.Post("/refresh", ar.AuthHandler.Refresh) r.Post("/register", ar.AuthHandler.Register) + r.Post("/verify/resend", ar.AuthHandler.SendEmailVerify) + r.Post("/verify", ar.AuthHandler.EmailVerify) }) } diff --git a/routes/course/route.go b/routes/course/route.go new file mode 100644 index 0000000000000000000000000000000000000000..6fb5f6ac2019b24687bbc28d593011846bed1057 --- /dev/null +++ b/routes/course/route.go @@ -0,0 +1,38 @@ +package course + +import ( + "github.com/go-chi/chi/v5" + "gitlab.informatika.org/ocw/ocw-backend/handler/course" +) + +type CourseRoutes struct { + course.CourseHandler +} + +func (c CourseRoutes) Register(r chi.Router) { + r.Route("/course", func(r chi.Router) { + // Get + r.Get("/", c.CourseHandler.GetCourses) + r.Get("/{id}", c.CourseHandler.GetCourse) + r.Get("/faculty", c.CourseHandler.GetFaculties) + r.Get("/faculty/{id}", c.CourseHandler.GetFaculty) + r.Get("/faculty/courses/{id}", c.CourseHandler.GetCoursesByFaculty) + r.Get("/faculty/majors/{id}", c.CourseHandler.GetMajorsByFaculty) + r.Get("/major", c.CourseHandler.GetMajors) + r.Get("/major/{id}", c.CourseHandler.GetMajor) + r.Get("/major/courses/{id}", c.CourseHandler.GetCoursesByMajor) + + // Add + r.Put("/", c.CourseHandler.AddCourse) + r.Put("/faculty", c.CourseHandler.AddFaculty) + r.Put("/major", c.CourseHandler.AddMajor) + + // Update + r.Patch("/{id}", c.CourseHandler.UpdateCourse) + r.Patch("/faculty/{id}", c.CourseHandler.UpdateFaculty) + r.Patch("/major/{id}", c.CourseHandler.UpdateMajor) + + // Delete + r.Delete("/{id}", c.CourseHandler.DeleteCourse) + }) +} diff --git a/routes/di.go b/routes/di.go index b44b2ed0ae71dda868755ffdf82ecf3c636a7a1e..a51997ed13bc22c420ab390bcd1c46de58b20bce 100644 --- a/routes/di.go +++ b/routes/di.go @@ -5,6 +5,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/routes/admin" "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/reset" "gitlab.informatika.org/ocw/ocw-backend/routes/swagger" ) @@ -15,6 +16,7 @@ var routesCollectionSet = wire.NewSet( wire.Struct(new(auth.AuthRoutes), "*"), wire.Struct(new(admin.AdminRoutes), "*"), wire.Struct(new(reset.ResetRoutes), "*"), + wire.Struct(new(course.CourseRoutes), "*"), ) var RoutesSet = wire.NewSet( diff --git a/routes/register.go b/routes/register.go index e1dd4a461fcdb80acf500d362e3fe89b083c2151..3a55652c12ea690b9bf40c9b5acf22f9a48e2e9f 100644 --- a/routes/register.go +++ b/routes/register.go @@ -21,7 +21,7 @@ func (app *AppRouter) Register() []RouteGroup { } collections = append(collections, handler) - } + } return collections } diff --git a/routes/reset/route.go b/routes/reset/route.go index 46b32e0f9f389bfe8f3ff730d2c683225dfcee91..c30cdeb5afa69c522fd8f0906d6acc7c9f1f77d8 100644 --- a/routes/reset/route.go +++ b/routes/reset/route.go @@ -12,7 +12,7 @@ type ResetRoutes struct { func (rr ResetRoutes) Register(r chi.Router) { r.Route("/reset", func(r chi.Router) { r.Post("/request", rr.ResetHandler.Request) - r.Post("/confirm", rr.ResetHandler.Confirm) - r.Post("/validate", rr.ResetHandler.Validate) + r.Put("/confirm", rr.ResetHandler.Confirm) + r.Get("/validate", rr.ResetHandler.Validate) }) } diff --git a/routes/routes.go b/routes/routes.go index 4c358d7a37e29d0c2e62fb603f5d5aa70469f061..da4d5b4a4f8efd320ee9b8151f0b8ae4cf4a3757 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -4,6 +4,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/routes/admin" "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/reset" "gitlab.informatika.org/ocw/ocw-backend/routes/swagger" @@ -17,6 +18,7 @@ type AppRouter struct { common.CommonRoutes auth.AuthRoutes reset.ResetRoutes + course.CourseRoutes // Utility Logger logger.Logger diff --git a/service/admin/addUser.go b/service/admin/addUser.go index 644bde9d0547485f4eb3ef45a4876d12327550c6..93e53b0a0cccb8921ec5a00b742c6d1b00df9c93 100644 --- a/service/admin/addUser.go +++ b/service/admin/addUser.go @@ -1,16 +1,29 @@ package admin -// import ( - // "errors" - // "time" +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + req "gitlab.informatika.org/ocw/ocw-backend/model/web/admin/addUser" +) - // "github.com/golang-jwt/jwt/v4" - // "gitlab.informatika.org/ocw/ocw-backend/model/web" - // "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/login" - // tokenModel "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" - // "gorm.io/gorm" -// ) +func (as AdminServiceImpl) AddUser(payload req.AdminAddUserPayload) error { + // change role payload from string to user.UserRole + var role user.UserRole -func (AdminServiceImpl) AddUser() string { - return "add user" + // TODO: move this + if (payload.Role == "admin") { + role = user.Admin + } else if (payload.Role == "contributor") { + role = user.Contributor + } else if (payload.Role == "member") { + role = user.Student + } + + err := as.UserRepository.Add(user.User{ + Email: payload.Email, + Name: payload.Name, + Role: role, + IsActivated: false, + }) + + return err } \ No newline at end of file diff --git a/service/admin/deleteUser.go b/service/admin/deleteUser.go index 0e34cf7227863d1865c5de527b1bfa12806c8573..2ed1a1dc451ed20ce9b9667a0722aa3dbd40cc96 100644 --- a/service/admin/deleteUser.go +++ b/service/admin/deleteUser.go @@ -1,16 +1,6 @@ package admin -// import ( - // "errors" - // "time" - - // "github.com/golang-jwt/jwt/v4" - // "gitlab.informatika.org/ocw/ocw-backend/model/web" - // "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/login" - // tokenModel "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" - // "gorm.io/gorm" -// ) - -func (AdminServiceImpl) DeleteUser() string { - return "delete user" -} +func (as AdminServiceImpl) DeleteUser(email string) error { + err := as.UserRepository.Delete(email) + return err +} \ No newline at end of file diff --git a/service/admin/getUserByEmail.go b/service/admin/getUserByEmail.go index e577536e5acccc895a4861a6845d9cd3b13018d8..3c43845eec62c4fac3ae33486233c8aa118bbe87 100644 --- a/service/admin/getUserByEmail.go +++ b/service/admin/getUserByEmail.go @@ -1,16 +1,11 @@ package admin -// import ( - // "errors" - // "time" +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" +) - // "github.com/golang-jwt/jwt/v4" - // "gitlab.informatika.org/ocw/ocw-backend/model/web" - // "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/login" - // tokenModel "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" - // "gorm.io/gorm" -// ) - -func (AdminServiceImpl) GetUserByEmail() string { - return "get user by email" +func (as AdminServiceImpl) GetUserByEmail(email string) (*user.User, error) { + var users *user.User + users, nil := as.UserRepository.Get(email) + return users, nil } \ No newline at end of file diff --git a/service/admin/impl.go b/service/admin/impl.go index 7bc0dd3364dc3f46e47d282c647bb26380d4a31c..7c1dc75819f6423ab8a5a15678526df81f0de402 100644 --- a/service/admin/impl.go +++ b/service/admin/impl.go @@ -6,4 +6,4 @@ import ( type AdminServiceImpl struct { UserRepository user.UserRepository -} \ No newline at end of file +} diff --git a/service/admin/type.go b/service/admin/type.go index d624c56274802d9fc9523aa092d21f1fed14a314..228b5674efdb4758be6fc0c80585eb698c9a95f2 100644 --- a/service/admin/type.go +++ b/service/admin/type.go @@ -2,12 +2,14 @@ package admin import ( "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + addUser "gitlab.informatika.org/ocw/ocw-backend/model/web/admin/addUser" + updateUser "gitlab.informatika.org/ocw/ocw-backend/model/web/admin/updateUser" ) type AdminService interface { GetAllUser() ([]user.User, error) - GetUserByEmail() string - AddUser() string - UpdateUser() string - DeleteUser() string + GetUserByEmail(email string) (*user.User, error) + AddUser(payload addUser.AdminAddUserPayload) error + UpdateUser(email string, payload updateUser.AdminUpdateUserPayload) error + DeleteUser(email string) error } diff --git a/service/admin/updateUser.go b/service/admin/updateUser.go index e3bce672ae924615b9eb4df9ebda6d12c8afad00..859f7d878195d925aa03852c29155887a099ad79 100644 --- a/service/admin/updateUser.go +++ b/service/admin/updateUser.go @@ -1,16 +1,29 @@ package admin -// import ( - // "errors" - // "time" +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + req "gitlab.informatika.org/ocw/ocw-backend/model/web/admin/updateUser" +) - // "github.com/golang-jwt/jwt/v4" - // "gitlab.informatika.org/ocw/ocw-backend/model/web" - // "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/login" - // tokenModel "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" - // "gorm.io/gorm" -// ) +func (as AdminServiceImpl) UpdateUser(email string, payload req.AdminUpdateUserPayload) error { + // change role payload from string to user.UserRole + var role user.UserRole -func (AdminServiceImpl) UpdateUser() string { - return "update user" + // TODO: move this + if (payload.Role == "admin") { + role = user.Admin + } else if (payload.Role == "contributor") { + role = user.Contributor + } else if (payload.Role == "member") { + role = user.Student + } + + err := as.UserRepository.Update(user.User{ + Email: payload.Email, + Name: payload.Name, + Role: role, // TODO: Change this + IsActivated: false, + }) + + return err } \ No newline at end of file diff --git a/service/auth/register.go b/service/auth/register.go index 6eb423b39900bb2502234c2558b0414a4e50f10c..b7941c50ba99c941bd7810ad82f3c3862f9ef26c 100644 --- a/service/auth/register.go +++ b/service/auth/register.go @@ -6,9 +6,15 @@ import ( ) func (auth AuthServiceImpl) Register(payload register.RegisterRequestPayload) error { - err := auth.UserRepository.Add(user.User{ + hashedPassword, err := auth.Hash(payload.Password) + + if err != nil { + return err + } + + err = auth.UserRepository.Add(user.User{ Email: payload.Email, - Password: payload.Password, + Password: hashedPassword, Name: payload.Name, Role: user.Student, IsActivated: false, diff --git a/service/auth/type.go b/service/auth/type.go index 2503726fe0562e9b2b26544dabae60017d13c386..f2eda67ef0abd2fa3a89f9a8249c8bd88a89af0b 100644 --- a/service/auth/type.go +++ b/service/auth/type.go @@ -4,10 +4,12 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/login" "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/refresh" "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/register" + "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/verification" ) type AuthService interface { Login(payload login.LoginRequestPayload) (*login.LoginResponsePayload, error) Refresh(payload refresh.RefreshRequestPayload) (*refresh.RefreshResponsePayload, error) Register(payload register.RegisterRequestPayload) error + SendVerifyEmail(payload verification.VerificationSendRequestPayload) error } diff --git a/service/auth/verify.go b/service/auth/verify.go new file mode 100644 index 0000000000000000000000000000000000000000..339078a7b6838b6264a9acb5c03b820063de0e47 --- /dev/null +++ b/service/auth/verify.go @@ -0,0 +1,7 @@ +package auth + +import "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/verification" + +func (auth AuthServiceImpl) SendVerifyEmail(payload verification.VerificationSendRequestPayload) error { + return auth.VerificationService.SendVerifyMail(payload.Email) +} diff --git a/service/course/add.go b/service/course/add.go new file mode 100644 index 0000000000000000000000000000000000000000..531d0ec4410bfa1fc3b87de40826ed94d93f3c91 --- /dev/null +++ b/service/course/add.go @@ -0,0 +1,141 @@ +package course + +import ( + "errors" + + "gitlab.informatika.org/ocw/ocw-backend/model/domain/course" + "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" + cadd "gitlab.informatika.org/ocw/ocw-backend/model/web/course/add" + fadd "gitlab.informatika.org/ocw/ocw-backend/model/web/course/faculty/add" + madd "gitlab.informatika.org/ocw/ocw-backend/model/web/course/major/add" + "gorm.io/gorm" +) + +func (c CourseServiceImpl) AddCourse(payload cadd.AddCourseRequestPayload) error { + // Validate Role + claim, err := c.TokenUtil.Validate(payload.AddCourseToken, 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) + } + + // If payload uses major abbreviation, set id + if payload.MajAbbr != "" { + major, err := c.CourseRepository.GetMajorByAbbr(payload.MajAbbr) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseErrorFromError(err, web.CourseNotExist) + } + // Some Uncaught error + return err + } + + payload.MajorID = major.ID + } + + exist, err := c.CourseRepository.IsCourseExist(payload.ID) + + if err != nil { + // Some uncaught error + return err + } + + if exist { + return web.NewResponseError("Course ID Already Exists", web.IDExists) + } + + err = c.CourseRepository.AddCourse(course.Course{ + ID: payload.ID, + Name: payload.Name, + Major_id: payload.MajorID, + Description: payload.Description, + Email: payload.Email, + Abbreviation: payload.Abbreviation, + }) + + if err != nil { + // Some uncaught error + return err + } + + return nil +} + +func (c CourseServiceImpl) AddMajor(payload madd.AddMajorRequestPayload) error { + // Validate Role + claim, err := c.TokenUtil.Validate(payload.AddMajorToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role != user.Admin { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + // If payload uses faculty abbreviation, set id + if payload.FacAbbr != "" { + faculty, err := c.CourseRepository.GetFacultyByAbbr(payload.FacAbbr) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseErrorFromError(err, web.CourseNotExist) + } + // Some Uncaught error + return err + } + + payload.FacultyID = faculty.ID + } + + err = c.CourseRepository.AddMajor(course.Major{ + Name: payload.Name, + Fac_id: payload.FacultyID, + Abbreviation: payload.Abbreviation, + }) + + if err != nil { + // Some uncaught error + return err + } + + return nil +} + +func (c CourseServiceImpl) AddFaculty(payload fadd.AddFacultyRequestPayload) error { + // Validate Role + claim, err := c.TokenUtil.Validate(payload.AddFacultyToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role != user.Admin { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + err = c.CourseRepository.AddFaculty(course.Faculty{ + Name: payload.Name, + Abbreviation: payload.Abbreviation, + }) + + if err != nil { + // Some uncaught error + return err + } + + return nil +} diff --git a/service/course/delete.go b/service/course/delete.go new file mode 100644 index 0000000000000000000000000000000000000000..a8bda5bc6baf2331980b2b7056282b34b2b272d8 --- /dev/null +++ b/service/course/delete.go @@ -0,0 +1,34 @@ +package course + +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/delete" +) + +// TODO: Authorization Checks + +func (c CourseServiceImpl) DeleteCourse(payload delete.DeleteByStringRequestPayload) error { + // Validate Role + claim, err := c.TokenUtil.Validate(payload.DeleteCourseToken, 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 = c.CourseRepository.DeleteCourse(payload.ID) + + if err != nil { + // Uncaught error + return err + } + + return nil +} \ No newline at end of file diff --git a/service/course/get.go b/service/course/get.go new file mode 100644 index 0000000000000000000000000000000000000000..001217d32d6604eae10f6150b1519935e2ffbe7a --- /dev/null +++ b/service/course/get.go @@ -0,0 +1,137 @@ +package course + +import ( + "errors" + + "gitlab.informatika.org/ocw/ocw-backend/model/domain/course" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course/get" + "gorm.io/gorm" +) + +func (c CourseServiceImpl) GetCourse(payload get.GetByStringRequestPayload) (*course.Course, error) { + packet, err := c.CourseRepository.GetCourse(payload.ID) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.CourseNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (c CourseServiceImpl) GetMajor(payload get.GetByUUIDRequestPayload) (*course.Major, error) { + packet, err := c.CourseRepository.GetMajor(payload.ID) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.MajorNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (c CourseServiceImpl) GetFaculty(payload get.GetByUUIDRequestPayload) (*course.Faculty, error) { + packet, err := c.CourseRepository.GetFaculty(payload.ID) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.FacultyNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (c CourseServiceImpl) GetAllCourse() ([]course.Course, error) { + packet, err := c.CourseRepository.GetAllCourse() + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.CourseNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (c CourseServiceImpl) GetAllMajor() ([]course.Major, error) { + packet, err := c.CourseRepository.GetAllMajor() + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.MajorNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (c CourseServiceImpl) GetAllFaculty() ([]course.Faculty, error) { + packet, err := c.CourseRepository.GetAllFaculty() + + if err != nil { + // This should not happen unless data is unpopulated, faculty is the root dependency so there should be at least one + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.FacultyNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (c CourseServiceImpl) GetAllCourseByMajor(payload get.GetByUUIDRequestPayload) ([]course.Course, error) { + packet, err := c.CourseRepository.GetAllCourseByMajor(payload.ID) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.CourseNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (c CourseServiceImpl) GetAllCourseByFaculty(payload get.GetByUUIDRequestPayload) ([]course.Course, error) { + packet, err := c.CourseRepository.GetAllCourseByFaculty(payload.ID) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.CourseNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} + +func (c CourseServiceImpl) GetAllMajorByFaculty(payload get.GetByUUIDRequestPayload) ([]course.Major, error) { + packet, err := c.CourseRepository.GetAllMajorByFaculty(payload.ID) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, web.NewResponseErrorFromError(err, web.MajorNotExist) + } + // Some Uncaught error + return nil, err + } + + return packet, nil +} \ No newline at end of file diff --git a/service/course/impl.go b/service/course/impl.go new file mode 100644 index 0000000000000000000000000000000000000000..355158f0bd0e78ad3747685f44b1d09d6f04e3c8 --- /dev/null +++ b/service/course/impl.go @@ -0,0 +1,19 @@ +package course + +import ( + "gitlab.informatika.org/ocw/ocw-backend/repository/cache" + "gitlab.informatika.org/ocw/ocw-backend/repository/course" + "gitlab.informatika.org/ocw/ocw-backend/repository/user" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "gitlab.informatika.org/ocw/ocw-backend/utils/token" +) + +type CourseServiceImpl struct { + user.UserRepository + cache.CacheRepository + course.CourseRepository + *env.Environment + token.TokenUtil + logger.Logger +} diff --git a/service/course/type.go b/service/course/type.go new file mode 100644 index 0000000000000000000000000000000000000000..33f45503ef7a54876001a7d15f15464c9a6cdfe4 --- /dev/null +++ b/service/course/type.go @@ -0,0 +1,33 @@ +package course + +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/course" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course/get" + "gitlab.informatika.org/ocw/ocw-backend/model/web/course/delete" + cadd "gitlab.informatika.org/ocw/ocw-backend/model/web/course/add" + madd "gitlab.informatika.org/ocw/ocw-backend/model/web/course/major/add" + fadd "gitlab.informatika.org/ocw/ocw-backend/model/web/course/faculty/add" + cupdate "gitlab.informatika.org/ocw/ocw-backend/model/web/course/update" + mupdate "gitlab.informatika.org/ocw/ocw-backend/model/web/course/major/update" + fupdate "gitlab.informatika.org/ocw/ocw-backend/model/web/course/faculty/update" + +) + +type CourseService interface { + AddCourse(payload cadd.AddCourseRequestPayload) error + AddMajor(payload madd.AddMajorRequestPayload) error + AddFaculty(payload fadd.AddFacultyRequestPayload) error + GetCourse(payload get.GetByStringRequestPayload) (*course.Course, error) + GetMajor(payload get.GetByUUIDRequestPayload) (*course.Major, error) + GetFaculty(payload get.GetByUUIDRequestPayload) (*course.Faculty, error) + GetAllCourse() ([]course.Course, error) + GetAllMajor() ([]course.Major, error) + GetAllFaculty() ([]course.Faculty, error) + GetAllCourseByMajor(payload get.GetByUUIDRequestPayload) ([]course.Course, error) + GetAllCourseByFaculty(payload get.GetByUUIDRequestPayload) ([]course.Course, error) + GetAllMajorByFaculty(payload get.GetByUUIDRequestPayload) ([]course.Major, error) + UpdateCourse(payload cupdate.UpdateCourseRequestPayload) error + UpdateMajor(payload mupdate.UpdateMajorRequestPayload) error + UpdateFaculty(payload fupdate.UpdateFacultyRequestPayload) error + DeleteCourse(payload delete.DeleteByStringRequestPayload) error +} diff --git a/service/course/update.go b/service/course/update.go new file mode 100644 index 0000000000000000000000000000000000000000..a08d0b3573c456651ab7287c4e9c9f160285edd3 --- /dev/null +++ b/service/course/update.go @@ -0,0 +1,145 @@ +package course + +import ( + "errors" + + "gitlab.informatika.org/ocw/ocw-backend/model/domain/course" + "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" + fupdate "gitlab.informatika.org/ocw/ocw-backend/model/web/course/faculty/update" + mupdate "gitlab.informatika.org/ocw/ocw-backend/model/web/course/major/update" + cupdate "gitlab.informatika.org/ocw/ocw-backend/model/web/course/update" + "gorm.io/gorm" +) + +// TODO: Authorization Checks + +func (c CourseServiceImpl) UpdateCourse(payload cupdate.UpdateCourseRequestPayload) error { + // Validate Role + claim, err := c.TokenUtil.Validate(payload.UpdateCourseToken, 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) + } + + // If payload uses major abbreviation, set id + if payload.MajAbbr != "" { + major, err := c.CourseRepository.GetMajorByAbbr(payload.MajAbbr) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseErrorFromError(err, web.CourseNotExist) + } + // Some Uncaught error + return err + } + + payload.MajorID = major.ID + } + + err = c.CourseRepository.UpdateCourse(course.Course{ + ID: payload.ID, + Name: payload.Name, + Major_id: payload.MajorID, + Description: payload.Description, + Email: payload.Email, + Abbreviation: payload.Abbreviation, + Lecturer: payload.Lecturer, + }) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseErrorFromError(err, web.CourseNotExist) + } + // Uncaught error + return err + } + + return nil +} + +func (c CourseServiceImpl) UpdateMajor(payload mupdate.UpdateMajorRequestPayload) error { + + // Validate Role + claim, err := c.TokenUtil.Validate(payload.UpdateMajorToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role != user.Admin { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + // If payload uses faculty abbreviation, set id + if payload.FacAbbr != "" { + faculty, err := c.CourseRepository.GetFacultyByAbbr(payload.FacAbbr) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseErrorFromError(err, web.CourseNotExist) + } + // Some Uncaught error + return err + } + + payload.FacultyID = faculty.ID + } + + err = c.CourseRepository.UpdateMajor(course.Major{ + ID: payload.ID, + Name: payload.Name, + Fac_id: payload.FacultyID, + Abbreviation: payload.Abbreviation, + }) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseErrorFromError(err, web.MajorNotExist) + } + // Uncaught error + return err + } + + return nil +} + +func (c CourseServiceImpl) UpdateFaculty(payload fupdate.UpdateFacultyRequestPayload) error { + // Validate Role + claim, err := c.TokenUtil.Validate(payload.UpdateFacultyToken, token.Access) + + // Invalid Token + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Unauthorized Role + if claim.Role != user.Admin { + return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + } + + err = c.CourseRepository.UpdateFaculty(course.Faculty{ + ID: payload.ID, + Name: payload.Name, + Abbreviation: payload.Abbreviation, + }) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseErrorFromError(err, web.FacultyNotExist) + } + // Uncaught error + return err + } + + return nil +} diff --git a/service/di.go b/service/di.go index 763b0502928cbdacbe020c1c3d1e4ba64e590989..6cdb051667f746ccdc3ff0244fd29bdf19f69561 100644 --- a/service/di.go +++ b/service/di.go @@ -2,14 +2,16 @@ package service import ( "github.com/google/wire" - "gitlab.informatika.org/ocw/ocw-backend/service/auth" "gitlab.informatika.org/ocw/ocw-backend/service/admin" + "gitlab.informatika.org/ocw/ocw-backend/service/auth" "gitlab.informatika.org/ocw/ocw-backend/service/common" "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" "gitlab.informatika.org/ocw/ocw-backend/service/reporter" "gitlab.informatika.org/ocw/ocw-backend/service/reset" "gitlab.informatika.org/ocw/ocw-backend/service/verification" + "gitlab.informatika.org/ocw/ocw-backend/service/course" ) var ServiceTestSet = wire.NewSet( @@ -48,6 +50,20 @@ var ServiceTestSet = wire.NewSet( wire.Struct(new(verification.VerificationServiceImpl), "*"), wire.Bind(new(verification.VerificationService), new(*verification.VerificationServiceImpl)), ), + + // course service + wire.NewSet( + wire.Struct(new(course.CourseServiceImpl), "*"), + wire.Bind(new(course.CourseService), new(*course.CourseServiceImpl)), + ), + + // Material Service + wire.NewSet( + wire.Struct(new(material.MaterialContentServiceImpl), "*"), + wire.Struct(new(material.MaterialServiceImpl), "*"), + wire.Bind(new(material.MaterialContentService), new(*material.MaterialContentServiceImpl)), + wire.Bind(new(material.MaterialService), new(*material.MaterialServiceImpl)), + ), ) var ServiceSet = wire.NewSet( diff --git a/service/material/content.go b/service/material/content.go new file mode 100644 index 0000000000000000000000000000000000000000..5bf0b5638d0e44d4857932fc3d782a6d0a36c45b --- /dev/null +++ b/service/material/content.go @@ -0,0 +1,47 @@ +package material + +import ( + "github.com/google/uuid" + materialDomain "gitlab.informatika.org/ocw/ocw-backend/model/domain/material" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/repository/material" + "gitlab.informatika.org/ocw/ocw-backend/repository/transaction" +) + +type MaterialContentServiceImpl struct { + transaction.TransactionBuilder + material.MaterialContentRepository +} + +func (m MaterialContentServiceImpl) AddContent(materialId uuid.UUID, user user.User, contents []materialDomain.Content) error { + isSuccess := false + tx := m.Build() + + tx.Begin() + defer tx.Auto(&isSuccess) + + // TODO : Check user aman ga nambah konten + + for _, content := range contents { + _, err := m.MaterialContentRepository.NewWithTransaction(tx, materialId, content.Type, content.Link) + + if err != nil { + return err + } + } + + isSuccess = true + return nil +} + +func (m MaterialContentServiceImpl) DeleteContent( + materialId uuid.UUID, user user.User, contentId uuid.UUID, +) error { + // TODO: check user aman ga delete konten + return m.MaterialContentRepository.Delete(contentId) +} + +func (m MaterialContentServiceImpl) UpdateContentLink(materialId uuid.UUID, user user.User, contentId uuid.UUID, link string) error { + // TODO: Check user aman ga update link + return m.MaterialContentRepository.UpdateLink(contentId, link) +} diff --git a/service/material/impl.go b/service/material/impl.go new file mode 100644 index 0000000000000000000000000000000000000000..1e7c95ff7014ec943f19982a4b95892baa52e2bf --- /dev/null +++ b/service/material/impl.go @@ -0,0 +1,45 @@ +package material + +import ( + "github.com/google/uuid" + materialRepo "gitlab.informatika.org/ocw/ocw-backend/model/domain/material" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/repository/material" + "gitlab.informatika.org/ocw/ocw-backend/repository/transaction" +) + +type MaterialServiceImpl struct { + transaction.TransactionBuilder + material.MaterialContentRepository + material.MaterialRepository +} + +func (m MaterialServiceImpl) Create(courseId string, user user.User, contents []materialRepo.Content) (uuid.UUID, error) { + isSuccess := false + tx := m.TransactionBuilder.Build() + + tx.Begin() + defer tx.Auto(&isSuccess) + + id, err := m.MaterialRepository.NewWithTransaction(tx, courseId, user.Email) + + if err != nil { + return uuid.Nil, err + } + + for _, content := range contents { + _, err = m.MaterialContentRepository.NewWithTransaction(tx, id, content.Type, content.Link) + + if err != nil { + return uuid.Nil, err + } + } + + isSuccess = true + return id, err +} + +func (m MaterialServiceImpl) Delete(materialId uuid.UUID, user user.User) error { + // TODO: Pengecekan user apakah kontributor course bukan + return m.MaterialRepository.Delete(materialId) +} diff --git a/service/material/type.go b/service/material/type.go new file mode 100644 index 0000000000000000000000000000000000000000..66fe99b49e176c375812c020389aaf855fb3d7bb --- /dev/null +++ b/service/material/type.go @@ -0,0 +1,18 @@ +package material + +import ( + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/material" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" +) + +type MaterialService interface { + Create(courseId string, user user.User, contents []material.Content) (uuid.UUID, error) + Delete(materialId uuid.UUID, user user.User) error +} + +type MaterialContentService interface { + AddContent(materialId uuid.UUID, user user.User, contents []material.Content) error + DeleteContent(materialId uuid.UUID, user user.User, contentId uuid.UUID) error + UpdateContentLink(materialId uuid.UUID, user user.User, contentId uuid.UUID, link string) error +} diff --git a/service/reset/confirm.go b/service/reset/confirm.go index 426af52f5514eec16fd0a6c4553bd817974177fd..66a42c1eef7843a9888fb9e4a5c0d34c17d9d86b 100644 --- a/service/reset/confirm.go +++ b/service/reset/confirm.go @@ -1,11 +1,47 @@ package reset import ( - // "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/cache" + "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/reset/confirm" ) func (rs ResetServiceImpl) Confirm(payload confirm.ConfirmRequestPayload) error { - // TODO replace dummy + // Double Layered Security + // Validate Token + _, err := rs.TokenUtil.Validate(payload.ConfirmToken, token.Access) + + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Check if Token is Cached + email, err := rs.CacheRepository.Get(*cache.NewKey(rs.RedisPrefixKey+"resetPassword", payload.ConfirmToken)) + + if err != nil { + return web.NewResponseErrorFromError(err, web.LinkNotAvailable) + } + + // Reset the password + user, err := rs.UserRepository.Get(email) + + if err != nil { + return err + } + + hashedPassword, err := rs.Hash(payload.Password) + + if err != nil { + return err + } + + user.Password = hashedPassword + err = rs.UserRepository.Update(*user) + + if err != nil { + return err + } + return nil -} \ No newline at end of file +} diff --git a/service/reset/impl.go b/service/reset/impl.go index b9986e2509f4af7f8c0deb9053bf8d30640e4168..215896549040599572bc40ca19c9301d9c314d8d 100644 --- a/service/reset/impl.go +++ b/service/reset/impl.go @@ -1,17 +1,23 @@ package reset import ( + "gitlab.informatika.org/ocw/ocw-backend/provider/mail" + "gitlab.informatika.org/ocw/ocw-backend/repository/cache" "gitlab.informatika.org/ocw/ocw-backend/repository/user" - "gitlab.informatika.org/ocw/ocw-backend/service/verification" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" "gitlab.informatika.org/ocw/ocw-backend/utils/env" "gitlab.informatika.org/ocw/ocw-backend/utils/password" + "gitlab.informatika.org/ocw/ocw-backend/utils/template" "gitlab.informatika.org/ocw/ocw-backend/utils/token" ) type ResetServiceImpl struct { user.UserRepository + cache.CacheRepository password.PasswordUtil *env.Environment token.TokenUtil - verification.VerificationService + logger.Logger + mail.MailQueue + template.TemplateWritterBuilder } diff --git a/service/reset/mail.go b/service/reset/mail.go new file mode 100644 index 0000000000000000000000000000000000000000..7de41926aa002e9e4fd233804ed82c056777aa44 --- /dev/null +++ b/service/reset/mail.go @@ -0,0 +1,7 @@ +package reset + +type mailPayload struct { + BaseUrl string + Email string + Token string +} diff --git a/service/reset/request.go b/service/reset/request.go index 05584e6a9bb3dcf0a7a78c4ec5e12e1c4502732d..f3d645f4a08c7f809a117614acde439cca870a08 100644 --- a/service/reset/request.go +++ b/service/reset/request.go @@ -1,11 +1,83 @@ package reset import ( - // "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "errors" + "time" + + "github.com/golang-jwt/jwt/v4" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + tokenModel "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + "gitlab.informatika.org/ocw/ocw-backend/provider/mail" + "gorm.io/gorm" + + "gitlab.informatika.org/ocw/ocw-backend/model/domain/cache" "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/request" ) func (rs ResetServiceImpl) Request(payload request.RequestRequestPayload) error { - // TODO replace dummy + // Fetch user data from email + user, err := rs.UserRepository.Get(payload.Email) + + if err != nil { + var errorObj error + + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + errorObj = web.NewResponseError("Email was not found", web.EmailNotExist) + default: + errorObj = err + } + + return errorObj + } + + if !user.IsActivated { + return web.NewResponseError("user is not activated yet", web.InactiveUser) + } + + // Mint JWT Token for 30 minutes + resetClaim := tokenModel.UserClaim{ + Name: user.Name, + Email: user.Email, + Role: user.Role, + Type: tokenModel.Access, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(rs.TokenAccessExpired*6) * time.Millisecond)), + Issuer: rs.TokenIssuer, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + resetToken, err := rs.TokenUtil.Generate(resetClaim, rs.TokenUtil.DefaultMethod()) + if err != nil { + return err + } + + // Cache Website on Redis, TTL 30 mins + rs.CacheRepository.Set(*cache.NewString(*cache.NewKey(rs.RedisPrefixKey+"resetPassword", resetToken), payload.Email, 30)) + + // Send Reset Email + mailBuilder, err := rs.TemplateWritterBuilder.Get("reset-password.format.html") + + if err != nil { + return err + } + + mailData, err := mailBuilder.Write(&mailPayload{ + BaseUrl: rs.FrontendBaseURL + rs.ResetPasswordPath, + Email: user.Email, + Token: resetToken, + }) + + if err != nil { + return err + } + + rs.MailQueue.Send(mail.Mail{ + To: []string{user.Email}, + Subject: "Reset Password", + Message: mailData, + }) + return nil -} \ No newline at end of file +} diff --git a/service/reset/validate.go b/service/reset/validate.go index 7cd09fe9bc83b23a2e4d0bbc0abf95fe1758b5cc..f1ae7028d7e0da998768720f97ff43adbf9f2c98 100644 --- a/service/reset/validate.go +++ b/service/reset/validate.go @@ -1,11 +1,27 @@ package reset import ( - // "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/cache" + "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/reset/validate" ) func (rs ResetServiceImpl) Validate(payload validate.ValidateRequestPayload) error { - // TODO replace dummy + // Double Layered Security + // Validate Token + _, err := rs.TokenUtil.Validate(payload.ValidateToken, token.Access) + + if err != nil { + return web.NewResponseErrorFromError(err, web.TokenError) + } + + // Check if Token is Cached + _, err = rs.CacheRepository.Get(*cache.NewKey(rs.RedisPrefixKey+"resetPassword", payload.ValidateToken)) + + if err != nil { + return web.NewResponseErrorFromError(err, web.LinkNotAvailable) + } + return nil -} \ No newline at end of file +} diff --git a/service/verification/impl.go b/service/verification/impl.go index 94db17485a4173f2ad820bbc100685d06f00d9b5..a74992b4fbf14b5c6e6174b5c00ee944afea812c 100644 --- a/service/verification/impl.go +++ b/service/verification/impl.go @@ -2,10 +2,18 @@ package verification import ( "gitlab.informatika.org/ocw/ocw-backend/provider/mail" + "gitlab.informatika.org/ocw/ocw-backend/repository/cache" "gitlab.informatika.org/ocw/ocw-backend/repository/user" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "gitlab.informatika.org/ocw/ocw-backend/utils/template" + "gitlab.informatika.org/ocw/ocw-backend/utils/token" ) type VerificationServiceImpl struct { mail.MailQueue user.UserRepository + *env.Environment + template.TemplateWritterBuilder + token.TokenUtil + cache.CacheRepository } diff --git a/service/verification/mail.go b/service/verification/mail.go new file mode 100644 index 0000000000000000000000000000000000000000..6b89f3f7b7a50f41711554ade1ab6f3871bcea19 --- /dev/null +++ b/service/verification/mail.go @@ -0,0 +1,7 @@ +package verification + +type mailPayload struct { + BaseUrl string + Email string + Token string +} diff --git a/service/verification/send.go b/service/verification/send.go index 9f404a803958d11c9f10a121ceacfc7aa87318d6..cfedd802ffa95cfb9aece68168ed30ae4c3ca580 100644 --- a/service/verification/send.go +++ b/service/verification/send.go @@ -1,6 +1,72 @@ package verification +import ( + "errors" + "time" + + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/cache" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/provider/mail" + "gorm.io/gorm" +) + func (v VerificationServiceImpl) SendVerifyMail(email string) error { - // TODO + _, err := v.UserRepository.Get(email) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.NewResponseError("user not found", web.EmailNotExist) + } + + return err + } + + res, err := v.CacheRepository.GetInteger(cache.Key{ + Id: v.RedisPrefixKey + "verify:cnt:" + email, + }) + + if err != nil { + if err.Error() != "redigo: nil returned" { + return err + } + } + + if res > v.Environment.EmailVerificationMaxRetry { + return nil + } + + id := uuid.New().String() + v.CacheRepository.Incr(v.RedisPrefixKey+"verify:cnt:"+email, v.EmailVerificationRetryInterval*int64(time.Minute)) + v.CacheRepository.Set(cache.String{ + Key: cache.Key{ + Id: v.RedisPrefixKey + "verify:id:" + id, + }, + Value: email, + ExpiryInMinutes: int(v.EmailVerificationExpire) * int(time.Second), + }) + + mailBuilder, err := v.TemplateWritterBuilder.Get("email-verification.format.html") + + if err != nil { + return err + } + + mailData, err := mailBuilder.Write(&mailPayload{ + BaseUrl: v.FrontendBaseURL + v.ResetPasswordPath, + Email: email, + Token: id, + }) + + if err != nil { + return err + } + + v.MailQueue.Send(mail.Mail{ + To: []string{email}, + Subject: "Email Verification", + Message: mailData, + }) + return nil } diff --git a/service/verification/type.go b/service/verification/type.go index e6df1d1939e37be6abf399db52d68632e0924a4c..d321c706ff491be4398c8abaae780ca02ffe0cda 100644 --- a/service/verification/type.go +++ b/service/verification/type.go @@ -2,5 +2,5 @@ package verification type VerificationService interface { SendVerifyMail(email string) error - SetVerification(email string, isVerified bool) error + DoVerification(id string) error } diff --git a/service/verification/verify.go b/service/verification/verify.go index b18ce1ecaa19fd332e495f81a8377b814a010246..4bfd2c115bb5f07811699599415be37bee5151da 100644 --- a/service/verification/verify.go +++ b/service/verification/verify.go @@ -1,6 +1,33 @@ package verification -func (v VerificationServiceImpl) SetVerification(email string, isVerified bool) error { +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/domain/cache" + "gitlab.informatika.org/ocw/ocw-backend/model/web" +) + +func (v VerificationServiceImpl) DoVerification(id string) error { // TODO + email, err := v.CacheRepository.Get(cache.Key{ + Id: v.RedisPrefixKey + "verify:id:" + id, + }) + + if err != nil { + return err + } + + if email == "" { + return web.NewResponseErrorf("VERIFY", "id '%s' is not valid", id) + } + + data, err := v.UserRepository.Get(email) + + if err != nil { + return web.NewResponseErrorf("VERIFY", "username '%s' is not found", email) + } + + data.IsActivated = true + + v.UserRepository.Update(*data) + return nil } diff --git a/test/di.go b/test/di.go index 9ce05c76ea2a7f5f97645eb7bde26d66fdb121f0..6e9f9ff3ac420b22ee453b42ef68835662842c43 100644 --- a/test/di.go +++ b/test/di.go @@ -18,6 +18,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/utils" "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "gitlab.informatika.org/ocw/ocw-backend/utils/template" ) func CreateServer(logger logger.Logger, envTest *env.Environment) (*ApiTestPack, error) { @@ -37,3 +38,11 @@ func CreateServer(logger logger.Logger, envTest *env.Environment) (*ApiTestPack, return nil, nil } + +func CreateTemplateBuilder() template.TemplateWritterBuilder { + wire.Build( + utils.UtilSetTest, + ) + + return nil +} diff --git a/test/middleware/cors_test.go b/test/middleware/cors_test.go index 78ec55168dd2b147f6ebab3273c9a838bc4a0f9e..74615aa27d9b04bcb60edf4173e0bf9f83b9eddf 100644 --- a/test/middleware/cors_test.go +++ b/test/middleware/cors_test.go @@ -45,7 +45,7 @@ func TestPreflight(t *testing.T) { assert.Contains(t, res.Header.Get("Access-Control-Allow-Methods"), "GET") }) - t.Run("PreflightNotAllowedPatchMethod", func(t *testing.T) { + t.Run("PreflightAllowedPatchMethod", func(t *testing.T) { res, _, err := test.ExecuteJSON(test.RequestData{ Method: "OPTIONS", Endpoint: "/ping", @@ -59,7 +59,7 @@ func TestPreflight(t *testing.T) { assert.Nil(t, err) assert.Equal(t, res.StatusCode, http.StatusOK) - assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "") - assert.Equal(t, res.Header.Get("Access-Control-Allow-Methods"), "") + assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://inkubatorit.com") + assert.Equal(t, res.Header.Get("Access-Control-Allow-Methods"), "PATCH") }) } diff --git a/test/utils/template/template_test.go b/test/utils/template/template_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a6e783ba29460022a978183bf452afba00c86c54 --- /dev/null +++ b/test/utils/template/template_test.go @@ -0,0 +1,66 @@ +package template + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.informatika.org/ocw/ocw-backend/test" +) + +type testTemplate struct { + Name string + Url string +} + +func TestTemplate(t *testing.T) { + builder := test.CreateTemplateBuilder() + template, err := builder.Get("test.html") + + assert.Nil(t, err) + + if err != nil { + return + } + + t.Run("BuilderSuccess", func(t *testing.T) { + data := &testTemplate{ + Name: "Bayu", + Url: "Hehe", + } + + result, err := template.Write(data) + + assert.Nil(t, err) + assert.Equal(t, result, "<h1>Hello, Bayu. This is your Hehe</h1>") + }) + + t.Run("ShouldBeReuseObject", func(t *testing.T) { + templateInner, _ := builder.Get("test.html") + + assert.Equal(t, template, templateInner) + }) + + t.Run("HtmlShouldBeParsed", func(t *testing.T) { + data := &testTemplate{ + Name: "<script>alert('hayoloh')</script>", + Url: "Hehe", + } + + result, err := template.Write(data) + + assert.Nil(t, err) + assert.NotEqual(t, result, "<h1>Hello, <script>alert('hayoloh')</script>. This is your Hehe</h1>") + }) + + t.Run("URLShouldBeParsed", func(t *testing.T) { + data := &testTemplate{ + Name: "Bayu", + Url: "http://localhost:8080/?q=' AND 10 AND \"", + } + + result, err := template.Write(data) + + assert.Nil(t, err) + assert.Equal(t, result, "<h1>Hello, Bayu. This is your http://localhost:8080/?q=' AND 10 AND "</h1>") + }) +} diff --git a/utils/di.go b/utils/di.go index d6b65b8b8f725bf8c3da03f3ab776ef8aca15e90..3987842c6ebf7c6b1103f237a4c80160b5fd4da2 100644 --- a/utils/di.go +++ b/utils/di.go @@ -8,6 +8,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/utils/log" "gitlab.informatika.org/ocw/ocw-backend/utils/password" "gitlab.informatika.org/ocw/ocw-backend/utils/res" + "gitlab.informatika.org/ocw/ocw-backend/utils/template" "gitlab.informatika.org/ocw/ocw-backend/utils/token" "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" ) @@ -40,6 +41,10 @@ var UtilSetTest = wire.NewSet( // Token utility wire.Struct(new(token.TokenUtilImpl), "*"), wire.Bind(new(token.TokenUtil), new(*token.TokenUtilImpl)), + + // Template Writter + template.NewBuilder, + wire.Bind(new(template.TemplateWritterBuilder), new(*template.TemplateWritterBuilderImpl)), ) var UtilSet = wire.NewSet( diff --git a/utils/env/env.go b/utils/env/env.go index a55416e52bf462568666b3b4de1afc886d97fa44..f53df6088f224fa4822983591509d93e7f4e93c4 100644 --- a/utils/env/env.go +++ b/utils/env/env.go @@ -36,6 +36,33 @@ type Environment struct { SmtpPassword string `env:"SMTP_PASSWORD"` SmtpServer string `env:"SMTP_SERVER"` SmtpPort int `env:"SMTP_PORT" envDefault:"25"` + + FrontendBaseURL string `env:"FE_BASE_URL"` + ResetPasswordPath string `env:"RESET_PASSWORD_PATH" envDefault:"/resetPassword"` + + EmailVerificationPath string `env:"EMAIL_VERIFICATION_PATH" envDefault:"/verification"` + EmailVerificationMaxRetry int64 `env:"EMAIL_VERIFICATION_MAX_RETRY" envDefault:"5"` + EmailVerificationRetryInterval int64 `env:"EMAIL_VERIFICATION_RESET_RETRY_INTERVAL_M" envDefault:"5"` + EmailVerificationExpire int64 `env:"EMAIL_VERIFICATION_EXPIRE_S" envDefault:"300"` + + RedisConnection string `env:"REDIS_STRING"` + RedisPort string `env:"REDIS_PORT" envDefault:"6379"` + RedisUsername string `env:"REDIS_USERNAME"` + RedisPassword string `env:"REDIS_PASSWORD"` + RedisUseAuth bool `env:"REDIS_USE_AUTH" envDefault:"false"` + RedisPrefixKey string `env:"REDIS_PREFIX_KEY" envDefault:"app:"` + + BucketEndpoint string `env:"BUCKET_ENDPOINT"` + BucketSecretKey string `env:"BUCKET_SECRET_KEY"` + BucketAccessKey string `env:"BUCKET_ACCESS_KEY"` + BucketTokenKey string `env:"BUCKET_TOKEN_KEY"` + BucketUseSSL bool `env:"BUCKET_USE_SSL" envDefault:"true"` + BucketName string `env:"BUCKET_NAME"` + + BucketSignedPutDuration int64 `env:"BUCKET_SIGNED_PUT_DURATION_S" envDefault:"36000"` + BucketSignedGetDuration int64 `env:"BUCKET_SIGNED_GET_DURATION_S" envDefault:"1800"` + + BucketMaterialBasePath string `env:"BUCKET_MATERIAL_BASE_PATH" envDefault:"materials/"` } func New() (*Environment, error) { @@ -57,7 +84,10 @@ func NewEnv() (*Environment, error) { } func NewDotEnv() (*Environment, error) { - err := godotenv.Load() + err := godotenv.Load( + ".env", + ".env.local", + ) if err != nil { return nil, err diff --git a/utils/res/data/email-verification.format.html b/utils/res/data/email-verification.format.html new file mode 100644 index 0000000000000000000000000000000000000000..a4a763e42aa997d4024843a5b3302be342e61a66 --- /dev/null +++ b/utils/res/data/email-verification.format.html @@ -0,0 +1,298 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html> + +<head> + <!-- Compiled with Bootstrap Email version: 1.3.1 --> + <meta http-equiv="x-ua-compatible" content="ie=edge"> + <meta name="x-apple-disable-message-reformatting"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style type="text/css"> + body, + table, + td { + font-family: Helvetica, Arial, sans-serif !important + } + + .ExternalClass { + width: 100% + } + + .ExternalClass, + .ExternalClass p, + .ExternalClass span, + .ExternalClass font, + .ExternalClass td, + .ExternalClass div { + line-height: 150% + } + + a { + text-decoration: none + } + + * { + color: inherit + } + + a[x-apple-data-detectors], + u+#body a, + #MessageViewBody a { + color: inherit; + text-decoration: none; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + line-height: inherit + } + + img { + -ms-interpolation-mode: bicubic + } + + table:not([class^=s-]) { + font-family: Helvetica, Arial, sans-serif; + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; + border-spacing: 0px; + border-collapse: collapse + } + + table:not([class^=s-]) td { + border-spacing: 0px; + border-collapse: collapse + } + + @media screen and (max-width: 600px) { + + .w-full, + .w-full>tbody>tr>td { + width: 100% !important + } + + *[class*=s-lg-]>tbody>tr>td { + font-size: 0 !important; + line-height: 0 !important; + height: 0 !important + } + + .s-2>tbody>tr>td { + font-size: 8px !important; + line-height: 8px !important; + height: 8px !important + } + + .s-5>tbody>tr>td { + font-size: 20px !important; + line-height: 20px !important; + height: 20px !important + } + + .s-10>tbody>tr>td { + font-size: 40px !important; + line-height: 40px !important; + height: 40px !important + } + } + </style> +</head> + +<body class="bg-light" + style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" + bgcolor="#f7fafc"> + <table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" + style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" + bgcolor="#f7fafc"> + <tbody> + <tr> + <td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc"> + <table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;"> + <tbody> + <tr> + <td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;"> + <!--[if (gte mso 9)|(IE)]> + <table align="center" role="presentation"> + <tbody> + <tr> + <td width="600"> + <![endif]--> + <table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" + style="width: 100%; max-width: 600px; margin: 0 auto;"> + <tbody> + <tr> + <td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> + <table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" + style="width: 100%;" width="100%"> + <tbody> + <tr> + <td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" + align="left" width="100%" height="40"> +   + </td> + </tr> + </tbody> + </table> + <table class="card" role="presentation" border="0" cellpadding="0" cellspacing="0" + style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" + bgcolor="#ffffff"> + <tbody> + <tr> + <td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left" + bgcolor="#ffffff"> + <table class="card-body" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;"> + <tbody> + <tr> + <td + style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 20px;" + align="left"> + <h1 class="h3" + style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" + align="left">Email Verifikasi</h1> + <table class="s-2 w-full" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;" width="100%"> + <tbody> + <tr> + <td + style="line-height: 8px; font-size: 8px; width: 100%; height: 8px; margin: 0;" + align="left" width="100%" height="8"> +   + </td> + </tr> + </tbody> + </table> + <table class="s-5 w-full" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;" width="100%"> + <tbody> + <tr> + <td + style="line-height: 20px; font-size: 20px; width: 100%; height: 20px; margin: 0;" + align="left" width="100%" height="20"> +   + </td> + </tr> + </tbody> + </table> + <table class="hr" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;"> + <tbody> + <tr> + <td + style="line-height: 24px; font-size: 16px; border-top-width: 1px; border-top-color: #e2e8f0; border-top-style: solid; height: 1px; width: 100%; margin: 0;" + align="left"> + </td> + </tr> + </tbody> + </table> + <table class="s-5 w-full" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;" width="100%"> + <tbody> + <tr> + <td + style="line-height: 20px; font-size: 20px; width: 100%; height: 20px; margin: 0;" + align="left" width="100%" height="20"> +   + </td> + </tr> + </tbody> + </table> + <div class="space-y-3"> + <p class="text-gray-700" + style="line-height: 24px; font-size: 16px; color: #4a5568; width: 100%; margin: 0;" + align="left"> + Anda telah melakukan pendaftaran pada OCW ITB menggunakan email {{ .Email + }}. Untuk menyelesaikan proses + pendaftaran, anda dapat menekan tombol dibawah ini. + </p> + </div> + <table class="s-5 w-full" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;" width="100%"> + <tbody> + <tr> + <td + style="line-height: 20px; font-size: 20px; width: 100%; height: 20px; margin: 0;" + align="left" width="100%" height="20"> +   + </td> + </tr> + </tbody> + </table> + <table class="hr" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;"> + <tbody> + <tr> + <td + style="line-height: 24px; font-size: 16px; border-top-width: 1px; border-top-color: #e2e8f0; border-top-style: solid; height: 1px; width: 100%; margin: 0;" + align="left"> + </td> + </tr> + </tbody> + </table> + <table class="s-5 w-full" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;" width="100%"> + <tbody> + <tr> + <td + style="line-height: 20px; font-size: 20px; width: 100%; height: 20px; margin: 0;" + align="left" width="100%" height="20"> +   + </td> + </tr> + </tbody> + </table> + <table class="btn btn-primary" role="presentation" border="0" cellpadding="0" + cellspacing="0" + style="border-radius: 6px; border-collapse: separate !important;"> + <tbody> + <tr> + <td + style="line-height: 24px; font-size: 16px; border-radius: 6px; margin: 0;" + align="center" bgcolor="#0d6efd"> + <a href="{{ .BaseUrl }}/?token={{ .Token }}" target="_blank" + style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: normal; white-space: nowrap; background-color: #0d6efd; padding: 8px 12px; border: 1px solid #0d6efd;">Reset + Password</a> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + <table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" + style="width: 100%;" width="100%"> + <tbody> + <tr> + <td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" + align="left" width="100%" height="40"> +   + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + <!--[if (gte mso 9)|(IE)]> + </td> + </tr> + </tbody> + </table> + <![endif]--> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> +</body> + +</html> \ No newline at end of file diff --git a/utils/res/data/reset-password.format.html b/utils/res/data/reset-password.format.html new file mode 100644 index 0000000000000000000000000000000000000000..16ea4415ab2c267cbc04ca806a87cbcc81ead636 --- /dev/null +++ b/utils/res/data/reset-password.format.html @@ -0,0 +1,297 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html> + +<head> + <!-- Compiled with Bootstrap Email version: 1.3.1 --> + <meta http-equiv="x-ua-compatible" content="ie=edge"> + <meta name="x-apple-disable-message-reformatting"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style type="text/css"> + body, + table, + td { + font-family: Helvetica, Arial, sans-serif !important + } + + .ExternalClass { + width: 100% + } + + .ExternalClass, + .ExternalClass p, + .ExternalClass span, + .ExternalClass font, + .ExternalClass td, + .ExternalClass div { + line-height: 150% + } + + a { + text-decoration: none + } + + * { + color: inherit + } + + a[x-apple-data-detectors], + u+#body a, + #MessageViewBody a { + color: inherit; + text-decoration: none; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + line-height: inherit + } + + img { + -ms-interpolation-mode: bicubic + } + + table:not([class^=s-]) { + font-family: Helvetica, Arial, sans-serif; + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; + border-spacing: 0px; + border-collapse: collapse + } + + table:not([class^=s-]) td { + border-spacing: 0px; + border-collapse: collapse + } + + @media screen and (max-width: 600px) { + + .w-full, + .w-full>tbody>tr>td { + width: 100% !important + } + + *[class*=s-lg-]>tbody>tr>td { + font-size: 0 !important; + line-height: 0 !important; + height: 0 !important + } + + .s-2>tbody>tr>td { + font-size: 8px !important; + line-height: 8px !important; + height: 8px !important + } + + .s-5>tbody>tr>td { + font-size: 20px !important; + line-height: 20px !important; + height: 20px !important + } + + .s-10>tbody>tr>td { + font-size: 40px !important; + line-height: 40px !important; + height: 40px !important + } + } + </style> +</head> + +<body class="bg-light" + style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" + bgcolor="#f7fafc"> + <table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" + style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" + bgcolor="#f7fafc"> + <tbody> + <tr> + <td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc"> + <table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;"> + <tbody> + <tr> + <td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;"> + <!--[if (gte mso 9)|(IE)]> + <table align="center" role="presentation"> + <tbody> + <tr> + <td width="600"> + <![endif]--> + <table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" + style="width: 100%; max-width: 600px; margin: 0 auto;"> + <tbody> + <tr> + <td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> + <table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" + style="width: 100%;" width="100%"> + <tbody> + <tr> + <td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" + align="left" width="100%" height="40"> +   + </td> + </tr> + </tbody> + </table> + <table class="card" role="presentation" border="0" cellpadding="0" cellspacing="0" + style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" + bgcolor="#ffffff"> + <tbody> + <tr> + <td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left" + bgcolor="#ffffff"> + <table class="card-body" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;"> + <tbody> + <tr> + <td + style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 20px;" + align="left"> + <h1 class="h3" + style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" + align="left">Reset Password</h1> + <table class="s-2 w-full" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;" width="100%"> + <tbody> + <tr> + <td + style="line-height: 8px; font-size: 8px; width: 100%; height: 8px; margin: 0;" + align="left" width="100%" height="8"> +   + </td> + </tr> + </tbody> + </table> + <table class="s-5 w-full" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;" width="100%"> + <tbody> + <tr> + <td + style="line-height: 20px; font-size: 20px; width: 100%; height: 20px; margin: 0;" + align="left" width="100%" height="20"> +   + </td> + </tr> + </tbody> + </table> + <table class="hr" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;"> + <tbody> + <tr> + <td + style="line-height: 24px; font-size: 16px; border-top-width: 1px; border-top-color: #e2e8f0; border-top-style: solid; height: 1px; width: 100%; margin: 0;" + align="left"> + </td> + </tr> + </tbody> + </table> + <table class="s-5 w-full" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;" width="100%"> + <tbody> + <tr> + <td + style="line-height: 20px; font-size: 20px; width: 100%; height: 20px; margin: 0;" + align="left" width="100%" height="20"> +   + </td> + </tr> + </tbody> + </table> + <div class="space-y-3"> + <p class="text-gray-700" + style="line-height: 24px; font-size: 16px; color: #4a5568; width: 100%; margin: 0;" + align="left"> + Anda telah mengirimkan reset password untuk email {{ .Email }}. Untuk + melakukan reset password tersebut, anda dapat menekan tombol dibawah ini. + </p> + </div> + <table class="s-5 w-full" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;" width="100%"> + <tbody> + <tr> + <td + style="line-height: 20px; font-size: 20px; width: 100%; height: 20px; margin: 0;" + align="left" width="100%" height="20"> +   + </td> + </tr> + </tbody> + </table> + <table class="hr" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;"> + <tbody> + <tr> + <td + style="line-height: 24px; font-size: 16px; border-top-width: 1px; border-top-color: #e2e8f0; border-top-style: solid; height: 1px; width: 100%; margin: 0;" + align="left"> + </td> + </tr> + </tbody> + </table> + <table class="s-5 w-full" role="presentation" border="0" cellpadding="0" + cellspacing="0" style="width: 100%;" width="100%"> + <tbody> + <tr> + <td + style="line-height: 20px; font-size: 20px; width: 100%; height: 20px; margin: 0;" + align="left" width="100%" height="20"> +   + </td> + </tr> + </tbody> + </table> + <table class="btn btn-primary" role="presentation" border="0" cellpadding="0" + cellspacing="0" + style="border-radius: 6px; border-collapse: separate !important;"> + <tbody> + <tr> + <td + style="line-height: 24px; font-size: 16px; border-radius: 6px; margin: 0;" + align="center" bgcolor="#0d6efd"> + <a href="{{ .BaseUrl }}/{{ .Token }}" target="_blank" + style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: normal; white-space: nowrap; background-color: #0d6efd; padding: 8px 12px; border: 1px solid #0d6efd;">Reset + Password</a> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + <table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" + style="width: 100%;" width="100%"> + <tbody> + <tr> + <td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" + align="left" width="100%" height="40"> +   + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + <!--[if (gte mso 9)|(IE)]> + </td> + </tr> + </tbody> + </table> + <![endif]--> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> +</body> + +</html> \ No newline at end of file diff --git a/utils/res/data/test.html b/utils/res/data/test.html new file mode 100644 index 0000000000000000000000000000000000000000..8d432ede97e9a3ddad9b5fcbbfefaece69f7fdb4 --- /dev/null +++ b/utils/res/data/test.html @@ -0,0 +1 @@ +<h1>Hello, {{ .Name }}. This is your {{ .Url }}</h1> \ No newline at end of file diff --git a/utils/res/embed.go b/utils/res/embed.go index 598ad06c4a4d25269165e6676bfb2d127df69fd9..19f072892de5c8faf0a23d205dc00caaa5be0425 100644 --- a/utils/res/embed.go +++ b/utils/res/embed.go @@ -2,6 +2,7 @@ package res import ( "embed" + "io/fs" ) //go:embed data/* @@ -13,6 +14,9 @@ func (EmbedResources) GetBytesResource(path string) ([]byte, error) { return data.ReadFile("data/" + path) } +func (EmbedResources) GetFile(path string) (fs.File, error) { + return data.Open("data/" + path) +} func (EmbedResources) GetStringResource(path string) (string, error) { content, err := data.ReadFile("data/" + path) @@ -22,4 +26,4 @@ func (EmbedResources) GetStringResource(path string) (string, error) { } return string(content), nil -} \ No newline at end of file +} diff --git a/utils/res/res.go b/utils/res/res.go index 387823d3a50c02d2a0f8630ae5e0c273ec5d29b1..f37288d570af4d2cf684bfb634769048d5001290 100644 --- a/utils/res/res.go +++ b/utils/res/res.go @@ -1,6 +1,9 @@ package res +import "io/fs" + type Resource interface { GetBytesResource(path string) ([]byte, error) GetStringResource(path string) (string, error) + GetFile(path string) (fs.File, error) } diff --git a/utils/template/builder.go b/utils/template/builder.go new file mode 100644 index 0000000000000000000000000000000000000000..b2317d5d72e83ff925ca019b8bc80cb860d10e44 --- /dev/null +++ b/utils/template/builder.go @@ -0,0 +1,5 @@ +package template + +type TemplateWritterBuilder interface { + Get(templatePath string) (TemplateWritter, error) +} diff --git a/utils/template/builder_impl.go b/utils/template/builder_impl.go new file mode 100644 index 0000000000000000000000000000000000000000..db5386d28fa19e33eeaedf8319ef5dd343e4cbcd --- /dev/null +++ b/utils/template/builder_impl.go @@ -0,0 +1,30 @@ +package template + +import "gitlab.informatika.org/ocw/ocw-backend/utils/res" + +type TemplateWritterBuilderImpl struct { + res res.Resource + templatePool map[string]TemplateWritter +} + +func NewBuilder(res res.Resource) *TemplateWritterBuilderImpl { + return &TemplateWritterBuilderImpl{ + res: res, + templatePool: map[string]TemplateWritter{}, + } +} + +func (t *TemplateWritterBuilderImpl) Get(templatePath string) (TemplateWritter, error) { + if t.templatePool[templatePath] == nil { + template, err := NewTemplateWritterImpl(t.res, templatePath) + + if err != nil { + return nil, err + } + + t.templatePool[templatePath] = template + return template, nil + } + + return t.templatePool[templatePath], nil +} diff --git a/utils/template/impl.go b/utils/template/impl.go new file mode 100644 index 0000000000000000000000000000000000000000..36574a484ca5eb9ec816b5de635051ae2af4929d --- /dev/null +++ b/utils/template/impl.go @@ -0,0 +1,39 @@ +package template + +import ( + "bytes" + "html/template" + + "gitlab.informatika.org/ocw/ocw-backend/utils/res" +) + +type TemplateWritterImpl struct { + template *template.Template +} + +func NewTemplateWritterImpl(res res.Resource, templatePath string) (*TemplateWritterImpl, error) { + file, err := res.GetStringResource(templatePath) + + if err != nil { + return nil, err + } + + templateData, err := template.New(templatePath).Parse(file) + + if err != nil { + return nil, err + } + + return &TemplateWritterImpl{templateData}, nil +} + +func (m TemplateWritterImpl) Write(data interface{}) (string, error) { + buffer := bytes.NewBuffer([]byte{}) + err := m.template.Execute(buffer, data) + + if err != nil { + return "", err + } + + return buffer.String(), nil +} diff --git a/utils/template/type.go b/utils/template/type.go new file mode 100644 index 0000000000000000000000000000000000000000..bd38384b0e32e125f951cd891d9040c8c817c84b --- /dev/null +++ b/utils/template/type.go @@ -0,0 +1,5 @@ +package template + +type TemplateWritter interface { + Write(data interface{}) (string, error) +}