kontenhumas-be/app/module/users/service/users.service.go

767 lines
24 KiB
Go

package service
import (
"encoding/base64"
"encoding/json"
"fmt"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/database/entity/users"
approvalWorkflowsRepo "netidhub-saas-be/app/module/approval_workflows/repository"
clientApprovalSettingsRepo "netidhub-saas-be/app/module/client_approval_settings/repository"
userLevelsRepository "netidhub-saas-be/app/module/user_levels/repository"
"netidhub-saas-be/app/module/users/mapper"
"netidhub-saas-be/app/module/users/repository"
"netidhub-saas-be/app/module/users/request"
"netidhub-saas-be/app/module/users/response"
"netidhub-saas-be/config/config"
"netidhub-saas-be/utils/paginator"
utilSvc "netidhub-saas-be/utils/service"
"strings"
"time"
paseto "aidanwoods.dev/go-paseto"
"github.com/Nerzal/gocloak/v13"
"github.com/google/uuid"
"github.com/rs/zerolog"
)
// UsersService
type usersService struct {
Repo repository.UsersRepository
UserLevelsRepo userLevelsRepository.UserLevelsRepository
ApprovalWorkflowsRepo approvalWorkflowsRepo.ApprovalWorkflowsRepository
ClientApprovalSettingsRepo clientApprovalSettingsRepo.ClientApprovalSettingsRepository
Log zerolog.Logger
Keycloak *config.KeycloakConfig
Smtp *config.SmtpConfig
}
// UsersService define interface of IUsersService
type UsersService interface {
All(authToken string, req request.UsersQueryRequest) (users []*response.UsersResponse, paging paginator.Pagination, err error)
Show(authToken string, id uint) (users *response.UsersResponse, err error)
ShowByUsername(authToken string, username string) (users *response.UsersResponse, err error)
CheckUsernameExists(username string) (exists bool, err error)
ShowUserInfo(authToken string) (users *response.UsersResponse, err error)
Save(authToken string, req request.UsersCreateRequest) (userReturn *users.Users, err error)
Login(req request.UserLogin) (res *gocloak.JWT, err error)
ParetoLogin(req request.UserLogin) (res *response.ParetoLoginResponse, err error)
Update(authToken string, id uint, req request.UsersUpdateRequest) (err error)
Delete(authToken string, id uint) error
SavePassword(authToken string, req request.UserSavePassword) (err error)
ResetPassword(req request.UserResetPassword) (err error)
ForgotPassword(authToken string, req request.UserForgotPassword) (err error)
EmailValidationPreLogin(authToken string, req request.UserEmailValidationRequest) (msgResponse *string, err error)
SetupEmail(authToken string, req request.UserEmailValidationRequest) (msgResponse *string, err error)
OtpRequest(req request.UserOtpRequest) (err error)
OtpValidation(req request.UserOtpValidation) (err error)
SendLoginOtp(name string, email string, otp string) error
SendRegistrationOtp(name string, email string, otp string) error
}
// NewUsersService init UsersService
func NewUsersService(
repo repository.UsersRepository,
userLevelsRepo userLevelsRepository.UserLevelsRepository,
approvalWorkflowsRepo approvalWorkflowsRepo.ApprovalWorkflowsRepository,
clientApprovalSettingsRepo clientApprovalSettingsRepo.ClientApprovalSettingsRepository,
log zerolog.Logger,
keycloak *config.KeycloakConfig,
smtp *config.SmtpConfig,
) UsersService {
return &usersService{
Repo: repo,
UserLevelsRepo: userLevelsRepo,
ApprovalWorkflowsRepo: approvalWorkflowsRepo,
ClientApprovalSettingsRepo: clientApprovalSettingsRepo,
Log: log,
Keycloak: keycloak,
Smtp: smtp,
}
}
// All implement interface of UsersService
func (_i *usersService) All(authToken string, req request.UsersQueryRequest) (users []*response.UsersResponse, paging paginator.Pagination, err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
results, paging, err := _i.Repo.GetAll(clientId, req)
if err != nil {
return
}
for _, result := range results {
users = append(users, mapper.UsersResponseMapper(result, _i.UserLevelsRepo, clientId))
}
return
}
func (_i *usersService) Show(authToken string, id uint) (users *response.UsersResponse, err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
result, err := _i.Repo.FindOne(clientId, id)
if err != nil {
return nil, err
}
return mapper.UsersResponseMapper(result, _i.UserLevelsRepo, clientId), nil
}
func (_i *usersService) ShowByUsername(authToken string, username string) (users *response.UsersResponse, err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
result, err := _i.Repo.FindByUsername(clientId, username)
if err != nil {
return nil, err
}
return mapper.UsersResponseMapper(result, _i.UserLevelsRepo, clientId), nil
}
func (_i *usersService) CheckUsernameExists(username string) (exists bool, err error) {
_i.Log.Info().Str("username", username).Msg("Checking if username exists")
// Check if username exists in repository
result, err := _i.Repo.FindByUsername(nil, username)
if err != nil {
// If error is "record not found", username doesn't exist
if strings.Contains(err.Error(), "record not found") {
return false, nil
}
_i.Log.Error().Err(err).Str("username", username).Msg("Failed to check username existence")
return false, err
}
// If result is not nil, username exists
if result != nil {
return true, nil
}
return false, nil
}
func (_i *usersService) ShowUserInfo(authToken string) (users *response.UsersResponse, err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
userInfo := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if userInfo == nil {
return nil, fmt.Errorf("user not found")
}
// Get approval workflow info
approvalWorkflowInfo, err := _i.getApprovalWorkflowInfo(clientId)
if err != nil {
_i.Log.Warn().Err(err).Msg("Failed to get approval workflow info")
// Don't return error, just log warning and continue without workflow info
}
// Map user response with approval workflow info
usersRes := mapper.UsersResponseMapper(userInfo, _i.UserLevelsRepo, clientId)
if usersRes != nil && approvalWorkflowInfo != nil {
usersRes.ApprovalWorkflowInfo = approvalWorkflowInfo
}
return usersRes, nil
}
// getApprovalWorkflowInfo retrieves approval workflow information for the client
func (_i *usersService) getApprovalWorkflowInfo(clientId *uuid.UUID) (*response.ApprovalWorkflowInfo, error) {
if clientId == nil {
return &response.ApprovalWorkflowInfo{
HasWorkflowSetup: false,
}, nil
}
// Check if client has approval settings
clientSettings, err := _i.ClientApprovalSettingsRepo.FindByClientId(*clientId)
if err != nil {
return nil, fmt.Errorf("failed to get client approval settings: %w", err)
}
workflowInfo := &response.ApprovalWorkflowInfo{
HasWorkflowSetup: clientSettings != nil,
}
if clientSettings != nil {
workflowInfo.RequiresApproval = clientSettings.RequiresApproval
workflowInfo.AutoPublishArticles = clientSettings.AutoPublishArticles
workflowInfo.IsApprovalActive = clientSettings.IsActive
// Get default workflow info if exists
if clientSettings.DefaultWorkflowId != nil {
workflowInfo.DefaultWorkflowId = clientSettings.DefaultWorkflowId
defaultWorkflow, err := _i.ApprovalWorkflowsRepo.FindOne(clientId, *clientSettings.DefaultWorkflowId)
if err != nil {
_i.Log.Warn().Err(err).Msg("Failed to get default workflow details")
} else if defaultWorkflow != nil {
workflowInfo.DefaultWorkflowName = &defaultWorkflow.Name
}
}
}
return workflowInfo, nil
}
func (_i *usersService) Save(authToken string, req request.UsersCreateRequest) (userReturn *users.Users, err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
} else {
clientId = req.ClientId
}
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
_i.Log.Info().Interface("AUTH TOKEN", authToken).Msg("")
if authToken != "" {
createdBy := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
newReq.CreatedById = &createdBy.ID
}
keycloakId, err := _i.Keycloak.CreateUser(req.Fullname, req.Email, req.Username, req.Password)
if err != nil {
return nil, err
}
newReq.KeycloakId = &keycloakId
newReq.TempPassword = &req.Password
newReq.ClientId = clientId
isEmailUpdated := true
newReq.IsEmailUpdated = &isEmailUpdated
return _i.Repo.Create(newReq)
}
func (_i *usersService) Login(req request.UserLogin) (res *gocloak.JWT, err error) {
_i.Log.Info().Interface("data", req).Msg("")
var loginResponse *gocloak.JWT
if req.RefreshToken == nil {
loginResponse, err = _i.Keycloak.Login(*req.Username, *req.Password)
} else {
loginResponse, err = _i.Keycloak.RefreshToken(*req.RefreshToken)
}
if err != nil {
return nil, err
}
return loginResponse, nil
}
func (_i *usersService) ParetoLogin(req request.UserLogin) (res *response.ParetoLoginResponse, err error) {
_i.Log.Info().Interface("data", req).Msg("")
var loginResponse *gocloak.JWT
token := paseto.NewToken()
secretKeyHex := "bdc42b1a0ba2bac3e27ba84241f9de06dee71b70f838af8d1beb0417f03d1d00"
secretKey, _ := paseto.V4SymmetricKeyFromHex(secretKeyHex)
// secretKey := paseto.NewV4SymmetricKey() // to change the secretKey periodically
if req.RefreshToken == nil {
loginResponse, err = _i.Keycloak.Login(*req.Username, *req.Password)
} else {
// Retrieve Refresh Token
parser := paseto.NewParser()
verifiedToken, err := parser.ParseV4Local(secretKey, *req.RefreshToken, nil)
if err != nil {
panic(err)
}
refreshToken, _ := verifiedToken.GetString("refresh_token")
_i.Log.Info().Interface("Pareto parse refresh token", refreshToken).Msg("")
loginResponse, err = _i.Keycloak.RefreshToken(refreshToken)
}
_i.Log.Info().Interface("loginResponse", loginResponse).Msg("")
if err != nil {
return nil, err
}
parseToken, err := ParseJWTToken(loginResponse.AccessToken)
if err != nil {
return nil, err
}
issuedAt := parseToken["iat"].(float64)
expirationTime := parseToken["exp"].(float64)
issuer := parseToken["iss"].(string)
jti := parseToken["jti"].(string)
subject := parseToken["sub"].(string)
token.SetIssuedAt(time.Unix(int64(issuedAt), 0))
token.SetNotBefore(time.Unix(int64(issuedAt), 0))
token.SetExpiration(time.Unix(int64(expirationTime), 0))
token.SetIssuer(issuer)
token.SetJti(jti)
token.SetSubject(subject)
token.SetString("access_token", loginResponse.AccessToken)
token.SetString("refresh_token", loginResponse.RefreshToken)
_i.Log.Info().Interface("Pareto Generated Key", secretKey.ExportHex()).Msg("")
tokenEncrypted := token.V4Encrypt(secretKey, nil)
parser := paseto.NewParser()
verifiedToken, err := parser.ParseV4Local(secretKey, tokenEncrypted, nil)
if err != nil {
panic(err)
}
tokenParsing, _ := verifiedToken.GetString("access_token")
_i.Log.Info().Interface("Pareto parse token", tokenParsing).Msg("")
resLogin := &response.ParetoLoginResponse{
AccessToken: tokenEncrypted,
}
if err != nil {
return nil, err
}
return resLogin, nil
}
func (_i *usersService) Update(authToken string, id uint, req request.UsersUpdateRequest) (err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
findUser, err := _i.Repo.FindOne(clientId, id)
if err != nil {
return err
}
err = _i.Keycloak.UpdateUser(findUser.KeycloakId, req.Fullname, req.Email)
if err != nil {
return err
}
// Set ClientId on entity
newReq.ClientId = clientId
return _i.Repo.Update(clientId, id, newReq)
}
func (_i *usersService) Delete(authToken string, id uint) error {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
result, err := _i.Repo.FindOne(clientId, id)
if err != nil {
return err
}
isActive := false
result.IsActive = &isActive
return _i.Repo.Update(clientId, id, result)
}
func (_i *usersService) SavePassword(authToken string, req request.UserSavePassword) (err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
_i.Log.Info().Interface("data", req).Msg("")
_i.Log.Info().Interface("AUTH TOKEN", authToken).Msg("")
if authToken != "" {
createdBy := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
tokenString := strings.TrimPrefix(authToken, "Bearer ")
err := _i.Keycloak.SetPassword(tokenString, *createdBy.KeycloakId, req.Password)
if err != nil {
return err
}
return nil
} else {
return fmt.Errorf("Invalid token")
}
}
func (_i *usersService) ResetPassword(req request.UserResetPassword) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
if req.Password != req.ConfirmPassword {
return fmt.Errorf("Invalid Password")
}
user, err := _i.Repo.FindByKeycloakId(req.UserId)
if err != nil {
return fmt.Errorf("User Id Not Found")
}
forgotPassword, err := _i.Repo.FindForgotPassword(req.UserId, req.CodeRequest)
if err != nil {
return fmt.Errorf("Invalid Request")
}
_i.Log.Info().Interface("data", forgotPassword).Msg("")
_i.Log.Info().Interface("dataForgotPassword", forgotPassword).Msg("")
if user != nil {
err := _i.Keycloak.SetPasswordWithoutToken(req.UserId, req.Password)
if err != nil {
return err
}
forgotPassword.IsActive = false
forgotPassword.UpdatedAt = time.Now()
err = _i.Repo.UpdateForgotPassword(forgotPassword.ID, forgotPassword)
if err != nil {
return err
}
return nil
} else {
return fmt.Errorf("User not found")
}
}
func (_i *usersService) ForgotPassword(authToken string, req request.UserForgotPassword) (err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
_i.Log.Info().Interface("data", req).Msg("")
user, err := _i.Repo.FindByUsername(clientId, req.Username)
if err != nil {
return err
}
if user != nil {
codeRequest, err := utilSvc.GenerateNumericCode(8)
if err != nil {
return nil
}
forgotPasswordReq := entity.ForgotPasswords{
KeycloakID: *user.KeycloakId,
CodeRequest: codeRequest,
IsActive: true,
}
err = _i.Repo.CreateForgotPassword(&forgotPasswordReq)
if err != nil {
return err
}
// send email forgot password
url := fmt.Sprintf("https://kontenhumas.com/setup-password?userId=%s&code=%s", *user.KeycloakId, codeRequest)
subject := "[HUMAS POLRI] Forgot Password"
htmlBody := "<p>Anda telah mengirimkan permintaan untuk melakukan reset password.</p>"
htmlBody += "<p style='padding-bottom: 10px;'>Silahkan buat password akun anda dengan menekan tombol di bawah ini, untuk membuat password baru</p>"
htmlBody += fmt.Sprintf("<a style='padding:8px 50px;border-radius:28px;background-color:#37c2b6;text-decoration:none;color:#f4f5f5;' href='%s'>Reset Password</a>", url)
htmlBody += "<p style='padding-top: 10px;'>Terimakasih.</p>"
err = _i.Smtp.SendEmail(subject, user.Email, user.Fullname, htmlBody)
return nil
} else {
return fmt.Errorf("User not found")
}
}
func (_i *usersService) OtpRequest(req request.UserOtpRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
codeRequest, err := utilSvc.GenerateNumericCode(6)
if req.Name == nil {
req.Name = &req.Email
}
if err != nil {
return err
}
otpReq := entity.OneTimePasswords{
Email: req.Email,
Name: req.Name,
OtpCode: codeRequest,
IsActive: true,
}
err = _i.Repo.CreateOtp(&otpReq)
if err != nil {
return err
}
err = _i.SendRegistrationOtp(*req.Name, req.Email, codeRequest)
if err != nil {
return err
}
return nil
}
func (_i *usersService) OtpValidation(req request.UserOtpValidation) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
var otp *entity.OneTimePasswords
if req.Email == nil {
otp, err = _i.Repo.FindOtpByIdentity(*req.Username, req.OtpCode)
if err != nil {
return fmt.Errorf("OTP is not valid")
}
} else {
otp, err = _i.Repo.FindOtpByEmail(*req.Email, req.OtpCode)
if err != nil {
return fmt.Errorf("OTP is not valid")
}
}
if otp != nil {
if otp.ValidUntil.Before(time.Now()) {
return fmt.Errorf("OTP has expired")
}
return nil
} else {
return fmt.Errorf("OTP is not valid")
}
}
func (_i *usersService) EmailValidationPreLogin(authToken string, req request.UserEmailValidationRequest) (msgResponse *string, err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
_i.Log.Info().Interface("data", req).Msg("")
var loginResponse *gocloak.JWT
loginResponse, err = _i.Keycloak.Login(*req.Username, *req.Password)
if loginResponse == nil || err != nil {
return nil, fmt.Errorf("username / password incorrect")
}
findUser, err := _i.Repo.FindByUsername(clientId, *req.Username)
if findUser == nil || err != nil {
return nil, fmt.Errorf("username / password incorrect")
}
_i.Log.Info().Interface("data user", findUser).Msg("")
if *findUser.IsEmailUpdated != true {
message := "Continue to setup email"
msgResponse = &message
} else {
codeRequest, err := utilSvc.GenerateNumericCode(6)
if err != nil {
return nil, err
}
otpReq := entity.OneTimePasswords{
Email: findUser.Email,
Identity: &findUser.Username,
OtpCode: codeRequest,
IsActive: true,
}
err = _i.Repo.CreateOtp(&otpReq)
if err != nil {
return nil, err
}
err = _i.SendLoginOtp(findUser.Fullname, findUser.Email, codeRequest)
if err != nil {
return nil, err
} else {
msg := "Email is valid and OTP has been sent"
msgResponse = &msg
}
}
return msgResponse, nil
}
func (_i *usersService) SetupEmail(authToken string, req request.UserEmailValidationRequest) (msgResponse *string, err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
_i.Log.Info().Interface("data", req).Msg("")
var loginResponse *gocloak.JWT
loginResponse, err = _i.Keycloak.Login(*req.Username, *req.Password)
if loginResponse == nil || err != nil {
return nil, fmt.Errorf("username / password incorrect")
}
_i.Log.Info().Interface("findUser", "").Msg("")
findUser, err := _i.Repo.FindByUsername(clientId, *req.Username)
_i.Log.Info().Interface("findUser", findUser).Msg("")
if findUser == nil || err != nil {
return nil, fmt.Errorf("username / password incorrect")
}
isTrue := true
if findUser.Email == *req.OldEmail {
findUser.Email = *req.NewEmail
findUser.IsEmailUpdated = &isTrue
_i.Log.Info().Interface("Update", "").Msg("")
err = _i.Repo.Update(clientId, findUser.ID, findUser)
_i.Log.Info().Interface("Update", err).Msg("")
if err != nil {
return nil, err
}
codeRequest, err := utilSvc.GenerateNumericCode(6)
if err != nil {
return nil, err
}
otpReq := entity.OneTimePasswords{
Email: findUser.Email,
Identity: &findUser.Username,
OtpCode: codeRequest,
IsActive: true,
}
err = _i.Repo.CreateOtp(&otpReq)
if err != nil {
return nil, err
}
err = _i.SendLoginOtp(findUser.Fullname, findUser.Email, codeRequest)
if err != nil {
return nil, err
} else {
msg := "Email is valid and OTP has been sent"
msgResponse = &msg
}
} else {
return nil, fmt.Errorf("the old email is not same")
}
return msgResponse, nil
}
func ParseJWTToken(token string) (map[string]interface{}, error) {
// Pisahkan JWT menjadi 3 bagian: header, payload, dan signature
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("Invalid JWT token")
}
// Decode bagian payload (index ke-1)
payloadData, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("Failed to decode payload: %v", err)
}
// Ubah payload menjadi map[string]interface{}
var payload map[string]interface{}
if err := json.Unmarshal(payloadData, &payload); err != nil {
return nil, fmt.Errorf("Failed to parse payload JSON: %v", err)
}
return payload, nil
}
func (_i *usersService) SendLoginOtp(name string, email string, otp string) error {
subject := "[HUMAS POLRI] Permintaan OTP"
htmlBody := fmt.Sprintf("<p>Hai %s !</p><p>Berikut kode OTP yang digunakan untuk Login.</p>", name)
htmlBody += fmt.Sprintf("<p style='padding: 10px 50px; background: #eef2f6; border-radius: 8px; max-width: 300px; text-align: center'><b>%s</b></p>", otp)
htmlBody += "<p style='padding-top: 10px;'>Kode diatas hanya berlaku selama 10 menit. Harap segera masukkan kode tersebut pada aplikasi HUMAS POLRI.</p>"
htmlBody += "<p style='padding-top: 10px; padding-bottom: 10px'>Demi menjaga kerahasiaan data kamu, mohon jangan membagikan kode OTP ke siapapun.</p>"
err := _i.Smtp.SendEmail(subject, email, name, htmlBody)
return err
}
func (_i *usersService) SendRegistrationOtp(name string, email string, otp string) error {
subject := "[HUMAS POLRI] Permintaan OTP"
htmlBody := fmt.Sprintf("<p>Hai %s !</p><p>Berikut kode OTP yang digunakan untuk Verifikasi Registrasi.</p>", name)
htmlBody += fmt.Sprintf("<p style='padding: 10px 50px; background: #eef2f6; border-radius: 8px; max-width: 300px; text-align: center'><b>%s</b></p>", otp)
htmlBody += "<p style='padding-top: 10px;'>Kode diatas hanya berlaku selama 10 menit. Harap segera masukkan kode tersebut pada aplikasi HUMAS POLRI.</p>"
htmlBody += "<p style='padding-top: 10px; padding-bottom: 10px'>Demi menjaga kerahasiaan data kamu, mohon jangan membagikan kode OTP ke siapapun.</p>"
err := _i.Smtp.SendEmail(subject, email, name, htmlBody)
return err
}