sipd-auth/controller/user.go
2025-09-16 08:32:11 +07:00

460 lines
13 KiB
Go

package controller
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
models "kemendagri/sipd/services/sipd_auth/model"
"kemendagri/sipd/services/sipd_auth/model/form"
"kemendagri/sipd/services/sipd_auth/utils"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/gofiber/storage/redis/v3"
"github.com/golang-jwt/jwt/v4"
"github.com/minio/minio-go/v7"
"golang.org/x/crypto/bcrypt"
_ "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type UserController struct {
contextTimeout time.Duration
pgxConn *pgxpool.Pool
pgxConnPegawai *pgxpool.Pool
pgxConnMstData *pgxpool.Pool
dbConnMapsAnggaran map[string]*pgxpool.Pool
minioClient *minio.Client
redisCl *redis.Storage
}
func NewUserController(conn, connPeg, connMstData *pgxpool.Pool, mapsDbAnggaran map[string]*pgxpool.Pool, mc *minio.Client, tot time.Duration, redisClient *redis.Storage) (controller *UserController) {
controller = &UserController{
pgxConn: conn,
pgxConnPegawai: connPeg,
pgxConnMstData: connMstData,
dbConnMapsAnggaran: mapsDbAnggaran,
minioClient: mc,
contextTimeout: tot,
redisCl: redisClient,
}
return
}
func (c *UserController) Logout(idPegawai string) error {
var err error
err = c.redisCl.Delete("peg:" + idPegawai)
log.Println("peg:" + idPegawai)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "Failed to remove token data. - " + err.Error(),
}
return err
}
return err
}
func (c *UserController) GeneratePasswordHash(userId, idDaerah int64, pwd string) error {
// periksa apakah sudah hashing atau belum, blokir jika sudah
var uHashed bool
q := `SELECT "hashed" FROM "user" WHERE "id_user"=$1 AND "id_daerah"=$2`
err := c.pgxConn.QueryRow(context.Background(), q, userId, idDaerah).Scan(&uHashed)
if err != nil {
return err
}
if uHashed {
return utils.RequestError{
Code: http.StatusForbidden,
Message: "sudah hashing",
}
}
// generate password hash
pHash, err := bcrypt.GenerateFromPassword([]byte(pwd), 14)
if err != nil {
return err
}
q = `UPDATE "user" SET "pass_user"=$1, "hashed"=true WHERE "id_user"=$2 AND "id_daerah"=$3`
_, err = c.pgxConn.Exec(context.Background(), q, pHash, userId, idDaerah)
if err != nil {
return err
}
return nil
}
/*
Profile melihat detail profile user dengan id wilayah yang sama dengan si pembuat user baru
route name auth-service/user/profile
*/
func (c *UserController) Profile(user *jwt.Token) (r models.UserDetail, err error) {
claims := user.Claims.(jwt.MapClaims)
// idDaerah := int64(claims["id_daerah"].(float64))
// kodeProv := claims["kode_provinsi"].(string)
// tahun := int(claims["tahun"].(float64))
// idRole := int(claims["id_role"].(float64))
// skpdId := int64(claims["id_skpd"].(float64))
idPegawai := int64(claims["id_pegawai"].(float64))
userId := int64(claims["id_user"].(float64))
// log.Printf("Id User: %d, Id Daerah: %d\n", userId, idDaerah)
// data user
qStr := `SELECT id_user, nip_user, nama_user, nik_user, npwp_user, alamat
FROM public."sipd_user"
WHERE id_user=$1`
err = c.pgxConn.QueryRow(context.Background(), qStr, userId).
Scan(&r.IdUser, &r.Nip, &r.Nama, &r.Nik, &r.Npwp, &r.Alamat)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal mengambil data user. - " + err.Error(),
}
return
}
// data pegawai
var tahunPegawai int
qStr = `SELECT id_daerah, id_skpd, id_role, tahun_pegawai, status from public.sipd_pegawai where id=$1`
err = c.pgxConnPegawai.QueryRow(context.Background(), qStr, idPegawai).
Scan(&r.IdDaerah, &r.IdSkpdLama, &r.IdRole, &tahunPegawai, &r.Status)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal mengambil data pegawai. - " + err.Error(),
}
return
}
// data daerah
var kodeDdn string
qStr = `SELECT nama_daerah, kode_ddn FROM public.mst_daerah where id_daerah=$1 and deleted_by=0`
err = c.pgxConnMstData.QueryRow(context.Background(), qStr, r.IdDaerah).
Scan(&r.NamaDaerah, &kodeDdn)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal mengambil data daerah. - " + err.Error(),
}
return
}
fmt.Print("kode_ddn=", kodeDdn)
if r.IdSkpdLama != 0 {
// data skpd
var dbMapCode string
dbMapCodeDraft := fmt.Sprintf("%s_%d", kodeDdn, tahunPegawai)
fmt.Printf("%s_%d", kodeDdn, tahunPegawai)
_, connExists := c.dbConnMapsAnggaran[dbMapCodeDraft]
if connExists {
dbMapCode = dbMapCodeDraft
} else {
dbMapCode = fmt.Sprintf("%s_%d", strings.Split(dbMapCodeDraft, ".")[0], tahunPegawai)
}
qStr = `select id_unik_skpd, kode_skpd, nama_skpd from public.mst_skpd where id_skpd_lama=$1`
err = c.dbConnMapsAnggaran[dbMapCode].QueryRow(context.Background(), qStr, r.IdSkpdLama).Scan(&r.IdUnikSkpd, &r.KodeSkpd, &r.NamaSkpd)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal mengambil informasi skpd. - " + err.Error(),
}
return r, err
}
} else {
r.IdUnikSkpd = "00000000-0000-0000-0000-000000000000"
r.KodeSkpd = "0.00.0.00.0.00.00.0000"
r.NamaSkpd = "TIDAK ADA SKPD"
}
return
}
func (c *UserController) UploadAvatar(user *jwt.Token, file *multipart.FileHeader) error {
var err error
claims := user.Claims.(jwt.MapClaims)
// sub := claims["sub"].(string)
userId := int64(claims["id_user"].(float64))
/*daerahId := int64(claims["id_daerah"].(float64))
kodeProv := claims["kode_provinsi"].(string)
skpdId := int64(claims["id_skpd"].(float64))
tahun := int(claims["tahun"].(float64))
idRole := int(claims["id_role"].(float64))*/
log.Println("userId: ", userId)
if file == nil {
return utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "file harus diunggah",
}
}
// validasi file
var fileExt string
fileExt = filepath.Ext(file.Filename)
if fileExt != ".jpg" && fileExt != ".jpeg" && fileExt != ".png" {
return utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "jenis file tidak valid, hanya jpg, jpeg, png yang diizinkan.",
}
}
// validasi ukuran file
maxFsize := int64(1024)
fSize := file.Size / 1000 // 3028/1000 = 3.028 byte
if fSize > maxFsize { // max 1mb (1024 kb)
err = utils.RequestError{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("lampiran ukuran nya terlalu besar (%d). maksimal %dkb", fSize, maxFsize),
}
return err
}
// Get Buffer from file
buffer, err := file.Open()
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal open file to buffer. - " + err.Error(),
}
}
defer buffer.Close()
buckets, err := c.minioClient.ListBuckets(context.Background())
if err != nil {
return err
}
for _, bucket := range buckets {
fmt.Println("bucket: ", bucket)
}
// Check to see if we already own this bucket (which happens if you run this twice)
bucketName := os.Getenv("MINIO_BUCKET_NAME")
exists, errBucketExists := c.minioClient.BucketExists(context.Background(), bucketName)
if errBucketExists != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal memeriksa ketersediaan bucket object storage. - " + err.Error(),
}
return err
}
if !exists {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "bucket object storage tidak tersedia. - (" + bucketName + ").",
}
return err
}
objectName := "avatar/" + file.Filename
fileBuffer := buffer
contentType := file.Header["Content-Type"][0]
fileSize := file.Size
log.Println("fileSize: ", fileSize)
// Upload the file with PutObject
info, err := c.minioClient.PutObject(
context.Background(),
bucketName,
objectName,
fileBuffer,
fileSize,
minio.PutObjectOptions{ContentType: contentType},
)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal upload file. - " + err.Error(),
}
return err
}
log.Printf("Successfully uploaded %s of size %d\n", objectName, info.Size)
return err
}
/*
UpdateProfile user update profile
route name auth-service/user/update-profile
*/
func (c *UserController) UpdateProfile(user *jwt.Token, pl form.UpdateUserProfileForm) error {
var err error
claims := user.Claims.(jwt.MapClaims)
// sub := claims["sub"].(string)
daerahId := int64(claims["id_daerah"].(float64))
userId := int64(claims["id_user"].(float64))
//skpdId := int64(claims["id_skpd"].(float64))
//roleId := int64(claims["id_role"].(float64))
// log.Printf("userId: %d, daerahId: %d", userId, daerahId)
// validasi tanggal lahir
_, err = time.Parse("2006-01-02", pl.TglLahir)
if err != nil {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "Salah satu field tidak valid",
Fields: []utils.DataValidationError{{Field: "tgl_lahir", Message: "Format tidak valid"}},
}
return err
}
// validasi npwp
if len(pl.Npwp) < 15 && len(pl.Npwp) > 18 {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "Salah satu field tidak valid",
Fields: []utils.DataValidationError{{Field: "npwp", Message: "NPWP tidak valid"}},
}
return err
}
q := `UPDATE "user" SET
"nama_user"=$1,
"nik_user"=$2,
"npwp_user"=$3,
"alamat"=$4,
"lahir_user"=$5,
"id_pang_gol"=$6
WHERE "id_user"=$7 AND "id_daerah"=$8`
_, err = c.pgxConn.Exec(
context.Background(), q,
pl.NamaUser,
pl.Nik,
pl.Npwp,
pl.Alamat,
pl.TglLahir,
pl.IdPangGol,
userId,
daerahId,
)
if err != nil {
return err
}
return err
}
func (c *UserController) ChangePassword(userId, idDaerah int64, pl form.ChangePasswordForm) error {
var err error
var q string
/*log.Println("idUser: ", userId)
log.Println("idDaerah: ", idDaerah)
log.Printf("%#v\\n", pl)*/
var passwordHash string
q = `SELECT "pass_user" FROM "user" WHERE id_user=$1 AND id_daerah=$2`
err = c.pgxConn.QueryRow(context.Background(), q, userId, idDaerah).Scan(&passwordHash)
if err != nil {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "Salah satu field tidak valid",
Fields: []utils.DataValidationError{{Field: "old_password", Message: "kata sandi sebelumnya tidak valid"}},
}
return err
}
// log.Println("passwordHash: ", passwordHash)
if passwordHash == "" {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "Salah satu field tidak valid",
Fields: []utils.DataValidationError{{Field: "old_password", Message: ""}},
}
return err
}
// validate oldPassword
err = bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(pl.OldPassword))
if err != nil {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "Salah satu field tidak valid",
Fields: []utils.DataValidationError{{Field: "old_password", Message: "kata sandi sebelumnya tidak valid"}},
}
return err
}
// validate password
if !c.isValidPassword(pl.NewPassword) {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "Salah satu field tidak valid",
Fields: []utils.DataValidationError{
{
Field: "new_password",
Message: "kata sandi baru tidak valid. minimal 8 karakter mengandung angka, huruf besar, huruf kecil dan karakter spesial.",
},
},
}
return err
}
// generate new password hash
var bytesPwd []byte
bytesPwd, err = bcrypt.GenerateFromPassword([]byte(pl.NewPassword), 14)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal enkripsi. - " + err.Error(),
}
return err
}
// log.Println("bytesPwd: ", bytesPwd)
// update to new password
q = `UPDATE "user" SET "pass_user"=$1 WHERE id_user=$2 AND id_daerah=$3`
_, err = c.pgxConn.Exec(context.Background(), q, bytesPwd, userId, idDaerah)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal set new password. - " + err.Error(),
}
return err
}
return nil
}
func (c *UserController) validatePassword(content, encrypted string) bool {
return strings.EqualFold(c.encodePasswd(content), encrypted)
}
func (c *UserController) encodePasswd(data string) string {
h := md5.New()
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
func (c *UserController) isValidPassword(password string) bool {
if len(password) < 8 {
return false
}
reLower := regexp.MustCompile(`[a-z]`)
reUpper := regexp.MustCompile(`[A-Z]`)
reDigit := regexp.MustCompile(`\d`)
reSpecial := regexp.MustCompile(`[@$!%*?&]`)
return reLower.MatchString(password) &&
reUpper.MatchString(password) &&
reDigit.MatchString(password) &&
reSpecial.MatchString(password)
}