commit f59cb4f927e96b24a18061b640766b6ca1a3fdbe Author: Aykhan Shahsuvarov Date: Fri Jan 19 16:11:54 2024 +0400 🎉 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0791332 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.21.6-alpine AS builder + +WORKDIR /ohmyurl + +COPY go.mod go.sum ./ +RUN go mod download +COPY ./app ./app + +RUN go build -o ./ohmyurl ./app/main.go + +FROM scratch + +COPY --from=builder /ohmyurl/app/templates/ /app/templates/ +COPY --from=builder /ohmyurl/ohmyurl /ohmyurl + +ENTRYPOINT ["/ohmyurl"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..34249ea --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Oh My URl! \ No newline at end of file diff --git a/app/config/.env.example b/app/config/.env.example new file mode 100644 index 0000000..fb3adae --- /dev/null +++ b/app/config/.env.example @@ -0,0 +1,10 @@ +DB=postgres +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_DB= +POSTGRES_HOST=ohmyurl-postgresql +POSTGRES_PORT=5432 +LISTEN_PORT_CREATE=8080 +LISTEN_PORT_FORWARD=8081 +FORWARD_DOMAIN=http://localhost/ +CREATE_DOMAIN=http://127.0.0.1 \ No newline at end of file diff --git a/app/config/settings.go b/app/config/settings.go new file mode 100644 index 0000000..9c61509 --- /dev/null +++ b/app/config/settings.go @@ -0,0 +1,78 @@ +package config + +import ( + "os" + "strings" +) + +type DBName string + +const ( + Postgres DBName = "postgres" + MongoDB DBName = "mongodb" + Cassandra DBName = "cassandra" +) + +type AppConfig struct { + LISTEN_PORT_CREATE string + LISTEN_PORT_FORWARD string + FORWARD_DOMAIN string + CREATE_DOMAIN string +} + +type PostgresConfig struct { + USER string + PASSWORD string + HOST string + PORT string + DBNAME string +} + +func GetAppConfig() *AppConfig { + return &AppConfig{ + LISTEN_PORT_CREATE: GetEnvOrPanic("LISTEN_PORT_CREATE"), + LISTEN_PORT_FORWARD: GetEnvOrPanic("LISTEN_PORT_FORWARD"), + FORWARD_DOMAIN: GetEnvOrPanic("FORWARD_DOMAIN"), + CREATE_DOMAIN: GetEnvOrPanic("CREATE_DOMAIN"), + } +} + +func GetPostgresConfig() *PostgresConfig { + return &PostgresConfig{ + USER: GetEnvOrPanic("POSTGRES_USER"), + PASSWORD: GetEnvOrPanic("POSTGRES_PASSWORD"), + HOST: GetEnvOrPanic("POSTGRES_HOST"), + PORT: GetEnvOrDefault("POSTGRES_PORT", "5432"), + DBNAME: GetEnvOrPanic("POSTGRES_DB"), + } +} + +func GetDB() DBName { + dbName := strings.ToLower(GetEnvOrPanic("DB")) + switch dbName { + case "postgres": + return Postgres + case "mongodb": + return MongoDB + case "cassandra": + return Cassandra + default: + panic("Unknown database") + } +} + +func GetEnvOrDefault(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + +func GetEnvOrPanic(key string) string { + value := os.Getenv(key) + if value == "" { + panic("Environment variable " + key + " is not set") + } + return value +} diff --git a/app/db/base.go b/app/db/base.go new file mode 100644 index 0000000..4482bc0 --- /dev/null +++ b/app/db/base.go @@ -0,0 +1,48 @@ +package db + +import ( + "fmt" + "github.com/aykhans/oh-my-url/app/config" + gormPostgres "gorm.io/driver/postgres" + "gorm.io/gorm" + "time" +) + +type DB interface { + Init() + CreateURL(url string) (string, error) + GetURL(key string) (string, error) +} + +func GetDB() DB { + db := config.GetDB() + switch db { + case "postgres": + postgresConf := config.GetPostgresConfig() + dsn := fmt.Sprintf( + "host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s", + postgresConf.HOST, + postgresConf.USER, + postgresConf.PASSWORD, + postgresConf.DBNAME, + postgresConf.PORT, + "disable", + "UTC", + ) + + var db *gorm.DB + var err error + for i := 0; i < 5; i++ { + db, err = gorm.Open(gormPostgres.Open(dsn), &gorm.Config{}) + if err == nil { + break + } + time.Sleep(3 * time.Second) + } + if err != nil { + panic(err) + } + return &Postgres{gormDB: db} + } + return nil +} diff --git a/app/db/cassandra.go b/app/db/cassandra.go new file mode 100644 index 0000000..3a49c63 --- /dev/null +++ b/app/db/cassandra.go @@ -0,0 +1 @@ +package db diff --git a/app/db/mongodb.go b/app/db/mongodb.go new file mode 100644 index 0000000..3a49c63 --- /dev/null +++ b/app/db/mongodb.go @@ -0,0 +1 @@ +package db diff --git a/app/db/postgres.go b/app/db/postgres.go new file mode 100644 index 0000000..eb97198 --- /dev/null +++ b/app/db/postgres.go @@ -0,0 +1,72 @@ +package db + +import ( + // "github.com/aykhans/oh-my-url/app/config" + "gorm.io/gorm" +) + +type Postgres struct { + gormDB *gorm.DB +} + +type url struct { + ID uint `gorm:"primaryKey"` + Key string `gorm:"unique;not null;size:15;default:null"` + URL string `gorm:"not null;default:null"` +} + +func (p *Postgres) Init() { + err := p.gormDB.AutoMigrate(&url{}) + if err != nil { + panic("failed to migrate database") + } + tx := p.gormDB.Exec(urlKeyCreateTrigger) + if tx.Error != nil { + panic("failed to create trigger") + } +} + +func (p *Postgres) CreateURL(mainUrl string) (string, error) { + url := url{URL: mainUrl} + tx := p.gormDB.Create(&url) + if tx.Error != nil { + return "", tx.Error + } + return url.Key, nil +} + +func (p *Postgres) GetURL(key string) (string, error) { + var result url + tx := p.gormDB.Where("key = ?", key).First(&result) + if tx.Error != nil { + return "", tx.Error + } + return result.URL, nil +} + +const urlKeyCreateTrigger = ` +CREATE OR REPLACE FUNCTION generate_url_key() + RETURNS TRIGGER AS $$ +DECLARE + key_characters TEXT := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + key TEXT := ''; + base INT := LENGTH(key_characters); + n INT := NEW.id; +BEGIN + WHILE n > 0 LOOP + n := n - 1; + key := SUBSTRING(key_characters FROM (n % base) + 1 FOR 1) || key; + n := n / base; + END LOOP; + NEW.key := key; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trigger_generate_url_key on "public"."urls"; + +CREATE TRIGGER trigger_generate_url_key + BEFORE INSERT ON urls + FOR EACH ROW + EXECUTE FUNCTION generate_url_key(); +` diff --git a/app/http_handlers/base.go b/app/http_handlers/base.go new file mode 100644 index 0000000..bebc420 --- /dev/null +++ b/app/http_handlers/base.go @@ -0,0 +1,27 @@ +package httpHandlers + +import ( + "github.com/aykhans/oh-my-url/app/db" + "github.com/aykhans/oh-my-url/app/utils" + "log" + "net/http" +) + +type HandlerCreate struct { + DB db.DB + ForwardDomain string +} + +type HandlerForward struct { + DB db.DB + CreateDomain string +} + +func FaviconHandler(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, utils.GetTemplatePaths("favicon.ico")[0]) +} + +func InternalServerError(w http.ResponseWriter, err error) { + log.Fatal(err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) +} diff --git a/app/http_handlers/url_create.go b/app/http_handlers/url_create.go new file mode 100644 index 0000000..1204828 --- /dev/null +++ b/app/http_handlers/url_create.go @@ -0,0 +1,75 @@ +package httpHandlers + +import ( + "github.com/aykhans/oh-my-url/app/utils" + "html/template" + "net/http" + netUrl "net/url" + "regexp" +) + +type CreateData struct { + ShortedURL string + MainURL string + Error string +} + +func (hl *HandlerCreate) UrlCreate(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + tmpl, err := template.ParseFiles(utils.GetTemplatePaths("index.html")...) + if err != nil { + InternalServerError(w, err) + return + } + + switch r.Method { + case http.MethodGet: + err = tmpl.Execute(w, nil) + if err != nil { + InternalServerError(w, err) + return + } + case http.MethodPost: + url := r.FormValue("url") + urlRegex := regexp.MustCompile(`^(http|https)://[a-zA-Z0-9.-]+(?:\:[0-9]+)?(?:/[^\s]*)?$`) + isValidUrl := urlRegex.MatchString(url) + if !isValidUrl { + data := CreateData{ + MainURL: url, + Error: "Invalid URL", + } + err = tmpl.Execute(w, data) + if err != nil { + InternalServerError(w, err) + } + return + } + + key, err := hl.DB.CreateURL(url) + if err != nil { + InternalServerError(w, err) + return + } + shortedURL, err := netUrl.JoinPath(hl.ForwardDomain, key) + if err != nil { + InternalServerError(w, err) + return + } + data := CreateData{ + ShortedURL: shortedURL, + MainURL: url, + } + err = tmpl.Execute(w, data) + if err != nil { + InternalServerError(w, err) + return + } + + default: + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + } +} diff --git a/app/http_handlers/url_forward.go b/app/http_handlers/url_forward.go new file mode 100644 index 0000000..d7f747a --- /dev/null +++ b/app/http_handlers/url_forward.go @@ -0,0 +1,26 @@ +package httpHandlers + +import ( + "net/http" + "strings" +) + +func (hl *HandlerForward) UrlForward(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + segments := strings.Split(path, "/") + if len(segments) > 2 { + http.NotFound(w, r) + return + } else if segments[1] == "" { + http.Redirect(w, r, hl.CreateDomain, http.StatusMovedPermanently) + return + } + + key := segments[1] + url, err := hl.DB.GetURL(key) + if err != nil { + http.NotFound(w, r) + return + } + http.Redirect(w, r, url, http.StatusMovedPermanently) +} diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..757eb6e --- /dev/null +++ b/app/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "github.com/aykhans/oh-my-url/app/config" + "github.com/aykhans/oh-my-url/app/db" + "github.com/aykhans/oh-my-url/app/http_handlers" + "net/http" + "sync" +) + +func main() { + config := config.GetAppConfig() + dbCreate := db.GetDB() + dbCreate.Init() + handlerCreate := httpHandlers.HandlerCreate{DB: dbCreate, ForwardDomain: config.FORWARD_DOMAIN} + urlCreateMux := http.NewServeMux() + urlCreateMux.HandleFunc("/", handlerCreate.UrlCreate) + urlCreateMux.HandleFunc("/favicon.ico", httpHandlers.FaviconHandler) + + dbRead := db.GetDB() + handlerForward := httpHandlers.HandlerForward{DB: dbRead, CreateDomain: config.CREATE_DOMAIN} + urlReadMux := http.NewServeMux() + urlReadMux.HandleFunc("/", handlerForward.UrlForward) + urlReadMux.HandleFunc("/favicon.ico", httpHandlers.FaviconHandler) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + panic(http.ListenAndServe(":"+config.LISTEN_PORT_CREATE, urlCreateMux)) + }() + go func() { + defer wg.Done() + panic(http.ListenAndServe(":"+config.LISTEN_PORT_FORWARD, urlReadMux)) + }() + wg.Wait() +} diff --git a/app/templates/favicon.ico b/app/templates/favicon.ico new file mode 100644 index 0000000..2d018e6 Binary files /dev/null and b/app/templates/favicon.ico differ diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..bed3309 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,82 @@ + + + + + Oh My URL + + + + +
+

Oh My URL!

+
+ {{ if .Error }} + + {{ end }} + + {{ if .ShortedURL }} + + {{ end }} + +
+
+ + + diff --git a/app/utils/short_key.go b/app/utils/short_key.go new file mode 100644 index 0000000..6051ead --- /dev/null +++ b/app/utils/short_key.go @@ -0,0 +1,26 @@ +package utils + +import ( + "strings" +) + +const keyCharacters string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +func GenerateKey(n int) string { + var result strings.Builder + base := len(keyCharacters) + + for n > 0 { + n-- + result.WriteByte(keyCharacters[n%base]) + n /= base + } + + key := result.String() + runes := []rune(key) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + + return string(runes) +} diff --git a/app/utils/templates.go b/app/utils/templates.go new file mode 100644 index 0000000..fc999be --- /dev/null +++ b/app/utils/templates.go @@ -0,0 +1,19 @@ +package utils + +import ( + "os" + "path/filepath" +) + +func GetTemplatePaths(filenames ...string) []string { + dir, err := os.Getwd() + if err != nil { + panic(err) + } + + templatePath := filepath.Join(dir, "app", "templates") + for i, filename := range filenames { + filenames[i] = filepath.Join(templatePath, filename) + } + return filenames +} diff --git a/config/nginx/nginx.conf b/config/nginx/nginx.conf new file mode 100644 index 0000000..1eaea90 --- /dev/null +++ b/config/nginx/nginx.conf @@ -0,0 +1,31 @@ +upstream create_server { + server ohmyurl-web:8080; +} + +upstream forward_server { + server ohmyurl-web:8081; +} + +server { + listen 80; + server_name 127.0.0.1; + + location / { + proxy_pass http://create_server; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + } +} + +server { + listen 80; + server_name localhost; + + location / { + proxy_pass http://forward_server; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + } +} \ No newline at end of file diff --git a/config/postgresql/.env.example b/config/postgresql/.env.example new file mode 100644 index 0000000..0892687 --- /dev/null +++ b/config/postgresql/.env.example @@ -0,0 +1,3 @@ +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_DB= \ No newline at end of file diff --git a/docker-compose-postgres.yml b/docker-compose-postgres.yml new file mode 100644 index 0000000..9637c16 --- /dev/null +++ b/docker-compose-postgres.yml @@ -0,0 +1,41 @@ +version: "3.9" + + +services: + ohmyurl-postgres: + container_name: "ohmyurl-postgresql" + image: postgres:16.1-alpine + ports: + - "5432:5432" + env_file: + - ./config/postgresql/.env + volumes: + - ohmyurl_postgresqo_data:/var/lib/postgresql/data + init: true + + ohmyurl-web: + container_name: "ohmyurl-web" + build: . + image: ohmyurl-web:1.1 + env_file: + - ./app/config/.env + ports: + - "8080:8080" + - "8081:8081" + depends_on: + - ohmyurl-postgres + init: true + + ohmyurl-nginx: + container_name: "ohmyurl-nginx" + image: nginx:1.25.3-alpine + ports: + - 80:80 + volumes: + - ./config/nginx/nginx.conf:/etc/nginx/conf.d/default.conf + depends_on: + - ohmyurl-web + init: true + +volumes: + ohmyurl_postgresqo_data: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7fa80e9 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/aykhans/oh-my-url + +go 1.21.4 + +require ( + gorm.io/driver/postgres v1.5.4 + gorm.io/gorm v1.25.5 +) + +require ( + github.com/buraksezer/consistent v0.10.0 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/jackc/pgx/v5 v5.5.2 // 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 + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..eed01a1 --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/buraksezer/consistent v0.10.0 h1:hqBgz1PvNLC5rkWcEBVAL9dFMBWz6I0VgUCW25rrZlU= +github.com/buraksezer/consistent v0.10.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +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/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/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/pgx/v5 v5.5.2 h1:iLlpgp4Cp/gC9Xuscl7lFL1PhhW+ZLtXZcrfCt4C3tA= +github.com/jackc/pgx/v5 v5.5.2/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +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/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=