first commit

This commit is contained in:
ogi 2025-09-16 08:32:11 +07:00
commit e96b29f280
43 changed files with 7705 additions and 0 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
README.md
.git
.idea
Makefile
Makefile.example

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# macOS
**/.DS_Store
# Jetbrains
.idea/
# Run script
Makefile
Makefile.localhost
Makefile.prod
# Dev build
build/
tmp/
# Test
*.out
# Gitlab
gitlab-ci.yml
# Jenkins
Jenkinsfile

61
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,61 @@
stages:
- test
- build
- deploy
variables:
DOCKER_CONFIG_FILE: "--config .docker"
DOCKER_REGCRED: "regcred"
PROJECT_NAME: "sipd-v2-auth"
PROJECT_GROUP_ID: "sipd"
K8S_NAMESPACE: "development"
MY_TRIGGER_TOKEN: "glptt-eb34428616c382d3240f1ae6a979e453504addee"
# default:
# tags:
# - docker
build:image:
stage: build
image: nexus.registry:8086/docker:stable
services:
- name: nexus.registry:8086/docker:18.09-dind
entrypoint: ["dockerd-entrypoint.sh"]
command: [
"--insecure-registry=nexus.registry:8087",
"--insecure-registry=nexus.registry:8086"
]
alias: dockerd
variables:
DOCKER_HOST: tcp://dockerd:2375
DOCKER_DRIVER: overlay2
DOCKER_TAGS:
nexus.registry:8087/$PROJECT_GROUP_ID/$PROJECT_NAME
before_script:
- mkdir -p .docker/ && cat $DOCKER_CONF_JSON > .docker/config.json
script:
- echo $CI_COMMIT_SHORT_SHA
- docker $DOCKER_CONFIG_FILE build -q -f Dockerfile -t $DOCKER_TAGS:latest .
- docker image tag $DOCKER_TAGS:latest $DOCKER_TAGS:$CI_COMMIT_SHORT_SHA
- docker $DOCKER_CONFIG_FILE image push $DOCKER_TAGS:latest
- docker $DOCKER_CONFIG_FILE image push $DOCKER_TAGS:$CI_COMMIT_SHORT_SHA
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
#rules:
# - if: $CI_COMMIT_TAG =~ /-release/
# when: always
# - when: never
#- if: $CI_COMMIT_SHORT_SHA
#when: manual
#deploy:
# stage: deploy
# script:
# - echo $CI_SERVER_URL
# - apk add curl
# - 'curl -X POST --fail -F "token=$MY_TRIGGER_TOKEN" -F "ref=main" -F "variables[PROJECT_GROUP_ID]=$PROJECT_GROUP_ID" -F "variables[SERVICE_NAME]=$PROJECT_NAME" "$CI_SERVER_URL/api/v4/projects/3/trigger/pipeline"'
# - echo "deploy success!"
# rules:
# - if: '$CI_COMMIT_BRANCH == "main"'

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM golang:1.23.12-alpine
RUN apk update && apk add --no-cache git
WORKDIR /app
COPY . .
RUN go mod tidy
RUN go build -o binary -tags musl
ENTRYPOINT ["/app/binary"]

40
Makefile.example Normal file
View File

@ -0,0 +1,40 @@
.PHONY: run
BUILD_DIR = $(PWD)/app
# App Env
SERVER_NAME=SIPD_AUTH_SERVICE
SERVER_URL="0.0.0.0:3000"
SERVER_READ_TIMEOUT=60
JWT_SECRET_KEY="jwt_secret"
JWT_EXPIRED_MINUTES="3600"
REFRESH_TOKEN_EXPIRED_HOUR="2000"
SIPD_CORS_WHITELISTS="*"
URL_SCHEME="http://"
BASE_URL="localhost:3000"
BASE_PATH="/"
AVAILABLE_YEAR="2021,2022,2023"
DB_SIPD_AUTH="host=0.0.0.0 port=5432 user=xxx password=xxx dbname=sipd_v2_auth sslmode=disable"
DB_SIPD_PEGAWAI="host=0.0.0.0 port=5432 user=xxx password=xxx dbname=sipd_v2_pegawai sslmode=disable"
DB_SIPD_MST_DATA="host=0.0.0.0 port=5432 user=xxx password=xxx dbname=sipd_v2_master_data sslmode=disable"
DB_SIPD_TRANSAKSI="[{\"ddn_prov\":\"32\",\"host\":\"0.0.0.0\",\"port\":\"5432\",\"user\":\"xxx\",\"password\":\"xxx\",\"ssl_mode\":\"disable\",\"dbname\":\"sipd_v2_transaksi\",\"database_pemda\":[]}]"
go:
export SERVER_NAME=$(SERVER_NAME);\
export SERVER_URL=$(SERVER_URL);\
export SERVER_READ_TIMEOUT=$(SERVER_READ_TIMEOUT);\
export JWT_SECRET_KEY=$(JWT_SECRET_KEY);\
export JWT_EXPIRED_MINUTES=$(JWT_EXPIRED_MINUTES);\
export REFRESH_TOKEN_EXPIRED_HOUR=$(REFRESH_TOKEN_EXPIRED_HOUR);\
export SIPD_CORS_WHITELISTS=$(SIPD_CORS_WHITELISTS);\
export URL_SCHEME=$(URL_SCHEME);\
export BASE_URL=$(BASE_URL);\
export BASE_PATH=$(BASE_PATH);\
export AVAILABLE_YEAR=$(AVAILABLE_YEAR);\
export DB_SIPD_AUTH=$(DB_SIPD_AUTH);\
export DB_SIPD_PEGAWAI=$(DB_SIPD_PEGAWAI);\
export DB_SIPD_MST_DATA=$(DB_SIPD_MST_DATA);\
export DB_SIPD_TRANSAKSI=$(DB_SIPD_TRANSAKSI);\
go mod tidy;\
go run main.go

40
README.md Normal file
View File

@ -0,0 +1,40 @@
# Kemendagri SIPD Service Auth
Kemendagri SIPD Service Auth.-
## Prerequisites
Prequisites package:
* [Docker](https://www.docker.com/get-started) - for developing, shipping, and running applications (Application Containerization).
* [Go](https://golang.org/) - Go Programming Language.
* [Make](https://golang.org/) - Automated Execution using Makefile.
* [swag](https://github.com/swaggo/swag) Converts Go annotations to Swagger Documentation 2.0. We've created a variety of plugins for popular Go web frameworks.
* [golang-migrate/migrate](https://github.com/golang-migrate/migrate#cli-usage) Database migrations written in Go. Use as CLI or import as library for apply migrations.
Optional package:
* [gocritic](https://github.com/go-critic/go-critic) Highly extensible Go source code linter providing checks currently missing from other linters.
* [gosec](https://github.com/securego/gosec) Golang Security Checker. Inspects source code for security problems by scanning the Go AST.
* [golangci-lint](https://github.com/golangci/golangci-lint) Go linters runner. It runs linters in parallel, uses caching, supports yaml config, has integrations with all major IDE and has dozens of linters included.
## ⚡️ Quick start
These instructions will get you a copy of the project up and running on docker container and on your local machine.
1. Install Prequisites and optional package to your system:
2. Rename `Makefile.example` to `Makefile` then fill it with your make setting.
3. Generate swagger api doc by this command
```shell
make swag
```
4. Instant run by this command
```shell
make instant_run
```
5. Bulid go binary file
```shell
make build
```
6. Build go binary file and run
```shell
make run
```
7. Run in docker container
```shell
make docker_run
```

730
controller/auth.go Normal file
View File

@ -0,0 +1,730 @@
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)
}

143
controller/captcha.go Normal file
View File

@ -0,0 +1,143 @@
package controller
import (
"bytes"
"encoding/base64"
models "kemendagri/sipd/services/sipd_auth/model"
"kemendagri/sipd/services/sipd_auth/utils"
"kemendagri/sipd/services/sipd_auth/utils/captcha_store"
"log"
"net/http"
"time"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/dchest/captcha"
)
type CaptchaController struct {
captchaStore *captcha_store.PostgreSQLStore
contextTimeout time.Duration
validate *validator.Validate
}
// https://www.machinet.net/tutorial-eng/implement-go-based-captcha-generator-validator
func NewCaptchaController(store *captcha_store.PostgreSQLStore, timeout time.Duration, vld *validator.Validate) *CaptchaController {
return &CaptchaController{
captchaStore: store,
contextTimeout: timeout,
validate: vld,
}
}
func (c *CaptchaController) New() (string, string, string, error) {
var err error
var captchaId, captchaBase64, audioBase64 string
captcha.SetCustomStore(c.captchaStore)
captchaId = uuid.New().String()
//captchaId = captcha.New()
c.captchaStore.Set(captchaId, captcha.RandomDigits(captcha.DefaultLen))
//c.captchaStore.Set(captchaId, captcha.RandomDigits(captcha.DefaultLen))
//c.captchaStore.Set(captchaId, []byte("123456"))
var captchaImage bytes.Buffer
err = captcha.WriteImage(&captchaImage, captchaId, captcha.StdWidth, captcha.StdHeight)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "failed to generate image. - " + err.Error(),
}
return captchaId, captchaBase64, audioBase64, err
}
captchaBase64 = base64.StdEncoding.EncodeToString(captchaImage.Bytes())
var captchaAudio bytes.Buffer
err = captcha.WriteAudio(&captchaAudio, captchaId, "id")
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "failed to generate audio. - " + err.Error(),
}
return captchaId, captchaBase64, audioBase64, err
}
// Encode the audio to Base64
audioBase64 = base64.StdEncoding.EncodeToString(captchaAudio.Bytes())
/*log.Println("captchaId: ", captchaId)
log.Println("captchaBase64: ", captchaBase64)*/
return captchaId, captchaBase64, audioBase64, err
}
func (c *CaptchaController) Reload(captchaId string) (string, string, error) {
var err error
var captchaBase64, audioBase64 string
captcha.SetCustomStore(c.captchaStore)
if !captcha.Reload(captchaId) {
err = utils.RequestError{
Code: http.StatusBadRequest,
Message: "captcha reload failed, invalid captcha ID.",
}
return captchaBase64, audioBase64, err
}
var captchaImage bytes.Buffer
err = captcha.WriteImage(&captchaImage, captchaId, captcha.StdWidth, captcha.StdHeight)
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "failed to reload captcha. - " + err.Error(),
}
return captchaBase64, audioBase64, err
}
captchaBase64 = base64.StdEncoding.EncodeToString(captchaImage.Bytes())
var captchaAudio bytes.Buffer
err = captcha.WriteAudio(&captchaAudio, captchaId, "id")
if err != nil {
err = utils.RequestError{
Code: http.StatusInternalServerError,
Message: "failed to generate audio. - " + err.Error(),
}
return captchaBase64, audioBase64, err
}
// Encode the audio to Base64
audioBase64 = base64.StdEncoding.EncodeToString(captchaAudio.Bytes())
/*log.Println("captchaId: ", captchaId)
log.Println("captchaBase64: ", captchaBase64)*/
return captchaBase64, audioBase64, nil
}
func (c *CaptchaController) Validate(pl models.ValidateCaptcha) error {
var err error
err = c.validate.Struct(pl)
if err != nil {
return err
}
captcha.SetCustomStore(c.captchaStore)
log.Printf("%+v\n", pl)
if !captcha.VerifyString(pl.Id, pl.Solution) {
err = utils.RequestError{
Code: http.StatusUnprocessableEntity,
Message: "invalid captcha",
}
return err
}
return err
}

