add forget password
This commit is contained in:
parent
7ea809cc09
commit
5a0dec6128
9
config/brevo.go
Normal file
9
config/brevo.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Brevo struct {
|
||||||
|
APIKey string `mapstructure:"api_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brevo) GetApiKey() string {
|
||||||
|
return b.APIKey
|
||||||
|
}
|
||||||
@ -29,6 +29,8 @@ type Config struct {
|
|||||||
Jwt Jwt `mapstructure:"jwt"`
|
Jwt Jwt `mapstructure:"jwt"`
|
||||||
OSSConfig OSSConfig `mapstructure:"oss"`
|
OSSConfig OSSConfig `mapstructure:"oss"`
|
||||||
Midtrans Midtrans `mapstructure:"midtrans"`
|
Midtrans Midtrans `mapstructure:"midtrans"`
|
||||||
|
Brevo Brevo `mapstructure:"brevo"`
|
||||||
|
Email Email `mapstructure:"email"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -67,5 +69,9 @@ func (c *Config) Auth() *AuthConfig {
|
|||||||
jwtTokenExpiresTTL: c.Jwt.Token.ExpiresTTL,
|
jwtTokenExpiresTTL: c.Jwt.Token.ExpiresTTL,
|
||||||
jwtOrderSecret: c.Jwt.TokenOrder.Secret,
|
jwtOrderSecret: c.Jwt.TokenOrder.Secret,
|
||||||
jwtOrderExpiresTTL: c.Jwt.TokenOrder.ExpiresTTL,
|
jwtOrderExpiresTTL: c.Jwt.TokenOrder.ExpiresTTL,
|
||||||
|
jwtSecretResetPassword: JWT{
|
||||||
|
secret: c.Jwt.TokenResetPassword.Secret,
|
||||||
|
expireTTL: c.Jwt.TokenResetPassword.ExpiresTTL,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,16 @@ package config
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
jwtTokenExpiresTTL int
|
jwtTokenExpiresTTL int
|
||||||
jwtTokenSecret string
|
jwtTokenSecret string
|
||||||
jwtOrderSecret string
|
jwtOrderSecret string
|
||||||
jwtOrderExpiresTTL int
|
jwtOrderExpiresTTL int
|
||||||
|
jwtSecretResetPassword JWT
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWT struct {
|
||||||
|
secret string
|
||||||
|
expireTTL int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AuthConfig) AccessTokenSecret() string {
|
func (c *AuthConfig) AccessTokenSecret() string {
|
||||||
@ -26,3 +32,12 @@ func (c *AuthConfig) AccessTokenExpiresDate() time.Time {
|
|||||||
duration := time.Duration(c.jwtTokenExpiresTTL)
|
duration := time.Duration(c.jwtTokenExpiresTTL)
|
||||||
return time.Now().UTC().Add(time.Minute * duration)
|
return time.Now().UTC().Add(time.Minute * duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *AuthConfig) AccessTokenResetPasswordSecret() string {
|
||||||
|
return c.jwtSecretResetPassword.secret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AuthConfig) AccessTokenResetPasswordExpire() time.Time {
|
||||||
|
duration := time.Duration(c.jwtSecretResetPassword.expireTTL)
|
||||||
|
return time.Now().UTC().Add(time.Minute * duration)
|
||||||
|
}
|
||||||
|
|||||||
32
config/email.go
Normal file
32
config/email.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Email struct {
|
||||||
|
Sender string `mapstructure:"sender"`
|
||||||
|
CustomReceiver string `mapstructure:"custom_receiver"`
|
||||||
|
ResetPassword EmailConfig `mapstructure:"reset_password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmailConfig struct {
|
||||||
|
Subject string `mapstructure:"subject"`
|
||||||
|
OpeningWord string `mapstructure:"opening_word"`
|
||||||
|
Link string `mapstructure:"link"`
|
||||||
|
Notes string `mapstructure:"note"`
|
||||||
|
ClosingWord string `mapstructure:"closing_word"`
|
||||||
|
TemplateName string `mapstructure:"template_name"`
|
||||||
|
TemplatePath string `mapstructure:"template_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmailMemberRequestActionConfig struct {
|
||||||
|
TemplateName string `mapstructure:"template_name"`
|
||||||
|
TemplatePath string `mapstructure:"template_path"`
|
||||||
|
Subject string `mapstructure:"subject"`
|
||||||
|
Content string `mapstructure:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Email) GetSender() string {
|
||||||
|
return e.Sender
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Email) GetCustomReceiver() string {
|
||||||
|
return e.CustomReceiver
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type Jwt struct {
|
type Jwt struct {
|
||||||
Token Token `mapstructure:"token"`
|
Token Token `mapstructure:"token"`
|
||||||
TokenOrder Token `mapstructure:"token-order"`
|
TokenOrder Token `mapstructure:"token-order"`
|
||||||
|
TokenResetPassword Token `mapstructure:"token-reset-password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Token struct {
|
type Token struct {
|
||||||
|
|||||||
5
go.mod
5
go.mod
@ -1,6 +1,6 @@
|
|||||||
module furtuna-be
|
module furtuna-be
|
||||||
|
|
||||||
go 1.19
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
@ -20,6 +20,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/antihax/optional v1.0.0 // indirect
|
||||||
github.com/bytedance/sonic v1.10.2 // indirect
|
github.com/bytedance/sonic v1.10.2 // indirect
|
||||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect
|
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
@ -63,6 +64,7 @@ require (
|
|||||||
go.uber.org/atomic v1.10.0 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
go.uber.org/multierr v1.8.0 // indirect
|
go.uber.org/multierr v1.8.0 // indirect
|
||||||
golang.org/x/arch v0.7.0 // indirect
|
golang.org/x/arch v0.7.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.16.0 // indirect
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/tools v0.17.0 // indirect
|
golang.org/x/tools v0.17.0 // indirect
|
||||||
@ -73,6 +75,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go v1.50.0
|
github.com/aws/aws-sdk-go v1.50.0
|
||||||
|
github.com/getbrevo/brevo-go v1.0.0
|
||||||
github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00
|
github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00
|
||||||
go.uber.org/zap v1.21.0
|
go.uber.org/zap v1.21.0
|
||||||
golang.org/x/net v0.20.0
|
golang.org/x/net v0.20.0
|
||||||
|
|||||||
8
go.sum
8
go.sum
@ -40,6 +40,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||||
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
github.com/aws/aws-sdk-go v1.50.0 h1:HBtrLeO+QyDKnc3t1+5DR1RxodOHCGr8ZcrHudpv7jI=
|
github.com/aws/aws-sdk-go v1.50.0 h1:HBtrLeO+QyDKnc3t1+5DR1RxodOHCGr8ZcrHudpv7jI=
|
||||||
github.com/aws/aws-sdk-go v1.50.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
github.com/aws/aws-sdk-go v1.50.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||||
@ -80,6 +82,10 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
|
|||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/getbrevo/brevo-go v1.0.0 h1:E/pRCsQeExvZeTCJU5vy+xHWcLaL5axWQ9QkxjlFke4=
|
||||||
|
github.com/getbrevo/brevo-go v1.0.0/go.mod h1:2TBMEnaDqq/oiAXUYtn6eykiEdHcEoS7tc63+YoFibw=
|
||||||
|
github.com/getbrevo/brevo-go v1.1.1 h1:6/SXEQ7ZfUjetPnJ4EncfLSUgXjQv4qUj1EQgLXnDto=
|
||||||
|
github.com/getbrevo/brevo-go v1.1.1/go.mod h1:ExhytIoPxt/cOBl6ZEMeEZNLUKrWEYA5U3hM/8WP2bg=
|
||||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
@ -400,6 +406,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
|
|||||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||||
|
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
|||||||
@ -36,3 +36,15 @@ midtrans:
|
|||||||
server_key: "SB-Mid-server-YOIvuaIlRw3In9SymCuFz-hB"
|
server_key: "SB-Mid-server-YOIvuaIlRw3In9SymCuFz-hB"
|
||||||
client_key: "SB-Mid-client-ulkZGFiS8PqBNOZz"
|
client_key: "SB-Mid-client-ulkZGFiS8PqBNOZz"
|
||||||
env: 1
|
env: 1
|
||||||
|
|
||||||
|
brevo:
|
||||||
|
api_key: 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
|
||||||
|
|
||||||
|
email:
|
||||||
|
sender: "siregaraditya@gmail.com"
|
||||||
|
reset_password:
|
||||||
|
template_name: "reset_password"
|
||||||
|
template_path: "templates/reset_password.html"
|
||||||
|
subject: "Reset Password"
|
||||||
|
opening_word: "Terima kasih sudah menjadi bagian dari Furtuna. Anda telah berhasil melakukan reset password, silakan masukan unik password yang dibuat oleh sistem dibawah ini:"
|
||||||
|
closing_word: "Silakan login kembali menggunakan email dan password anda diatas, sistem akan secara otomatis meminta anda untuk membuat password baru setelah berhasil login. Mohon maaf atas kendala yang dialami."
|
||||||
@ -53,6 +53,13 @@ func NewServiceException(errType ErrType) *ServiceException {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewError(errType ErrType, message string) *ServiceException {
|
||||||
|
return &ServiceException{
|
||||||
|
errorType: errType,
|
||||||
|
message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ServiceException) ErrorType() ErrType {
|
func (s *ServiceException) ErrorType() ErrType {
|
||||||
return s.errorType
|
return s.errorType
|
||||||
}
|
}
|
||||||
@ -95,7 +102,10 @@ func (s *ServiceException) MapErrorsToCode() Code {
|
|||||||
case errUserIsNotFound:
|
case errUserIsNotFound:
|
||||||
return BadRequest
|
return BadRequest
|
||||||
|
|
||||||
|
case errInvalidLogin:
|
||||||
|
return BadRequest
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return ServerError
|
return BadRequest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ type UserDB struct {
|
|||||||
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deleted_at"`
|
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deleted_at"`
|
||||||
CreatedBy int64 `gorm:"column:created_by" json:"created_by"`
|
CreatedBy int64 `gorm:"column:created_by" json:"created_by"`
|
||||||
UpdatedBy int64 `gorm:"column:updated_by" json:"updated_by"`
|
UpdatedBy int64 `gorm:"column:updated_by" json:"updated_by"`
|
||||||
|
ResetPassword bool `gorm:"column:reset_password" json:"reset_password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserDB) ToUser() *User {
|
func (u *UserDB) ToUser() *User {
|
||||||
@ -42,18 +43,19 @@ func (u *UserDB) ToUser() *User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userEntity := &User{
|
userEntity := &User{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
Name: u.Name,
|
Name: u.Name,
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
Status: u.Status,
|
Status: u.Status,
|
||||||
CreatedAt: u.CreatedAt,
|
CreatedAt: u.CreatedAt,
|
||||||
UpdatedAt: u.UpdatedAt,
|
UpdatedAt: u.UpdatedAt,
|
||||||
RoleID: role.Role(u.RoleID),
|
RoleID: role.Role(u.RoleID),
|
||||||
RoleName: u.RoleName,
|
RoleName: u.RoleName,
|
||||||
PartnerID: u.PartnerID,
|
PartnerID: u.PartnerID,
|
||||||
PartnerName: u.PartnerName,
|
PartnerName: u.PartnerName,
|
||||||
SiteID: u.SiteID,
|
SiteID: u.SiteID,
|
||||||
SiteName: u.SiteName,
|
SiteName: u.SiteName,
|
||||||
|
ResetPassword: u.ResetPassword,
|
||||||
}
|
}
|
||||||
|
|
||||||
return userEntity
|
return userEntity
|
||||||
@ -92,6 +94,7 @@ func (u *UserDB) ToUserAuthenticate(signedToken string) *AuthenticateUser {
|
|||||||
PartnerStatus: u.PartnerStatus,
|
PartnerStatus: u.PartnerStatus,
|
||||||
SiteID: u.SiteID,
|
SiteID: u.SiteID,
|
||||||
SiteName: u.SiteName,
|
SiteName: u.SiteName,
|
||||||
|
ResetPassword: u.ResetPassword,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
internal/entity/email.go
Normal file
13
internal/entity/email.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
type (
|
||||||
|
SendEmailNotificationParam struct {
|
||||||
|
Sender string
|
||||||
|
Recipient string
|
||||||
|
CcEmails []string
|
||||||
|
Subject string
|
||||||
|
TemplateName string
|
||||||
|
TemplatePath string
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
)
|
||||||
@ -10,20 +10,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
Status userstatus.UserStatus
|
Status userstatus.UserStatus
|
||||||
NIK string
|
NIK string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
RoleID role.Role
|
RoleID role.Role
|
||||||
RoleName string
|
RoleName string
|
||||||
PartnerID *int64
|
PartnerID *int64
|
||||||
SiteID *int64
|
SiteID *int64
|
||||||
SiteName string
|
SiteName string
|
||||||
PartnerName string
|
PartnerName string
|
||||||
|
ResetPassword bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthenticateUser struct {
|
type AuthenticateUser struct {
|
||||||
@ -36,6 +37,7 @@ type AuthenticateUser struct {
|
|||||||
PartnerStatus string
|
PartnerStatus string
|
||||||
SiteID *int64
|
SiteID *int64
|
||||||
SiteName string
|
SiteName string
|
||||||
|
ResetPassword bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRoleDB struct {
|
type UserRoleDB struct {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"furtuna-be/internal/constants/role"
|
"furtuna-be/internal/constants/role"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@ -19,6 +20,8 @@ type AuthHandler struct {
|
|||||||
func (a *AuthHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
func (a *AuthHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
||||||
authRoute := group.Group("/auth")
|
authRoute := group.Group("/auth")
|
||||||
authRoute.POST("/login", a.AuthLogin)
|
authRoute.POST("/login", a.AuthLogin)
|
||||||
|
authRoute.POST("/forgot-password", a.ForgotPassword)
|
||||||
|
authRoute.POST("/reset-password", jwt, a.ResetPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthHandler(service services.Auth) *AuthHandler {
|
func NewAuthHandler(service services.Auth) *AuthHandler {
|
||||||
@ -77,7 +80,8 @@ func (h *AuthHandler) AuthLogin(c *gin.Context) {
|
|||||||
ID: int64(authUser.RoleID),
|
ID: int64(authUser.RoleID),
|
||||||
Role: authUser.RoleName,
|
Role: authUser.RoleName,
|
||||||
},
|
},
|
||||||
Site: site,
|
Site: site,
|
||||||
|
ResetPassword: authUser.ResetPassword,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, response.BaseResponse{
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
@ -87,3 +91,72 @@ func (h *AuthHandler) AuthLogin(c *gin.Context) {
|
|||||||
Data: resp,
|
Data: resp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForgotPassword handles the request for password reset.
|
||||||
|
// @Summary Request password reset
|
||||||
|
// @Description Sends a password reset link to the user's email.
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param bodyParam body auth2.ForgotPasswordRequest true "User email"
|
||||||
|
// @Success 200 {object} response.BaseResponse "Password reset link sent"
|
||||||
|
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
|
||||||
|
// @Router /api/v1/auth/forgot-password [post]
|
||||||
|
// @Tags Auth Password API's
|
||||||
|
func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
||||||
|
var bodyParam auth2.ResetPasswordRequest
|
||||||
|
if err := c.ShouldBindJSON(&bodyParam); err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.service.SendPasswordResetLink(c, bodyParam.Email)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Message: "Password reset link sent",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword handles the password reset process.
|
||||||
|
// @Summary Reset user password
|
||||||
|
// @Description Resets the user's password using the provided token.
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param bodyParam body auth2.ResetPasswordRequest true "Reset password details"
|
||||||
|
// @Success 200 {object} response.BaseResponse "Password reset successful"
|
||||||
|
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
|
||||||
|
// @Router /api/v1/auth/reset-password [post]
|
||||||
|
// @Tags Auth Password API's
|
||||||
|
func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
||||||
|
ctx := auth2.GetMyContext(c)
|
||||||
|
|
||||||
|
var req auth2.ResetPasswordChangeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.NewError(
|
||||||
|
errors.ErrorBadRequest.ErrorType(),
|
||||||
|
fmt.Sprintf("invalid request %v", err.Error())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.service.ResetPassword(ctx, req.OldPassword, req.NewPassword)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Message: "Password reset successful",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
@ -10,6 +16,63 @@ type ResetPasswordRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ResetPasswordChangeRequest struct {
|
type ResetPasswordChangeRequest struct {
|
||||||
Token string `json:"token" validate:"required"`
|
OldPassword string `json:"old_password" validate:"required"`
|
||||||
Password string `json:"password" validate:"required"`
|
NewPassword string `json:"new_password" validate:"required,strongpwd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ResetPasswordChangeRequest) Validate() error {
|
||||||
|
validate := validator.New()
|
||||||
|
|
||||||
|
validate.RegisterValidation("strongpwd", validateStrongPassword)
|
||||||
|
|
||||||
|
if err := validate.Struct(e); err != nil {
|
||||||
|
// Handle the validation errors
|
||||||
|
for _, err := range err.(validator.ValidationErrors) {
|
||||||
|
switch err.Field() {
|
||||||
|
case "NewPassword":
|
||||||
|
return fmt.Errorf("%w", validatePasswordError(err.Tag()))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateStrongPassword(fl validator.FieldLevel) bool {
|
||||||
|
password := fl.Field().String()
|
||||||
|
|
||||||
|
var (
|
||||||
|
hasMinLen = len(password) >= 8
|
||||||
|
)
|
||||||
|
|
||||||
|
return hasMinLen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error messages for password validation
|
||||||
|
var (
|
||||||
|
ErrPasswordTooShort = errors.New("password must be at least 8 characters long")
|
||||||
|
ErrPasswordNoUpper = errors.New("password must contain at least one uppercase letter")
|
||||||
|
ErrPasswordNoLower = errors.New("password must contain at least one lowercase letter")
|
||||||
|
ErrPasswordNoNumber = errors.New("password must contain at least one digit")
|
||||||
|
ErrPasswordNoSpecial = errors.New("password must contain at least one special character (!@#$%^&*)")
|
||||||
|
ErrPasswordValidation = errors.New("password does not meet the strength requirements")
|
||||||
|
)
|
||||||
|
|
||||||
|
func validatePasswordError(tag string) error {
|
||||||
|
switch tag {
|
||||||
|
case "min":
|
||||||
|
return ErrPasswordTooShort
|
||||||
|
case "uppercase":
|
||||||
|
return ErrPasswordNoUpper
|
||||||
|
case "lowercase":
|
||||||
|
return ErrPasswordNoLower
|
||||||
|
case "number":
|
||||||
|
return ErrPasswordNoNumber
|
||||||
|
case "special":
|
||||||
|
return ErrPasswordNoSpecial
|
||||||
|
default:
|
||||||
|
return ErrPasswordValidation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
package response
|
package response
|
||||||
|
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Role Role `json:"role"`
|
Role Role `json:"role"`
|
||||||
Partner *Partner `json:"partner"`
|
Partner *Partner `json:"partner"`
|
||||||
Site *SiteName `json:"site,omitempty"`
|
Site *SiteName `json:"site,omitempty"`
|
||||||
|
ResetPassword bool `json:"reset_password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Role struct {
|
type Role struct {
|
||||||
|
|||||||
@ -26,7 +26,8 @@ func (r *AuthRepository) CheckExistsUserAccount(ctx context.Context, email strin
|
|||||||
|
|
||||||
err := r.db.
|
err := r.db.
|
||||||
Table("users").
|
Table("users").
|
||||||
Select("users.*, user_roles.role_id, user_roles.partner_id, user_roles.site_id, sites.name, roles.role_name, partners.name as partner_name, partners.status as partner_status").
|
Select("users.*, user_roles.role_id, user_roles.partner_id, user_roles.site_id,"+
|
||||||
|
" sites.name, roles.role_name, partners.name as partner_name, partners.status as partner_status, users.reset_password").
|
||||||
Where("users.email = ?", email).
|
Where("users.email = ?", email).
|
||||||
Joins("left join user_roles on users.id = user_roles.user_id").
|
Joins("left join user_roles on users.id = user_roles.user_id").
|
||||||
Joins("left join roles on user_roles.role_id = roles.role_id").
|
Joins("left join roles on user_roles.role_id = roles.role_id").
|
||||||
@ -49,3 +50,49 @@ func (r *AuthRepository) CheckExistsUserAccount(ctx context.Context, email strin
|
|||||||
|
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *AuthRepository) CheckExistsUserAccountByID(ctx context.Context, userID int64) (*entity.UserDB, error) {
|
||||||
|
var user entity.UserDB
|
||||||
|
|
||||||
|
err := r.db.
|
||||||
|
Table("users").
|
||||||
|
Select("users.*, user_roles.role_id, user_roles.partner_id, user_roles.site_id, sites.name, roles.role_name, partners.name as partner_name, partners.status as partner_status").
|
||||||
|
Where("users.id = ?", userID).
|
||||||
|
Joins("left join user_roles on users.id = user_roles.user_id").
|
||||||
|
Joins("left join roles on user_roles.role_id = roles.role_id").
|
||||||
|
Joins("left join partners on user_roles.partner_id = partners.id").
|
||||||
|
Joins("left join sites on user_roles.site_id = sites.id").
|
||||||
|
First(&user).Error
|
||||||
|
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fmt.Errorf("user with not exist") // or use a custom error type
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.ContextLogger(ctx).Error(fmt.Sprintf("Failed to get user"), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AuthRepository) UpdatePassword(ctx context.Context, trx *gorm.DB, newHashedPassword string, userID int64, resetPassword bool) error {
|
||||||
|
// Perform the update using a single Updates call
|
||||||
|
err := trx.Model(&entity.UserDB{}).
|
||||||
|
Where("id = ?", userID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"password": newHashedPassword,
|
||||||
|
"reset_password": resetPassword,
|
||||||
|
}).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error(fmt.Sprintf("Failed to update password for user with id: %d", userID), zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
88
internal/repository/brevo/init.go
Normal file
88
internal/repository/brevo/init.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package brevo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"furtuna-be/internal/entity"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
brevo "github.com/getbrevo/brevo-go/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config interface {
|
||||||
|
GetApiKey() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceImpl struct {
|
||||||
|
brevoConn *brevo.APIClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ServiceImpl) SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error {
|
||||||
|
templateFile, err := ioutil.ReadFile(param.TemplatePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
renderedTemplate, err := template.New(param.TemplateName).Parse(string(templateFile))
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.sendEmail(ctx, renderedTemplate, param)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ServiceImpl) sendEmail(ctx context.Context, tmpl *template.Template, param entity.SendEmailNotificationParam) error {
|
||||||
|
var body bytes.Buffer
|
||||||
|
err := tmpl.Execute(&body, param.Data)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := brevo.SendSmtpEmail{
|
||||||
|
Sender: &brevo.SendSmtpEmailSender{
|
||||||
|
Email: "siregaraditya@gmail.com",
|
||||||
|
},
|
||||||
|
To: []brevo.SendSmtpEmailTo{
|
||||||
|
{
|
||||||
|
Email: "avranata01@gmail.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Subject: param.Subject,
|
||||||
|
HtmlContent: body.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(param.CcEmails) != 0 {
|
||||||
|
for _, email := range param.CcEmails {
|
||||||
|
payload.Cc = append(payload.Cc, brevo.SendSmtpEmailCc{
|
||||||
|
Email: email,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = s.brevoConn.TransactionalEmailsApi.SendTransacEmail(ctx, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(conf Config) *ServiceImpl {
|
||||||
|
cfg := brevo.NewConfiguration()
|
||||||
|
cfg.AddDefaultHeader("api-key", conf.GetApiKey())
|
||||||
|
client := brevo.NewAPIClient(cfg)
|
||||||
|
result, resp, err := client.AccountApi.GetAccount(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error when calling AccountApi->get_account: ", err.Error())
|
||||||
|
log.Fatal("error")
|
||||||
|
}
|
||||||
|
fmt.Println("GetAccount Object:", result, " GetAccount Response: ", resp)
|
||||||
|
|
||||||
|
return &ServiceImpl{brevoConn: client}
|
||||||
|
}
|
||||||
@ -23,6 +23,8 @@ type CryptoConfig interface {
|
|||||||
AccessTokenOrderSecret() string
|
AccessTokenOrderSecret() string
|
||||||
AccessTokenOrderExpiresDate() time.Time
|
AccessTokenOrderExpiresDate() time.Time
|
||||||
AccessTokenExpiresDate() time.Time
|
AccessTokenExpiresDate() time.Time
|
||||||
|
AccessTokenResetPasswordSecret() string
|
||||||
|
AccessTokenResetPasswordExpire() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type CryptoImpl struct {
|
type CryptoImpl struct {
|
||||||
@ -83,6 +85,30 @@ func (c *CryptoImpl) GenerateJWT(user *entity.User) (string, error) {
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CryptoImpl) GenerateJWTReseetPassword(user *entity.User) (string, error) {
|
||||||
|
claims := &entity.JWTAuthClaims{
|
||||||
|
StandardClaims: jwt.StandardClaims{
|
||||||
|
Subject: strconv.FormatInt(user.ID, 10),
|
||||||
|
ExpiresAt: c.Config.AccessTokenResetPasswordExpire().Unix(),
|
||||||
|
IssuedAt: time.Now().Unix(),
|
||||||
|
NotBefore: time.Now().Unix(),
|
||||||
|
},
|
||||||
|
UserID: user.ID,
|
||||||
|
Name: user.Name,
|
||||||
|
Email: user.Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.
|
||||||
|
NewWithClaims(jwt.SigningMethodHS256, claims).
|
||||||
|
SignedString([]byte(c.Config.AccessTokenResetPasswordSecret()))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *CryptoImpl) ParseAndValidateJWT(tokenString string) (*entity.JWTAuthClaims, error) {
|
func (c *CryptoImpl) ParseAndValidateJWT(tokenString string) (*entity.JWTAuthClaims, error) {
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &entity.JWTAuthClaims{}, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(tokenString, &entity.JWTAuthClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
return []byte(c.Config.AccessTokenSecret()), nil
|
return []byte(c.Config.AccessTokenSecret()), nil
|
||||||
@ -140,3 +166,23 @@ func (c *CryptoImpl) ValidateJWTOrder(tokenString string) (int64, int64, error)
|
|||||||
|
|
||||||
return claims.PartnerID, claims.OrderID, nil
|
return claims.PartnerID, claims.OrderID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CryptoImpl) ValidateResetPassword(tokenString string) (int64, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &entity.JWTOrderClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return []byte(c.Config.AccessTokenResetPasswordSecret()), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*entity.JWTAuthClaims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
return 0, fmt.Errorf("invalid token %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims.UserID, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"furtuna-be/internal/repository/branches"
|
"furtuna-be/internal/repository/branches"
|
||||||
|
"furtuna-be/internal/repository/brevo"
|
||||||
mdtrns "furtuna-be/internal/repository/midtrans"
|
mdtrns "furtuna-be/internal/repository/midtrans"
|
||||||
"furtuna-be/internal/repository/orders"
|
"furtuna-be/internal/repository/orders"
|
||||||
"furtuna-be/internal/repository/oss"
|
"furtuna-be/internal/repository/oss"
|
||||||
@ -26,45 +27,49 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RepoManagerImpl struct {
|
type RepoManagerImpl struct {
|
||||||
Crypto Crypto
|
Crypto Crypto
|
||||||
Auth Auth
|
Auth Auth
|
||||||
Event Event
|
Event Event
|
||||||
User User
|
User User
|
||||||
Branch Branch
|
Branch Branch
|
||||||
Studio Studio
|
Studio Studio
|
||||||
Product Product
|
Product Product
|
||||||
Order Order
|
Order Order
|
||||||
OSS OSSRepository
|
OSS OSSRepository
|
||||||
Partner PartnerRepository
|
Partner PartnerRepository
|
||||||
Site SiteRepository
|
Site SiteRepository
|
||||||
Trx TransactionManager
|
Trx TransactionManager
|
||||||
Wallet WalletRepository
|
Wallet WalletRepository
|
||||||
Midtrans Midtrans
|
Midtrans Midtrans
|
||||||
Payment Payment
|
Payment Payment
|
||||||
|
EmailService EmailService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
|
func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
|
||||||
return &RepoManagerImpl{
|
return &RepoManagerImpl{
|
||||||
Crypto: crypto.NewCrypto(cfg.Auth()),
|
Crypto: crypto.NewCrypto(cfg.Auth()),
|
||||||
Auth: auth.NewAuthRepository(db),
|
Auth: auth.NewAuthRepository(db),
|
||||||
Event: event.NewEventRepo(db),
|
Event: event.NewEventRepo(db),
|
||||||
User: users.NewUserRepository(db),
|
User: users.NewUserRepository(db),
|
||||||
Branch: branches.NewBranchRepository(db),
|
Branch: branches.NewBranchRepository(db),
|
||||||
Studio: studios.NewStudioRepository(db),
|
Studio: studios.NewStudioRepository(db),
|
||||||
Product: products.NewProductRepository(db),
|
Product: products.NewProductRepository(db),
|
||||||
Order: orders.NewOrderRepository(db),
|
Order: orders.NewOrderRepository(db),
|
||||||
OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig),
|
OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig),
|
||||||
Partner: partners.NewPartnerRepository(db),
|
Partner: partners.NewPartnerRepository(db),
|
||||||
Site: sites.NewSiteRepository(db),
|
Site: sites.NewSiteRepository(db),
|
||||||
Trx: trx.NewGormTransactionManager(db),
|
Trx: trx.NewGormTransactionManager(db),
|
||||||
Wallet: repository.NewWalletRepository(db),
|
Wallet: repository.NewWalletRepository(db),
|
||||||
Midtrans: mdtrns.New(&cfg.Midtrans),
|
Midtrans: mdtrns.New(&cfg.Midtrans),
|
||||||
Payment: payment.NewPaymentRepository(db),
|
Payment: payment.NewPaymentRepository(db),
|
||||||
|
EmailService: brevo.New(&cfg.Brevo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth interface {
|
type Auth interface {
|
||||||
CheckExistsUserAccount(ctx context.Context, email string) (*entity.UserDB, error)
|
CheckExistsUserAccount(ctx context.Context, email string) (*entity.UserDB, error)
|
||||||
|
CheckExistsUserAccountByID(ctx context.Context, userID int64) (*entity.UserDB, error)
|
||||||
|
UpdatePassword(ctx context.Context, trx *gorm.DB, newHashedPassword string, userID int64, resetPassword bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Event interface {
|
type Event interface {
|
||||||
@ -79,8 +84,10 @@ type Crypto interface {
|
|||||||
CompareHashAndPassword(hash string, password string) bool
|
CompareHashAndPassword(hash string, password string) bool
|
||||||
ValidateWT(tokenString string) (*jwt.Token, error)
|
ValidateWT(tokenString string) (*jwt.Token, error)
|
||||||
GenerateJWT(user *entity.User) (string, error)
|
GenerateJWT(user *entity.User) (string, error)
|
||||||
|
GenerateJWTReseetPassword(user *entity.User) (string, error)
|
||||||
GenerateJWTOrder(order *entity.Order) (string, error)
|
GenerateJWTOrder(order *entity.Order) (string, error)
|
||||||
ValidateJWTOrder(tokenString string) (int64, int64, error)
|
ValidateJWTOrder(tokenString string) (int64, int64, error)
|
||||||
|
ValidateResetPassword(tokenString string) (int64, error)
|
||||||
ParseAndValidateJWT(token string) (*entity.JWTAuthClaims, error)
|
ParseAndValidateJWT(token string) (*entity.JWTAuthClaims, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,3 +178,7 @@ type Payment interface {
|
|||||||
FindByOrderAndPartnerID(ctx context.Context, orderID, partnerID int64) (*entity.Payment, error)
|
FindByOrderAndPartnerID(ctx context.Context, orderID, partnerID int64) (*entity.Payment, error)
|
||||||
FindByReferenceID(ctx context.Context, db *gorm.DB, referenceID string) (*entity.Payment, error)
|
FindByReferenceID(ctx context.Context, db *gorm.DB, referenceID string) (*entity.Payment, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EmailService interface {
|
||||||
|
SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error
|
||||||
|
}
|
||||||
|
|||||||
@ -2,7 +2,10 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"furtuna-be/config"
|
||||||
|
"furtuna-be/internal/common/mycontext"
|
||||||
"furtuna-be/internal/entity"
|
"furtuna-be/internal/entity"
|
||||||
|
"furtuna-be/internal/utils"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
@ -14,12 +17,23 @@ import (
|
|||||||
type AuthServiceImpl struct {
|
type AuthServiceImpl struct {
|
||||||
authRepo repository.Auth
|
authRepo repository.Auth
|
||||||
crypto repository.Crypto
|
crypto repository.Crypto
|
||||||
|
user repository.User
|
||||||
|
emailSvc repository.EmailService
|
||||||
|
emailCfg config.Email
|
||||||
|
trxRepo repository.TransactionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(authRepo repository.Auth, crypto repository.Crypto) *AuthServiceImpl {
|
func New(authRepo repository.Auth,
|
||||||
|
crypto repository.Crypto, user repository.User, emailSvc repository.EmailService,
|
||||||
|
emailCfg config.Email, trxRepo repository.TransactionManager,
|
||||||
|
) *AuthServiceImpl {
|
||||||
return &AuthServiceImpl{
|
return &AuthServiceImpl{
|
||||||
authRepo: authRepo,
|
authRepo: authRepo,
|
||||||
crypto: crypto,
|
crypto: crypto,
|
||||||
|
user: user,
|
||||||
|
emailSvc: emailSvc,
|
||||||
|
emailCfg: emailCfg,
|
||||||
|
trxRepo: trxRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,3 +60,115 @@ func (u *AuthServiceImpl) AuthenticateUser(ctx context.Context, email, password
|
|||||||
|
|
||||||
return user.ToUserAuthenticate(signedToken), nil
|
return user.ToUserAuthenticate(signedToken), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *AuthServiceImpl) SendPasswordResetLink(ctx context.Context, email string) error {
|
||||||
|
// Check if the user exists
|
||||||
|
user, err := u.authRepo.CheckExistsUserAccount(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("error when getting user", zap.Error(err))
|
||||||
|
return errors.ErrorInternalServer
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
return errors.ErrorUserIsNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin a transaction
|
||||||
|
trx, err := u.trxRepo.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("error when beginning transaction", zap.Error(err))
|
||||||
|
return errors.ErrorInternalServer
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
u.trxRepo.Rollback(trx)
|
||||||
|
logger.ContextLogger(ctx).Error("panic recovered in SendPasswordResetLink", zap.Any("recover", r))
|
||||||
|
err = errors.ErrorInternalServer
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Generate a new password
|
||||||
|
generatedPassword := utils.GenerateRandomString(10)
|
||||||
|
hashPassword, err := user.ToUser().HashedPassword(generatedPassword)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("error when generating hashed password", zap.Error(err))
|
||||||
|
u.trxRepo.Rollback(trx)
|
||||||
|
return errors.ErrorInternalServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the user's password in the database
|
||||||
|
if err := u.authRepo.UpdatePassword(ctx, trx, hashPassword, user.ID, true); err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("error when updating user password", zap.Error(err))
|
||||||
|
u.trxRepo.Rollback(trx)
|
||||||
|
return errors.ErrorInternalServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a custom receiver is specified, override the email
|
||||||
|
if u.emailCfg.CustomReceiver != "" {
|
||||||
|
email = u.emailCfg.CustomReceiver
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the email notification parameters
|
||||||
|
renewPasswordRequest := entity.SendEmailNotificationParam{
|
||||||
|
Recipient: email,
|
||||||
|
Subject: u.emailCfg.ResetPassword.Subject,
|
||||||
|
TemplateName: u.emailCfg.ResetPassword.TemplateName,
|
||||||
|
TemplatePath: u.emailCfg.ResetPassword.TemplatePath,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Name": user.Name,
|
||||||
|
"OpeningWord": u.emailCfg.ResetPassword.OpeningWord,
|
||||||
|
"Password": generatedPassword,
|
||||||
|
"ClosingWord": u.emailCfg.ResetPassword.ClosingWord,
|
||||||
|
"Note": u.emailCfg.ResetPassword.Notes,
|
||||||
|
"Link": u.emailCfg.ResetPassword.Link,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
u.trxRepo.Rollback(trx)
|
||||||
|
logger.ContextLogger(ctx).Error("panic recovered in SendPasswordResetLink", zap.Any("recover", r))
|
||||||
|
err = errors.ErrorInternalServer
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Send the email notification
|
||||||
|
err = u.emailSvc.SendEmailTransactional(ctx, renewPasswordRequest)
|
||||||
|
if err != nil {
|
||||||
|
u.trxRepo.Rollback(trx)
|
||||||
|
logger.ContextLogger(ctx).Error("error when sending password reset email", zap.Error(err))
|
||||||
|
return errors.ErrorInternalServer
|
||||||
|
}
|
||||||
|
|
||||||
|
trx.Commit()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *AuthServiceImpl) ResetPassword(ctx mycontext.Context, oldPassword, newPassword string) error {
|
||||||
|
user, err := u.authRepo.CheckExistsUserAccountByID(ctx, ctx.RequestedBy())
|
||||||
|
if err != nil {
|
||||||
|
return errors.ErrorInvalidRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := u.crypto.CompareHashAndPassword(user.Password, oldPassword); !ok {
|
||||||
|
return errors.ErrorUserInvalidLogin
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := user.ToUser().HashedPassword(newPassword)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ErrorInvalidRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
trx, _ := u.trxRepo.Begin(ctx)
|
||||||
|
|
||||||
|
err = u.authRepo.UpdatePassword(ctx, trx, password, user.ID, false)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ErrorInternalServer
|
||||||
|
}
|
||||||
|
|
||||||
|
trx.Commit()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ type ServiceManagerImpl struct {
|
|||||||
|
|
||||||
func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl {
|
func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl {
|
||||||
return &ServiceManagerImpl{
|
return &ServiceManagerImpl{
|
||||||
AuthSvc: auth.New(repo.Auth, repo.Crypto),
|
AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx),
|
||||||
EventSvc: event.NewEventService(repo.Event),
|
EventSvc: event.NewEventService(repo.Event),
|
||||||
UserSvc: users.NewUserService(repo.User, repo.Branch),
|
UserSvc: users.NewUserService(repo.User, repo.Branch),
|
||||||
BranchSvc: branch.NewBranchService(repo.Branch),
|
BranchSvc: branch.NewBranchService(repo.Branch),
|
||||||
@ -52,6 +52,8 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
|
|||||||
|
|
||||||
type Auth interface {
|
type Auth interface {
|
||||||
AuthenticateUser(ctx context.Context, email, password string) (*entity.AuthenticateUser, error)
|
AuthenticateUser(ctx context.Context, email, password string) (*entity.AuthenticateUser, error)
|
||||||
|
SendPasswordResetLink(ctx context.Context, email string) error
|
||||||
|
ResetPassword(ctx mycontext.Context, oldPassword, newPassword string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Event interface {
|
type Event interface {
|
||||||
|
|||||||
260
templates/reset_password.html
Normal file
260
templates/reset_password.html
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>
|
||||||
|
</title>
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<!--<![endif]-->
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style type="text/css">
|
||||||
|
#outlook a {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
border-collapse: collapse;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
height: auto;
|
||||||
|
line-height: 100%;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<noscript>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG/>
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
</noscript>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if lte mso 11]>
|
||||||
|
<style type="text/css">
|
||||||
|
.mj-outlook-group-fix { width:100% !important; }
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
|
||||||
|
<style type="text/css">
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||||
|
</style>
|
||||||
|
<!--<![endif]-->
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (min-width:480px) {
|
||||||
|
.mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style media="screen and (min-width:480px)">
|
||||||
|
.moz-text-html .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.bodyStyle {
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="word-spacing:normal;background-color:#f1f0f7;">
|
||||||
|
<div class="bodyStyle" style="padding: 50px; background-color: #f1f0f7;">
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-top:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:20px;padding-top:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:24px;line-height:25px;text-align:left;color:#0a2062;">Halo {{ .Name }},</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:25px;text-align:left;color:#000000;">{{ .OpeningWord }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:25px;text-align:left;color:#000000;">Password: <b style="font-size:19px">{{ .Password }}</b></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:25px;text-align:left;color:#000000;">{{ .ClosingWord }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:25px;text-align:left;color:#000000;">Terima kasih</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||||
|
<p style="border-top:solid 1px #808080;font-size:1px;margin:0px auto;width:100%;">
|
||||||
|
</p>
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #808080;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">
|
||||||
|
</td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:12px;line-height:15px;text-align:left;color:#808080;">Email ini tergenerate oleh sistem secara otomatis, anda tidak perlu membalas email ini.</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user