From 601dc1d088d2380b8002e064b9ed7e97150fd4da Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sun, 8 Dec 2024 16:00:13 +0700 Subject: [PATCH] feat: create api for login and register --- .env.example | 13 ++ .gitignore | 25 ++++ README.md | 2 + config/connection.go | 63 ++++++++++ domain/address.go | 20 ++++ domain/role.go | 7 ++ domain/user.go | 22 ++++ dto/address.go | 73 ++++++++++++ dto/user.go | 82 +++++++++++++ go.mod | 38 ++++++ go.sum | 78 ++++++++++++ internal/api/routes.go | 20 ++++ internal/controllers/address.go | 198 +++++++++++++++++++++++++++++++ internal/controllers/auth.go | 151 +++++++++++++++++++++++ internal/middleware/auth.go | 43 +++++++ internal/repositories/address.go | 48 ++++++++ internal/repositories/auth.go | 85 +++++++++++++ internal/services/address.go | 77 ++++++++++++ internal/services/auth.go | 80 +++++++++++++ presentation/main.go | 30 +++++ utils/response.go | 21 ++++ 21 files changed, 1176 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/connection.go create mode 100644 domain/address.go create mode 100644 domain/role.go create mode 100644 domain/user.go create mode 100644 dto/address.go create mode 100644 dto/user.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/api/routes.go create mode 100644 internal/controllers/address.go create mode 100644 internal/controllers/auth.go create mode 100644 internal/middleware/auth.go create mode 100644 internal/repositories/address.go create mode 100644 internal/repositories/auth.go create mode 100644 internal/services/address.go create mode 100644 internal/services/auth.go create mode 100644 presentation/main.go create mode 100644 utils/response.go diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4dfeb60 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# SERVER SETTINGS +SERVER_HOST=localhost +SERVER_PORT= # isi listen port anda (bebas) + +# DATABASE SETTINGS +DB_HOST=localhost +DB_PORT=5432 # port default postgres +DB_NAME= # nama_database di postgres +DB_USER= # username yang digunakan di postgres +DB_PASSWORD= # password yang digunakan di postgres + +# api keyauth +API_KEY= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64ae7f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7a7fc2 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# build_api_golang +this is rest API using go lang and go fiber with postgreql database for my personal project. diff --git a/config/connection.go b/config/connection.go new file mode 100644 index 0000000..452b35d --- /dev/null +++ b/config/connection.go @@ -0,0 +1,63 @@ +package config + +import ( + "fmt" + "log" + "os" + + "github.com/pahmiudahgede/senggoldong/domain" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +var ( + DB *gorm.DB + DBHost string + DBPort string + DBName string + DBUser string + DBPassword string + + APIKey string + ServerHost string + ServerPort string +) + +func InitConfig() { + ServerHost = os.Getenv("SERVER_HOST") + ServerPort = os.Getenv("SERVER_PORT") + DBHost = os.Getenv("DB_HOST") + DBPort = os.Getenv("DB_PORT") + DBName = os.Getenv("DB_NAME") + DBUser = os.Getenv("DB_USER") + DBPassword = os.Getenv("DB_PASSWORD") + APIKey = os.Getenv("API_KEY") + + if ServerHost == "" || ServerPort == "" || DBHost == "" || DBPort == "" || DBName == "" || DBUser == "" || DBPassword == "" || APIKey == "" { + log.Fatal("Error: environment variables yang dibutuhkan tidak ada") + } +} + +func InitDatabase() { + InitConfig() + + dsn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable", + DBHost, DBPort, DBUser, DBName, DBPassword) + + var err error + DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatal("gagal terhubung ke database: ", err) + } + + err = DB.AutoMigrate( + &domain.User{}, + &domain.UserRole{}, + &domain.Address{}, + ) + if err != nil { + log.Fatal("Error: Failed to auto migrate domain:", err) + } + + fmt.Println("Koneksi ke database berhasil dan migrasi dilakukan") +} diff --git a/domain/address.go b/domain/address.go new file mode 100644 index 0000000..b47b4ed --- /dev/null +++ b/domain/address.go @@ -0,0 +1,20 @@ +package domain + +import ( + "time" +) + +type Address struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserID string `gorm:"not null" json:"userId"` + User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` + Province string `gorm:"not null" json:"province"` + District string `gorm:"not null" json:"district"` + Subdistrict string `gorm:"not null" json:"subdistrict"` + PostalCode int `gorm:"not null" json:"postalCode"` + Village string `gorm:"not null" json:"village"` + Detail string `gorm:"not null" json:"detail"` + Geography string `gorm:"not null" json:"geography"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} diff --git a/domain/role.go b/domain/role.go new file mode 100644 index 0000000..5e4825b --- /dev/null +++ b/domain/role.go @@ -0,0 +1,7 @@ +package domain + +type UserRole struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + RoleName string `gorm:"unique;not null" json:"roleName"` + Users []User `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"users"` +} diff --git a/domain/user.go b/domain/user.go new file mode 100644 index 0000000..80582e4 --- /dev/null +++ b/domain/user.go @@ -0,0 +1,22 @@ +package domain + +import ( + "time" +) + +type User struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + Avatar *string `json:"avatar,omitempty"` + Username string `gorm:"unique;not null" json:"username"` + Name string `gorm:"not null" json:"name"` + Phone string `gorm:"not null" json:"phone"` + Email string `gorm:"unique;not null" json:"email"` + EmailVerified bool `gorm:"default:false" json:"emailVerified"` + Password string `gorm:"not null" json:"password"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` + RoleID string `gorm:"not null" json:"roleId"` + Role UserRole `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"role"` + AddressId *string `gorm:"default:null" json:"addressId"` + Addresses []Address `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"addresses"` +} diff --git a/dto/address.go b/dto/address.go new file mode 100644 index 0000000..4740552 --- /dev/null +++ b/dto/address.go @@ -0,0 +1,73 @@ +package dto + +import ( + "fmt" + + "github.com/go-playground/validator/v10" +) + +type AddressInput struct { + Province string `json:"province" validate:"required"` + District string `json:"district" validate:"required"` + Subdistrict string `json:"subdistrict" validate:"required"` + PostalCode int `json:"postalCode" validate:"required,numeric"` + Village string `json:"village" validate:"required"` + Detail string `json:"detail" validate:"required"` + Geography string `json:"geography" validate:"required"` +} + +var validate = validator.New() + +func (c *AddressInput) ValidatePost() error { + err := validate.Struct(c) + if err != nil { + + for _, e := range err.(validator.ValidationErrors) { + + switch e.Field() { + case "Province": + return fmt.Errorf("provinsi harus diisisi") + case "District": + return fmt.Errorf("kabupaten harus diisi") + case "Subdistrict": + return fmt.Errorf("kecamatan harus diisi") + case "PostalCode": + return fmt.Errorf("postal code harus diisi dan berupa angka") + case "Village": + return fmt.Errorf("desa harus diisi") + case "Detail": + return fmt.Errorf("detail wajib diisi") + case "Geography": + return fmt.Errorf("lokasi kordinat harus diisi") + } + } + } + return nil +} + +func (c *AddressInput) ValidateUpdate() error { + err := validate.Struct(c) + if err != nil { + + for _, e := range err.(validator.ValidationErrors) { + + switch e.Field() { + case "Province": + return fmt.Errorf("provinsi harus diisisi") + case "District": + return fmt.Errorf("kabupaten harus diisi") + case "Subdistrict": + return fmt.Errorf("kecamatan harus diisi") + case "PostalCode": + return fmt.Errorf("postal code harus diisi dan berupa angka") + case "Village": + return fmt.Errorf("desa harus diisi") + case "Detail": + return fmt.Errorf("detail wajib diisi") + case "Geography": + return fmt.Errorf("lokasi kordinat harus diisi") + } + } + } + return nil +} diff --git a/dto/user.go b/dto/user.go new file mode 100644 index 0000000..1873fee --- /dev/null +++ b/dto/user.go @@ -0,0 +1,82 @@ +package dto + +import ( + "errors" + "regexp" +) + +func ValidateEmail(email string) error { + if email == "" { + return errors.New("email harus diisi") + } + + emailRegex := `^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$` + re := regexp.MustCompile(emailRegex) + if !re.MatchString(email) { + return errors.New("format email belum sesuai") + } + return nil +} + +func ValidatePhone(phone string) error { + if phone == "" { + return errors.New("nomor telepon harus diisi") + } + + phoneRegex := `^\+?[0-9]{10,15}$` + re := regexp.MustCompile(phoneRegex) + if !re.MatchString(phone) { + return errors.New("nomor telepon tidak valid") + } + + return nil +} + +func ValidatePassword(password string) error { + if password == "" { + return errors.New("password harus diisi") + } + + if len(password) < 8 { + return errors.New("password minimal 8 karakter") + } + return nil +} + +type RegisterUserInput struct { + Username string `json:"username"` + Name string `json:"name"` + Email string `json:"email"` + Phone string `json:"phone"` + Password string `json:"password"` + RoleId string `json:"roleId"` +} + +func (input *RegisterUserInput) Validate() error { + + if input.Username == "" { + return errors.New("username harus diisi") + } + + if input.Name == "" { + return errors.New("nama harus diisi") + } + + if err := ValidateEmail(input.Email); err != nil { + return err + } + + if err := ValidatePhone(input.Phone); err != nil { + return err + } + + if err := ValidatePassword(input.Password); err != nil { + return err + } + + if input.RoleId == "" { + return errors.New("roleId harus diisi") + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..532c615 --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module github.com/pahmiudahgede/senggoldong + +go 1.23.3 + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/gofiber/fiber/v2 v2.52.5 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lucsky/cuid v1.2.1 // 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.15 // indirect + github.com/rivo/uniseg v0.2.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/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.20.0 // indirect + gorm.io/driver/postgres v1.5.11 // indirect + gorm.io/gorm v1.25.12 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4620fde --- /dev/null +++ b/go.sum @@ -0,0 +1,78 @@ +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +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.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= +github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +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/lucsky/cuid v1.2.1 h1:MtJrL2OFhvYufUIn48d35QGXyeTC8tn0upumW9WwTHg= +github.com/lucsky/cuid v1.2.1/go.mod h1:QaaJqckboimOmhRSJXSx/+IT+VTfxfPGSo/6mfgUfmE= +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.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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.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= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/internal/api/routes.go b/internal/api/routes.go new file mode 100644 index 0000000..2f58b82 --- /dev/null +++ b/internal/api/routes.go @@ -0,0 +1,20 @@ +package api + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/internal/controllers" + "github.com/pahmiudahgede/senggoldong/internal/middleware" +) + +func AppRouter(app *fiber.App) { + app.Post("/register", controllers.Register) + app.Post("/login", controllers.Login) + + app.Get("/user", middleware.AuthMiddleware, controllers.GetUserInfo) + + app.Get("/list-address", middleware.AuthMiddleware, controllers.GetListAddress) + app.Get("/address/:id", middleware.AuthMiddleware, controllers.GetAddressByID) + app.Post("/create-address", middleware.AuthMiddleware, controllers.CreateAddress) + app.Put("/address/:id", middleware.AuthMiddleware, controllers.UpdateAddress) + app.Delete("/address/:id", middleware.AuthMiddleware, controllers.DeleteAddress) +} diff --git a/internal/controllers/address.go b/internal/controllers/address.go new file mode 100644 index 0000000..46b7078 --- /dev/null +++ b/internal/controllers/address.go @@ -0,0 +1,198 @@ +package controllers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/utils" +) + +func CreateAddress(c *fiber.Ctx) error { + var input dto.AddressInput + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + "Mohon masukkan alamat dengan benar", + nil, + )) + } + + if err := input.ValidatePost(); err != nil { + + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + err.Error(), + nil, + )) + } + + userID := c.Locals("userID").(string) + + address, err := services.CreateAddress(userID, input) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + fiber.StatusInternalServerError, + "Failed to create address", + nil, + )) + } + + addressResponse := map[string]interface{}{ + "id": address.ID, + "province": address.Province, + "district": address.District, + "subdistrict": address.Subdistrict, + "postalCode": address.PostalCode, + "village": address.Village, + "detail": address.Detail, + "geography": address.Geography, + "createdAt": address.CreatedAt, + "updatedAt": address.UpdatedAt, + } + + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "Address created successfully", + addressResponse, + )) +} + +func GetListAddress(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + addresses, err := services.GetAllAddressesByUserID(userID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(utils.FormatResponse( + fiber.StatusNotFound, + "Addresses not found", + nil, + )) + } + + addressResponses := []map[string]interface{}{} + + for _, address := range addresses { + addressResponse := map[string]interface{}{ + "id": address.ID, + "province": address.Province, + "district": address.District, + "subdistrict": address.Subdistrict, + "postalCode": address.PostalCode, + "village": address.Village, + "detail": address.Detail, + "geography": address.Geography, + "createdAt": address.CreatedAt, + "updatedAt": address.UpdatedAt, + } + addressResponses = append(addressResponses, addressResponse) + } + + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "Addresses fetched successfully", + addressResponses, + )) +} + +func GetAddressByID(c *fiber.Ctx) error { + + addressID := c.Params("id") + + address, err := services.GetAddressByID(addressID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(utils.FormatResponse( + fiber.StatusNotFound, + "Address not found", + nil, + )) + } + + addressResponse := map[string]interface{}{ + "id": address.ID, + "province": address.Province, + "district": address.District, + "subdistrict": address.Subdistrict, + "postalCode": address.PostalCode, + "village": address.Village, + "detail": address.Detail, + "geography": address.Geography, + "createdAt": address.CreatedAt, + "updatedAt": address.UpdatedAt, + } + + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "Address fetched successfully", + addressResponse, + )) +} + +func UpdateAddress(c *fiber.Ctx) error { + + addressID := c.Params("id") + + var input dto.AddressInput + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + "Invalid input data", + nil, + )) + } + + if err := input.ValidateUpdate(); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + err.Error(), + nil, + )) + } + + address, err := services.UpdateAddress(addressID, input) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + fiber.StatusInternalServerError, + "Failed to update address", + nil, + )) + } + + addressResponse := map[string]interface{}{ + "id": address.ID, + "province": address.Province, + "district": address.District, + "subdistrict": address.Subdistrict, + "postalCode": address.PostalCode, + "village": address.Village, + "detail": address.Detail, + "geography": address.Geography, + "createdAt": address.CreatedAt, + "updatedAt": address.UpdatedAt, + } + + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "Address updated successfully", + addressResponse, + )) +} + +func DeleteAddress(c *fiber.Ctx) error { + + addressID := c.Params("id") + + err := services.DeleteAddress(addressID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + fiber.StatusInternalServerError, + "Failed to delete address", + nil, + )) + } + + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "Address deleted successfully", + nil, + )) +} diff --git a/internal/controllers/auth.go b/internal/controllers/auth.go new file mode 100644 index 0000000..06eb87c --- /dev/null +++ b/internal/controllers/auth.go @@ -0,0 +1,151 @@ +package controllers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/utils" +) + +func Register(c *fiber.Ctx) error { + var userInput dto.RegisterUserInput + + if err := c.BodyParser(&userInput); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + "Invalid input data", + nil, + )) + } + + if err := userInput.Validate(); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + err.Error(), + nil, + )) + } + + err := services.RegisterUser(userInput.Username, userInput.Name, userInput.Email, userInput.Phone, userInput.Password, userInput.RoleId) + if err != nil { + + if err.Error() == "email is already registered" { + return c.Status(fiber.StatusConflict).JSON(utils.FormatResponse( + fiber.StatusConflict, + "Email is already registered", + nil, + )) + } + if err.Error() == "username is already registered" { + return c.Status(fiber.StatusConflict).JSON(utils.FormatResponse( + fiber.StatusConflict, + "Username is already registered", + nil, + )) + } + if err.Error() == "phone number is already registered" { + return c.Status(fiber.StatusConflict).JSON(utils.FormatResponse( + fiber.StatusConflict, + "Phone number is already registered", + nil, + )) + } + + return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + fiber.StatusInternalServerError, + "Failed to create user", + nil, + )) + } + + user, err := repositories.GetUserByEmailOrUsername(userInput.Email) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( + fiber.StatusInternalServerError, + "Failed to fetch user after registration", + nil, + )) + } + + userResponse := map[string]interface{}{ + "id": user.ID, + "username": user.Username, + "name": user.Name, + "email": user.Email, + "phone": user.Phone, + "roleId": user.RoleID, + "createdAt": user.CreatedAt, + "updatedAt": user.UpdatedAt, + } + + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "User registered successfully", + userResponse, + )) +} + +func Login(c *fiber.Ctx) error { + var credentials struct { + EmailOrUsername string `json:"email_or_username"` + Password string `json:"password"` + } + + if err := c.BodyParser(&credentials); err != nil { + + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + "Invalid input data", + nil, + )) + } + + token, err := services.LoginUser(credentials.EmailOrUsername, credentials.Password) + if err != nil { + + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + err.Error(), + nil, + )) + } + + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "Login successful", + map[string]string{"token": token}, + )) +} + +func GetUserInfo(c *fiber.Ctx) error { + + userID := c.Locals("userID").(string) + + user, err := services.GetUserByID(userID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(utils.FormatResponse( + fiber.StatusNotFound, + "user tidak ditemukan", + nil, + )) + } + + userResponse := map[string]interface{}{ + "id": user.ID, + "username": user.Username, + "nama": user.Name, + "nohp": user.Phone, + "email": user.Email, + "statusverifikasi": user.EmailVerified, + "role": user.Role.RoleName, + "createdAt": user.CreatedAt, + "updatedAt": user.UpdatedAt, + } + + return c.Status(fiber.StatusOK).JSON(utils.FormatResponse( + fiber.StatusOK, + "data user berhasil ditampilkan", + userResponse, + )) +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go new file mode 100644 index 0000000..96c7748 --- /dev/null +++ b/internal/middleware/auth.go @@ -0,0 +1,43 @@ +package middleware + +import ( + "os" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +func AuthMiddleware(c *fiber.Ctx) error { + tokenString := c.Get("Authorization") + tokenString = strings.TrimPrefix(tokenString, "Bearer ") + + if tokenString == "" { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "message": "Missing or invalid token", + }) + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte(os.Getenv("API_KEY")), nil + }) + + if err != nil || !token.Valid { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "message": "Invalid or expired token", + }) + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "message": "Invalid token claims", + }) + } + + userID := claims["sub"].(string) + + c.Locals("userID", userID) + + return c.Next() +} diff --git a/internal/repositories/address.go b/internal/repositories/address.go new file mode 100644 index 0000000..64b1eb4 --- /dev/null +++ b/internal/repositories/address.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "errors" + + "github.com/pahmiudahgede/senggoldong/config" + "github.com/pahmiudahgede/senggoldong/domain" +) + +func CreateAddress(address *domain.Address) error { + result := config.DB.Create(address) + if result.Error != nil { + return result.Error + } + return nil +} + +func GetAddressesByUserID(userID string) ([]domain.Address, error) { + var addresses []domain.Address + err := config.DB.Where("user_id = ?", userID).Find(&addresses).Error + if err != nil { + return nil, err + } + return addresses, nil +} + +func GetAddressByID(addressID string) (domain.Address, error) { + var address domain.Address + if err := config.DB.Where("id = ?", addressID).First(&address).Error; err != nil { + return address, errors.New("address not found") + } + return address, nil +} + +func UpdateAddress(address domain.Address) (domain.Address, error) { + if err := config.DB.Save(&address).Error; err != nil { + return address, err + } + return address, nil +} + +func DeleteAddress(addressID string) error { + var address domain.Address + if err := config.DB.Where("id = ?", addressID).Delete(&address).Error; err != nil { + return err + } + return nil +} diff --git a/internal/repositories/auth.go b/internal/repositories/auth.go new file mode 100644 index 0000000..d1d398e --- /dev/null +++ b/internal/repositories/auth.go @@ -0,0 +1,85 @@ +package repositories + +import ( + "errors" + "fmt" + + "github.com/pahmiudahgede/senggoldong/config" + "github.com/pahmiudahgede/senggoldong/domain" +) + +func IsEmailExist(email string) bool { + var user domain.User + if err := config.DB.Where("email = ?", email).First(&user).Error; err == nil { + return true + } + return false +} + +func IsUsernameExist(username string) bool { + var user domain.User + if err := config.DB.Where("username = ?", username).First(&user).Error; err == nil { + return true + } + return false +} + +func IsPhoneExist(phone string) bool { + var user domain.User + if err := config.DB.Where("phone = ?", phone).First(&user).Error; err == nil { + return true + } + return false +} + +func CreateUser(username, name, email, phone, password, roleId string) error { + + if IsEmailExist(email) { + return errors.New("email is already registered") + } + + if IsUsernameExist(username) { + return errors.New("username is already registered") + } + + if IsPhoneExist(phone) { + return errors.New("phone number is already registered") + } + + user := domain.User{ + Username: username, + Name: name, + Email: email, + Phone: phone, + Password: password, + RoleID: roleId, + } + + result := config.DB.Create(&user) + if result.Error != nil { + return errors.New("failed to create user") + } + return nil +} + +func GetUserByEmailOrUsername(emailOrUsername string) (domain.User, error) { + var user domain.User + if err := config.DB.Where("email = ? OR username = ?", emailOrUsername, emailOrUsername).First(&user).Error; err != nil { + return user, errors.New("user not found") + } + return user, nil +} + +func GetUserByID(userID string) (domain.User, error) { + var user domain.User + if err := config.DB. + Preload("Role"). + Where("id = ?", userID). + First(&user).Error; err != nil { + return user, errors.New("user not found") + } + + fmt.Printf("User ID: %s, Role: %v\n", user.ID, user.Role) + + return user, nil +} diff --git a/internal/services/address.go b/internal/services/address.go new file mode 100644 index 0000000..db18d1d --- /dev/null +++ b/internal/services/address.go @@ -0,0 +1,77 @@ +package services + +import ( + "errors" + + "github.com/pahmiudahgede/senggoldong/domain" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/repositories" +) + +func CreateAddress(userID string, input dto.AddressInput) (domain.Address, error) { + address := domain.Address{ + UserID: userID, + Province: input.Province, + District: input.District, + Subdistrict: input.Subdistrict, + PostalCode: input.PostalCode, + Village: input.Village, + Detail: input.Detail, + Geography: input.Geography, + } + + err := repositories.CreateAddress(&address) + if err != nil { + return domain.Address{}, err + } + + return address, nil +} + +func GetAllAddressesByUserID(userID string) ([]domain.Address, error) { + + addresses, err := repositories.GetAddressesByUserID(userID) + if err != nil { + return nil, err + } + return addresses, nil +} + +func GetAddressByID(addressID string) (domain.Address, error) { + address, err := repositories.GetAddressByID(addressID) + if err != nil { + return address, errors.New("address not found") + } + return address, nil +} + +func UpdateAddress(addressID string, input dto.AddressInput) (domain.Address, error) { + + address, err := repositories.GetAddressByID(addressID) + if err != nil { + return address, errors.New("address not found") + } + + address.Province = input.Province + address.District = input.District + address.Subdistrict = input.Subdistrict + address.PostalCode = input.PostalCode + address.Village = input.Village + address.Detail = input.Detail + address.Geography = input.Geography + + updatedAddress, err := repositories.UpdateAddress(address) + if err != nil { + return updatedAddress, errors.New("failed to update address") + } + + return updatedAddress, nil +} + +func DeleteAddress(addressID string) error { + err := repositories.DeleteAddress(addressID) + if err != nil { + return errors.New("failed to delete address") + } + return nil +} diff --git a/internal/services/auth.go b/internal/services/auth.go new file mode 100644 index 0000000..6b30d3a --- /dev/null +++ b/internal/services/auth.go @@ -0,0 +1,80 @@ +package services + +import ( + "errors" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/pahmiudahgede/senggoldong/domain" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "golang.org/x/crypto/bcrypt" +) + +func RegisterUser(username, name, email, phone, password, roleId string) error { + + if repositories.IsEmailExist(email) { + return errors.New("email is already registered") + } + if repositories.IsUsernameExist(username) { + return errors.New("username is already registered") + } + if repositories.IsPhoneExist(phone) { + return errors.New("phone number is already registered") + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return errors.New("failed to hash password") + } + + err = repositories.CreateUser(username, name, email, phone, string(hashedPassword), roleId) + if err != nil { + return err + } + + return nil +} + +func LoginUser(emailOrUsername, password string) (string, error) { + if emailOrUsername == "" || password == "" { + return "", errors.New("email/username and password must be provided") + } + + user, err := repositories.GetUserByEmailOrUsername(emailOrUsername) + if err != nil { + return "", errors.New("invalid email/username or password") + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return "", errors.New("invalid email/username or password") + } + + token := generateJWT(user.ID) + + return token, nil +} + +func generateJWT(userID string) string { + claims := jwt.MapClaims{ + "sub": userID, + "exp": time.Now().Add(time.Hour * 24).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + t, err := token.SignedString([]byte(os.Getenv("API_KEY"))) + if err != nil { + return "" + } + + return t +} + +func GetUserByID(userID string) (domain.User, error) { + user, err := repositories.GetUserByID(userID) + if err != nil { + return user, errors.New("user not found") + } + return user, nil +} diff --git a/presentation/main.go b/presentation/main.go new file mode 100644 index 0000000..ec2679d --- /dev/null +++ b/presentation/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "log" + "os" + + "github.com/gofiber/fiber/v2" + "github.com/joho/godotenv" + "github.com/pahmiudahgede/senggoldong/config" + "github.com/pahmiudahgede/senggoldong/internal/api" +) + +func init() { + err := godotenv.Load() + if err != nil { + log.Fatal("error saat memuat file .env") + } + + config.InitConfig() + config.InitDatabase() + +} + +func main() { + app := fiber.New() + + api.AppRouter(app) + + log.Fatal(app.Listen(":" + os.Getenv("SERVER_PORT"))) +} diff --git a/utils/response.go b/utils/response.go new file mode 100644 index 0000000..28303c1 --- /dev/null +++ b/utils/response.go @@ -0,0 +1,21 @@ +package utils + +type Meta struct { + StatusCode int `json:"statusCode"` + Message string `json:"message"` +} + +type ApiResponse struct { + Meta Meta `json:"meta"` + Data interface{} `json:"data"` +} + +func FormatResponse(statusCode int, message string, data interface{}) ApiResponse { + return ApiResponse{ + Meta: Meta{ + StatusCode: statusCode, + Message: message, + }, + Data: data, + } +}