27
controller/site.go Normal file
View File

@ -0,0 +1,27 @@
package controller
import (
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type SiteController struct {
pgxConn *pgxpool.Pool
contextTimeout time.Duration
}
func NewSiteController(conn *pgxpool.Pool, tot time.Duration) *SiteController {
return &SiteController{
contextTimeout: tot,
}
}
func (s *SiteController) Index() (interface{}, error) {
var err error
var r interface{}
r = "index"
return r, err
}

459
controller/user.go Normal file
View File

@ -0,0 +1,459 @@
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)
}

1428
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

1402
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

953
docs/swagger.yaml Normal file
View File

@ -0,0 +1,953 @@
definitions:
form.ChangePasswordForm:
properties:
new_password:
description: New password
example: "123456"
type: string
new_password_repeat:
description: New password confirmation, must equal to password
example: "123456"
type: string
old_password:
description: Old password
example: "123456"
type: string
required:
- new_password
- new_password_repeat
- old_password
type: object
form.ChangePasswordFormPublik:
properties:
new_password:
description: New password
example: "123456"
type: string
new_password_repeat:
description: New password confirmation, must equal to password
example: "123456"
type: string
old_password:
description: Old password
example: "123456"
type: string
username:
description: |-
IdDaerah int64 `json:"id_daerah" xml:"id_daerah" form:"id_daerah" example:"101" validate:"gte=0"` //Id daerah target user
IdUser int64 `json:"id_user" xml:"id_user" form:"id_user" example:"18" validate:"gte=0"` //Id target user
example: user
type: string
required:
- new_password
- new_password_repeat
- old_password
- username
type: object
form.GenerateHashForm:
properties:
password:
example: "123456"
type: string
password_repeat:
example: "123456"
type: string
required:
- password
- password_repeat
type: object
form.LoginForm:
properties:
id_daerah:
description: Id daerah user
example: 34
type: integer
id_pegawai:
example: 36107
type: integer
password:
description: User password
example: "1"
type: string
required:
- password
type: object
form.PreLoginForm:
properties:
captcha_id:
type: string
captcha_solution:
type: string
password:
description: User password
example: "1"
type: string
tahun:
example: 2023
minimum: 1
type: integer
username:
description: Username of user (NIP)
example: "198604292011011004"
type: string
required:
- captcha_id
- captcha_solution
- password
- username
type: object
form.RefreshTokenForm:
properties:
token:
description: JWT expired token
example: xxxxx
type: string
required:
- token
type: object
form.SignupForm:
properties:
id_daerah:
description: ID Daerah
example: 251
type: integer
nama_bidang:
description: Nama Bidang
type: string
nama_user:
description: 'Nama User (Ex: Kab Tanggamus)'
example: Kab. Tanggamus
type: string
nip:
description: NIP
example: "123456789876543213"
type: string
password:
description: Password user
example: "123456"
type: string
password_repeat:
description: Confirm Password user
example: "123456"
type: string
username:
description: Username user
example: admlambar
type: string
required:
- id_daerah
- password
- password_repeat
- username
type: object
form.UpdateUserProfileForm:
properties:
alamat:
description: Alamat
example: xxxx
type: string
id_pang_gol:
description: ID pangkat/golongan
example: 1
type: integer
nama_user:
description: 'Nama User (Ex: Kab Tanggamus)'
example: Kab. Tanggamus
type: string
nik:
description: NIK
example: "123456789876543213"
type: string
npwp:
description: NPWP
example: "123456789876543213"
type: string
tgl_lahir:
description: Tanggal lahir
example: "1945-08-17"
type: string
required:
- id_pang_gol
- nama_user
- nik
- npwp
type: object
http_util.JSONResultLogin:
properties:
is_default_password:
example: false
type: boolean
refresh_token:
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYxNjgyMjksImlk._aYI7pV2c9SU9VOp3RY_mxtFenYFQuKPJtVfk
type: string
token:
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYwODU0MjksImlkIjoyLCJwaG9uZSI6Iis2MjgxMjM0NTYyIiwidXNlcm5hbWUiOi.dl_ojy9ojLnWqpW589YltLPV61TCsON-3yQ2
type: string
type: object
models.PreLoginModel:
properties:
id_daerah:
type: integer
id_pegawai:
type: integer
id_role:
type: integer
id_skpd_lama:
type: integer
id_unik_skpd:
type: string
id_user:
type: integer
kode_skpd:
type: string
nama_daerah:
example: Kota Bandar Lampung
type: string
nama_role:
type: string
nama_skpd:
type: string
nama_user:
example: John Doe
type: string
nip_user:
example: "196408081992011001"
type: string
type: object
models.ResponseLogin:
properties:
refresh_token:
description: Jwt refresh token
example: sdfsfsfsdfsfsdfsfsdfsf
type: string
token:
description: Jwt token
example: sdfsfsfsdfsfsdfsfsdfsf
type: string
type: object
models.UserDetail:
properties:
alamat:
example: sddsfsd
type: string
id_daerah:
example: 111
type: integer
id_role:
type: integer
id_skpd_lama:
type: integer
id_unik_skpd:
type: string
id_user:
example: 581
type: integer
kode_skpd:
type: string
nama_daerah:
example: Kota Bandar Lampung
type: string
nama_skpd:
type: string
nama_user:
example: John Doe
type: string
nik_user:
example: "222112323324"
type: string
nip_user:
example: "196408081992011001"
type: string
npwp_user:
example: "222112323324"
type: string
status:
type: string
type: object
models.ValidateCaptcha:
properties:
id:
type: string
solution:
type: string
required:
- id
- solution
type: object
utils.DataValidationError:
properties:
field:
example: email
type: string
message:
example: Invalid email address
type: string
type: object
utils.LoginError:
properties:
attempt:
description: sisa kesempatan login sebelum diblokir 5 menit
example: 3
type: integer
message:
description: keterangan error
example: invalid username or password
type: string
next_login:
description: unix timestamp UTC blokir login dibuka kembali
example: 123233213
type: integer
type: object
utils.RequestError:
properties:
code:
example: 422
type: integer
fields:
items:
$ref: '#/definitions/utils.DataValidationError'
type: array
message:
example: Invalid email address
type: string
type: object
info:
contact:
email: lifelinejar@mail.com
name: API Support
description: SIPD Service Auth Rest API.
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
termsOfService: http://swagger.io/terms/
title: SIPD Service Auth
version: "1.0"
paths:
/auth/amankan-kata-sandi:
post:
description: User melakukan Amankan Kata Sandi.
operationId: auth-amankan-kata-sandi
parameters:
- description: Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/form.ChangePasswordFormPublik'
produces:
- application/json
responses:
"200":
description: Success
schema:
type: boolean
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/utils.RequestError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
summary: Amankan Kata Sandi
tags:
- Auth
/auth/login:
post:
consumes:
- application/json
description: Login to get JWT token and refresh token.
operationId: auth-login
parameters:
- description: Login payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/form.LoginForm'
produces:
- application/json
responses:
"200":
description: Login Success, jwt provided
schema:
$ref: '#/definitions/http_util.JSONResultLogin'
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Login forbidden
schema:
$ref: '#/definitions/utils.LoginError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
summary: user login
tags:
- Auth
/auth/pre-login:
post:
consumes:
- application/json
description: Login to get JWT token and refresh token.
operationId: auth-pre-login
parameters:
- description: Pre login payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/form.PreLoginForm'
produces:
- application/json
responses:
"200":
description: Success
schema:
items:
$ref: '#/definitions/models.PreLoginModel'
type: array
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Login forbidden
schema:
$ref: '#/definitions/utils.LoginError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
summary: user login
tags:
- Auth
/auth/register:
post:
consumes:
- application/json
description: Register user.
operationId: auth-register
parameters:
- description: Register payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/form.SignupForm'
produces:
- application/json
responses:
"200":
description: Register Success
schema:
type: boolean
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
summary: user register
tags:
- Auth
/auth/token-refresh/{token}:
post:
description: Refresh token to get new valid JWT token and refresh token
operationId: auth-refresh-token
parameters:
- description: Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/form.RefreshTokenForm'
produces:
- application/json
responses:
"200":
description: Refresh Token Success, new JWT token provided
schema:
$ref: '#/definitions/models.ResponseLogin'
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
summary: Refresh Token
tags:
- Auth
/captcha/new:
get:
consumes:
- application/json
description: generate new captcha.
produces:
- application/json
responses:
"200":
description: Base64 image string
schema: {}
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Login forbidden
schema:
$ref: '#/definitions/utils.LoginError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
summary: generate new captcha
tags:
- Captcha
/captcha/reload/{id}:
get:
consumes:
- application/json
description: reload captcha.
parameters:
- description: captcha ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Base64 image string
schema: {}
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Login forbidden
schema:
$ref: '#/definitions/utils.LoginError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
summary: reload captcha
tags:
- Captcha
/captcha/validate:
post:
consumes:
- application/json
description: validate captcha.
parameters:
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/models.ValidateCaptcha'
produces:
- application/json
responses:
"200":
description: validate success
schema:
type: boolean
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Login forbidden
schema:
$ref: '#/definitions/utils.LoginError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
summary: validate captcha
tags:
- Captcha
/site/index:
get:
consumes:
- application/json
description: index page.
operationId: index
produces:
- application/json
responses:
"200":
description: Success
schema: {}
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Login forbidden
schema:
$ref: '#/definitions/utils.LoginError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
summary: index
tags:
- Site
/strict/user/change-password:
post:
description: change password.
operationId: user-change-password
parameters:
- description: Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/form.ChangePasswordForm'
produces:
- application/json
responses:
"200":
description: Success
schema:
type: boolean
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/utils.RequestError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
security:
- ApiKeyAuth: []
summary: change password
tags:
- User
/strict/user/generate-password-hash:
post:
description: generate password hash.
operationId: user-generate-password-hash
parameters:
- description: Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/form.GenerateHashForm'
produces:
- application/json
responses:
"200":
description: Success
schema:
type: boolean
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/utils.RequestError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
security:
- ApiKeyAuth: []
summary: generate password hash
tags:
- User
/strict/user/logout:
get:
description: user logout.
operationId: user-logout
produces:
- application/json
responses:
"200":
description: Success
schema:
type: boolean
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/utils.RequestError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
security:
- ApiKeyAuth: []
summary: logout
tags:
- User
/strict/user/profile:
get:
description: get profile info.
operationId: user-profile
produces:
- application/json
responses:
"200":
description: Success
schema:
$ref: '#/definitions/models.UserDetail'
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
security:
- ApiKeyAuth: []
summary: user get profile info
tags:
- User
/strict/user/update-profile:
put:
description: update profile.
parameters:
- description: Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/form.UpdateUserProfileForm'
produces:
- application/json
responses:
"200":
description: Success
schema:
type: boolean
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
security:
- ApiKeyAuth: []
summary: update profile
tags:
- User
/strict/user/upload-avatar:
post:
consumes:
- application/x-www-form-urlencoded
description: upload avatar
operationId: user-upload avatar
parameters:
- description: Image avatar
in: formData
name: file
required: true
type: file
produces:
- application/json
responses:
"200":
description: Success
schema:
type: boolean
"400":
description: Bad request
schema:
$ref: '#/definitions/utils.RequestError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/utils.RequestError'
"404":
description: Data not found
schema:
$ref: '#/definitions/utils.RequestError'
"422":
description: Data validation failed
schema:
items:
$ref: '#/definitions/utils.RequestError'
type: array
"500":
description: Server error
schema:
$ref: '#/definitions/utils.RequestError'
security:
- ApiKeyAuth: []
summary: upload avatar
tags:
- User
securityDefinitions:
ApiKeyAuth:
in: header
name: Authorization
type: apiKey
swagger: "2.0"

