From e96b29f2807fe2ca3f3805ee49aafe7dbd0b1fa6 Mon Sep 17 00:00:00 2001 From: ogi Date: Tue, 16 Sep 2025 08:32:11 +0700 Subject: [PATCH] first commit --- .dockerignore | 5 + .gitignore | 23 + .gitlab-ci.yml | 61 + Dockerfile | 13 + Makefile.example | 40 + README.md | 40 + controller/auth.go | 730 +++++++++++ controller/captcha.go | 143 +++ controller/site.go | 27 + controller/user.go | 459 +++++++ docs/docs.go | 1428 +++++++++++++++++++++ docs/swagger.json | 1402 ++++++++++++++++++++ docs/swagger.yaml | 953 ++++++++++++++ go.mod | 74 ++ go.sum | 373 ++++++ handler/http/auth.go | 189 +++ handler/http/captcha.go | 100 ++ handler/http/configs/fiber_config.go | 77 ++ handler/http/http_util/json_result.go | 21 + handler/http/http_util/server.go | 44 + handler/http/http_util/token_generator.go | 50 + handler/http/middleware/middleware.go | 207 +++ handler/http/site.go | 48 + handler/http/user.go | 249 ++++ main.go | 359 ++++++ model/app_const.go | 7 + model/auth_model.go | 8 + model/captcha.go | 6 + model/db/db_conn_model.go | 22 + model/form/auth_form.go | 20 + model/form/generateHash.go | 6 + model/form/signup.go | 26 + model/form/site.go | 9 + model/form/token_refresh.go | 5 + model/form/user.go | 30 + model/form/user_manager.go | 60 + model/list_user.go | 21 + model/user.go | 70 + utils/app_errors.go | 30 + utils/captcha_store/postgresql_store.go | 88 ++ utils/jwt_manager.go | 120 ++ utils/strUtility.go | 27 + utils/validator.go | 35 + 43 files changed, 7705 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 Dockerfile create mode 100644 Makefile.example create mode 100644 README.md create mode 100644 controller/auth.go create mode 100644 controller/captcha.go create mode 100644 controller/site.go create mode 100644 controller/user.go create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handler/http/auth.go create mode 100644 handler/http/captcha.go create mode 100644 handler/http/configs/fiber_config.go create mode 100644 handler/http/http_util/json_result.go create mode 100644 handler/http/http_util/server.go create mode 100644 handler/http/http_util/token_generator.go create mode 100644 handler/http/middleware/middleware.go create mode 100644 handler/http/site.go create mode 100644 handler/http/user.go create mode 100644 main.go create mode 100644 model/app_const.go create mode 100644 model/auth_model.go create mode 100644 model/captcha.go create mode 100644 model/db/db_conn_model.go create mode 100644 model/form/auth_form.go create mode 100644 model/form/generateHash.go create mode 100644 model/form/signup.go create mode 100644 model/form/site.go create mode 100644 model/form/token_refresh.go create mode 100644 model/form/user.go create mode 100644 model/form/user_manager.go create mode 100644 model/list_user.go create mode 100644 model/user.go create mode 100644 utils/app_errors.go create mode 100644 utils/captcha_store/postgresql_store.go create mode 100644 utils/jwt_manager.go create mode 100644 utils/strUtility.go create mode 100644 utils/validator.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c99d3b2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +README.md +.git +.idea +Makefile +Makefile.example \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..881958f --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e9a83a4 --- /dev/null +++ b/.gitlab-ci.yml @@ -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"' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6e23d53 --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/Makefile.example b/Makefile.example new file mode 100644 index 0000000..13c9f41 --- /dev/null +++ b/Makefile.example @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ea8de0 --- /dev/null +++ b/README.md @@ -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 +``` \ No newline at end of file diff --git a/controller/auth.go b/controller/auth.go new file mode 100644 index 0000000..3d92745 --- /dev/null +++ b/controller/auth.go @@ -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) +} diff --git a/controller/captcha.go b/controller/captcha.go new file mode 100644 index 0000000..1b84083 --- /dev/null +++ b/controller/captcha.go @@ -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 +} diff --git a/controller/site.go b/controller/site.go new file mode 100644 index 0000000..34db738 --- /dev/null +++ b/controller/site.go @@ -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 +} diff --git a/controller/user.go b/controller/user.go new file mode 100644 index 0000000..d9af4d0 --- /dev/null +++ b/controller/user.go @@ -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) +} diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..a0fd20c --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,1428 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "email": "lifelinejar@mail.com" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/auth/amankan-kata-sandi": { + "post": { + "description": "User melakukan Amankan Kata Sandi.", + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Amankan Kata Sandi", + "operationId": "auth-amankan-kata-sandi", + "parameters": [ + { + "description": "Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.ChangePasswordFormPublik" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/auth/login": { + "post": { + "description": "Login to get JWT token and refresh token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "user login", + "operationId": "auth-login", + "parameters": [ + { + "description": "Login payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.LoginForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/auth/pre-login": { + "post": { + "description": "Login to get JWT token and refresh token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "user login", + "operationId": "auth-pre-login", + "parameters": [ + { + "description": "Pre login payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.PreLoginForm" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.PreLoginModel" + } + } + }, + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Register user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "user register", + "operationId": "auth-register", + "parameters": [ + { + "description": "Register payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.SignupForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/auth/token-refresh/{token}": { + "post": { + "description": "Refresh token to get new valid JWT token and refresh token", + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Refresh Token", + "operationId": "auth-refresh-token", + "parameters": [ + { + "description": "Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.RefreshTokenForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/captcha/new": { + "get": { + "description": "generate new captcha.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Captcha" + ], + "summary": "generate new captcha", + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/captcha/reload/{id}": { + "get": { + "description": "reload captcha.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Captcha" + ], + "summary": "reload captcha", + "parameters": [ + { + "type": "string", + "description": "captcha ID", + "name": "id", + "in": "path", + "required": true + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/captcha/validate": { + "post": { + "description": "validate captcha.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Captcha" + ], + "summary": "validate captcha", + "parameters": [ + { + "description": "payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ValidateCaptcha" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/site/index": { + "get": { + "description": "index page.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "index", + "operationId": "index", + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/change-password": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "change password.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "change password", + "operationId": "user-change-password", + "parameters": [ + { + "description": "Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.ChangePasswordForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/generate-password-hash": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "generate password hash.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "generate password hash", + "operationId": "user-generate-password-hash", + "parameters": [ + { + "description": "Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.GenerateHashForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/logout": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "user logout.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "logout", + "operationId": "user-logout", + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/profile": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get profile info.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "user get profile info", + "operationId": "user-profile", + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/update-profile": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update profile.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "update profile", + "parameters": [ + { + "description": "Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.UpdateUserProfileForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/upload-avatar": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "upload avatar", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "upload avatar", + "operationId": "user-upload avatar", + "parameters": [ + { + "type": "file", + "description": "Image avatar", + "name": "file", + "in": "formData", + "required": true + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + } + }, + "definitions": { + "form.ChangePasswordForm": { + "type": "object", + "required": [ + "new_password", + "new_password_repeat", + "old_password" + ], + "properties": { + "new_password": { + "description": "New password", + "type": "string", + "example": "123456" + }, + "new_password_repeat": { + "description": "New password confirmation, must equal to password", + "type": "string", + "example": "123456" + }, + "old_password": { + "description": "Old password", + "type": "string", + "example": "123456" + } + } + }, + "form.ChangePasswordFormPublik": { + "type": "object", + "required": [ + "new_password", + "new_password_repeat", + "old_password", + "username" + ], + "properties": { + "new_password": { + "description": "New password", + "type": "string", + "example": "123456" + }, + "new_password_repeat": { + "description": "New password confirmation, must equal to password", + "type": "string", + "example": "123456" + }, + "old_password": { + "description": "Old password", + "type": "string", + "example": "123456" + }, + "username": { + "description": "IdDaerah int64 ` + "`" + `json:\"id_daerah\" xml:\"id_daerah\" form:\"id_daerah\" example:\"101\" validate:\"gte=0\"` + "`" + ` //Id daerah target user\nIdUser int64 ` + "`" + `json:\"id_user\" xml:\"id_user\" form:\"id_user\" example:\"18\" validate:\"gte=0\"` + "`" + ` //Id target user", + "type": "string", + "example": "user" + } + } + }, + "form.GenerateHashForm": { + "type": "object", + "required": [ + "password", + "password_repeat" + ], + "properties": { + "password": { + "type": "string", + "example": "123456" + }, + "password_repeat": { + "type": "string", + "example": "123456" + } + } + }, + "form.LoginForm": { + "type": "object", + "required": [ + "password" + ], + "properties": { + "id_daerah": { + "description": "Id daerah user", + "type": "integer", + "example": 34 + }, + "id_pegawai": { + "type": "integer", + "example": 36107 + }, + "password": { + "description": "User password", + "type": "string", + "example": "1" + } + } + }, + "form.PreLoginForm": { + "type": "object", + "required": [ + "captcha_id", + "captcha_solution", + "password", + "username" + ], + "properties": { + "captcha_id": { + "type": "string" + }, + "captcha_solution": { + "type": "string" + }, + "password": { + "description": "User password", + "type": "string", + "example": "1" + }, + "tahun": { + "type": "integer", + "minimum": 1, + "example": 2023 + }, + "username": { + "description": "Username of user (NIP)", + "type": "string", + "example": "198604292011011004" + } + } + }, + "form.RefreshTokenForm": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "description": "JWT expired token", + "type": "string", + "example": "xxxxx" + } + } + }, + "form.SignupForm": { + "type": "object", + "required": [ + "id_daerah", + "password", + "password_repeat", + "username" + ], + "properties": { + "id_daerah": { + "description": "ID Daerah", + "type": "integer", + "example": 251 + }, + "nama_bidang": { + "description": "Nama Bidang", + "type": "string" + }, + "nama_user": { + "description": "Nama User (Ex: Kab Tanggamus)", + "type": "string", + "example": "Kab. Tanggamus" + }, + "nip": { + "description": "NIP", + "type": "string", + "example": "123456789876543213" + }, + "password": { + "description": "Password user", + "type": "string", + "example": "123456" + }, + "password_repeat": { + "description": "Confirm Password user", + "type": "string", + "example": "123456" + }, + "username": { + "description": "Username user", + "type": "string", + "example": "admlambar" + } + } + }, + "form.UpdateUserProfileForm": { + "type": "object", + "required": [ + "id_pang_gol", + "nama_user", + "nik", + "npwp" + ], + "properties": { + "alamat": { + "description": "Alamat", + "type": "string", + "example": "xxxx" + }, + "id_pang_gol": { + "description": "ID pangkat/golongan", + "type": "integer", + "example": 1 + }, + "nama_user": { + "description": "Nama User (Ex: Kab Tanggamus)", + "type": "string", + "example": "Kab. Tanggamus" + }, + "nik": { + "description": "NIK", + "type": "string", + "example": "123456789876543213" + }, + "npwp": { + "description": "NPWP", + "type": "string", + "example": "123456789876543213" + }, + "tgl_lahir": { + "description": "Tanggal lahir", + "type": "string", + "example": "1945-08-17" + } + } + }, + "http_util.JSONResultLogin": { + "type": "object", + "properties": { + "is_default_password": { + "type": "boolean", + "example": false + }, + "refresh_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYxNjgyMjksImlk._aYI7pV2c9SU9VOp3RY_mxtFenYFQuKPJtVfk" + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYwODU0MjksImlkIjoyLCJwaG9uZSI6Iis2MjgxMjM0NTYyIiwidXNlcm5hbWUiOi.dl_ojy9ojLnWqpW589YltLPV61TCsON-3yQ2" + } + } + }, + "models.PreLoginModel": { + "type": "object", + "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": { + "type": "string", + "example": "Kota Bandar Lampung" + }, + "nama_role": { + "type": "string" + }, + "nama_skpd": { + "type": "string" + }, + "nama_user": { + "type": "string", + "example": "John Doe" + }, + "nip_user": { + "type": "string", + "example": "196408081992011001" + } + } + }, + "models.ResponseLogin": { + "type": "object", + "properties": { + "refresh_token": { + "description": "Jwt refresh token", + "type": "string", + "example": "sdfsfsfsdfsfsdfsfsdfsf" + }, + "token": { + "description": "Jwt token", + "type": "string", + "example": "sdfsfsfsdfsfsdfsfsdfsf" + } + } + }, + "models.UserDetail": { + "type": "object", + "properties": { + "alamat": { + "type": "string", + "example": "sddsfsd" + }, + "id_daerah": { + "type": "integer", + "example": 111 + }, + "id_role": { + "type": "integer" + }, + "id_skpd_lama": { + "type": "integer" + }, + "id_unik_skpd": { + "type": "string" + }, + "id_user": { + "type": "integer", + "example": 581 + }, + "kode_skpd": { + "type": "string" + }, + "nama_daerah": { + "type": "string", + "example": "Kota Bandar Lampung" + }, + "nama_skpd": { + "type": "string" + }, + "nama_user": { + "type": "string", + "example": "John Doe" + }, + "nik_user": { + "type": "string", + "example": "222112323324" + }, + "nip_user": { + "type": "string", + "example": "196408081992011001" + }, + "npwp_user": { + "type": "string", + "example": "222112323324" + }, + "status": { + "type": "string" + } + } + }, + "models.ValidateCaptcha": { + "type": "object", + "required": [ + "id", + "solution" + ], + "properties": { + "id": { + "type": "string" + }, + "solution": { + "type": "string" + } + } + }, + "utils.DataValidationError": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "email" + }, + "message": { + "type": "string", + "example": "Invalid email address" + } + } + }, + "utils.LoginError": { + "type": "object", + "properties": { + "attempt": { + "description": "sisa kesempatan login sebelum diblokir 5 menit", + "type": "integer", + "example": 3 + }, + "message": { + "description": "keterangan error", + "type": "string", + "example": "invalid username or password" + }, + "next_login": { + "description": "unix timestamp UTC blokir login dibuka kembali", + "type": "integer", + "example": 123233213 + } + } + }, + "utils.RequestError": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "example": 422 + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.DataValidationError" + } + }, + "message": { + "type": "string", + "example": "Invalid email address" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "SIPD Service Auth", + Description: "SIPD Service Auth Rest API.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..168c295 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,1402 @@ +{ + "swagger": "2.0", + "info": { + "description": "SIPD Service Auth Rest API.", + "title": "SIPD Service Auth", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "email": "lifelinejar@mail.com" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "paths": { + "/auth/amankan-kata-sandi": { + "post": { + "description": "User melakukan Amankan Kata Sandi.", + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Amankan Kata Sandi", + "operationId": "auth-amankan-kata-sandi", + "parameters": [ + { + "description": "Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.ChangePasswordFormPublik" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/auth/login": { + "post": { + "description": "Login to get JWT token and refresh token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "user login", + "operationId": "auth-login", + "parameters": [ + { + "description": "Login payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.LoginForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/auth/pre-login": { + "post": { + "description": "Login to get JWT token and refresh token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "user login", + "operationId": "auth-pre-login", + "parameters": [ + { + "description": "Pre login payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.PreLoginForm" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.PreLoginModel" + } + } + }, + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Register user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "user register", + "operationId": "auth-register", + "parameters": [ + { + "description": "Register payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.SignupForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/auth/token-refresh/{token}": { + "post": { + "description": "Refresh token to get new valid JWT token and refresh token", + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Refresh Token", + "operationId": "auth-refresh-token", + "parameters": [ + { + "description": "Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.RefreshTokenForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/captcha/new": { + "get": { + "description": "generate new captcha.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Captcha" + ], + "summary": "generate new captcha", + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/captcha/reload/{id}": { + "get": { + "description": "reload captcha.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Captcha" + ], + "summary": "reload captcha", + "parameters": [ + { + "type": "string", + "description": "captcha ID", + "name": "id", + "in": "path", + "required": true + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/captcha/validate": { + "post": { + "description": "validate captcha.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Captcha" + ], + "summary": "validate captcha", + "parameters": [ + { + "description": "payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ValidateCaptcha" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/site/index": { + "get": { + "description": "index page.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "index", + "operationId": "index", + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/change-password": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "change password.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "change password", + "operationId": "user-change-password", + "parameters": [ + { + "description": "Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.ChangePasswordForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/generate-password-hash": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "generate password hash.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "generate password hash", + "operationId": "user-generate-password-hash", + "parameters": [ + { + "description": "Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.GenerateHashForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/logout": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "user logout.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "logout", + "operationId": "user-logout", + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/profile": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get profile info.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "user get profile info", + "operationId": "user-profile", + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/update-profile": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update profile.", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "update profile", + "parameters": [ + { + "description": "Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.UpdateUserProfileForm" + } + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + }, + "/strict/user/upload-avatar": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "upload avatar", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "upload avatar", + "operationId": "user-upload avatar", + "parameters": [ + { + "type": "file", + "description": "Image avatar", + "name": "file", + "in": "formData", + "required": true + } + ], + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RequestError" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "$ref": "#/definitions/utils.RequestError" + } + } + } + } + } + }, + "definitions": { + "form.ChangePasswordForm": { + "type": "object", + "required": [ + "new_password", + "new_password_repeat", + "old_password" + ], + "properties": { + "new_password": { + "description": "New password", + "type": "string", + "example": "123456" + }, + "new_password_repeat": { + "description": "New password confirmation, must equal to password", + "type": "string", + "example": "123456" + }, + "old_password": { + "description": "Old password", + "type": "string", + "example": "123456" + } + } + }, + "form.ChangePasswordFormPublik": { + "type": "object", + "required": [ + "new_password", + "new_password_repeat", + "old_password", + "username" + ], + "properties": { + "new_password": { + "description": "New password", + "type": "string", + "example": "123456" + }, + "new_password_repeat": { + "description": "New password confirmation, must equal to password", + "type": "string", + "example": "123456" + }, + "old_password": { + "description": "Old password", + "type": "string", + "example": "123456" + }, + "username": { + "description": "IdDaerah int64 `json:\"id_daerah\" xml:\"id_daerah\" form:\"id_daerah\" example:\"101\" validate:\"gte=0\"` //Id daerah target user\nIdUser int64 `json:\"id_user\" xml:\"id_user\" form:\"id_user\" example:\"18\" validate:\"gte=0\"` //Id target user", + "type": "string", + "example": "user" + } + } + }, + "form.GenerateHashForm": { + "type": "object", + "required": [ + "password", + "password_repeat" + ], + "properties": { + "password": { + "type": "string", + "example": "123456" + }, + "password_repeat": { + "type": "string", + "example": "123456" + } + } + }, + "form.LoginForm": { + "type": "object", + "required": [ + "password" + ], + "properties": { + "id_daerah": { + "description": "Id daerah user", + "type": "integer", + "example": 34 + }, + "id_pegawai": { + "type": "integer", + "example": 36107 + }, + "password": { + "description": "User password", + "type": "string", + "example": "1" + } + } + }, + "form.PreLoginForm": { + "type": "object", + "required": [ + "captcha_id", + "captcha_solution", + "password", + "username" + ], + "properties": { + "captcha_id": { + "type": "string" + }, + "captcha_solution": { + "type": "string" + }, + "password": { + "description": "User password", + "type": "string", + "example": "1" + }, + "tahun": { + "type": "integer", + "minimum": 1, + "example": 2023 + }, + "username": { + "description": "Username of user (NIP)", + "type": "string", + "example": "198604292011011004" + } + } + }, + "form.RefreshTokenForm": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "description": "JWT expired token", + "type": "string", + "example": "xxxxx" + } + } + }, + "form.SignupForm": { + "type": "object", + "required": [ + "id_daerah", + "password", + "password_repeat", + "username" + ], + "properties": { + "id_daerah": { + "description": "ID Daerah", + "type": "integer", + "example": 251 + }, + "nama_bidang": { + "description": "Nama Bidang", + "type": "string" + }, + "nama_user": { + "description": "Nama User (Ex: Kab Tanggamus)", + "type": "string", + "example": "Kab. Tanggamus" + }, + "nip": { + "description": "NIP", + "type": "string", + "example": "123456789876543213" + }, + "password": { + "description": "Password user", + "type": "string", + "example": "123456" + }, + "password_repeat": { + "description": "Confirm Password user", + "type": "string", + "example": "123456" + }, + "username": { + "description": "Username user", + "type": "string", + "example": "admlambar" + } + } + }, + "form.UpdateUserProfileForm": { + "type": "object", + "required": [ + "id_pang_gol", + "nama_user", + "nik", + "npwp" + ], + "properties": { + "alamat": { + "description": "Alamat", + "type": "string", + "example": "xxxx" + }, + "id_pang_gol": { + "description": "ID pangkat/golongan", + "type": "integer", + "example": 1 + }, + "nama_user": { + "description": "Nama User (Ex: Kab Tanggamus)", + "type": "string", + "example": "Kab. Tanggamus" + }, + "nik": { + "description": "NIK", + "type": "string", + "example": "123456789876543213" + }, + "npwp": { + "description": "NPWP", + "type": "string", + "example": "123456789876543213" + }, + "tgl_lahir": { + "description": "Tanggal lahir", + "type": "string", + "example": "1945-08-17" + } + } + }, + "http_util.JSONResultLogin": { + "type": "object", + "properties": { + "is_default_password": { + "type": "boolean", + "example": false + }, + "refresh_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYxNjgyMjksImlk._aYI7pV2c9SU9VOp3RY_mxtFenYFQuKPJtVfk" + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzYwODU0MjksImlkIjoyLCJwaG9uZSI6Iis2MjgxMjM0NTYyIiwidXNlcm5hbWUiOi.dl_ojy9ojLnWqpW589YltLPV61TCsON-3yQ2" + } + } + }, + "models.PreLoginModel": { + "type": "object", + "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": { + "type": "string", + "example": "Kota Bandar Lampung" + }, + "nama_role": { + "type": "string" + }, + "nama_skpd": { + "type": "string" + }, + "nama_user": { + "type": "string", + "example": "John Doe" + }, + "nip_user": { + "type": "string", + "example": "196408081992011001" + } + } + }, + "models.ResponseLogin": { + "type": "object", + "properties": { + "refresh_token": { + "description": "Jwt refresh token", + "type": "string", + "example": "sdfsfsfsdfsfsdfsfsdfsf" + }, + "token": { + "description": "Jwt token", + "type": "string", + "example": "sdfsfsfsdfsfsdfsfsdfsf" + } + } + }, + "models.UserDetail": { + "type": "object", + "properties": { + "alamat": { + "type": "string", + "example": "sddsfsd" + }, + "id_daerah": { + "type": "integer", + "example": 111 + }, + "id_role": { + "type": "integer" + }, + "id_skpd_lama": { + "type": "integer" + }, + "id_unik_skpd": { + "type": "string" + }, + "id_user": { + "type": "integer", + "example": 581 + }, + "kode_skpd": { + "type": "string" + }, + "nama_daerah": { + "type": "string", + "example": "Kota Bandar Lampung" + }, + "nama_skpd": { + "type": "string" + }, + "nama_user": { + "type": "string", + "example": "John Doe" + }, + "nik_user": { + "type": "string", + "example": "222112323324" + }, + "nip_user": { + "type": "string", + "example": "196408081992011001" + }, + "npwp_user": { + "type": "string", + "example": "222112323324" + }, + "status": { + "type": "string" + } + } + }, + "models.ValidateCaptcha": { + "type": "object", + "required": [ + "id", + "solution" + ], + "properties": { + "id": { + "type": "string" + }, + "solution": { + "type": "string" + } + } + }, + "utils.DataValidationError": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "email" + }, + "message": { + "type": "string", + "example": "Invalid email address" + } + } + }, + "utils.LoginError": { + "type": "object", + "properties": { + "attempt": { + "description": "sisa kesempatan login sebelum diblokir 5 menit", + "type": "integer", + "example": 3 + }, + "message": { + "description": "keterangan error", + "type": "string", + "example": "invalid username or password" + }, + "next_login": { + "description": "unix timestamp UTC blokir login dibuka kembali", + "type": "integer", + "example": 123233213 + } + } + }, + "utils.RequestError": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "example": 422 + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.DataValidationError" + } + }, + "message": { + "type": "string", + "example": "Invalid email address" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..1d68f2a --- /dev/null +++ b/docs/swagger.yaml @@ -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" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..99c6332 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5f69b62 --- /dev/null +++ b/go.sum @@ -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= diff --git a/handler/http/auth.go b/handler/http/auth.go new file mode 100644 index 0000000..56d3598 --- /dev/null +++ b/handler/http/auth.go @@ -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) +} diff --git a/handler/http/captcha.go b/handler/http/captcha.go new file mode 100644 index 0000000..62daa44 --- /dev/null +++ b/handler/http/captcha.go @@ -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) +} diff --git a/handler/http/configs/fiber_config.go b/handler/http/configs/fiber_config.go new file mode 100644 index 0000000..5fce903 --- /dev/null +++ b/handler/http/configs/fiber_config.go @@ -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 +} diff --git a/handler/http/http_util/json_result.go b/handler/http/http_util/json_result.go new file mode 100644 index 0000000..415c87f --- /dev/null +++ b/handler/http/http_util/json_result.go @@ -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"` +} diff --git a/handler/http/http_util/server.go b/handler/http/http_util/server.go new file mode 100644 index 0000000..1e6fc9b --- /dev/null +++ b/handler/http/http_util/server.go @@ -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) + } +} diff --git a/handler/http/http_util/token_generator.go b/handler/http/http_util/token_generator.go new file mode 100644 index 0000000..781753a --- /dev/null +++ b/handler/http/http_util/token_generator.go @@ -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 +} diff --git a/handler/http/middleware/middleware.go b/handler/http/middleware/middleware.go new file mode 100644 index 0000000..3947378 --- /dev/null +++ b/handler/http/middleware/middleware.go @@ -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} +} diff --git a/handler/http/site.go b/handler/http/site.go new file mode 100644 index 0000000..8e0cfef --- /dev/null +++ b/handler/http/site.go @@ -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) +} diff --git a/handler/http/user.go b/handler/http/user.go new file mode 100644 index 0000000..a92f654 --- /dev/null +++ b/handler/http/user.go @@ -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) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c986e82 --- /dev/null +++ b/main.go @@ -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...) +} diff --git a/model/app_const.go b/model/app_const.go new file mode 100644 index 0000000..6a06771 --- /dev/null +++ b/model/app_const.go @@ -0,0 +1,7 @@ +package models + +const ( + ConstUserStatusInnactive = 0 + ConstUserStatusActive = 1 + ConstUserStatusDeleted = 2 +) diff --git a/model/auth_model.go b/model/auth_model.go new file mode 100644 index 0000000..8b658a5 --- /dev/null +++ b/model/auth_model.go @@ -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"` +} diff --git a/model/captcha.go b/model/captcha.go new file mode 100644 index 0000000..8362a52 --- /dev/null +++ b/model/captcha.go @@ -0,0 +1,6 @@ +package models + +type ValidateCaptcha struct { + Id string `json:"id" validate:"required"` + Solution string `json:"solution" validate:"required"` +} diff --git a/model/db/db_conn_model.go b/model/db/db_conn_model.go new file mode 100644 index 0000000..ede2dcc --- /dev/null +++ b/model/db/db_conn_model.go @@ -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"` +} diff --git a/model/form/auth_form.go b/model/form/auth_form.go new file mode 100644 index 0000000..bf05594 --- /dev/null +++ b/model/form/auth_form.go @@ -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"` +} diff --git a/model/form/generateHash.go b/model/form/generateHash.go new file mode 100644 index 0000000..25b8886 --- /dev/null +++ b/model/form/generateHash.go @@ -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"` +} diff --git a/model/form/signup.go b/model/form/signup.go new file mode 100644 index 0000000..9ce3ea8 --- /dev/null +++ b/model/form/signup.go @@ -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 +} diff --git a/model/form/site.go b/model/form/site.go new file mode 100644 index 0000000..fbda7d5 --- /dev/null +++ b/model/form/site.go @@ -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"`*/ +} diff --git a/model/form/token_refresh.go b/model/form/token_refresh.go new file mode 100644 index 0000000..4a79d02 --- /dev/null +++ b/model/form/token_refresh.go @@ -0,0 +1,5 @@ +package form + +type TokenRefreshForm struct { + TokenRefresh string `json:"token_refresh" form:"token_refresh" xml:"token_refresh" validate:"required" example:"sadfakjahoiajfdjahlkjfhakjajfalkjfhadsfda"` +} diff --git a/model/form/user.go b/model/form/user.go new file mode 100644 index 0000000..8812993 --- /dev/null +++ b/model/form/user.go @@ -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 +} diff --git a/model/form/user_manager.go b/model/form/user_manager.go new file mode 100644 index 0000000..5b6c73b --- /dev/null +++ b/model/form/user_manager.go @@ -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 +} diff --git a/model/list_user.go b/model/list_user.go new file mode 100644 index 0000000..5c6e951 --- /dev/null +++ b/model/list_user.go @@ -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 +} diff --git a/model/user.go b/model/user.go new file mode 100644 index 0000000..4a5cd8b --- /dev/null +++ b/model/user.go @@ -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 +} diff --git a/utils/app_errors.go b/utils/app_errors.go new file mode 100644 index 0000000..dfdcf0d --- /dev/null +++ b/utils/app_errors.go @@ -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 +} diff --git a/utils/captcha_store/postgresql_store.go b/utils/captcha_store/postgresql_store.go new file mode 100644 index 0000000..73a9e1b --- /dev/null +++ b/utils/captcha_store/postgresql_store.go @@ -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 +} diff --git a/utils/jwt_manager.go b/utils/jwt_manager.go new file mode 100644 index 0000000..d3186d2 --- /dev/null +++ b/utils/jwt_manager.go @@ -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") +} diff --git a/utils/strUtility.go b/utils/strUtility.go new file mode 100644 index 0000000..08e21e4 --- /dev/null +++ b/utils/strUtility.go @@ -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 +} diff --git a/utils/validator.go b/utils/validator.go new file mode 100644 index 0000000..365f88e --- /dev/null +++ b/utils/validator.go @@ -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 +}