From f59cb4f927e96b24a18061b640766b6ca1a3fdbe Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Fri, 19 Jan 2024 16:11:54 +0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20first=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Dockerfile | 16 ++++++ README.md | 1 + app/config/.env.example | 10 ++++ app/config/settings.go | 78 +++++++++++++++++++++++++++++ app/db/base.go | 48 ++++++++++++++++++ app/db/cassandra.go | 1 + app/db/mongodb.go | 1 + app/db/postgres.go | 72 +++++++++++++++++++++++++++ app/http_handlers/base.go | 27 ++++++++++ app/http_handlers/url_create.go | 75 ++++++++++++++++++++++++++++ app/http_handlers/url_forward.go | 26 ++++++++++ app/main.go | 37 ++++++++++++++ app/templates/favicon.ico | Bin 0 -> 4612 bytes app/templates/index.html | 82 +++++++++++++++++++++++++++++++ app/utils/short_key.go | 26 ++++++++++ app/utils/templates.go | 19 +++++++ config/nginx/nginx.conf | 31 ++++++++++++ config/postgresql/.env.example | 3 ++ docker-compose-postgres.yml | 41 ++++++++++++++++ go.mod | 22 +++++++++ go.sum | 50 +++++++++++++++++++ 22 files changed, 667 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app/config/.env.example create mode 100644 app/config/settings.go create mode 100644 app/db/base.go create mode 100644 app/db/cassandra.go create mode 100644 app/db/mongodb.go create mode 100644 app/db/postgres.go create mode 100644 app/http_handlers/base.go create mode 100644 app/http_handlers/url_create.go create mode 100644 app/http_handlers/url_forward.go create mode 100644 app/main.go create mode 100644 app/templates/favicon.ico create mode 100644 app/templates/index.html create mode 100644 app/utils/short_key.go create mode 100644 app/utils/templates.go create mode 100644 config/nginx/nginx.conf create mode 100644 config/postgresql/.env.example create mode 100644 docker-compose-postgres.yml create mode 100644 go.mod create mode 100644 go.sum 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 0000000000000000000000000000000000000000..2d018e65b429760569257e4c3766a0b3c11d47b1 GIT binary patch literal 4612 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 literal 0 HcmV?d00001 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=