diff --git a/.env b/.env index 43bf5fd412feeda40517d5f5e722333b92271442..568639cff029d91346e8d347f2f5cbab64407d2e 100644 --- a/.env +++ b/.env @@ -4,6 +4,4 @@ PORT=8080 LOGTAIL_TOKEN= HTTP_TIMEOUT_SEC=2 LOG_FLUSH_INTERVAL_MS=1000 -SMTP_USERNAME="noreply@ocw.id" -SMTP_PORT=1025 FE_BASE_URL="http://localhost:3000" diff --git a/.gitignore b/.gitignore index d3eeb070587bb63e0a455b491181f8fc408c8df1..c18416209344fed18ffcb6096897cbbef2ff3dd8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ bin/ tmp/ wire_gen.go __debug_bin* -.env.local +.env.local* diff --git a/.scannerwork/.sonar_lock b/.scannerwork/.sonar_lock new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.scannerwork/report-task.txt b/.scannerwork/report-task.txt new file mode 100644 index 0000000000000000000000000000000000000000..5254c69f7309289db73b05aa53239cf41b055c97 --- /dev/null +++ b/.scannerwork/report-task.txt @@ -0,0 +1,6 @@ +projectKey=ocw-backend +serverUrl=http://localhost:9000 +serverVersion=10.0.0.68432 +dashboardUrl=http://localhost:9000/dashboard?id=ocw-backend +ceTaskId=AYeyCUSQt5Mtvce1YZdB +ceTaskUrl=http://localhost:9000/api/ce/task?id=AYeyCUSQt5Mtvce1YZdB diff --git a/Dockerfile b/Dockerfile index 7673b438cedcef2e1e5775bdc6b25c6049c8d090..fb0d84339ffdb25a43bb456c86cc1262af5ef6bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,13 @@ FROM golang:1.20.0-alpine3.17 AS build RUN apk add --update make +RUN apk add --update git COPY . /app WORKDIR /app RUN go get -RUN go install github.com/swaggo/swag/cmd/swag@latest +RUN go install github.com/swaggo/swag/cmd/swag@v1.8.10 RUN go install github.com/google/wire/cmd/wire@latest RUN make build @@ -17,6 +18,8 @@ RUN mkdir /app WORKDIR /app COPY --from=build /app/bin/server.app /app +RUN touch /app/.env +RUN touch /app/.env.local STOPSIGNAL SIGKILL ENTRYPOINT [ "/app/server.app" ] diff --git a/Dockerfile.dev b/Dockerfile.dev index 03526f3e75455e51850f7f1186b2d2b05f140392..6f6f47c0283f6de5b2139a540fd21e919298a0ba 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,9 +1,10 @@ FROM cosmtrek/air RUN apt install -y make +RUN apt install -y git RUN go install github.com/google/wire/cmd/wire@latest -RUN go install github.com/swaggo/swag/cmd/swag@latest +RUN go install github.com/swaggo/swag/cmd/swag@v1.8.10 COPY . /app diff --git a/Dockerfile.test b/Dockerfile.test index 7b6d3bf4ffb8bbfdc1eed6dfa334889e3d2ec072..d61a7100118f3f92eccfbf3f3b7b3de8948fd4f3 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -1,12 +1,13 @@ FROM golang:1.20.0-alpine3.17 AS build RUN apk add --update make +RUN apk add --update git COPY . /app WORKDIR /app RUN go get -RUN go install github.com/swaggo/swag/cmd/swag@latest +RUN go install github.com/swaggo/swag/cmd/swag@v1.8.10 RUN go install github.com/google/wire/cmd/wire@latest RUN make test-dependency diff --git a/docker-compose.yml b/docker-compose.yml index 3eccf485e7a93a9172dcdfca800f8bcf2b732655..5194e4f7085bd96c5ae3182c99c6a8e0cb195294 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,11 +14,12 @@ services: networks: - api_network backend: - build: + build: context: . - dockerfile: Dockerfile.dev + dockerfile: Dockerfile volumes: - - .:/app + - .env.local:/app/.env.local + - .env:/app/.env ports: - 8888:8080 env_file: .env.docker diff --git a/docs/docs.go b/docs/docs.go index 98c0da896fb894756cfb57df0d77c23623da0ae7..e996242b584017791f99aa699f8d1fda341115c3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2270,6 +2270,67 @@ const docTemplate = `{ } } }, + "/quiz/{id}/finish": { + "post": { + "description": "Finish quiz session and get the score", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "quiz" + ], + "summary": "Finish Quiz", + "parameters": [ + { + "type": "string", + "description": "Authenticate User (any role)", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Quiz Finish payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/quiz.FinishQuizPayload" + } + }, + { + "type": "string", + "format": "uuid", + "description": "Quiz id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/quiz.QuizDetail" + } + } + } + ] + } + } + } + } + }, "/quiz/{id}/solution": { "get": { "description": "Take a quiz", @@ -2324,7 +2385,7 @@ const docTemplate = `{ }, "/quiz/{id}/take": { "post": { - "description": "Finish quiz session and get the score", + "description": "Take a quiz", "consumes": [ "application/json" ], @@ -2334,7 +2395,7 @@ const docTemplate = `{ "tags": [ "quiz" ], - "summary": "Finish Quiz", + "summary": "Take Quiz", "parameters": [ { "type": "string", @@ -2343,15 +2404,6 @@ const docTemplate = `{ "in": "header", "required": true }, - { - "description": "Quiz Finish payload", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/quiz.FinishQuizPayload" - } - }, { "type": "string", "format": "uuid", diff --git a/docs/swagger.json b/docs/swagger.json index 3a0ebf5ec30dc04a8de1349ace8866095354c853..59ed537c3af3c9c5b38d3f1c98a12421d3128acd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2261,6 +2261,67 @@ } } }, + "/quiz/{id}/finish": { + "post": { + "description": "Finish quiz session and get the score", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "quiz" + ], + "summary": "Finish Quiz", + "parameters": [ + { + "type": "string", + "description": "Authenticate User (any role)", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Quiz Finish payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/quiz.FinishQuizPayload" + } + }, + { + "type": "string", + "format": "uuid", + "description": "Quiz id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/quiz.QuizDetail" + } + } + } + ] + } + } + } + } + }, "/quiz/{id}/solution": { "get": { "description": "Take a quiz", @@ -2315,7 +2376,7 @@ }, "/quiz/{id}/take": { "post": { - "description": "Finish quiz session and get the score", + "description": "Take a quiz", "consumes": [ "application/json" ], @@ -2325,7 +2386,7 @@ "tags": [ "quiz" ], - "summary": "Finish Quiz", + "summary": "Take Quiz", "parameters": [ { "type": "string", @@ -2334,15 +2395,6 @@ "in": "header", "required": true }, - { - "description": "Quiz Finish payload", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/quiz.FinishQuizPayload" - } - }, { "type": "string", "format": "uuid", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ed665f4c6707b7edbac8818752a98ba9c9cc2c35..7c62c7b01c3e2db940405858fe0ea0d9c9eb5ba2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2013,6 +2013,44 @@ paths: summary: Get Quiz Detail tags: - quiz + /quiz/{id}/finish: + post: + consumes: + - application/json + description: Finish quiz session and get the score + parameters: + - description: Authenticate User (any role) + in: header + name: Authorization + required: true + type: string + - description: Quiz Finish payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/quiz.FinishQuizPayload' + - description: Quiz id + format: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/web.BaseResponse' + - properties: + data: + $ref: '#/definitions/quiz.QuizDetail' + type: object + summary: Finish Quiz + tags: + - quiz /quiz/{id}/solution: get: consumes: @@ -2049,19 +2087,13 @@ paths: post: consumes: - application/json - description: Finish quiz session and get the score + description: Take a quiz parameters: - description: Authenticate User (any role) in: header name: Authorization required: true type: string - - description: Quiz Finish payload - in: body - name: data - required: true - schema: - $ref: '#/definitions/quiz.FinishQuizPayload' - description: Quiz id format: uuid in: path @@ -2080,7 +2112,7 @@ paths: data: $ref: '#/definitions/quiz.QuizDetail' type: object - summary: Finish Quiz + summary: Take Quiz tags: - quiz /reset/confirm: diff --git a/handler/quiz/take.go b/handler/quiz/take.go index 530f4f77823001b1a5027b76d610b8c9ccafcaad..06499ed4839bd8e9ce07d009b3122656d311fb9c 100644 --- a/handler/quiz/take.go +++ b/handler/quiz/take.go @@ -138,7 +138,7 @@ func (m QuizHandlerImpl) GetQuizSolution(w http.ResponseWriter, r *http.Request) // @Param data body quiz.FinishQuizPayload true "Quiz Finish payload" // @Param id path string true "Quiz id" Format(uuid) // @Success 200 {object} web.BaseResponse{data=quiz.QuizDetail} -// @Router /quiz/{id}/take [post] +// @Router /quiz/{id}/finish [post] func (m QuizHandlerImpl) FinishQuiz(w http.ResponseWriter, r *http.Request) { payload := quiz.FinishQuizPayload{} diff --git a/provider/mail/smtp/smtp.go b/provider/mail/smtp/smtp.go index cc1d4060a460ed00840c8f52072ee2ee7b20234f..0dacbabc2f84bf2effb400ce67e9ec46ab397e1c 100644 --- a/provider/mail/smtp/smtp.go +++ b/provider/mail/smtp/smtp.go @@ -13,10 +13,20 @@ type SmtpMailProvider struct { } func New(env *env.Environment) *SmtpMailProvider { - auth := smtp.CRAMMD5Auth( - env.SmtpUsername, - env.SmtpPassword, - ) + var auth smtp.Auth + if env.SmtpAuthType == "CRAM" { + auth = smtp.CRAMMD5Auth( + env.SmtpUsername, + env.SmtpPassword, + ) + } else if env.SmtpAuthType == "plain" { + auth = smtp.PlainAuth( + env.SmtpIdentity, + env.SmtpUsername, + env.SmtpPassword, + env.SmtpServer, + ) + } return &SmtpMailProvider{ Environment: env, diff --git a/provider/redis/cache.go b/provider/redis/cache.go index 9d656ae2093191e41cdd8666e7f20f5b2c185a92..729c3e4f82cffd75685eb44160f5754695da93e9 100644 --- a/provider/redis/cache.go +++ b/provider/redis/cache.go @@ -43,23 +43,29 @@ func NewRedisEnv( IdleTimeout: 240 * time.Second, Dial: func() (redis.Conn, error) { defer resolver(log) - conn, err := redis.Dial("tcp", env.RedisConnection+":"+env.RedisPort) - if err != nil { - log.Warning("failed connect to redis server: tcp " + env.RedisConnection + ":" + env.RedisPort) - log.Warning(err.Error()) - - return nil, err + dialOptions := []redis.DialOption{ + redis.DialUseTLS(env.RedisUseTLS), } if env.RedisUseAuth { - if _, err := conn.Do("AUTH", env.RedisUsername, env.RedisPassword); err != nil { - conn.Close() + dialOptions = append(dialOptions, + redis.DialUsername(env.RedisUsername), + redis.DialPassword(env.RedisPassword), + ) + } - log.Warning("failed connect to redis server: authentication failed") + conn, err := redis.Dial( + "tcp", + env.RedisConnection+":"+env.RedisPort, + dialOptions..., + ) - return nil, err - } + if err != nil { + log.Warning("failed connect to redis server: tcp " + env.RedisConnection + ":" + env.RedisPort) + log.Warning(err.Error()) + + return nil, err } return conn, err diff --git a/provider/storage/manager.go b/provider/storage/manager.go index cfeae57144fe4d23729a2382984360253e5d4659..6cf93f7291323ebb26246670dcafd9978aeda76f 100644 --- a/provider/storage/manager.go +++ b/provider/storage/manager.go @@ -1,7 +1,10 @@ package storage import ( + "bufio" + "bytes" "context" + "io" "github.com/minio/minio-go/v7" ) @@ -18,7 +21,15 @@ func (s S3Storage) Get(ctx context.Context, path string) ([]byte, error) { return result, err } - _, err = obj.Read(result) + var buffer bytes.Buffer + writter := bufio.NewWriter(&buffer) + + _, err = io.Copy(writter, obj) + if err != nil { + return result, err + } + + result = buffer.Bytes() return result, err } diff --git a/repository/quiz/impl.go b/repository/quiz/impl.go index e51495422f1a10911dd47bf00b1764eb541446ab..72ff3800d9dba38ab9285c9bcea6c5f22ed25118 100644 --- a/repository/quiz/impl.go +++ b/repository/quiz/impl.go @@ -42,9 +42,11 @@ func (q *QuizRepositoryImpl) GetQuizDetail(quizId uuid.UUID) (*quiz.Quiz, error) func (q *QuizRepositoryImpl) UpdateScore(takeId uuid.UUID, score int) error { return q.db. Model(&quiz.QuizTake{}). - Update("score", score). - Update("is_finished", true). - Where("id = ?", takeId).Error + Where("id = ?", takeId). + Updates(quiz.QuizTake{ + Score: score, + IsFinished: true, + }).Error } func (q *QuizRepositoryImpl) NewTake(quizId uuid.UUID, userEmail string) (uuid.UUID, error) { @@ -64,18 +66,18 @@ func (q *QuizRepositoryImpl) NewTake(quizId uuid.UUID, userEmail string) (uuid.U } func (q *QuizRepositoryImpl) IsActiveTake(quizId uuid.UUID, userEmail string) (bool, error) { - result := struct{ cnt int }{} + var result int64 = 0 err := q.db. - Select("COUNT(*) as cnt"). + Model(&quiz.QuizTake{}). Where("quiz_id = ? AND email = ? AND is_finished = false", quizId, userEmail). - Find(result). + Count(&result). Error if err != nil { return false, nil } - return result.cnt > 0, nil + return result > 0, nil } func (q *QuizRepositoryImpl) GetAllTake(quizId uuid.UUID, userEmail string) ([]quiz.QuizTake, error) { diff --git a/service/auth/impl.go b/service/auth/impl.go index 9ac5f343fb68b92e44a3a51491389a21d5c70c5a..5175a0a02b2b13654f7f1328a131fb97447ea432 100644 --- a/service/auth/impl.go +++ b/service/auth/impl.go @@ -2,6 +2,7 @@ package auth import ( "gitlab.informatika.org/ocw/ocw-backend/repository/user" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" "gitlab.informatika.org/ocw/ocw-backend/service/verification" "gitlab.informatika.org/ocw/ocw-backend/utils/env" "gitlab.informatika.org/ocw/ocw-backend/utils/password" @@ -14,4 +15,5 @@ type AuthServiceImpl struct { *env.Environment token.TokenUtil verification.VerificationService + logger.Logger } diff --git a/service/auth/register.go b/service/auth/register.go index b7941c50ba99c941bd7810ad82f3c3862f9ef26c..5227bd811d8e9631c09d38f19d4cfef9c364fc45 100644 --- a/service/auth/register.go +++ b/service/auth/register.go @@ -1,8 +1,11 @@ package auth import ( + "errors" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/register" + "gorm.io/gorm" ) func (auth AuthServiceImpl) Register(payload register.RegisterRequestPayload) error { @@ -20,8 +23,19 @@ func (auth AuthServiceImpl) Register(payload register.RegisterRequestPayload) er IsActivated: false, }) + if errors.Is(err, gorm.ErrDuplicatedKey) { + err := auth.SendVerifyMail(payload.Email) + if err != nil { + auth.Logger.Warning("Failed to send email: " + err.Error()) + } + return nil + } + if err == nil { - auth.SendVerifyMail(payload.Email) + err := auth.SendVerifyMail(payload.Email) + if err != nil { + auth.Logger.Warning("Failed to send email: " + err.Error()) + } } return err diff --git a/test/mail/mail_test.go b/test/mail/mail_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1f1f48e7d346dbb06ca453c27e48d24364e8e7e0 --- /dev/null +++ b/test/mail/mail_test.go @@ -0,0 +1,24 @@ +package mail + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.informatika.org/ocw/ocw-backend/provider/mail/smtp" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" +) + +func IgnoreTestSendMail(t *testing.T) { + smtpClient := smtp.New(&env.Environment{ + SmtpUsername: "", + SmtpPassword: "", + SmtpIdentity: "", + SmtpServer: "", + SmtpPort: 22, + SmtpAuthType: "", + }) + + err := smtpClient.Send([]string{"bayusamudra.55.02.com@gmail.com"}, "Testing", "Ini test") + + assert.Nil(t, err) +} diff --git a/test/middleware/cors_test.go b/test/middleware/cors_test.go index 74615aa27d9b04bcb60edf4173e0bf9f83b9eddf..b793862aed471c20d8332eab34efdf0bc1dc2e53 100644 --- a/test/middleware/cors_test.go +++ b/test/middleware/cors_test.go @@ -16,14 +16,14 @@ func TestPreflight(t *testing.T) { Headers: map[string]string{ "Access-Control-Request-Method": "GET", "Access-Control-Request-Headers": "accept, origin, authorization, content-type, referer", - "Origin": "https://inkubatorit.com", + "Origin": "https://test.com", }, }) assert.Nil(t, err) assert.Equal(t, res.StatusCode, http.StatusOK) - assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://inkubatorit.com") + assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://test.com") assert.Contains(t, res.Header.Get("Access-Control-Allow-Methods"), "GET") }) @@ -34,14 +34,14 @@ func TestPreflight(t *testing.T) { Headers: map[string]string{ "Access-Control-Request-Method": "GET", "Access-Control-Request-Headers": "accept, origin, authorization, content-type, referer", - "Origin": "https://inkubatorit.com", + "Origin": "https://test.com", }, }) assert.Nil(t, err) assert.Equal(t, res.StatusCode, http.StatusOK) - assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://inkubatorit.com") + assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://test.com") assert.Contains(t, res.Header.Get("Access-Control-Allow-Methods"), "GET") }) @@ -52,14 +52,14 @@ func TestPreflight(t *testing.T) { Headers: map[string]string{ "Access-Control-Request-Method": "PATCH", "Access-Control-Request-Headers": "accept, origin, authorization, content-type, referer", - "Origin": "https://inkubatorit.com", + "Origin": "https://test.com", }, }) assert.Nil(t, err) assert.Equal(t, res.StatusCode, http.StatusOK) - assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://inkubatorit.com") + assert.Equal(t, res.Header.Get("Access-Control-Allow-Origin"), "https://test.com") assert.Equal(t, res.Header.Get("Access-Control-Allow-Methods"), "PATCH") }) } diff --git a/utils/env/env.go b/utils/env/env.go index ff09c00b0ff32c969ddd850d791ca8e35ad8fc9d..85aa99986a2c0a45cb68f07927cbe954137a16e3 100644 --- a/utils/env/env.go +++ b/utils/env/env.go @@ -36,6 +36,7 @@ type Environment struct { SmtpPassword string `env:"SMTP_PASSWORD"` SmtpServer string `env:"SMTP_SERVER"` SmtpPort int `env:"SMTP_PORT" envDefault:"25"` + SmtpAuthType string `env:"SMTP_TYPE" envDefault:"CRAM"` FrontendBaseURL string `env:"FE_BASE_URL"` ResetPasswordPath string `env:"RESET_PASSWORD_PATH" envDefault:"/resetPassword"` @@ -51,6 +52,7 @@ type Environment struct { RedisPassword string `env:"REDIS_PASSWORD"` RedisUseAuth bool `env:"REDIS_USE_AUTH" envDefault:"false"` RedisPrefixKey string `env:"REDIS_PREFIX_KEY" envDefault:"app:"` + RedisUseTLS bool `env:"REDIS_USE_TLS" envDefault:"false"` BucketEndpoint string `env:"BUCKET_ENDPOINT"` BucketSecretKey string `env:"BUCKET_SECRET_KEY"`