diff --git a/docs/docs.go b/docs/docs.go index 6fece1a6822d55bcc98a20a155a4039e5fb1a903..2fe14821849e7ef1f405a20cdbc22010c2f2f1f4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -284,7 +284,7 @@ const docTemplate = `{ }, "/auth/register": { "post": { - "description": "Generate New Account as Member", + "description": "Do Email Verification", "consumes": [ "application/json" ], @@ -294,7 +294,7 @@ const docTemplate = `{ "tags": [ "auth" ], - "summary": "Register New Account", + "summary": "Email Verification", "parameters": [ { "description": "Register Payload", @@ -302,7 +302,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/register.RegisterRequestPayload" + "$ref": "#/definitions/verification.VerificationRequestPayload" } } ], @@ -406,6 +406,20 @@ const docTemplate = `{ } } }, + "verification.VerificationRequestPayload": { + "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..617b67b5f885882f794268f5787ca82e203c8789 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -276,7 +276,7 @@ }, "/auth/register": { "post": { - "description": "Generate New Account as Member", + "description": "Do Email Verification", "consumes": [ "application/json" ], @@ -286,7 +286,7 @@ "tags": [ "auth" ], - "summary": "Register New Account", + "summary": "Email Verification", "parameters": [ { "description": "Register Payload", @@ -294,7 +294,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/register.RegisterRequestPayload" + "$ref": "#/definitions/verification.VerificationRequestPayload" } } ], @@ -398,6 +398,20 @@ } } }, + "verification.VerificationRequestPayload": { + "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..75cac2181b8759ae62b9f486898e42e1f2171dd5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -56,6 +56,16 @@ definitions: - password - password_validation type: object + verification.VerificationRequestPayload: + 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: {} @@ -244,14 +254,14 @@ paths: post: consumes: - application/json - description: Generate New Account as Member + description: Do Email Verification parameters: - description: Register Payload in: body name: data required: true schema: - $ref: '#/definitions/register.RegisterRequestPayload' + $ref: '#/definitions/verification.VerificationRequestPayload' produces: - application/json responses: @@ -267,7 +277,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/web.BaseResponse' - summary: Register New Account + summary: Email Verification tags: - auth swagger: "2.0" diff --git a/handler/auth/types.go b/handler/auth/types.go index 71b4d1eba14c50a290f9c43565da1dd707e73f29..1b70a41ef00b627a24521bb24ab8804bfe24c640 100644 --- a/handler/auth/types.go +++ b/handler/auth/types.go @@ -6,4 +6,5 @@ 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) } diff --git a/handler/auth/verify.go b/handler/auth/verify.go new file mode 100644 index 0000000000000000000000000000000000000000..6ab6b25233d8c7da2f4339abe7bd439736a979f1 --- /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 Email Verification +// @Description Do Email Verification +// @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/register [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.AuthService.VerifyEmail(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/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/auth/verification/request.go b/model/web/auth/verification/request.go new file mode 100644 index 0000000000000000000000000000000000000000..52b9cbc8b19133a25aca771ca96f4633e07deafa --- /dev/null +++ b/model/web/auth/verification/request.go @@ -0,0 +1,8 @@ +package verification + +// Email Verification Request Payload +// @Description Information that should be passed when request verify +type VerificationRequestPayload struct { + // User Email + Email string `json:"email" validate:"required,email" example:"someone@example.com"` +} diff --git a/routes/auth/route.go b/routes/auth/route.go index d7ea811b69fd604111f9a013b7821a10ef005619..02205f37e1215d7ff63512613a7a7117f05edb7c 100644 --- a/routes/auth/route.go +++ b/routes/auth/route.go @@ -14,5 +14,6 @@ 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", ar.AuthHandler.EmailVerify) }) } diff --git a/service/auth/type.go b/service/auth/type.go index 2503726fe0562e9b2b26544dabae60017d13c386..937521d8a9aacc0e95cfd1061e6cbc627b545686 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 + VerifyEmail(payload verification.VerificationRequestPayload) error } diff --git a/service/auth/verify.go b/service/auth/verify.go new file mode 100644 index 0000000000000000000000000000000000000000..a8462cec12a42a999b2910ef831768c8f6f4826d --- /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) VerifyEmail(payload verification.VerificationRequestPayload) error { + return nil +} diff --git a/utils/env/env.go b/utils/env/env.go index a55416e52bf462568666b3b4de1afc886d97fa44..e1b914f0d6842317f23ab5288ed18f9d8a4a38b1 100644 --- a/utils/env/env.go +++ b/utils/env/env.go @@ -36,6 +36,10 @@ 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:""` + EmailVerificationPath string `env:"EMAIL_VERIFICATION_PATH" envDefault:""` } func New() (*Environment, error) { diff --git a/utils/res/data/email-verification.format.html b/utils/res/data/email-verification.format.html new file mode 100644 index 0000000000000000000000000000000000000000..2a611470df9f6d6c48f3b32c72645633b2ebc617 --- /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..eef10ba0102657a6407c96d4f2a1b82bc2d6fd2e --- /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={{ .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