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

562 lines
17 KiB
Go
Raw Normal View History

2026-02-24 09:37:19 +00:00
package service
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"web-qudo-be/app/database/entity"
"web-qudo-be/app/database/entity/users"
userLevelsRepository "web-qudo-be/app/module/user_levels/repository"
"web-qudo-be/app/module/users/mapper"
"web-qudo-be/app/module/users/repository"
"web-qudo-be/app/module/users/request"
"web-qudo-be/app/module/users/response"
"web-qudo-be/config/config"
"web-qudo-be/utils/paginator"
utilSvc "web-qudo-be/utils/service"
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
Log zerolog.Logger
Keycloak *config.KeycloakConfig
Smtp *config.SmtpConfig
}
// UsersService define interface of IUsersService
type UsersService interface {
All(clientId *uuid.UUID, req request.UsersQueryRequest) (users []*response.UsersResponse, paging paginator.Pagination, err error)
Show(clientId *uuid.UUID, id uint) (users *response.UsersResponse, err error)
ShowByUsername(clientId *uuid.UUID, username string) (users *response.UsersResponse, err error)
ShowUserInfo(clientId *uuid.UUID, authToken string) (users *response.UsersResponse, err error)
Save(clientId *uuid.UUID, req request.UsersCreateRequest, authToken string) (userReturn *users.Users, err error)
Login(req request.UserLogin) (res *gocloak.JWT, err error)
ParetoLogin(req request.UserLogin) (res *response.ParetoLoginResponse, err error)
Update(clientId *uuid.UUID, id uint, req request.UsersUpdateRequest) (err error)
Delete(clientId *uuid.UUID, id uint) error
SavePassword(clientId *uuid.UUID, req request.UserSavePassword, authToken string) (err error)
ResetPassword(req request.UserResetPassword) (err error)
ForgotPassword(clientId *uuid.UUID, req request.UserForgotPassword) (err error)
EmailValidationPreLogin(clientId *uuid.UUID, req request.UserEmailValidationRequest) (msgResponse *string, err error)
SetupEmail(clientId *uuid.UUID, 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, log zerolog.Logger, keycloak *config.KeycloakConfig, smtp *config.SmtpConfig) UsersService {
return &usersService{
Repo: repo,
UserLevelsRepo: userLevelsRepo,
Log: log,
Keycloak: keycloak,
Smtp: smtp,
}
}
// All implement interface of UsersService
func (_i *usersService) All(clientId *uuid.UUID, req request.UsersQueryRequest) (users []*response.UsersResponse, paging paginator.Pagination, err error) {
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(clientId *uuid.UUID, id uint) (users *response.UsersResponse, err error) {
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(clientId *uuid.UUID, username string) (users *response.UsersResponse, err error) {
result, err := _i.Repo.FindByUsername(clientId, username)
if err != nil {
return nil, err
}
return mapper.UsersResponseMapper(result, _i.UserLevelsRepo, clientId), nil
}
func (_i *usersService) ShowUserInfo(clientId *uuid.UUID, authToken string) (users *response.UsersResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
return mapper.UsersResponseMapper(userInfo, _i.UserLevelsRepo, clientId), nil
}
func (_i *usersService) Save(clientId *uuid.UUID, req request.UsersCreateRequest, authToken string) (userReturn *users.Users, err error) {
_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
// Set ClientId on entity
newReq.ClientId = clientId
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(clientId *uuid.UUID, id uint, req request.UsersUpdateRequest) (err error) {
_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(clientId *uuid.UUID, id uint) error {
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(clientId *uuid.UUID, req request.UserSavePassword, authToken string) (err error) {
_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(clientId *uuid.UUID, req request.UserForgotPassword) (err error) {
_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(clientId *uuid.UUID, req request.UserEmailValidationRequest) (msgResponse *string, err error) {
_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(clientId *uuid.UUID, req request.UserEmailValidationRequest) (msgResponse *string, err error) {
_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
}