404 lines
12 KiB
Go
404 lines
12 KiB
Go
package service
|
|
|
|
import (
|
|
paseto "aidanwoods.dev/go-paseto"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/Nerzal/gocloak/v13"
|
|
"github.com/rs/zerolog"
|
|
"go-humas-be/app/database/entity"
|
|
userLevelsRepository "go-humas-be/app/module/user_levels/repository"
|
|
"go-humas-be/app/module/users/mapper"
|
|
"go-humas-be/app/module/users/repository"
|
|
"go-humas-be/app/module/users/request"
|
|
"go-humas-be/app/module/users/response"
|
|
"go-humas-be/config/config"
|
|
"go-humas-be/utils/paginator"
|
|
utilSvc "go-humas-be/utils/service"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 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(req request.UsersQueryRequest) (users []*response.UsersResponse, paging paginator.Pagination, err error)
|
|
Show(id uint) (users *response.UsersResponse, err error)
|
|
ShowUserInfo(authToken string) (users *response.UsersResponse, err error)
|
|
Save(req request.UsersCreateRequest, authToken string) (userReturn *entity.Users, err error)
|
|
Login(req request.UserLogin) (res *gocloak.JWT, err error)
|
|
ParetoLogin(req request.UserLogin) (res *response.ParetoLoginResponse, err error)
|
|
Update(id uint, req request.UsersUpdateRequest) (err error)
|
|
Delete(id uint) error
|
|
SavePassword(req request.UserSavePassword, authToken string) (err error)
|
|
ResetPassword(req request.UserResetPassword) (err error)
|
|
ForgotPassword(req request.UserForgotPassword) (err error)
|
|
OtpRequest(req request.UserOtpRequest) (err error)
|
|
OtpValidation(req request.UserOtpValidation) (err 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(req request.UsersQueryRequest) (users []*response.UsersResponse, paging paginator.Pagination, err error) {
|
|
results, paging, err := _i.Repo.GetAll(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, result := range results {
|
|
users = append(users, mapper.UsersResponseMapper(result, _i.UserLevelsRepo))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (_i *usersService) Show(id uint) (users *response.UsersResponse, err error) {
|
|
result, err := _i.Repo.FindOne(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return mapper.UsersResponseMapper(result, _i.UserLevelsRepo), nil
|
|
}
|
|
|
|
func (_i *usersService) ShowUserInfo(authToken string) (users *response.UsersResponse, err error) {
|
|
userInfo := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
|
|
|
|
return mapper.UsersResponseMapper(userInfo, _i.UserLevelsRepo), nil
|
|
}
|
|
|
|
func (_i *usersService) Save(req request.UsersCreateRequest, authToken string) (userReturn *entity.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
|
|
|
|
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(id uint, req request.UsersUpdateRequest) (err error) {
|
|
_i.Log.Info().Interface("data", req).Msg("")
|
|
newReq := req.ToEntity()
|
|
|
|
findUser, err := _i.Repo.FindOne(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = _i.Keycloak.UpdateUser(findUser.KeycloakId, req.Fullname, req.Email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return _i.Repo.Update(id, newReq)
|
|
}
|
|
|
|
func (_i *usersService) Delete(id uint) error {
|
|
result, err := _i.Repo.FindOne(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isActive := false
|
|
result.IsActive = &isActive
|
|
return _i.Repo.Update(id, result)
|
|
}
|
|
|
|
func (_i *usersService) SavePassword(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)
|
|
|
|
err := _i.Keycloak.SetPassword(authToken, *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(req request.UserForgotPassword) (err error) {
|
|
_i.Log.Info().Interface("data", req).Msg("")
|
|
|
|
user, err := _i.Repo.FindByUsername(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 err != nil {
|
|
return err
|
|
}
|
|
otpReq := entity.RegistrationOtps{
|
|
Email: req.Email,
|
|
Name: req.Name,
|
|
OtpCode: codeRequest,
|
|
IsActive: true,
|
|
}
|
|
|
|
subject := "[HUMAS POLRI] Permintaan OTP"
|
|
htmlBody := fmt.Sprintf("<p>Hai %s !</p><p>Berikut kode OTP yang digunakan untuk verifikasi.</p>", *req.Name)
|
|
htmlBody += fmt.Sprintf("<p style='padding: 10px 50px; background: #eef2f6; border-radius: 8px; max-width: 300px; text-align: center'><b>%s</b></p>", codeRequest)
|
|
htmlBody += "<p style='padding-top: 10px;'>Kode diatas hanya berlaku selama 10 menit. Harap segera masukan 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, req.Email, req.Email, htmlBody)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = _i.Repo.CreateRegistrationOtps(&otpReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// send otp to email
|
|
|
|
return nil
|
|
}
|
|
|
|
func (_i *usersService) OtpValidation(req request.UserOtpValidation) (err error) {
|
|
_i.Log.Info().Interface("data", req).Msg("")
|
|
|
|
registrationOtp, err := _i.Repo.FindRegistrationOtps(req.Email, req.OtpCode)
|
|
if err != nil {
|
|
return fmt.Errorf("OTP is not valid")
|
|
}
|
|
|
|
if registrationOtp != nil {
|
|
if registrationOtp.ValidUntil.Before(time.Now()) {
|
|
return fmt.Errorf("OTP has expired")
|
|
}
|
|
|
|
return nil
|
|
} else {
|
|
return fmt.Errorf("OTP is not valid")
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|