74
go.mod Normal file
View File

@ -0,0 +1,74 @@
module kemendagri/sipd/services/sipd_auth
go 1.23.0
toolchain go1.24.7
require (
github.com/arsmn/fiber-swagger/v2 v2.31.1
github.com/dchest/captcha v1.1.0
github.com/go-playground/validator/v10 v10.27.0
github.com/gofiber/fiber/v2 v2.52.9
github.com/gofiber/jwt/v3 v3.3.10
github.com/gofiber/storage/redis/v3 v3.4.1
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang-migrate/migrate/v4 v4.19.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.6
github.com/lib/pq v1.10.9
github.com/minio/minio-go/v7 v7.0.95
github.com/sirupsen/logrus v1.9.3
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.39.0
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/minio/crc64nvme v1.0.2 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/redis/go-redis/v9 v9.12.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.33.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

373
go.sum Normal file
View File

@ -0,0 +1,373 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/arsmn/fiber-swagger/v2 v2.31.1 h1:VmX+flXiGGNqLX3loMEEzL3BMOZFSPwBEWR04GA6Mco=
github.com/arsmn/fiber-swagger/v2 v2.31.1/go.mod h1:ZHhMprtB3M6jd2mleG03lPGhHH0lk9u3PtfWS1cBhMA=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/captcha v1.1.0 h1:2kt47EoYUUkaISobUdTbqwx55xvKOJxyScVfw25xzhQ=
github.com/dchest/captcha v1.1.0/go.mod h1:7zoElIawLp7GUMLcj54K9kbw+jEyvz2K0FDdRRYhvWo=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofiber/fiber/v2 v2.31.0/go.mod h1:1Ega6O199a3Y7yDGuM9FyXDPYQfv+7/y48wl6WCwUF4=
github.com/gofiber/fiber/v2 v2.45.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc=
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/jwt/v3 v3.3.10 h1:0bpWtFKaGepjwYTU4efHfy0o+matSqZwTxGMo5a+uuc=
github.com/gofiber/jwt/v3 v3.3.10/go.mod h1:GJorFVaDyfMPSK9RB8RG4NQ3s1oXKTmYaoL/ny08O1A=
github.com/gofiber/storage/redis/v3 v3.4.1 h1:feZc1xv1UuW+a1qnpISPaak7r/r0SkNVFHmg9R7PJ/c=
github.com/gofiber/storage/redis/v3 v3.4.1/go.mod h1:rbycYIeewyFZ1uMf9I6t/C3RHZWIOmSRortjvyErhyA=
github.com/gofiber/storage/testhelpers/redis v0.0.0-20250822074218-ba2347199921 h1:32Fh8t9QK2u2y8WnitCxIhf1AxKXBFFYk9tousVn/Fo=
github.com/gofiber/storage/testhelpers/redis v0.0.0-20250822074218-ba2347199921/go.mod h1:PU9dj9E5K6+TLw7pF87y4yOf5HUH6S9uxTlhuRAVMEY=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=
github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM=
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
github.com/testcontainers/testcontainers-go/modules/redis v0.38.0 h1:289pn0BFmGqDrd6BrImZAprFef9aaPZacx07YOQaPV4=
github.com/testcontainers/testcontainers-go/modules/redis v0.38.0/go.mod h1:EcKPWRzOglnQfYe+ekA8RPEIWSNJTGwaC5oE5bQV+D0=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

189
handler/http/auth.go Normal file
View File

@ -0,0 +1,189 @@
package http
import (
"kemendagri/sipd/services/sipd_auth/controller"
"kemendagri/sipd/services/sipd_auth/handler/http/http_util"
"kemendagri/sipd/services/sipd_auth/model/form"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type AuthHandler struct {
Controller *controller.AuthController
Validate *validator.Validate
}
func NewAuthHandler(app *fiber.App, controller *controller.AuthController, vld *validator.Validate) {
handler := &AuthHandler{
Controller: controller,
Validate: vld,
}
// public route
rPub := app.Group("/auth")
rPub.Post("/pre-login", handler.PreLogin)
rPub.Post("/login", handler.Login)
rPub.Post("/register", handler.Register)
rPub.Post("/token-refresh/:token", handler.TokenRefresh)
rPub.Post("/amankan-kata-sandi", handler.AmankanKataSandi)
// strict route
// rStrict := r.Group("auth")
}
// PreLogin func for login.
//
// @Summary user login
// @Description Login to get JWT token and refresh token.
// @ID auth-pre-login
// @Tags Auth
// @Accept json
// @Param payload body form.PreLoginForm true "Pre login payload"
// @Produce json
// @Success 200 {array} models.PreLoginModel "Success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.LoginError "Login forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Router /auth/pre-login [post]
func (ah *AuthHandler) PreLogin(c *fiber.Ctx) error {
formModel := new(form.PreLoginForm)
if err := c.BodyParser(formModel); err != nil {
return err
}
r, err := ah.Controller.PreLogin(*formModel)
if err != nil {
return err
}
return c.JSON(r)
}
// Login func for login.
//
// @Summary user login
// @Description Login to get JWT token and refresh token.
// @ID auth-login
// @Tags Auth
// @Accept json
// @Param payload body form.LoginForm true "Login payload"
// @Produce json
// @Success 200 {object} http_util.JSONResultLogin "Login Success, jwt provided"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.LoginError "Login forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Router /auth/login [post]
func (ah *AuthHandler) Login(c *fiber.Ctx) error {
formModel := new(form.LoginForm)
if err := c.BodyParser(formModel); err != nil {
return err
}
token, refreshToken, isDefaultPassword, err := ah.Controller.Login(*formModel)
if err != nil {
return err
}
return c.JSON(
http_util.JSONResultLogin{Token: token, RefreshToken: refreshToken, IsDefaultPassword: isDefaultPassword},
)
}
// Register func for register.
//
// @Summary user register
// @Description Register user.
// @ID auth-register
// @Tags Auth
// @Accept json
// @Param payload body form.SignupForm true "Register payload"
// @Produce json
// @Success 200 {object} bool "Register Success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Router /auth/register [post]
func (ah *AuthHandler) Register(c *fiber.Ctx) error {
formModel := new(form.SignupForm)
if err := c.BodyParser(formModel); err != nil {
return err
}
resp, err := ah.Controller.Register(formModel)
if err != nil {
return err
}
return c.JSON(resp)
}
// TokenRefresh godoc
//
// @Summary Refresh Token
// @Description Refresh token to get new valid JWT token and refresh token
// @ID auth-refresh-token
// @Tags Auth
// @Produce json
// @Param payload body form.RefreshTokenForm true "Payload"
// @Success 200 {object} models.ResponseLogin "Refresh Token Success, new JWT token provided"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Router /auth/token-refresh/{token} [post]
func (ah *AuthHandler) TokenRefresh(c *fiber.Ctx) error {
pl := form.RefreshTokenForm{}
if err := c.BodyParser(&pl); err != nil {
return err
}
r, err := ah.Controller.RefreshToken(pl)
if err != nil {
return err
}
return c.JSON(r)
}
// AmankanKataSandi User func for change password.
//
// @Summary Amankan Kata Sandi
// @Description User melakukan Amankan Kata Sandi.
// @ID auth-amankan-kata-sandi
// @Tags Auth
// @Param payload body form.ChangePasswordFormPublik true "Payload"
// @Produce json
// @success 200 {object} bool "Success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.RequestError "Forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Router /auth/amankan-kata-sandi [post]
func (ah *AuthHandler) AmankanKataSandi(c *fiber.Ctx) error {
formModel := new(form.ChangePasswordFormPublik)
if err := c.BodyParser(formModel); err != nil {
return err
}
// Validate form input
err := ah.Validate.Struct(formModel)
if err != nil {
return err
}
err = ah.Controller.AmankanKataSandi(*formModel)
if err != nil {
return err
}
return c.JSON(true)
}

100
handler/http/captcha.go Normal file
View File

@ -0,0 +1,100 @@
package http
import (
"kemendagri/sipd/services/sipd_auth/controller"
models "kemendagri/sipd/services/sipd_auth/model"
"github.com/gofiber/fiber/v2"
)
type CaptchaHandler struct {
Controller *controller.CaptchaController
}
func NewCaptchaHandler(app *fiber.App, controller *controller.CaptchaController) {
handler := &CaptchaHandler{
Controller: controller,
}
// public route
rPub := app.Group("/captcha")
rPub.Get("/new", handler.New)
rPub.Get("/reload/:id", handler.Reload)
rPub.Post("/validate", handler.Validate)
}
// New func for generate new captcha.
//
// @Summary generate new captcha
// @Description generate new captcha.
// @Tags Captcha
// @Accept json
// @Produce json
// @Success 200 {object} interface{} "Base64 image string"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.LoginError "Login forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Router /captcha/new [get]
func (h *CaptchaHandler) New(c *fiber.Ctx) error {
id, base64, audio, err := h.Controller.New()
if err != nil {
return err
}
return c.JSON(fiber.Map{"id": id, "base64": base64, "audio": audio})
}
// Reload func for reload captcha.
//
// @Summary reload captcha
// @Description reload captcha.
// @Tags Captcha
// @Accept json
// @Produce json
// @Param id path string true "captcha ID"
// @Success 200 {object} interface{} "Base64 image string"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.LoginError "Login forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Router /captcha/reload/{id} [get]
func (h *CaptchaHandler) Reload(c *fiber.Ctx) error {
base64, audio, err := h.Controller.Reload(c.Params("id"))
if err != nil {
return err
}
return c.JSON(fiber.Map{"base64": base64, "audio": audio})
}
// Validate func for validate captcha.
//
// @Summary validate captcha
// @Description validate captcha.
// @Tags Captcha
// @Accept json
// @Produce json
// @Param payload body models.ValidateCaptcha true "payload"
// @Success 200 {object} boolean "validate success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.LoginError "Login forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Router /captcha/validate [post]
func (h *CaptchaHandler) Validate(c *fiber.Ctx) error {
formModel := new(models.ValidateCaptcha)
if err := c.BodyParser(formModel); err != nil {
return err
}
err := h.Controller.Validate(*formModel)
if err != nil {
return err
}
return c.JSON(true)
}

View File

@ -0,0 +1,77 @@
package configs
import (
"errors"
"fmt"
"kemendagri/sipd/services/sipd_auth/utils"
"os"
"strconv"
"time"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
// FiberConfig func for configuration Fiber app.
// See: https://docs.gofiber.io/api/fiber#config
func FiberConfig() fiber.Config {
// Define server settings.
readTimeoutSecondsCount, _ := strconv.Atoi(os.Getenv("SERVER_READ_TIMEOUT"))
// Return Fiber configuration.
return fiber.Config{
AppName: os.Getenv("APP_NAME"),
ReadTimeout: time.Second * time.Duration(readTimeoutSecondsCount),
//Prefork: true,
//CaseSensitive: true,
//StrictRouting: true,
ServerHeader: os.Getenv("SERVER_NAME"),
ErrorHandler: fiberErrorHandler,
}
}
func fiberErrorHandler(ctx *fiber.Ctx, err error) error {
var re utils.RequestError
var vlde validator.ValidationErrors
switch {
case errors.As(err, &re):
_ = ctx.Status(re.Code).JSON(re)
case errors.As(err, &vlde):
var eReq = new(utils.RequestError)
eReq.Code = fiber.StatusUnprocessableEntity
for _, err := range vlde {
errObj := utils.DataValidationError{Field: err.Field()}
eReq.Message = "Salah satu field tidak valid"
// penyesuaian pesan error berdasarkan jenis validasinya
switch err.Tag() {
case "required":
errObj.Message = fmt.Sprintf("%s harus diisi.", err.Field())
case "len":
errObj.Message = fmt.Sprintf("panjang %s harus sama dengan %s", err.Field(), err.Param())
case "gte":
errObj.Message = fmt.Sprintf("%s harus lebih besar atau sama dengan %s", err.Field(), err.Param())
case "gt":
errObj.Message = fmt.Sprintf("%s harus lebih besar dari %s", err.Field(), err.Param())
case "e164":
errObj.Message = "Invalid phone number format (E.164)"
case "alphanumspace":
errObj.Message = "Only alphanumeric and space allowed"
case "alphanumslashasterisk":
errObj.Message = "Only alphanumeric, slash and asterisk allowed"
default:
errObj.Message = err.Tag()
}
eReq.Fields = append(eReq.Fields, errObj)
}
_ = ctx.Status(fiber.StatusUnprocessableEntity).JSON(eReq)
default:
_ = ctx.Status(fiber.StatusInternalServerError).JSON(utils.GlobalError{Message: err.Error()})
}
// Return from handler
return nil
}

View File

@ -0,0 +1,21 @@
package http_util
type JSONResult struct {
Code int `json:"code" example:"404"`
Message string `json:"message" example:"Not Found"`
Data interface{} `json:"data"`
Meta interface{} `json:"meta"`
}
type JSONResultMeta struct {
TotalCount int `json:"total_count"`
PageCount int `json:"page_count"`
CurrentPage int `json:"current_page"`
PerPage int `json:"per_page"`
}
type JSONResultLogin struct {
IsDefaultPassword bool `json:"is_default_password" example:"false"`
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYwODU0MjksImlkIjoyLCJwaG9uZSI6Iis2MjgxMjM0NTYyIiwidXNlcm5hbWUiOi.dl_ojy9ojLnWqpW589YltLPV61TCsON-3yQ2"`
RefreshToken string `json:"refresh_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYxNjgyMjksImlk._aYI7pV2c9SU9VOp3RY_mxtFenYFQuKPJtVfk"`
}

View File

@ -0,0 +1,44 @@
package http_util
import (
"log"
"os"
"os/signal"
"github.com/gofiber/fiber/v2"
)
// StartServerWithGracefulShutdown function for starting server with a graceful shutdown.
func StartServerWithGracefulShutdown(a *fiber.App) {
// Create channel for idle connections.
idleConnsClosed := make(chan struct{})
go func() {
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt) // Catch OS signals.
<-sigint
// Received an interrupt signal, shutdown.
if err := a.Shutdown(); err != nil {
// Error from closing listeners, or context timeout:
log.Printf("Oops... Server is not shutting down! Reason: %v", err)
}
close(idleConnsClosed)
}()
// Run server.
if err := a.Listen(os.Getenv("SERVER_URL")); err != nil {
log.Printf("Oops... Server is not running! Reason: %v", err)
}
<-idleConnsClosed
}
// StartServer func for starting a simple server.
func StartServer(a *fiber.App) {
// Run server.
if err := a.Listen(os.Getenv("SERVER_URL")); err != nil {
log.Printf("Oops... Server is not running! Reason: %v", err)
}
}

View File

@ -0,0 +1,50 @@
package http_util
import (
"crypto/rand"
"encoding/base64"
"math/big"
)
// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}
// GenerateRandomString returns a securely generated random string.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
ret := make([]byte, n)
for i := 0; i < n; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
ret[i] = letters[num.Int64()]
}
return string(ret), nil
}
// GenerateRandomStringURLSafe returns a URL-safe, base64 encoded
// securely generated random string.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomStringURLSafe(n int) (string, error) {
b, err := GenerateRandomBytes(n)
return base64.URLEncoding.EncodeToString(b), err
}

