feat: create api for login and register

This commit is contained in:
pahmiudahgede 2024-12-08 16:00:13 +07:00
commit 601dc1d088
21 changed files with 1176 additions and 0 deletions

13
.env.example Normal file
View File

@ -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=

25
.gitignore vendored Normal file
View File

@ -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

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# build_api_golang
this is rest API using go lang and go fiber with postgreql database for my personal project.

63
config/connection.go Normal file
View File

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

20
domain/address.go Normal file
View File

@ -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"`
}

7
domain/role.go Normal file
View File

@ -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"`
}

22
domain/user.go Normal file
View File

@ -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"`
}

73
dto/address.go Normal file
View File

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

82
dto/user.go Normal file
View File

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

38
go.mod Normal file
View File

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

78
go.sum Normal file
View File

@ -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=

20
internal/api/routes.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

80
internal/services/auth.go Normal file
View File

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

30
presentation/main.go Normal file
View File

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

21
utils/response.go Normal file
View File

@ -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,
}
}