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) }