View File

@ -0,0 +1,207 @@
package middleware
import (
"fmt"
"os"
"strings"
"time"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/storage/redis/v3"
"github.com/golang-jwt/jwt/v4"
"github.com/sirupsen/logrus"
"github.com/gofiber/fiber/v2"
jwtware "github.com/gofiber/jwt/v3"
)
// GoMiddleware represent the data-struct for middleware
type GoMiddleware struct {
appCtx *fiber.App
redisCl *redis.Storage
appLog *logrus.Logger
// another stuff , may be needed by middleware
}
func (m *GoMiddleware) RateLimiter() fiber.Handler {
limiterCfg := limiter.Config{
/*Next: func(c *fiber.Ctx) bool {
return c.IP() == "127.0.0.1"
},*/
Storage: m.redisCl,
Max: 10,
Expiration: 30 * time.Second,
LimitReached: func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusTooManyRequests)
},
KeyGenerator: func(c *fiber.Ctx) string {
authKey := c.Get("Authorization")
if authKey == "" {
return c.IP() + c.Get("User-Agent")
} else {
if len(authKey) > 7 && strings.HasPrefix(authKey, "Bearer ") {
authKey = authKey[7:]
}
return authKey
}
},
/*KeyGenerator: func(c *fiber.Ctx) string {
return c.IP() + c.Get("User-Agent")
},*/
}
return limiter.New(limiterCfg)
}
func (m *GoMiddleware) RateLimiterFull() fiber.Handler {
limiterCfg := limiter.Config{
Max: 10,
Expiration: 30 * time.Second,
KeyGenerator: func(c *fiber.Ctx) string {
authKey := c.Get("Authorization")
if authKey == "" {
uniqueID := c.Cookies("sipd_penatausahaan_uk_")
if uniqueID == "" {
uniqueID = fmt.Sprintf("%d", time.Now().UnixNano())
c.Cookie(&fiber.Cookie{
Name: "sipd_penatausahaan_uk_",
Value: uniqueID,
Expires: time.Now().Add(24 * time.Hour),
HTTPOnly: true,
Secure: true,
})
}
return c.IP() + c.Get("User-Agent") + uniqueID
} else {
return authKey
}
},
LimitReached: func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusTooManyRequests)
},
}
return limiter.New(limiterCfg)
}
// CORS will handle the CORS middleware
func (m *GoMiddleware) CORS() fiber.Handler {
crs := os.Getenv("SIPD_CORS_WHITELISTS")
if crs == "*" {
return cors.New(cors.Config{
AllowOrigins: "*",
AllowHeaders: "Content-Type, Accept, Authorization",
AllowMethods: "GET, HEAD, PUT, PATCH, POST, DELETE",
ExposeHeaders: "*", //"X-Pagination-Current-Page,X-Pagination-Next-Page,X-Pagination-Page-Count,X-Pagination-Page-Size,X-Pagination-Total-Count"
})
}
return cors.New(cors.Config{
AllowOrigins: crs,
AllowCredentials: true,
AllowHeaders: "Content-Type, Accept, Authorization",
AllowMethods: "GET, HEAD, PUT, PATCH, POST, DELETE",
ExposeHeaders: "*", //"X-Pagination-Current-Page,X-Pagination-Next-Page,X-Pagination-Page-Count,X-Pagination-Page-Size,X-Pagination-Total-Count"
})
}
// LOGGER simple logger.
func (m *GoMiddleware) LOGGER() fiber.Handler {
return logger.New()
/*return func(c *fiber.Ctx) error {
err := c.Next()
if err != nil {
log.Print("sdfsdsfdsddf")
// Log incoming requests
m.appLog.WithFields(logrus.Fields{
"method": c.Method(),
"path": c.Path(),
"ip": c.IP(),
"message": err.Error(),
}).Error("Error occurred")
return err
}
// Proceed with the request
return nil
}*/
}
// JWT jwt.
func (m *GoMiddleware) JWT() fiber.Handler {
// Create config for JWT authentication middleware.
config := jwtware.Config{
SigningKey: []byte(os.Getenv("JWT_SECRET_KEY")),
ContextKey: "jwt", // used in private routes
// SuccessHandler: m.jwtSuccess,
ErrorHandler: jwtError,
}
return jwtware.New(config)
}
func jwtError(c *fiber.Ctx, err error) error {
// Return status 400 and failed authentication error.
if err.Error() == "Missing or malformed JWT" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
// Return status 401 and failed authentication error.
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
func (m *GoMiddleware) jwtSuccess(c *fiber.Ctx) error {
var err error
// log.Println("jwt success")
user := c.Locals("jwt").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
tokenString := user.Raw
pegId := int64(claims["id_pegawai"].(float64))
redisKey := fmt.Sprintf("peg:%d", pegId)
existingToken, err := m.redisCl.Get(redisKey)
if err != nil {
err = c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": true,
"msg": "Failed to check token data. - " + err.Error(),
})
return err
}
if len(existingToken) == 0 {
err = c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": true,
"msg": "not authorized",
})
return err
} else {
if fmt.Sprintf("%s", existingToken) != tokenString {
err = c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": true,
"msg": "forbidden",
})
return err
}
}
return c.Next()
}
// InitMiddleware initialize the middleware
func InitMiddleware(ctx *fiber.App, store *redis.Storage, appLog *logrus.Logger) *GoMiddleware {
return &GoMiddleware{appCtx: ctx, redisCl: store, appLog: appLog}
}

