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

731 lines
19 KiB
Go

package controller
import (
"context"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
models "kemendagri/sipd/services/sipd_auth/model"
"kemendagri/sipd/services/sipd_auth/model/form"
"kemendagri/sipd/services/sipd_auth/utils"
"net/http"
"regexp"
"strings"
"time"
// "github.com/dchest/captcha"
"github.com/go-playground/validator/v10"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/crypto/bcrypt"
)
type AuthController struct {
contextTimeout time.Duration
pgxConn *pgxpool.Pool
pgxConnPegawai *pgxpool.Pool
pgxConnMstData *pgxpool.Pool
dbConnMapsAnggaran map[string]*pgxpool.Pool
jwtManager *utils.JWTManager
Validate *validator.Validate
}
func NewAuthController(
conn, connPegawai,
connMstData *pgxpool.Pool,
mapsDbAnggaran map[string]*pgxpool.Pool,
timeout time.Duration,
jm *utils.JWTManager,
vld *validator.Validate,
) (controller *AuthController) {
controller = &AuthController{
pgxConn: conn,
pgxConnMstData: connMstData,
pgxConnPegawai: connPegawai,
dbConnMapsAnggaran: mapsDbAnggaran,
contextTimeout: timeout,
jwtManager: jm,
Validate: vld,
}
return
}
func detectHashType(s string) string {
// Cek bcrypt (biasanya panjang 60 dan prefix $2a$, $2b$, $2y$)
if len(s) == 60 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$") {
return "bcrypt"
}
// Cek md5 (panjang 32 dan hex)
matchMD5, _ := regexp.MatchString("^[a-fA-F0-9]{32}$", s)
if matchMD5 {
return "md5"
}
return "unknown"
}
func (ac *AuthController) PreLogin(f form.PreLoginForm) (r []models.PreLoginModel, err error) {
r = make([]models.PreLoginModel, 0)
// validate captcha
// if !captcha.VerifyString(f.CaptchaId, f.CaptchaSolution) {
// err = utils.RequestError{
// Code: http.StatusUnprocessableEntity,
// Message: "invalid captcha",
// }
// return
// }
// Validate form input
err = ac.Validate.Struct(f)
if err != nil {
return
}
var hashed bool
var idUser, loginAttp, nextLogin int
var nipUser, namaUser, passUser string
q := `SELECT id_user, nip_user, nama_user, pass_user, hashed, login_attempt, next_login
FROM "sipd_user"
WHERE nip_user = $1`
err = ac.pgxConn.QueryRow(context.Background(), q, f.Username).Scan(
&idUser,
&nipUser,
&namaUser,
&passUser,
&hashed,
&loginAttp,
&nextLogin,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "invalid username or password",
}
return
}
return
}
tNow := time.Now()
if int64(nextLogin) > tNow.Unix() {
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "Login dibatasi",
}
return
}
switch detectHashType(passUser) {
case "bcrypt":
if err = bcrypt.CompareHashAndPassword([]byte(passUser), []byte(f.Password)); err != nil {
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "invalid username or password ----",
}
return
}
case "md5":
if ac.validatePassword(f.Password, passUser) == false {
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "invalid username or password",
}
return
}
default:
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "invalid username or password (*)",
}
return
}
// ambil list data pegawai user ini di service pegawai
type pegawaiJabatanModel struct {
IdPegawai int64 `json:"id_pegawai" xml:"id_pegawai" example:"1"`
IdUser int64 `json:"id_user" xml:"id_user" example:"1"` // ID user
Nip string `json:"nip_user" xml:"nip_user" example:"196408081992011001"`
Nama string `json:"nama_user" xml:"nama_user" example:"John Doe"`
IdDaerah int64 `json:"id_daerah" xml:"id_daerah" example:"229"`
NamaDaerah string `json:"nama_daerah" xml:"nama_daerah" example:"Kota Bandar Lampung"`
IdUnikSkpd string `json:"id_unik_skpd" xml:"id_unik_skpd"`
IdSkpdLama int64 `json:"id_skpd_lama" xml:"id_skpd_lama"`
KodeSkpd string `json:"kode_skpd" xml:"kode_skpd"`
NamaSkpd string `json:"nama_skpd" xml:"nama_skpd"`
IdRole int `json:"id_role" xml:"id_role" example:"1"`
NamaRole string `json:"nama_role" xml:"nama_role" exampe:"BENDAHARA UMUM DAERAH"`
}
var rows pgx.Rows
q = `SELECT
p.id,
p.id_daerah,
p.id_skpd,
p.id_role,
p.id_user,
pr.nama_role
FROM sipd_pegawai p
LEFT JOIN sipd_roles pr on p.id_role = pr.id_role
WHERE
p.id_user=$1
AND p.tahun_pegawai=$2
and p.locked=false
and p.deleted=false ORDER BY p.id_skpd,p.id_role`
rows, err = ac.pgxConnPegawai.Query(context.Background(), q, idUser, f.Tahun)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "invalid username or password",
}
} else {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal mengambil informasi pegawai. - " + err.Error(),
}
}
return r, err
}
defer rows.Close()
for rows.Next() {
mPeg := pegawaiJabatanModel{}
err = rows.Scan(
&mPeg.IdPegawai,
&mPeg.IdDaerah,
&mPeg.IdSkpdLama,
&mPeg.IdRole,
&mPeg.IdUser,
&mPeg.NamaRole,
)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal scan informasi pegawai. - " + err.Error(),
}
return r, err
}
var kodeDdn string
q = `select kode_ddn, nama_daerah from public.mst_daerah where id_daerah=$1`
err = ac.pgxConnMstData.QueryRow(context.Background(), q, mPeg.IdDaerah).Scan(&kodeDdn, &mPeg.NamaDaerah)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal mengambil informasi daerah. - " + err.Error(),
}
return r, err
}
if mPeg.IdSkpdLama != 0 {
// ambil data skpd
var dbMapCode string
dbMapCodeDraft := fmt.Sprintf("%s_%d", kodeDdn, f.Tahun)
_, connExists := ac.dbConnMapsAnggaran[dbMapCodeDraft]
if connExists {
dbMapCode = dbMapCodeDraft
} else {
dbMapCode = fmt.Sprintf("%s_%d", strings.Split(dbMapCodeDraft, ".")[0], f.Tahun)
}
fmt.Println("ini id_skpd_lama = ", mPeg.IdSkpdLama)
q = `select id_unik_skpd, kode_skpd, nama_skpd from public.mst_skpd where id_skpd_lama=$1`
err = ac.dbConnMapsAnggaran[dbMapCode].QueryRow(context.Background(), q, mPeg.IdSkpdLama).Scan(&mPeg.IdUnikSkpd, &mPeg.KodeSkpd, &mPeg.NamaSkpd)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal mengambil informasi skpd. - " + err.Error(),
}
return r, err
}
} else {
mPeg.IdUnikSkpd = "00000000-0000-0000-0000-000000000000"
mPeg.KodeSkpd = "0.00.0.00.0.00.00.0000"
mPeg.NamaSkpd = "TIDAK ADA SKPD"
}
m := models.PreLoginModel{
IdPegawai: mPeg.IdPegawai,
IdUser: mPeg.IdUser,
Nip: nipUser,
Nama: namaUser,
IdDaerah: mPeg.IdDaerah,
NamaDaerah: mPeg.NamaDaerah,
IdSkpdLama: mPeg.IdSkpdLama,
IdUnikSkpd: mPeg.IdUnikSkpd,
KodeSkpd: mPeg.KodeSkpd,
NamaSkpd: mPeg.NamaSkpd,
IdRole: mPeg.IdRole,
NamaRole: mPeg.NamaRole,
}
r = append(r, m)
}
if rows.Err() != nil {
rows.Close()
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal mengambil informasi pegawai (rows). - " + rows.Err().Error(),
}
return r, err
}
rows.Close()
return
}
func (ac *AuthController) Login(f form.LoginForm) (token, refreshToken string, IsDefaultPassword bool, err error) {
var q string
user := models.User{
IdPegawai: f.IdPegawai,
}
// Validate form input
err = ac.Validate.Struct(f)
if err != nil {
return
}
var tahunPegawai int
q = `select id_user, id_skpd, id_daerah, id_role, tahun_pegawai from public.sipd_pegawai where id=$1 and locked=false and deleted=false`
err = ac.pgxConnPegawai.QueryRow(context.Background(), q, f.IdPegawai).
Scan(&user.IdUser, &user.IdSkpd, &user.IdDaerah, &user.IdRole, &tahunPegawai)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal mengambil data. - " + err.Error(),
}
return
}
var passUser string
q = `SELECT pass_user FROM "sipd_user" WHERE id_user = $1 and deleted_by=0`
err = ac.pgxConn.QueryRow(context.Background(), q, user.IdUser).Scan(&passUser)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
err = utils.RequestError{
Code: http.StatusUnauthorized,
Message: "invalid username or password-",
}
return
}
return
}
switch detectHashType(passUser) {
case "bcrypt":
if err = bcrypt.CompareHashAndPassword([]byte(passUser), []byte(f.Password)); err != nil {
err = utils.RequestError{
Code: http.StatusUnauthorized,
Message: "invalid username or password" + err.Error(),
}
return
}
case "md5":
if ac.validatePassword(f.Password, passUser) == false {
err = utils.RequestError{
Code: http.StatusUnauthorized,
Message: "invalid username or password",
}
return
}
default:
err = utils.RequestError{
Code: http.StatusUnauthorized,
Message: "invalid username or password" + err.Error(),
}
return
}
// ambil kode wilayah user
q = `SELECT nama_daerah, kode_ddn FROM public.mst_daerah where id_daerah=$1 and deleted_by=0`
err = ac.pgxConnMstData.QueryRow(context.Background(), q, user.IdDaerah).
Scan(&user.SubDomainDaerah, &user.KodeDdn)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "Data Pemerintah Daerah Tidak Tersedia. - " + err.Error(),
}
return
}
user.KodeProvinsi = strings.Split(user.KodeDdn, ".")[0]
if len(user.KodeDdn) == 2 {
user.KodeDdn += ".00"
}
token, refreshToken, _, err = ac.jwtManager.Generate(ac.pgxConn, user, tahunPegawai, f.IdPegawai)
if err != nil {
return
}
return
}
func (ac *AuthController) Register(f *form.SignupForm) (response bool, err error) {
// Validate form input
err = ac.Validate.Struct(f)
if err != nil {
return
}
//currentTime := time.Now()
// validasi username
var usernameExists bool
qStr := `SELECT EXISTS(SELECT 1 FROM "user" WHERE "nip_user"=$1 and deleted_by=0)`
err = ac.pgxConn.QueryRow(context.Background(), qStr, f.Username).Scan(&usernameExists)
if err != nil {
return
}
if usernameExists {
err = utils.RequestError{
Fields: []utils.DataValidationError{{Field: "username", Message: "username already taken"}},
}
return
}
// validasi nip
/*var nipExists bool
qStr = `SELECT EXISTS(SELECT 1 FROM "user" WHERE "nip"=$1)`
err = ac.pgxConn.QueryRow(context.Background(), qStr, f.Nip).Scan(&nipExists)
if err != nil {
return
}
if nipExists {
err = utils.RequestError{
Fields: []utils.DataValidationError{{Field: "nip", Message: "nip already taken"}},
}
return
}*/
var maxIdUser int64
// jika id_user tidak ditemukan maka otomatis akan menjadi 0
qStr = `SELECT COALESCE(MAX(id_user),0) AS last_user_id FROM "user" WHERE "id_daerah"=$1`
err = ac.pgxConn.QueryRow(context.Background(), qStr, f.IdDaerah).Scan(&maxIdUser)
/*strQuery := `INSERT INTO "user" (
id_user,
id_daerah,
nip,
nama_user,
jabatan,
id_profil,
login_name,
login_passwd,
id_level,
akses_user,
is_locked,
is_deleted,
created_at,
updated_at,
is_login,
last_login,
last_ip_login,
nama_bidang,
id_unik,
token_login)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20)`
_, err = ac.pgxConn.Exec(context.Background(), strQuery,
maxIdUser+1,
f.IdDaerah,
f.Nip,
f.NamaUser,
"-",
0,
f.Username,
ac.encodePasswd(f.Password),
2,
"1|1|1|1|1|1|1|1|1|1|1|1|1",
0,
0,
currentTime.Format("2006-01-02 15:04:05"),
currentTime.Format("2006-01-02 15:04:05"),
0,
currentTime.Format("2006-01-02 15:04:05"),
"116.206.43.97:61968, 116.206.43.97",
f.NamaBidang,
"37fa8cde-849d-4801-99ac-9067b6b4d5ac",
"61dd0d091b2c5-1641876745")
if err != nil {
return
}*/
response = true
return
}
func (ac *AuthController) RefreshToken(pl form.RefreshTokenForm) (r models.ResponseLogin, err error) {
var user models.User
err = ac.Validate.Struct(pl)
if err != nil {
return
}
claims, err := ac.jwtManager.Verify(pl.Token)
if err != nil {
return
}
user.IdUser = claims.IdUser
user.IdDaerah = claims.IdDaerah
user.IdSkpd = claims.IdSkpd
user.IdRole = claims.IdRole
user.IdPegawai = claims.IdPegawai
/*subArr := strings.Split(claims.Subject, ".")
if len(subArr) != 2 {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "Akun tidak valid",
}
return
}
// log.Println(subArr)
userId, err := strconv.ParseUint(subArr[0], 10, 64)
if err != nil {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "Akun tidak valid",
}
return
}
userIdDaerah, err := strconv.ParseUint(subArr[1], 10, 64)
if err != nil {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "Akun tidak valid",
}
return
}*/
var idUser, idDaerah uint64
var passwdUser string
q := `SELECT id_user, id_daerah, pass_user FROM "user" WHERE id_user = $1 and deleted_by=0`
err = ac.pgxConn.QueryRow(context.Background(), q, claims.IdUser).Scan(&idUser, &idDaerah, &passwdUser)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "Akun tidak valid",
}
return
}
return
}
r.Token, r.RefreshToken, _, err = ac.jwtManager.Generate(ac.pgxConn, user, claims.Tahun, claims.IdPegawai)
if err != nil {
return
}
return
}
func (c *AuthController) AmankanKataSandi(pl form.ChangePasswordFormPublik) error {
var err error
var q string
/*log.Println("idUser: ", userId)
log.Println("idDaerah: ", idDaerah)
log.Printf("%#v\\n", pl)*/
var hashed bool
var idUser, idDaerah, loginAttp, nextLogin int
var passUser string
q = `SELECT id_user, id_daerah, pass_user, hashed, login_attempt, next_login
FROM "user"
WHERE nip_user = $1 and deleted_by = 0`
err = c.pgxConn.QueryRow(context.Background(), q, pl.Username).Scan(
&idUser,
&idDaerah,
&passUser,
&hashed,
&loginAttp,
&nextLogin,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "invalid username or password",
}
return err
}
return err
}
tNow := time.Now()
if int64(nextLogin) > tNow.Unix() {
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "Login dibatasi",
}
return err
}
if pl.NewPassword != pl.NewPasswordRepeat {
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "Password tidak sama dengan konfirmasi password",
}
return err
}
if hashed {
// validasi password terhadap hash
if err = bcrypt.CompareHashAndPassword([]byte(passUser), []byte(pl.OldPassword)); err != nil {
/*errAtp := ac.wrongLoginCounter(user.IdUser, user.IdDaerah, tNow)
if errAtp != nil {
err = utils.RequestError{Code: http.StatusBadRequest, Message: errAtp.Error()}
return err
}*/
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "invalid username or password",
}
return err
}
} else {
if c.validatePassword(pl.OldPassword, passUser) == false {
/*errAtp := ac.wrongLoginCounter(user.IdUser, user.IdDaerah, tNow)
if errAtp != nil {
err = utils.RequestError{Code: http.StatusBadRequest, Message: errAtp.Error()}
return err
}*/
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "invalid username or password",
}
return err
}
}
// log.Println("passwordHash: ", passwordHash)
if passUser == "" {
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(passUser), []byte(pl.OldPassword))
if err != nil {
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
Message: "invalid username or password",
}
return err
}
// validate password
if !c.isValidPassword(pl.NewPassword) {
err = utils.LoginError{
NextLogin: nextLogin,
Attempt: loginAttp,
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, idUser, idDaerah)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "gagal set new password. - " + err.Error(),
}
return err
}
return nil
}
func (ac *AuthController) validatePassword(content, encrypted string) bool {
return strings.EqualFold(ac.encodePasswd(content), encrypted)
}
func (ac *AuthController) encodePasswd(data string) string {
h := md5.New()
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
func (ac *AuthController) generatePasswordHash(password string) (string, error) {
bytesPwd, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytesPwd), err
}
func (ac *AuthController) wrongLoginCounter(idUser, idDaerah int64, t time.Time) error {
var loginAtp uint32
q := `UPDATE "user" SET "login_attempt"="user"."login_attempt"-1 WHERE "id_user"=$1 AND "id_daerah"=$2 RETURNING "login_attempt"`
err := ac.pgxConn.QueryRow(context.Background(), q, idUser, idDaerah).Scan(&loginAtp)
if err != nil {
return err
}
if loginAtp <= 0 {
q = `UPDATE "user" SET "next_login"=$1 WHERE "id_user"=$2 AND "id_daerah"=$3 RETURNING "login_attempt"`
_, err = ac.pgxConn.Exec(context.Background(), q, t.Add(time.Minute*5).Unix(), idUser, idDaerah)
if err != nil {
return err
}
}
return nil
}
func (ac *AuthController) 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)
}