diff --git a/.env b/.env index 82b15d176cf462dcfa34cb48b3330b8ed01c8094..3958664c25ea7dec0d286c76fdaa96d87af56610 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ PORT=8080 LOGTAIL_TOKEN= HTTP_TIMEOUT_SEC=2 LOG_FLUSH_INTERVAL_MS=1000 -DB_STRING="host=localhost user=postgres password=postgres dbname=ocwdb port=5432 sslmode=disable TimeZone=Asia/Shanghai" +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=localhost SMTP_PORT=1025 \ No newline at end of file diff --git a/handler/di.go b/handler/di.go index ef48f67cd3b02eaa4df9d34dc17fb09fc317824b..835b134c53081d8bfb44e80ddda91bb03f7f7eeb 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/reset" "gitlab.informatika.org/ocw/ocw-backend/handler/swagger" ) @@ -24,4 +25,8 @@ var HandlerSet = wire.NewSet( // Auth wire.Struct(new(auth.AuthHandlerImpl), "*"), wire.Bind(new(auth.AuthHandler), new(*auth.AuthHandlerImpl)), + + // Reset + wire.Struct(new(reset.ResetHandlerImpl), "*"), + wire.Bind(new(reset.ResetHandler), new(*reset.ResetHandlerImpl)), ) diff --git a/handler/reset/confirm.go b/handler/reset/confirm.go new file mode 100644 index 0000000000000000000000000000000000000000..e68ba0568c58bacb594068f183d79c7d5a20f94b --- /dev/null +++ b/handler/reset/confirm.go @@ -0,0 +1,91 @@ +package reset + +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/reset/confirm" +) + +func (rs ResetHandlerImpl) Confirm(w http.ResponseWriter, r *http.Request) { + payload := confirm.ConfirmRequestPayload{} + validate := validator.New() + + // Validate payload + if r.Header.Get("Content-Type") != "application/json" { + payload := rs.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + rs.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := rs.HttpUtil.ParseJson(r, &payload); err != nil { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + rs.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := rs.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := rs.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Confirm Valid Website Token + confirmTokenHeader := r.Header.Get("Authorization") + + if confirmTokenHeader == "" { + payload := rs.WrapperUtil.ErrorResponseWrap("token is required", nil) + rs.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(confirmTokenHeader, " ") + + if len(token) != 2 { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + // Service to handle + payload.ConfirmToken = token[1] + err := rs.ResetService.Confirm(payload) + + if err != nil { + if strings.Contains(err.Error(), "expired/not exist") { + err = web.NewResponseErrorFromError(err, web.EmailNotExist) + } + + respErr, ok := err.(web.ResponseError) + if ok { + payload := rs.WrapperUtil.ErrorResponseWrap("email was not found", respErr) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + rs.Logger.Error( + fmt.Sprintf("[RESET] some error happened when requesting password reset: %s", err.Error()), + ) + payload := rs.WrapperUtil.ErrorResponseWrap("internal server error", nil) + rs.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil) + rs.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/reset/handler.go b/handler/reset/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..b9ee5874d324580d9f1ceebeb7c296c0aa1b09a4 --- /dev/null +++ b/handler/reset/handler.go @@ -0,0 +1,15 @@ +package reset + +import ( + "gitlab.informatika.org/ocw/ocw-backend/service/reset" + "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 ResetHandlerImpl struct { + reset.ResetService + httputil.HttpUtil + wrapper.WrapperUtil + logger.Logger +} diff --git a/handler/reset/request.go b/handler/reset/request.go new file mode 100644 index 0000000000000000000000000000000000000000..f2dc0d450e58ca4c26d33f574a403e3e3269c221 --- /dev/null +++ b/handler/reset/request.go @@ -0,0 +1,65 @@ +package reset + +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/reset/request" +) + +func (rs ResetHandlerImpl) Request(w http.ResponseWriter, r *http.Request) { + payload := request.RequestRequestPayload{} + validate := validator.New() + + if r.Header.Get("Content-Type") != "application/json" { + payload := rs.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + rs.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := rs.HttpUtil.ParseJson(r, &payload); err != nil { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + rs.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := rs.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := rs.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + err := rs.ResetService.Request(payload) + + if err != nil { + if strings.Contains(err.Error(), "unknown key") { + err = web.NewResponseErrorFromError(err, web.EmailNotExist) + } + + respErr, ok := err.(web.ResponseError) + if ok { + payload := rs.WrapperUtil.ErrorResponseWrap("email was not found", respErr) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + rs.Logger.Error( + fmt.Sprintf("[RESET] some error happened when requesting reset email: %s", err.Error()), + ) + payload := rs.WrapperUtil.ErrorResponseWrap("internal server error", nil) + rs.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil) + rs.HttpUtil.WriteSuccessJson(w, responsePayload) +} \ No newline at end of file diff --git a/handler/reset/types.go b/handler/reset/types.go new file mode 100644 index 0000000000000000000000000000000000000000..7f0a8dbcfb276d41ad8067d6848d857535d739c4 --- /dev/null +++ b/handler/reset/types.go @@ -0,0 +1,9 @@ +package reset + +import "net/http" + +type ResetHandler interface { + Request(w http.ResponseWriter, r *http.Request) + Confirm(w http.ResponseWriter, r *http.Request) + Validate(w http.ResponseWriter, r *http.Request) +} diff --git a/handler/reset/validate.go b/handler/reset/validate.go new file mode 100644 index 0000000000000000000000000000000000000000..19d48dbfdd2e9cf42e5c874779a1f03c8db100f9 --- /dev/null +++ b/handler/reset/validate.go @@ -0,0 +1,57 @@ +package reset + +import ( + "fmt" + "net/http" + "strings" + + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/validate" +) + +func (rs ResetHandlerImpl) Validate(w http.ResponseWriter, r *http.Request) { + payload := validate.ValidateRequestPayload{} + validateTokenHeader := r.Header.Get("Authorization") + + if validateTokenHeader == "" { + payload := rs.WrapperUtil.ErrorResponseWrap("token is required", nil) + rs.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + return + } + + token := strings.Split(validateTokenHeader, " ") + + if len(token) != 2 { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + if token[0] != "Bearer" { + payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil) + rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + payload.ValidateToken = token[1] + err := rs.ResetService.Validate(payload) + + if err != nil { + if errData, ok := err.(web.ResponseError); ok { + payload := rs.WrapperUtil.ErrorResponseWrap(errData.Error(), errData) + rs.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload) + return + } + + rs.Logger.Error( + fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()), + ) + payload := rs.WrapperUtil.ErrorResponseWrap("internal server error", nil) + rs.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil) + rs.HttpUtil.WriteSuccessJson(w, responsePayload) + +} \ No newline at end of file diff --git a/model/web/error_code.go b/model/web/error_code.go index 0999a12b14af2383a1aea2f79dd7aa490754a88a..e610d20e8224716d3e4ff4ea6daa8c66948eb093 100644 --- a/model/web/error_code.go +++ b/model/web/error_code.go @@ -7,6 +7,8 @@ const ( UnauthorizedAccess string = "UNAUTHORIZED" InactiveUser string = "INACTIVE_ACCOUNT" EmailExist string = "EMAIL_EXIST" + EmailNotExist string = "EMAIL_NOT_EXIST" + LinkNotAvailable string = "LINK_NOT_AVAILABLE" TokenError string = "TOKEN_ERROR" ) diff --git a/model/web/reset/confirm/request.go b/model/web/reset/confirm/request.go new file mode 100644 index 0000000000000000000000000000000000000000..6d864a35e49373198dfff3fe18da82094519c052 --- /dev/null +++ b/model/web/reset/confirm/request.go @@ -0,0 +1,14 @@ +package confirm + +// Confirm Request Payload +// @Description Information that should be available when you confirm a password reset +type ConfirmRequestPayload struct { + // Web Token that was appended to the link + ConfirmToken string + + // User Password + Password string `json:"password" validate:"required" example:"secret"` + + // User Password Validation, must be same as user + PasswordValidation string `json:"password_validation" validate:"required,eqfield=Password" example:"secret"` +} diff --git a/model/web/reset/request/request.go b/model/web/reset/request/request.go new file mode 100644 index 0000000000000000000000000000000000000000..cdd5d15bfbbf4cdccbe381d1af776bf58a299b36 --- /dev/null +++ b/model/web/reset/request/request.go @@ -0,0 +1,8 @@ +package request + +// Request Request Payload +// @Description Information that should be available when password reset is requested +type RequestRequestPayload struct { + // User Email + Email string `json:"email" validate:"required,email" example:"someone@example.com"` +} diff --git a/model/web/reset/validate/request.go b/model/web/reset/validate/request.go new file mode 100644 index 0000000000000000000000000000000000000000..f13973a086b4048d637a660d08c75076a5ec3378 --- /dev/null +++ b/model/web/reset/validate/request.go @@ -0,0 +1,8 @@ +package validate + +// Validate Request Payload +// @Description Information that should be available when link validation is done +type ValidateRequestPayload struct { + // Web Token that was appended to the link + ValidateToken string +} diff --git a/routes/di.go b/routes/di.go index 51ee85b71e73dbda4683d1e4a8cc620d088c8e3a..b44b2ed0ae71dda868755ffdf82ecf3c636a7a1e 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/reset" "gitlab.informatika.org/ocw/ocw-backend/routes/swagger" ) @@ -13,6 +14,7 @@ var routesCollectionSet = wire.NewSet( wire.Struct(new(swagger.SwaggerRoutes), "*"), wire.Struct(new(auth.AuthRoutes), "*"), wire.Struct(new(admin.AdminRoutes), "*"), + wire.Struct(new(reset.ResetRoutes), "*"), ) var RoutesSet = wire.NewSet( diff --git a/routes/reset/route.go b/routes/reset/route.go new file mode 100644 index 0000000000000000000000000000000000000000..46b32e0f9f389bfe8f3ff730d2c683225dfcee91 --- /dev/null +++ b/routes/reset/route.go @@ -0,0 +1,18 @@ +package reset + +import ( + "github.com/go-chi/chi/v5" + "gitlab.informatika.org/ocw/ocw-backend/handler/reset" +) + +type ResetRoutes struct { + reset.ResetHandler +} + +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) + }) +} diff --git a/routes/routes.go b/routes/routes.go index b29a111f5ffa31d00b6ad6615d0d72f399966a79..4c358d7a37e29d0c2e62fb603f5d5aa70469f061 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/reset" "gitlab.informatika.org/ocw/ocw-backend/routes/swagger" "gitlab.informatika.org/ocw/ocw-backend/service/logger" @@ -15,6 +16,7 @@ type AppRouter struct { admin.AdminRoutes common.CommonRoutes auth.AuthRoutes + reset.ResetRoutes // Utility Logger logger.Logger diff --git a/service/di.go b/service/di.go index c8ca39249a908a62ba8ed3742e3215cce1bfe4e4..763b0502928cbdacbe020c1c3d1e4ba64e590989 100644 --- a/service/di.go +++ b/service/di.go @@ -8,6 +8,7 @@ import ( "gitlab.informatika.org/ocw/ocw-backend/service/logger" "gitlab.informatika.org/ocw/ocw-backend/service/logger/hooks" "gitlab.informatika.org/ocw/ocw-backend/service/reporter" + "gitlab.informatika.org/ocw/ocw-backend/service/reset" "gitlab.informatika.org/ocw/ocw-backend/service/verification" ) @@ -36,6 +37,12 @@ var ServiceTestSet = wire.NewSet( wire.Bind(new(admin.AdminService), new(*admin.AdminServiceImpl)), ), + // reset service + wire.NewSet( + wire.Struct(new(reset.ResetServiceImpl), "*"), + wire.Bind(new(reset.ResetService), new(*reset.ResetServiceImpl)), + ), + // verification service wire.NewSet( wire.Struct(new(verification.VerificationServiceImpl), "*"), diff --git a/service/reset/confirm.go b/service/reset/confirm.go new file mode 100644 index 0000000000000000000000000000000000000000..426af52f5514eec16fd0a6c4553bd817974177fd --- /dev/null +++ b/service/reset/confirm.go @@ -0,0 +1,11 @@ +package reset + +import ( + // "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/confirm" +) + +func (rs ResetServiceImpl) Confirm(payload confirm.ConfirmRequestPayload) error { + // TODO replace dummy + return nil +} \ No newline at end of file diff --git a/service/reset/impl.go b/service/reset/impl.go new file mode 100644 index 0000000000000000000000000000000000000000..b9986e2509f4af7f8c0deb9053bf8d30640e4168 --- /dev/null +++ b/service/reset/impl.go @@ -0,0 +1,17 @@ +package reset + +import ( + "gitlab.informatika.org/ocw/ocw-backend/repository/user" + "gitlab.informatika.org/ocw/ocw-backend/service/verification" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "gitlab.informatika.org/ocw/ocw-backend/utils/password" + "gitlab.informatika.org/ocw/ocw-backend/utils/token" +) + +type ResetServiceImpl struct { + user.UserRepository + password.PasswordUtil + *env.Environment + token.TokenUtil + verification.VerificationService +} diff --git a/service/reset/request.go b/service/reset/request.go new file mode 100644 index 0000000000000000000000000000000000000000..05584e6a9bb3dcf0a7a78c4ec5e12e1c4502732d --- /dev/null +++ b/service/reset/request.go @@ -0,0 +1,11 @@ +package reset + +import ( + // "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/request" +) + +func (rs ResetServiceImpl) Request(payload request.RequestRequestPayload) error { + // TODO replace dummy + return nil +} \ No newline at end of file diff --git a/service/reset/type.go b/service/reset/type.go new file mode 100644 index 0000000000000000000000000000000000000000..f1658d2fa25575421a24bcf7fa8ac331a88b5dbe --- /dev/null +++ b/service/reset/type.go @@ -0,0 +1,13 @@ +package reset + +import ( + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/request" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/confirm" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/validate" +) + +type ResetService interface { + Request(payload request.RequestRequestPayload) error + Confirm(payload confirm.ConfirmRequestPayload) error + Validate(payload validate.ValidateRequestPayload) error +} diff --git a/service/reset/validate.go b/service/reset/validate.go new file mode 100644 index 0000000000000000000000000000000000000000..7cd09fe9bc83b23a2e4d0bbc0abf95fe1758b5cc --- /dev/null +++ b/service/reset/validate.go @@ -0,0 +1,11 @@ +package reset + +import ( + // "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" + "gitlab.informatika.org/ocw/ocw-backend/model/web/reset/validate" +) + +func (rs ResetServiceImpl) Validate(payload validate.ValidateRequestPayload) error { + // TODO replace dummy + return nil +} \ No newline at end of file