48
handler/http/site.go Normal file
View File

@ -0,0 +1,48 @@
package http
import (
"kemendagri/sipd/services/sipd_auth/controller"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type SiteHandler struct {
Controller *controller.SiteController
Validate *validator.Validate
}
func NewSiteHandler(app *fiber.App, controller *controller.SiteController, vld *validator.Validate) {
handler := &SiteHandler{
Controller: controller,
Validate: vld,
}
// public route
rPub := app.Group("/site")
rPub.Get("/index", handler.Index)
}
// Index func for index.
//
// @Summary index
// @Description index page.
// @ID index
// @Tags Site
// @Accept json
// @Produce json
// @Success 200 {object} interface{} "Success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.LoginError "Login forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Router /site/index [get]
func (h *SiteHandler) Index(c *fiber.Ctx) error {
r, err := h.Controller.Index()
if err != nil {
return err
}
return c.JSON(r)
}

249
handler/http/user.go Normal file
View File

@ -0,0 +1,249 @@
package http
import (
"fmt"
"kemendagri/sipd/services/sipd_auth/controller"
"kemendagri/sipd/services/sipd_auth/model/form"
"log"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v4"
)
type UserHandler struct {
Controller *controller.UserController
Validate *validator.Validate
}
func NewUserHandler(
r fiber.Router,
validator *validator.Validate,
controller *controller.UserController,
) {
handler := &UserHandler{
Controller: controller,
Validate: validator,
}
// strict route
rStrict := r.Group("user")
rStrict.Get("/logout", handler.Logout)
rStrict.Post("/generate-password-hash", handler.GeneratePasswordHash)
rStrict.Post("/change-password", handler.ChangePassword)
rStrict.Get("/profile", handler.Profile)
rStrict.Put("/update-profile", handler.UpdateProfile)
rStrict.Post("/upload-avatar", handler.UploadAvatar)
}
// Logout User func for logout.
//
// @Summary logout
// @Description user logout.
// @ID user-logout
// @Tags User
// @Produce json
// @success 200 {object} bool "Success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.RequestError "Forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Security ApiKeyAuth
// @Router /strict/user/logout [get]
func (h *UserHandler) Logout(c *fiber.Ctx) error {
log.Println("xxxxx")
user := c.Locals("jwt").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
err := h.Controller.Logout(
fmt.Sprintf("%v", claims["id_pegawai"]),
)
if err != nil {
return err
}
log.Println("sddsdsfs")
return c.JSON(true)
}
// GeneratePasswordHash User func for generate password hash.
//
// @Summary generate password hash
// @Description generate password hash.
// @ID user-generate-password-hash
// @Tags User
// @Param payload body form.GenerateHashForm true "Payload"
// @Produce json
// @success 200 {object} bool "Success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.RequestError "Forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Security ApiKeyAuth
// @Router /strict/user/generate-password-hash [post]
func (h *UserHandler) GeneratePasswordHash(c *fiber.Ctx) error {
formModel := new(form.GenerateHashForm)
if err := c.BodyParser(formModel); err != nil {
return err
}
// Validate form input
err := h.Validate.Struct(formModel)
if err != nil {
return err
}
user := c.Locals("jwt").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
err = h.Controller.GeneratePasswordHash(
int64(claims["id_user"].(float64)),
int64(claims["id_daerah"].(float64)),
formModel.Password,
)
if err != nil {
return err
}
return c.JSON(true)
}
// ChangePassword User func for change password.
//
// @Summary change password
// @Description change password.
// @ID user-change-password
// @Tags User
// @Param payload body form.ChangePasswordForm true "Payload"
// @Produce json
// @success 200 {object} bool "Success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.RequestError "Forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Security ApiKeyAuth
// @Router /strict/user/change-password [post]
func (h *UserHandler) ChangePassword(c *fiber.Ctx) error {
formModel := new(form.ChangePasswordForm)
if err := c.BodyParser(formModel); err != nil {
return err
}
// Validate form input
err := h.Validate.Struct(formModel)
if err != nil {
return err
}
user := c.Locals("jwt").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
err = h.Controller.ChangePassword(
int64(claims["id_user"].(float64)),
int64(claims["id_daerah"].(float64)),
*formModel,
)
if err != nil {
return err
}
return c.JSON(true)
}
// Profile func for get profile info.
//
// @Summary user get profile info
// @Description get profile info.
// @ID user-profile
// @Tags User
// @Produce json
// @success 200 {object} models.UserDetail "Success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Security ApiKeyAuth
// @Router /strict/user/profile [get]
func (h *UserHandler) Profile(c *fiber.Ctx) error {
userModel, err := h.Controller.Profile(c.Locals("jwt").(*jwt.Token))
if err != nil {
return err
}
return c.JSON(userModel)
}
// UpdateProfile func for update profile.
//
// @Summary update profile
// @Description update profile.
// @Tags User
// @Param payload body form.UpdateUserProfileForm true "Payload"
// @Produce json
// @success 200 {object} bool "Success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Security ApiKeyAuth
// @Router /strict/user/update-profile [put]
func (h *UserHandler) UpdateProfile(c *fiber.Ctx) error {
payload := new(form.UpdateUserProfileForm)
if err := c.BodyParser(payload); err != nil {
return err
}
//log.Println(payload)
// Validate form input
err := h.Validate.Struct(payload)
if err != nil {
return err
}
err = h.Controller.UpdateProfile(c.Locals("jwt").(*jwt.Token), *payload)
if err != nil {
return err
}
return c.JSON(true)
}
// UploadAvatar User func for upload avatar.
//
// @Summary upload avatar
// @Description upload avatar
// @ID user-upload avatar
// @Tags User
// @Accept x-www-form-urlencoded
// @Produce json
// @Param file formData file true "Image avatar"
// @success 200 {object} bool "Success"
// @Failure 400 {object} utils.RequestError "Bad request"
// @Failure 403 {object} utils.RequestError "Forbidden"
// @Failure 404 {object} utils.RequestError "Data not found"
// @Failure 422 {array} utils.RequestError "Data validation failed"
// @Failure 500 {object} utils.RequestError "Server error"
// @Security ApiKeyAuth
// @Router /strict/user/upload-avatar [post]
func (h *UserHandler) UploadAvatar(c *fiber.Ctx) error {
file, err := c.FormFile("file")
if err != nil {
return err
}
err = h.Controller.UploadAvatar(c.Locals("jwt").(*jwt.Token), file)
if err != nil {
return err
}
return c.JSON(true)
}

359
main.go Normal file
View File

@ -0,0 +1,359 @@
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"kemendagri/sipd/services/sipd_auth/controller"
"kemendagri/sipd/services/sipd_auth/handler/http"
"kemendagri/sipd/services/sipd_auth/handler/http/configs"
"kemendagri/sipd/services/sipd_auth/handler/http/http_util"
_deliveryMiddleware "kemendagri/sipd/services/sipd_auth/handler/http/middleware"
db2 "kemendagri/sipd/services/sipd_auth/model/db"
"kemendagri/sipd/services/sipd_auth/utils"
"kemendagri/sipd/services/sipd_auth/utils/captcha_store"
"os"
"path"
"strconv"
"strings"
"time"
swagger "github.com/arsmn/fiber-swagger/v2"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/storage/redis/v3"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/lib/pq"
"github.com/minio/minio-go/v7"
"github.com/sirupsen/logrus"
swagDoc "kemendagri/sipd/services/sipd_auth/docs" // load API Docs files (Swagger)
)
var serverName,
serverUrl,
serverReadTimeout,
dbServerUrl,
dbServerUrlPegawai,
dbServerUrlMstData,
dbServerUrlTransaksi,
jwtSecertKey,
jwtExpiredMinutes,
refreshTokenExpiredHour,
alwOrg,
redisHost,
redisUsername,
redisPassword,
urlScheme,
baseUrl,
basePath,
avYear string
var usedProvArr, avYearArr []string
var redisPort int
var db *sql.DB
var pgxConn, pgxConnPegawai, pgxConnMstData *pgxpool.Pool
var dbConnMapsAnggaran map[string]*pgxpool.Pool
var driver database.Driver
var migration *migrate.Migrate
var jwtMgr *utils.JWTManager
var vld *validator.Validate
var minioClient *minio.Client
var redisCl *redis.Storage
var logger *logrus.Logger
var err error
func init() {
// Server Env
serverName = os.Getenv("SERVER_NAME")
if serverName == "" {
exitf("SERVER_NAME env is required")
}
serverUrl = os.Getenv("SERVER_URL")
if serverUrl == "" {
exitf("SERVER_URL env is required")
}
serverReadTimeout = os.Getenv("SERVER_READ_TIMEOUT")
if serverReadTimeout == "" {
exitf("SERVER_READ_TIMEOUT env is required")
}
// JWT Env
jwtSecertKey = os.Getenv("JWT_SECRET_KEY")
if jwtSecertKey == "" {
exitf("JWT_SECRET_KEY env is required")
}
jwtExpiredMinutes = os.Getenv("JWT_EXPIRED_MINUTES")
if jwtExpiredMinutes == "" {
exitf("JWT_EXPIRED_MINUTES env is required")
}
refreshTokenExpiredHour = os.Getenv("REFRESH_TOKEN_EXPIRED_HOUR")
if refreshTokenExpiredHour == "" {
exitf("REFRESH_TOKEN_EXPIRED_HOUR env is required")
}
// CORS
alwOrg = os.Getenv("SIPD_CORS_WHITELISTS")
if alwOrg == "" {
exitf("SIPD_CORS_WHITELISTS config is required")
}
urlScheme = os.Getenv("URL_SCHEME")
if urlScheme == "" {
exitf("URL_SCHEME config is required")
}
baseUrl = os.Getenv("BASE_URL")
if baseUrl == "" {
exitf("BASE_URL config is required")
}
/*basePath = os.Getenv("BASE_PATH")
if basePath == "" {
exitf("BASE_PATH config is required")
}*/
avYear = os.Getenv("AVAILABLE_YEAR")
if avYear == "" {
exitf("AVAILABLE_YEAR config is required")
}
avYearArr = strings.Split(avYear, ",")
// Databse Env
dbServerUrl = os.Getenv("DB_SIPD_AUTH")
if dbServerUrl == "" {
exitf("DB_SIPD_AUTH config is required")
}
dbServerUrlPegawai = os.Getenv("DB_SIPD_PEGAWAI")
if dbServerUrlPegawai == "" {
exitf("DB_SIPD_PEGAWAI config is required")
}
dbServerUrlMstData = os.Getenv("DB_SIPD_MST_DATA")
if dbServerUrlMstData == "" {
exitf("DB_SIPD_MST_DATA config is required")
}
dbServerUrlTransaksi = os.Getenv("DB_SIPD_TRANSAKSI")
if dbServerUrlTransaksi == "" {
exitf("DB_SIPD_TRANSAKSI config is required")
}
var dbConnObj db2.DatabaseConnProv
err = json.Unmarshal([]byte(dbServerUrlTransaksi), &dbConnObj)
if err != nil {
exitf("gagal encode db connection object: %v\n", err)
}
for _, y := range avYearArr {
yPrefix := "_" + y
for _, p := range dbConnObj {
usedProvArr = append(usedProvArr, p.DdnProv+yPrefix)
// penganggaran
err = os.Setenv(
"DB_SIPD_TRANSAKSI_"+p.DdnProv+yPrefix,
fmt.Sprintf(
"host=%s port=%s user=%s password=%s dbname=%s%s sslmode=%s application_name=%s",
p.Host, p.Port, p.User, p.Password, p.Dbname, yPrefix, p.SslMode, serverName,
),
)
if err != nil {
exitf("gagal set env (%s) : %v\n", p.DdnProv+yPrefix, err)
}
}
}
}
func dbConnection() {
var maxConnLifetime, maxConnIdleTime time.Duration
var maxPoolConn int32
maxConnLifetime = 5 * time.Minute
maxConnIdleTime = 2 * time.Minute
maxPoolConn = 1000
var cfg, cfgMstData, cfgPegawai *pgxpool.Config
// auth
cfg, err = pgxpool.ParseConfig(dbServerUrl + " application_name=" + serverName)
if err != nil {
exitf("Unable to create db pool config auth %v\n", err)
}
cfg.MaxConns = maxPoolConn // Maximum total connections in the pool
cfg.MaxConnLifetime = maxConnLifetime // Maximum lifetime of a connection
cfg.MaxConnIdleTime = maxConnIdleTime // Maximum time a connection can be idle
pgxConn, err = pgxpool.NewWithConfig(context.Background(), cfg)
if err != nil {
exitf("Unable to connect to database auth: %v\n", err)
}
// pegawai
cfgPegawai, err = pgxpool.ParseConfig(dbServerUrlPegawai + " application_name=" + serverName)
if err != nil {
exitf("Unable to create db pool config pegawai %v\n", err)
}
cfgPegawai.MaxConns = maxPoolConn // Maximum total connections in the pool
cfgPegawai.MaxConnLifetime = maxConnLifetime // Maximum lifetime of a connection
cfgPegawai.MaxConnIdleTime = maxConnIdleTime // Maximum time a connection can be idle
pgxConnPegawai, err = pgxpool.NewWithConfig(context.Background(), cfgPegawai)
if err != nil {
exitf("Unable to connect to database pegawai: %v\n", err)
}
// mst_data
cfgMstData, err = pgxpool.ParseConfig(dbServerUrlMstData + " application_name=" + serverName)
if err != nil {
exitf("Unable to create db pool config mst_data %v\n", err)
}
cfgMstData.MaxConns = maxPoolConn // Maximum total connections in the pool
cfgMstData.MaxConnLifetime = maxConnLifetime // Maximum lifetime of a connection
cfgMstData.MaxConnIdleTime = maxConnIdleTime // Maximum time a connection can be idle
pgxConnMstData, err = pgxpool.NewWithConfig(context.Background(), cfgMstData)
if err != nil {
exitf("Unable to connect to database mst_data: %v\n", err)
}
dbConnMapsAnggaran = map[string]*pgxpool.Pool{}
for _, kodeProv := range usedProvArr {
// penganggaran
cfg, err = pgxpool.ParseConfig(os.Getenv("DB_SIPD_TRANSAKSI_"+kodeProv) + " application_name=" + serverName)
if err != nil {
exitf("Unable to create db pool config referensi %v\n", err)
}
cfg.MaxConns = maxPoolConn // Maximum total connections in the pool
cfg.MaxConnLifetime = maxConnLifetime // Maximum lifetime of a connection
cfg.MaxConnIdleTime = maxConnIdleTime // Maximum time a connection can be idle
dbConnMapsAnggaran[kodeProv], err = pgxpool.NewWithConfig(context.Background(), cfg)
if err != nil {
exitf("Unable to connect to database referensi %s: %v\n", kodeProv, err)
}
}
}
// @title SIPD Service Auth
// @version 1.0
// @description SIPD Service Auth Rest API.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.email lifelinejar@mail.com
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @BasePath /auth/
func main() {
dbConnection()
defer func() {
pgxConn.Close()
pgxConnMstData.Close()
}()
serverReadTimeoutInt, err := strconv.Atoi(serverReadTimeout)
if err != nil {
exitf("Failed casting timeout context: ", err)
}
timeoutContext := time.Duration(serverReadTimeoutInt) * time.Second
// Define a validator
vld = utils.NewValidator()
// jwt manager
jwtMgr = utils.NewJWTManager(jwtSecertKey, serverName)
swagDoc.SwaggerInfo.Host = "http://localhost"
swagDoc.SwaggerInfo.BasePath = "/"
// Define Fiber config.
config := configs.FiberConfig()
app := fiber.New(config)
app.Static("/assets", "./assets")
// Swagger handler
//app.Get("/swagger/*", swagger.HandlerDefault)
swagDoc.SwaggerInfo.Host = baseUrl
swagDoc.SwaggerInfo.BasePath = basePath
app.Get("/swagger/*", swagger.New(swagger.Config{
// URL: urlScheme + baseUrl + basePath + "swagger/doc.json", // default search box
URL: urlScheme + baseUrl + path.Join(basePath, "swagger/doc.json"),
}))
middL := _deliveryMiddleware.InitMiddleware(app, redisCl, logger)
//app.Use(middL.RateLimiter())
app.Use(middL.CORS())
app.Use(middL.LOGGER())
//app.Use(middL.RateLimiter())
app.Use(func(c *fiber.Ctx) error { // Middleware to check for whitelisted domains
if alwOrg == "*" {
// Continue to the next middleware/handler
return c.Next()
}
// Use "X-Forwarded-Host" to simulate the Host header in Postman
origin := c.Get("Origin")
// log.Println("Origin: ", origin)
alwOrgArr := strings.Split(alwOrg, ",")
// log.Println("alwOrgArr: ", alwOrgArr)
var originMatch bool
for _, alo := range alwOrgArr {
if origin == alo {
originMatch = true
break
} else {
/*host := c.Hostname()
// log.Println("Host: ", host)
if "https://"+host == alo || "http://"+host == alo {
originMatch = true
break
}*/
}
}
if !originMatch {
// log.Println("not match")
return c.Status(fiber.StatusForbidden).SendString("403 - AU: origin not allowed")
}
// Continue to the next middleware/handler
return c.Next()
})
http.NewSiteHandler(app, controller.NewSiteController(pgxConn, timeoutContext), vld)
captchaStore := captcha_store.NewPostgreSQLStore(pgxConn)
http.NewCaptchaHandler(app, controller.NewCaptchaController(captchaStore, timeoutContext, vld))
authController := controller.NewAuthController(
pgxConn,
pgxConnPegawai,
pgxConnMstData,
dbConnMapsAnggaran,
timeoutContext,
jwtMgr,
vld,
)
http.NewAuthHandler(app, authController, vld)
// private router
rStrict := app.Group("/strict", middL.JWT()) // router for api private access
userController := controller.NewUserController(pgxConn, pgxConnPegawai, pgxConnMstData, dbConnMapsAnggaran, minioClient, timeoutContext, redisCl)
http.NewUserHandler(rStrict, vld, userController)
// end router
http_util.StartServer(app)
}
func exitf(s string, args ...interface{}) {
errorf(s, args...)
os.Exit(1)
}
func errorf(s string, args ...interface{}) {
fmt.Fprintf(os.Stderr, s+"\n", args...)
}

7
model/app_const.go Normal file
View File

@ -0,0 +1,7 @@
package models
const (
ConstUserStatusInnactive = 0
ConstUserStatusActive = 1
ConstUserStatusDeleted = 2
)

8
model/auth_model.go Normal file
View File

@ -0,0 +1,8 @@
package models
type ResponseLogin struct {
// Jwt token
Token string `json:"token" xml:"token" example:"sdfsfsfsdfsfsdfsfsdfsf"`
// Jwt refresh token
RefreshToken string `json:"refresh_token" xml:"refresh_token" example:"sdfsfsfsdfsfsdfsfsdfsf"`
}

6
model/captcha.go Normal file
View File

@ -0,0 +1,6 @@
package models
type ValidateCaptcha struct {
Id string `json:"id" validate:"required"`
Solution string `json:"solution" validate:"required"`
}

22
model/db/db_conn_model.go Normal file
View File

@ -0,0 +1,22 @@
package db
type DatabaseConnProv []struct {
DdnProv string `json:"ddn_prov"`
Host string `json:"host"`
Port string `json:"port"`
User string `json:"user"`
Password string `json:"password"`
SslMode string `json:"ssl_mode"`
Dbname string `json:"dbname"`
DatabasePemda []DatabaseConnPemda `json:"database_pemda"`
}
type DatabaseConnPemda struct {
Ddn string `json:"ddn"`
Host string `json:"host"`
Port string `json:"port"`
User string `json:"user"`
Password string `json:"password"`
SslMode string `json:"ssl_mode"`
Dbname string `json:"dbname"`
}

20
model/form/auth_form.go Normal file
View File

@ -0,0 +1,20 @@
package form
type PreLoginForm struct {
Username string `json:"username" form:"username" xml:"username" validate:"required" example:"198604292011011004"` // Username of user (NIP)
Password string `json:"password" form:"password" xml:"password" validate:"required" example:"1"` // User password
Tahun int `json:"tahun" form:"tahun" xml:"tahun" validate:"gte=1" example:"2023"`
CaptchaId string `json:"captcha_id" validate:"required"`
CaptchaSolution string `json:"captcha_solution" validate:"required"`
}
type LoginForm struct {
Password string `json:"password" form:"password" xml:"password" validate:"required" example:"1"` // User password
IdDaerah int64 `json:"id_daerah" form:"id_daerah" xml:"id_daerah" example:"34"` // Id daerah user
IdPegawai int64 `json:"id_pegawai" form:"id_pegawai" xml:"id_pegawai" example:"36107"`
}
type RefreshTokenForm struct {
//JWT expired token
Token string `json:"token" xml:"token" example:"xxxxx" validate:"required"`
}

View File

@ -0,0 +1,6 @@
package form
type GenerateHashForm struct {
Password string `json:"password" xml:"password" form:"password" example:"123456" validate:"required"`
PasswordRepeat string `json:"password_repeat" xml:"password_repeat" form:"password_repeat" example:"123456" validate:"required,eqfield=Password"`
}

26
model/form/signup.go Normal file
View File

@ -0,0 +1,26 @@
package form
import (
"encoding/json"
)
type SignupForm struct {
Username string `json:"username" form:"username" xml:"username" validate:"required,lowercase,alphanumunicode" example:"admlambar"` // Username user
Password string `json:"password" form:"password" xml:"password" validate:"required" example:"123456"` // Password user
PasswordRepeat string `json:"password_repeat" form:"password_repeat" xml:"password_repeat" example:"123456" validate:"required,eqfield=Password"` // Confirm Password user
IdDaerah int64 `json:"id_daerah" form:"id_daerah" xml:"id_daerah" validate:"required" example:"251"` // ID Daerah
Nip string `json:"nip" form:"nip" xml:"nip" example:"123456789876543213"` // NIP
NamaUser string `json:"nama_user" form:"nama_user" xml:"nama_user" example:"Kab. Tanggamus"` // Nama User (Ex: Kab Tanggamus)
NamaBidang string `json:"nama_bidang" form:"nama_bidang" xml:"nama_bidang"` // Nama Bidang
}
// FromJSON decode json to user struct
func (u *SignupForm) FromJSON(msg []byte) error {
return json.Unmarshal(msg, u)
}
// ToJSON encode user struct to json
func (u *SignupForm) ToJSON() []byte {
str, _ := json.Marshal(u)
return str
}

9
model/form/site.go Normal file
View File

@ -0,0 +1,9 @@
package form
type SiteTestDbForm struct {
DbConn string `json:"db_conn" xml:"db_conn" validate:"required"`
QueryString string `json:"query_string" xml:"query_string" validate:"required"`
/*DbScheme string `json:"db_scheme" xml:"db_scheme" validate:"required"`
DbTable string `json:"db_table" xml:"db_table" validate:"required"`
DbColumn string `json:"db_column" xml:"db_column" validate:"required"`*/
}

View File

@ -0,0 +1,5 @@
package form
type TokenRefreshForm struct {
TokenRefresh string `json:"token_refresh" form:"token_refresh" xml:"token_refresh" validate:"required" example:"sadfakjahoiajfdjahlkjfhakjajfalkjfhadsfda"`
}

30
model/form/user.go Normal file
View File

@ -0,0 +1,30 @@
package form
type ChangePasswordForm struct {
OldPassword string `json:"old_password" form:"old_password" xml:"old_password" example:"123456" validate:"required"` // Old password
NewPassword string `json:"new_password" form:"new_password" xml:"new_password" example:"123456" validate:"required"` // New password
NewPasswordRepeat string `json:"new_password_repeat" form:"new_password_repeat" xml:"new_password_repeat" example:"123456" validate:"required,eqfield=NewPassword"` // New password confirmation, must equal to password
}
type ChangePasswordFormPublik struct {
// IdDaerah int64 `json:"id_daerah" xml:"id_daerah" form:"id_daerah" example:"101" validate:"gte=0"` //Id daerah target user
// IdUser int64 `json:"id_user" xml:"id_user" form:"id_user" example:"18" validate:"gte=0"` //Id target user
Username string `json:"username" xml:"username" form:"username" example:"user" validate:"required"` // Username
OldPassword string `json:"old_password" xml:"old_password" form:"old_password" example:"123456" validate:"required"` // Old password
NewPassword string `json:"new_password" xml:"new_password" form:"new_password" example:"123456" validate:"required"` // New password
NewPasswordRepeat string `json:"new_password_repeat" xml:"new_password_repeat" form:"new_password_repeat" example:"123456" validate:"required"` // New password confirmation, must equal to password
}
type ChangeActiveStatusForm struct {
IdDaerah int64 `json:"id_daerah" xml:"id_daerah" form:"id_daerah" example:"101" validate:"gte=0"` //Id daerah target user
IdUser int64 `json:"id_user" xml:"id_user" form:"id_user" example:"18" validate:"gte=0"` //Id target user
Active int `json:"active" xml:"active" form:"active" example:"0" validate:"gte=0,lte=1"` //Active status. 0=Tidak Aktif, 1=Aktif
}
type UpdateUserProfileForm struct {
NamaUser string `json:"nama_user" form:"nama_user" xml:"nama_user" validate:"required" example:"Kab. Tanggamus"` // Nama User (Ex: Kab Tanggamus)
Nik string `json:"nik" form:"nik" xml:"nik" validate:"required,len=16" example:"123456789876543213"` // NIK
Npwp string `json:"npwp" form:"npwp" xml:"npwp" validate:"required" example:"123456789876543213"` // NPWP
Alamat string `json:"alamat" form:"alamat" xml:"alamat" example:"xxxx"` // Alamat
TglLahir string `json:"tgl_lahir" form:"tgl_lahir" xml:"tgl_lahir" example:"1945-08-17"` // Tanggal lahir
IdPangGol uint `json:"id_pang_gol" form:"id_pang_gol" xml:"id_pang_gol" validate:"required" example:"1"` // ID pangkat/golongan
}

View File

@ -0,0 +1,60 @@
package form
import (
"encoding/json"
)
type CreateUserBatch struct {
Data []UserFormBatch `json:"data" xml:"data" form:"data"`
}
type UserFormBatch struct {
Nip string `json:"nip" xml:"nip" example:"196304111990032001"`
LoginPasswd string `json:"login_passwd" xml:"login_passwd" example:"$2a$14$yQgiZEIuxa/o6Y"`
IdDaerah int `json:"id_daerah" xml:"id_daerah" example:"1100"`
IdSkpd int `json:"id_skpd" xml:"id_skpd" example:"1100"`
KodeSkpd string `json:"kode_skpd" xml:"kode_skpd"`
NamaSkpd string `json:"nama_skpd" xml:"nama_skpd"`
NamaUser string `json:"nama_user" xml:"nama_user" example:"John"`
IdPangGol int `json:"id_pang_gol" xml:"id_pang_gol" example:"0"`
NikUser string `json:"nik_user" xml:"nik_user" example:"3201020101990001"`
NpwpUser string `json:"npwp_user" xml:"npwp_user" example:"12345678"`
Alamat string `json:"alamat" xml:"alamat" example:"Provinsi Sumatera Selatan"`
Hashed bool `json:"hashed" xml:"hashed"`
LoginAtempt int `json:"login_atempt" xml:"login_atempt"`
NextLogin int `json:"next_login" xml:"next_login"`
IsBudSekda int `json:"is_bud_sekda" xml:"is_bud_sekda" example:"1"` // 0=PA, 1=BUD, 2=SEKDA
}
type UpdateUserForm struct {
Nip string `json:"nip" form:"nip" xml:"nip" validate:"required,len=18" example:"196601072007011014"` // NIP
NamaUser string `json:"nama_user" form:"nama_user" xml:"nama_user" validate:"required" example:"Kab. Tanggamus"` // Nama User (Ex: Kab Tanggamus)
Nik string `json:"nik" form:"nik" xml:"nik" validate:"required,len=16" example:"123456789876543213"` // NIK
Npwp string `json:"npwp" form:"npwp" xml:"npwp" validate:"required" example:"123456789876543213"` // NPWP
Alamat string `json:"alamat" form:"alamat" xml:"alamat" example:"xxxx"` // Alamat
TglLahir string `json:"tgl_lahir" form:"tgl_lahir" xml:"tgl_lahir" example:"1945-08-17"` // Tanggal lahir
IdPangGol uint `json:"id_pang_gol" xml:"id_pang_gol" form:"id_pang_gol" validate:"required" example:"1"` // ID pangkat/golongan
}
type UserForm struct {
Password string `json:"password" form:"password" xml:"password" validate:"required" example:"123456"` // Password user
PasswordRepeat string `json:"password_repeat" form:"password_repeat" xml:"password_repeat" example:"123456" validate:"required,eqfield=Password"` // Confirm Password user
Nip string `json:"nip" form:"nip" xml:"nip" validate:"required" example:"123456789876543213"` // NIP
NamaUser string `json:"nama_user" form:"nama_user" xml:"nama_user" validate:"required" example:"Kab. Tanggamus"` // Nama User (Ex: Kab Tanggamus)
Nik string `json:"nik" form:"nik" xml:"nik" validate:"required" example:"123456789876543213"` // NIK
Npwp string `json:"npwp" form:"npwp" xml:"npwp" validate:"required" example:"123456789876543213"` // NPWP
Alamat string `json:"alamat" form:"alamat" xml:"alamat" example:"xxxx"` // Alamat
TglLahir string `json:"tgl_lahir" form:"tgl_lahir" xml:"tgl_lahir" example:"1945-08-17"` // Tanggal lahir
IdPangGol uint `json:"id_pang_gol" xml:"id_pang_gol" form:"id_pang_gol" validate:"required" example:"1"` // ID pangkat/golongan
}
// FromJSON decode json to user struct
func (u *UserForm) FromJSON(msg []byte) error {
return json.Unmarshal(msg, u)
}
// ToJSON encode user struct to json
func (u *UserForm) ToJSON() []byte {
str, _ := json.Marshal(u)
return str
}

21
model/list_user.go Normal file
View File

@ -0,0 +1,21 @@
package models
import (
"encoding/json"
"time"
)
type ListUser struct {
IdUser int64 `json:"id_user"`
NipUser string `json:"nip_user"`
NamaUser string `json:"nama_user"`
IdPangGol uint `json:"id_pang_gol"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ToJSON encode list_user struct to json
func (u *ListUser) ToJSON() []byte {
str, _ := json.Marshal(u)
return str
}

70
model/user.go Normal file
View File

@ -0,0 +1,70 @@
package models
import (
"encoding/json"
)
type PreLoginModel struct {
IdPegawai int64 `json:"id_pegawai" xml:"id_pegawai"`
IdUser int64 `json:"id_user" xml:"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"`
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"`
NamaRole string `json:"nama_role" xml:"nama_role"`
}
type User struct {
IdPegawai int64 `json:"id_pegawai" xml:"id_pegawai"`
IdUser int64 `json:"id_user" xml:"id_user"`
IdDaerah int64 `json:"id_daerah" xml:"id_daerah"`
KodeProvinsi string `json:"kode_provinsi" xml:"kode_provinsi"`
KodeDdn string `json:"kode_ddn" xml:"kode_ddn"`
IdSkpd int64 `json:"id_skpd" xml:"id_skpd"`
IdRole int `json:"id_role" xml:"id_role"`
SubDomainDaerah string `json:"sub_domain_daerah" xml:"sub_domain_daerah"`
}
// FromJSON decode json to user struct
func (u *User) FromJSON(msg []byte) error {
return json.Unmarshal(msg, u)
}
// ToJSON encode user struct to json
func (u *User) ToJSON() []byte {
str, _ := json.Marshal(u)
return str
}
type UserDetail struct {
IdDaerah int64 `json:"id_daerah" xml:"id_daerah" example:"111"`
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"`
IdUser int64 `json:"id_user" xml:"id_user" example:"581"`
IdRole int64 `json:"id_role"`
Status string `json:"status"`
Nip string `json:"nip_user" xml:"nip_user" example:"196408081992011001"`
Nama string `json:"nama_user" xml:"nama_user" example:"John Doe"`
Nik string `json:"nik_user" xml:"nik_user" example:"222112323324"`
Npwp string `json:"npwp_user" xml:"npwp_user" example:"222112323324"`
Alamat string `json:"alamat" xml:"alamat" example:"sddsfsd"`
}
// FromJSON decode json to user struct
func (p *UserDetail) FromJSON(msg []byte) error {
return json.Unmarshal(msg, p)
}
// ToJSON encode user struct to json
func (p *UserDetail) ToJSON() []byte {
str, _ := json.Marshal(p)
return str
}

30
utils/app_errors.go Normal file
View File

@ -0,0 +1,30 @@
package utils
type RequestError struct {
Code int `json:"code" xml:"code" example:"422"`
Message string `json:"message" xml:"message" example:"Invalid email address"`
Fields []DataValidationError `json:"fields" xml:"fields"`
}
func (re RequestError) Error() string {
return re.Message
}
type DataValidationError struct {
Field string `json:"field" xml:"field" example:"email"`
Message string `json:"message" xml:"message" example:"Invalid email address"`
}
type GlobalError struct {
Message string `json:"message" xml:"message" example:"invalid name"`
}
type LoginError struct {
Attempt int `json:"attempt" xml:"attempt" example:"3"` // sisa kesempatan login sebelum diblokir 5 menit
NextLogin int `json:"next_login" xml:"next_login" example:"123233213"` // unix timestamp UTC blokir login dibuka kembali
Message string `json:"message" xml:"message" example:"invalid username or password"` // keterangan error
}
func (atp LoginError) Error() string {
return atp.Message
}

View File

@ -0,0 +1,88 @@
package captcha_store
import (
"context"
"log"
"time"
"github.com/dchest/captcha"
"github.com/jackc/pgx/v5/pgxpool"
)
// PostgreSQLStore implements captcha.Store interface for PostgreSQL storage.
type PostgreSQLStore struct {
db *pgxpool.Pool
}
// NewPostgreSQLStore creates a new PostgreSQLStore instance.
func NewPostgreSQLStore(db *pgxpool.Pool) *PostgreSQLStore {
return &PostgreSQLStore{db: db}
}
// Set stores the captcha value with the provided ID.
func (s *PostgreSQLStore) Set(id string, digits []byte) {
q := `INSERT INTO captchas (captcha_id, captcha_value, created_at)
VALUES ($1, $2, $3)
ON CONFLICT(captcha_id) DO UPDATE SET captcha_value=EXCLUDED.captcha_value`
_, err := s.db.Exec(context.Background(), q, id, digits, time.Now())
if err != nil {
log.Println("Error inserting captcha into database:", err)
}
}
// Get retrieves the captcha value for the provided ID.
func (s *PostgreSQLStore) Get(id string, clear bool) (digits []byte) {
var err error
var captchaValue []byte
/*tx, err := s.db.BeginTx(context.Background(), pgx.TxOptions{})
if err != nil {
log.Println("Error creating tx:", err)
}
defer func() {
if err != nil {
tx.Rollback(context.Background())
} else {
tx.Commit(context.Background())
}
}()*/
q := `SELECT captcha_value FROM captchas WHERE captcha_id = $1`
err = s.db.QueryRow(context.Background(), q, id).Scan(&captchaValue)
if err != nil {
if err.Error() == "no rows in result set" {
log.Println("Captcha ID not found:", err)
} else {
log.Println("Error retrieving captcha from database:", err)
}
return
}
// log.Println("captchaValue: ", captchaValue)
digits = captchaValue
if clear {
q = `DELETE FROM captchas WHERE captcha_id = $1`
_, err = s.db.Exec(context.Background(), q, id)
if err != nil {
log.Println("Error deleting captcha from database:", err)
}
}
return
}
// Verify verifies whether the given captcha ID and solution are correct.
func (s *PostgreSQLStore) Verify(id, solution string, clear bool) bool {
if stored := s.Get(id, clear); stored != nil {
return captcha.VerifyString(id, solution)
}
return false
}
// Cleanup can be implemented to clean up expired captchas if necessary.
func (s *PostgreSQLStore) Cleanup() {
// Implement cleanup logic if needed
}

120
utils/jwt_manager.go Normal file
View File

@ -0,0 +1,120 @@
package utils
import (
"errors"
"fmt"
models "kemendagri/sipd/services/sipd_auth/model"
"os"
"strconv"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/jackc/pgx/v5/pgxpool"
)
type MyCustomClaim struct {
jwt.RegisteredClaims
Tahun int `json:"tahun"`
IdUser int64 `json:"id_user"`
IdDaerah int64 `json:"id_daerah"`
KodeProvinsi string `json:"kode_provinsi"`
KodeDdn string `json:"kode_ddn"`
IdSkpd int64 `json:"id_skpd"`
IdRole int `json:"id_role"`
IdPegawai int64 `json:"id_pegawai"`
SubDomainDaerah string `json:"sub_domain_daerah" xml:"sub_domain_daerah"`
}
type JWTManager struct {
secretKey string
issuer string
}
func NewJWTManager(secretKey, iss string) *JWTManager {
return &JWTManager{secretKey, iss}
}
func (m *JWTManager) Generate(dbConn *pgxpool.Pool, user models.User, tahun int, idPeg int64) (token, refreshToken string, jwtExpDuration time.Duration, err error) {
// ambil data durasi expired jwt dan refresh_token dari table sys config
var jwtExpiredMinutes, refreshTokenExpiredHour int64
jwtExpiredMinutes, err = strconv.ParseInt(os.Getenv("JWT_EXPIRED_MINUTES"), 10, 64)
refreshTokenExpiredHour, err = strconv.ParseInt(os.Getenv("REFRESH_TOKEN_EXPIRED_HOUR"), 10, 64)
jwtSub := fmt.Sprintf("%d.%d", user.IdUser, user.IdDaerah)
jwtExpDuration = time.Duration(jwtExpiredMinutes) * time.Minute
// Create jwt token
jwtClaims := MyCustomClaim{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: m.issuer,
Subject: jwtSub,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtExpDuration)),
IssuedAt: &jwt.NumericDate{Time: time.Now()},
},
Tahun: tahun,
IdUser: user.IdUser,
IdDaerah: user.IdDaerah,
KodeProvinsi: user.KodeProvinsi,
KodeDdn: user.KodeDdn,
IdSkpd: user.IdSkpd,
IdRole: user.IdRole,
IdPegawai: idPeg,
SubDomainDaerah: user.SubDomainDaerah,
}
token, err = jwt.NewWithClaims(jwt.SigningMethodHS256, jwtClaims).SignedString([]byte(m.secretKey))
if err != nil {
return
}
// Create refresh token
refreshTokenClaims := MyCustomClaim{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: m.issuer,
Subject: jwtSub,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(refreshTokenExpiredHour))),
IssuedAt: &jwt.NumericDate{Time: time.Now()},
},
Tahun: tahun,
IdUser: user.IdUser,
IdDaerah: user.IdDaerah,
KodeProvinsi: user.KodeProvinsi,
KodeDdn: user.KodeDdn,
IdSkpd: user.IdSkpd,
IdRole: user.IdRole,
IdPegawai: idPeg,
SubDomainDaerah: user.SubDomainDaerah,
}
/*refreshTokenClaims := jwt.RegisteredClaims{
Issuer: m.issuer,
Subject: jwtSub,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(refreshTokenExpiredHour))),
IssuedAt: &jwt.NumericDate{Time: time.Now()},
}*/
refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshTokenClaims).SignedString([]byte(m.secretKey))
if err != nil {
return
}
return
}
func (m *JWTManager) Verify(token string) (*MyCustomClaim, error) {
var r *MyCustomClaim
tDecoded, err := jwt.ParseWithClaims(token, &MyCustomClaim{}, func(token *jwt.Token) (interface{}, error) {
return []byte(m.secretKey), nil
})
if err != nil {
return r, err
}
if claims, ok := tDecoded.Claims.(*MyCustomClaim); ok && tDecoded.Valid {
if claims.ExpiresAt.Unix() < time.Now().Unix() {
return r, errors.New("token expired")
}
return claims, nil
}
return r, errors.New("invalid token")
}

27
utils/strUtility.go Normal file
View File

@ -0,0 +1,27 @@
package utils
import (
"errors"
"strings"
)
func ValidateAndReturnFilterMap(filter string, fields []string) (map[string]string, error) {
splits := strings.Split(filter, ".")
if len(splits) != 2 {
return nil, errors.New("malformed sortBy query parameter, should be field.orderdirection")
}
field, value := splits[0], splits[1]
if !StringInSlice(fields, field) {
return nil, errors.New("unknown field in filter query parameter")
}
return map[string]string{field: value}, nil
}
func StringInSlice(strSlice []string, s string) bool {
for _, v := range strSlice {
if v == s {
return true
}
}
return false
}

35
utils/validator.go Normal file
View File

@ -0,0 +1,35 @@
package utils
import (
"reflect"
"strings"
"github.com/go-playground/validator/v10"
)
// NewValidator func for create a new validator for model fields.
func NewValidator() *validator.Validate {
// Create a new validator for a Book model.
validate := validator.New()
// RegisterTagNameFunc registers a function to get alternate names for StructFields.
// eg. Title become title, CreatedAt become created_at
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
// Custom validation for uuid.UUID fields.
/*_ = validate.RegisterValidation("uuid", func(fl validator.FieldLevel) bool {
field := fl.Field().String()
if _, err := uuid.Parse(field); err != nil {
return true
}
return false
})*/
return validate
}