├── .env.example ├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── controllers ├── job_controller.go └── user_controller.go ├── database └── database.sql ├── main.go ├── models ├── job.go └── user.go ├── repositories ├── job_repository.go └── user_repository.go ├── requests └── requests.go ├── routes └── routes.go └── utils ├── caching └── caching.go ├── crypto └── crypto.go ├── database └── database.go └── validate └── validate.go /.env.example: -------------------------------------------------------------------------------- 1 | PGHOST=localhost 2 | PGUSER=postgres 3 | PGPASS=postgres 4 | PGDB=std_lib_golang_api 5 | PGPORT=5432 6 | 7 | REDIS_ADDR=127.0.0.1:PORT 8 | REDIS_PASS= 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.swp 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 15 | .glide/ 16 | 17 | vendor/ 18 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/go-redis/redis" 6 | packages = [".","internal","internal/consistenthash","internal/hashtag","internal/pool","internal/proto"] 7 | revision = "e5e021257bfc6d5fbf0beac33e30194311fb189f" 8 | version = "v6.7.4" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/lib/pq" 13 | packages = [".","oid"] 14 | revision = "83612a56d3dd153a94a629cd64925371c9adad78" 15 | 16 | [[projects]] 17 | branch = "master" 18 | name = "golang.org/x/crypto" 19 | packages = ["pbkdf2","scrypt"] 20 | revision = "94eea52f7b742c7cbe0b03b22f0c4c8631ece122" 21 | 22 | [solve-meta] 23 | analyzer-name = "dep" 24 | analyzer-version = 1 25 | inputs-digest = "d5e39a41941d53c74b0acf1f5a5c1c94fbc41f76773cd617b1e95ba7e46c3772" 26 | solver-name = "gps-cdcl" 27 | solver-version = 1 28 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | branch = "master" 26 | name = "github.com/lib/pq" 27 | 28 | [[constraint]] 29 | name = "github.com/go-redis/redis" 30 | version = "6.7.4" 31 | -------------------------------------------------------------------------------- /controllers/job_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "path" 10 | "strconv" 11 | 12 | "github.com/rymccue/golang-standard-lib-rest-api/repositories" 13 | "github.com/rymccue/golang-standard-lib-rest-api/requests" 14 | "github.com/rymccue/golang-standard-lib-rest-api/utils/caching" 15 | ) 16 | 17 | type JobController struct { 18 | DB *sql.DB 19 | Cache caching.Cache 20 | } 21 | 22 | func NewJobController(db *sql.DB, c caching.Cache) *JobController { 23 | return &JobController{ 24 | DB: db, 25 | Cache: c, 26 | } 27 | } 28 | 29 | func (jc *JobController) Create(w http.ResponseWriter, r *http.Request) { 30 | if r.Method != "POST" { 31 | http.Error(w, "Not found", http.StatusNotFound) 32 | return 33 | } 34 | token := r.Header.Get("token") 35 | userIDStr, err := jc.Cache.Get(fmt.Sprintf("token_%s", token)) 36 | if err != nil { 37 | http.Error(w, "Invalid token", http.StatusForbidden) 38 | return 39 | } 40 | userID, err := strconv.Atoi(userIDStr) 41 | if err != nil { 42 | log.Fatalf("Convert user id to int: %s", err) 43 | http.Error(w, "", http.StatusInternalServerError) 44 | return 45 | } 46 | decoder := json.NewDecoder(r.Body) 47 | var cjr requests.CreateJobRequest 48 | err = decoder.Decode(&cjr) 49 | if err != nil { 50 | http.Error(w, "Invalid request body", http.StatusBadRequest) 51 | return 52 | } 53 | _, err = repositories.CreateJob(jc.DB, cjr.Title, cjr.Description, userID) 54 | if err != nil { 55 | log.Fatalf("Creating a job: %s", err) 56 | http.Error(w, "", http.StatusInternalServerError) 57 | return 58 | } 59 | w.WriteHeader(http.StatusCreated) 60 | } 61 | 62 | func (jc *JobController) Job(w http.ResponseWriter, r *http.Request) { 63 | if r.Method == "POST" { 64 | http.Error(w, "Not Found", http.StatusNotFound) 65 | return 66 | } 67 | jobID, err := strconv.Atoi(path.Base(r.URL.Path)) 68 | if err != nil { 69 | http.Error(w, "Not Found", http.StatusNotFound) 70 | return 71 | } 72 | job, err := repositories.GetJobByID(jc.DB, jobID) 73 | if err != nil { 74 | http.Error(w, "Not Found", http.StatusNotFound) 75 | return 76 | } 77 | if r.Method == "GET" { 78 | w.Header().Set("Content-Type", "application/json") 79 | json.NewEncoder(w).Encode(job) 80 | } 81 | token := r.Header.Get("token") 82 | userIDStr, err := jc.Cache.Get(fmt.Sprintf("token_%s", token)) 83 | if err != nil { 84 | http.Error(w, "Invalid token", http.StatusForbidden) 85 | return 86 | } 87 | userID, err := strconv.Atoi(userIDStr) 88 | if err != nil { 89 | log.Fatalf("Convert user id to int: %s", err) 90 | http.Error(w, "", http.StatusInternalServerError) 91 | return 92 | } 93 | if userID != job.UserID { 94 | http.Error(w, "Unauthorized", http.StatusUnauthorized) 95 | return 96 | } 97 | if r.Method == "PUT" { 98 | decoder := json.NewDecoder(r.Body) 99 | var ujr requests.UpdateJobRequest 100 | err = decoder.Decode(&ujr) 101 | if err != nil { 102 | http.Error(w, "Invalid request body", http.StatusBadRequest) 103 | return 104 | } 105 | err = repositories.UpdateJob(jc.DB, job.ID, ujr.Title, ujr.Description) 106 | if err != nil { 107 | log.Fatalf("Updating a job: %s", err) 108 | http.Error(w, "", http.StatusInternalServerError) 109 | return 110 | } 111 | } 112 | if r.Method == "DELETE" { 113 | err = repositories.DeleteJob(jc.DB, job.ID) 114 | if err != nil { 115 | http.Error(w, "", http.StatusInternalServerError) 116 | return 117 | } 118 | } 119 | } 120 | 121 | func (jc *JobController) Feed(w http.ResponseWriter, r *http.Request) { 122 | var err error 123 | if r.Method != "GET" { 124 | http.Error(w, "Not found", http.StatusNotFound) 125 | return 126 | } 127 | page := 1 128 | pageStr, ok := r.URL.Query()["page"] 129 | if ok { 130 | page, err = strconv.Atoi(pageStr[0]) 131 | if err != nil { 132 | page = 1 133 | } 134 | } 135 | 136 | resultsPerPage := 10 137 | resultsPerPageStr, ok := r.URL.Query()["results_per_page"] 138 | if ok { 139 | resultsPerPage, err = strconv.Atoi(resultsPerPageStr[0]) 140 | if err != nil { 141 | resultsPerPage = 1 142 | } 143 | } 144 | jobs, err := repositories.GetJobs(jc.DB, page, resultsPerPage) 145 | w.Header().Set("Content-Type", "application/json") 146 | if err != nil { 147 | http.Error(w, "", http.StatusInternalServerError) 148 | return 149 | } 150 | json.NewEncoder(w).Encode(jobs) 151 | } 152 | -------------------------------------------------------------------------------- /controllers/user_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/rymccue/golang-standard-lib-rest-api/repositories" 13 | "github.com/rymccue/golang-standard-lib-rest-api/requests" 14 | "github.com/rymccue/golang-standard-lib-rest-api/utils/caching" 15 | "github.com/rymccue/golang-standard-lib-rest-api/utils/crypto" 16 | ) 17 | 18 | type UserController struct { 19 | DB *sql.DB 20 | Cache caching.Cache 21 | } 22 | 23 | func NewUserController(db *sql.DB, c caching.Cache) *UserController { 24 | return &UserController{ 25 | DB: db, 26 | Cache: c, 27 | } 28 | } 29 | 30 | func (jc *UserController) Register(w http.ResponseWriter, r *http.Request) { 31 | if r.Method != "POST" { 32 | http.Error(w, "Not found", http.StatusNotFound) 33 | return 34 | } 35 | decoder := json.NewDecoder(r.Body) 36 | var rr requests.RegisterRequest 37 | err := decoder.Decode(&rr) 38 | if err != nil { 39 | http.Error(w, "Invalid request body", http.StatusBadRequest) 40 | return 41 | } 42 | id, err := repositories.CreateUser(jc.DB, rr.Email, rr.Name, rr.Password) 43 | if err != nil { 44 | log.Fatalf("Add user to database error: %s", err) 45 | http.Error(w, "", http.StatusInternalServerError) 46 | return 47 | } 48 | token, err := crypto.GenerateToken() 49 | if err != nil { 50 | log.Fatalf("Generate token Error: %s", err) 51 | http.Error(w, "", http.StatusInternalServerError) 52 | return 53 | } 54 | oneMonth := time.Duration(60*60*24*30) * time.Second 55 | err = jc.Cache.Set(fmt.Sprintf("token_%s", token), strconv.Itoa(id), oneMonth) 56 | if err != nil { 57 | log.Fatalf("Add token to redis Error: %s", err) 58 | http.Error(w, "", http.StatusInternalServerError) 59 | return 60 | } 61 | p := map[string]string{ 62 | "token": token, 63 | } 64 | w.Header().Set("Content-Type", "application/json") 65 | json.NewEncoder(w).Encode(p) 66 | } 67 | 68 | func (jc *UserController) Login(w http.ResponseWriter, r *http.Request) { 69 | if r.Method != "POST" { 70 | http.Error(w, "Not found", http.StatusNotFound) 71 | return 72 | } 73 | decoder := json.NewDecoder(r.Body) 74 | var lr requests.LoginRequest 75 | err := decoder.Decode(&lr) 76 | if err != nil { 77 | http.Error(w, "Invalid request body", http.StatusBadRequest) 78 | return 79 | } 80 | user, err := repositories.GetPrivateUserDetailsByEmail(jc.DB, lr.Email) 81 | if err != nil { 82 | if err == sql.ErrNoRows { 83 | http.Error(w, "Invalid username or password", http.StatusBadRequest) 84 | return 85 | } 86 | log.Fatalf("Create User Error: %s", err) 87 | http.Error(w, "", http.StatusInternalServerError) 88 | return 89 | } 90 | password := crypto.HashPassword(lr.Password, user.Salt) 91 | if user.Password != password { 92 | http.Error(w, "Invalid username or password", http.StatusBadRequest) 93 | return 94 | } 95 | token, err := crypto.GenerateToken() 96 | if err != nil { 97 | log.Fatalf("Create User Error: %s", err) 98 | http.Error(w, "", http.StatusInternalServerError) 99 | return 100 | } 101 | oneMonth := time.Duration(60*60*24*30) * time.Second 102 | err = jc.Cache.Set(fmt.Sprintf("token_%s", token), strconv.Itoa(user.ID), oneMonth) 103 | if err != nil { 104 | log.Fatalf("Create User Error: %s", err) 105 | http.Error(w, "", http.StatusInternalServerError) 106 | return 107 | } 108 | p := map[string]string{ 109 | "token": token, 110 | } 111 | w.Header().Set("Content-Type", "application/json") 112 | json.NewEncoder(w).Encode(p) 113 | } 114 | -------------------------------------------------------------------------------- /database/database.sql: -------------------------------------------------------------------------------- 1 | create table users ( 2 | id serial primary key, 3 | name varchar(60) not null, 4 | email varchar(150) not null, 5 | password char(64) not null, 6 | salt char(32) not null, 7 | created_at timestamp default current_timestamp 8 | ); 9 | 10 | create table jobs ( 11 | id serial primary key, 12 | title varchar(150) not null, 13 | description text not null, 14 | user_id int not null, 15 | created_at timestamp default current_timestamp 16 | ); 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/rymccue/golang-standard-lib-rest-api/controllers" 9 | "github.com/rymccue/golang-standard-lib-rest-api/routes" 10 | "github.com/rymccue/golang-standard-lib-rest-api/utils/caching" 11 | "github.com/rymccue/golang-standard-lib-rest-api/utils/database" 12 | ) 13 | 14 | func main() { 15 | db, err := database.Connect(os.Getenv("PGUSER"), os.Getenv("PGPASS"), os.Getenv("PGDB"), os.Getenv("PGHOST"), os.Getenv("PGPORT")) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | cache := &caching.Redis{ 20 | Client: caching.Connect(os.Getenv("REDIS_ADDR"), os.Getenv("REDIS_PASSWORD"), 0), 21 | } 22 | 23 | userController := controllers.NewUserController(db, cache) 24 | jobController := controllers.NewJobController(db, cache) 25 | 26 | mux := http.NewServeMux() 27 | routes.CreateRoutes(mux, userController, jobController) 28 | 29 | if err := http.ListenAndServe(":8000", mux); err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /models/job.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Job struct { 4 | ID int `json:"id"` 5 | Title string `json:"title"` 6 | Description string `json:"description"` 7 | UserID int `json:"user_id"` 8 | } 9 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | ID int `json:"id"` 5 | Email string `json:"email"` 6 | Name string `json:"name"` 7 | } 8 | 9 | type PrivateUserDetails struct { 10 | ID int 11 | Password string 12 | Salt string 13 | } 14 | -------------------------------------------------------------------------------- /repositories/job_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/rymccue/golang-standard-lib-rest-api/models" 6 | ) 7 | 8 | func CreateJob(db *sql.DB, title, description string, userID int) (int, error) { 9 | const query = ` 10 | insert into jobs ( 11 | title, 12 | description, 13 | user_id 14 | ) values ( 15 | $1, 16 | $2, 17 | $3 18 | ) returning id 19 | ` 20 | var id int 21 | err := db.QueryRow(query, title, description, userID).Scan(&id) 22 | return id, err 23 | } 24 | 25 | func UpdateJob(db *sql.DB, jobID int, title, description string) error { 26 | const query = ` 27 | update jobs set 28 | title = $1, 29 | description = $2 30 | where id = $3 31 | ` 32 | _, err := db.Exec(query, title, description, jobID) 33 | return err 34 | } 35 | 36 | func DeleteJob(db *sql.DB, id int) error { 37 | const query = `delete from jobs where id = $1` 38 | _, err := db.Exec(query, id) 39 | return err 40 | } 41 | 42 | func GetJobByID(db *sql.DB, id int) (*models.Job, error) { 43 | const query = ` 44 | select 45 | id, 46 | title, 47 | description, 48 | user_id 49 | from 50 | jobs 51 | where 52 | id = $1 53 | ` 54 | var job models.Job 55 | err := db.QueryRow(query, id).Scan(&job.ID, &job.Title, &job.Description, &job.UserID) 56 | return &job, err 57 | } 58 | 59 | func GetJobs(db *sql.DB, page, resultsPerPage int) ([]*models.Job, error) { 60 | const query = ` 61 | select 62 | id, 63 | title, 64 | description, 65 | user_id 66 | from 67 | jobs 68 | limit $1 offset $2 69 | ` 70 | jobs := make([]*models.Job, 0) 71 | offset := (page - 1) * resultsPerPage 72 | 73 | rows, err := db.Query(query, resultsPerPage, offset) 74 | if err != nil { 75 | return nil, err 76 | } 77 | for rows.Next() { 78 | var job models.Job 79 | err = rows.Scan(&job.ID, &job.Title, &job.Description, &job.UserID) 80 | if err != nil { 81 | return nil, err 82 | } 83 | jobs = append(jobs, &job) 84 | } 85 | return jobs, err 86 | } 87 | -------------------------------------------------------------------------------- /repositories/user_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/rymccue/golang-standard-lib-rest-api/models" 6 | "github.com/rymccue/golang-standard-lib-rest-api/utils/crypto" 7 | ) 8 | 9 | func GetUserByID(db *sql.DB, id int) (*models.User, error) { 10 | const query = ` 11 | select 12 | id, 13 | email, 14 | name 15 | from 16 | users 17 | where 18 | id = $1 19 | ` 20 | var user models.User 21 | err := db.QueryRow(query, id).Scan(&user.ID, &user.Email, &user.Name) 22 | return &user, err 23 | } 24 | 25 | func GetUserByEmail(db *sql.DB, email string) (*models.User, error) { 26 | const query = ` 27 | select 28 | id, 29 | email, 30 | name 31 | from 32 | users 33 | where 34 | email = $1 35 | ` 36 | var user models.User 37 | err := db.QueryRow(query, email).Scan(&user.ID, &user.Email, &user.Name) 38 | return &user, err 39 | } 40 | 41 | func GetPrivateUserDetailsByEmail(db *sql.DB, email string) (*models.PrivateUserDetails, error) { 42 | const query = ` 43 | select 44 | id, 45 | password, 46 | salt 47 | from 48 | users 49 | where 50 | email = $1 51 | ` 52 | var u models.PrivateUserDetails 53 | err := db.QueryRow(query, email).Scan(&u.ID, &u.Password, &u.Salt) 54 | return &u, err 55 | } 56 | 57 | func CreateUser(db *sql.DB, email, name, password string) (int, error) { 58 | const query = ` 59 | insert into users ( 60 | email, 61 | name, 62 | password, 63 | salt 64 | ) values ( 65 | $1, 66 | $2, 67 | $3, 68 | $4 69 | ) returning id 70 | ` 71 | salt := crypto.GenerateSalt() 72 | hashedPassword := crypto.HashPassword(password, salt) 73 | var id int 74 | err := db.QueryRow(query, email, name, hashedPassword, salt).Scan(&id) 75 | return id, err 76 | } 77 | -------------------------------------------------------------------------------- /requests/requests.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | type RegisterRequest struct { 4 | Email string `json:"email"` 5 | Name string `json:"name"` 6 | Password string `json:"password"` 7 | } 8 | 9 | type LoginRequest struct { 10 | Email string `json:"email"` 11 | Password string `json:"password"` 12 | } 13 | 14 | type CreateJobRequest struct { 15 | Title string `json:"title"` 16 | Description string `json:"description"` 17 | } 18 | 19 | type UpdateJobRequest struct { 20 | Title string `json:"title"` 21 | Description string `json:"description"` 22 | } 23 | -------------------------------------------------------------------------------- /routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/rymccue/golang-standard-lib-rest-api/controllers" 7 | ) 8 | 9 | func CreateRoutes(mux *http.ServeMux, uc *controllers.UserController, jc *controllers.JobController) { 10 | mux.HandleFunc("/register", uc.Register) 11 | mux.HandleFunc("/login", uc.Login) 12 | 13 | mux.HandleFunc("/job", jc.Create) 14 | mux.HandleFunc("/job/", jc.Job) 15 | mux.HandleFunc("/job/feed", jc.Feed) 16 | } 17 | -------------------------------------------------------------------------------- /utils/caching/caching.go: -------------------------------------------------------------------------------- 1 | package caching 2 | 3 | import ( 4 | "github.com/go-redis/redis" 5 | "time" 6 | ) 7 | 8 | type Cache interface { 9 | Get(key string) (string, error) 10 | Set(key, value string, expiration time.Duration) error 11 | } 12 | 13 | type Redis struct { 14 | Client *redis.Client 15 | } 16 | 17 | func Connect(addr, password string, db int) *redis.Client { 18 | return redis.NewClient(&redis.Options{ 19 | Addr: addr, 20 | Password: password, 21 | DB: db, 22 | }) 23 | } 24 | 25 | func (r *Redis) Get(key string) (string, error) { 26 | return r.Client.Get(key).Result() 27 | } 28 | 29 | func (r *Redis) Set(key, value string, expiration time.Duration) error { 30 | return r.Client.Set(key, value, expiration).Err() 31 | } 32 | -------------------------------------------------------------------------------- /utils/crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "io" 8 | "log" 9 | 10 | "golang.org/x/crypto/scrypt" 11 | ) 12 | 13 | // GenerateSalt generates a random salt 14 | func GenerateSalt() string { 15 | saltBytes := make([]byte, 16) 16 | _, err := io.ReadFull(rand.Reader, saltBytes) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | salt := make([]byte, 32) 21 | hex.Encode(salt, saltBytes) 22 | return string(salt) 23 | } 24 | 25 | // HashPassword hashes a string 26 | func HashPassword(password, salt string) string { 27 | hashedPasswordBytes, err := scrypt.Key([]byte(password), []byte(salt), 16384, 8, 1, 32) 28 | if err != nil { 29 | log.Fatal("Unable to hash password") 30 | } 31 | hashedPassword := make([]byte, 64) 32 | hex.Encode(hashedPassword, hashedPasswordBytes) 33 | return string(hashedPassword) 34 | } 35 | 36 | func GenerateToken() (string, error) { 37 | b := make([]byte, 64) 38 | _, err := rand.Read(b) 39 | if err != nil { 40 | return "", err 41 | } 42 | str := base64.URLEncoding.EncodeToString(b) 43 | return str, nil 44 | } 45 | -------------------------------------------------------------------------------- /utils/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | _ "github.com/lib/pq" 7 | ) 8 | 9 | func Connect(user, password, dbname, host, port string) (*sql.DB, error) { 10 | connStr := fmt.Sprintf("user=%s password=%s dbname=%s host=%s port=%s", 11 | user, password, dbname, host, port) 12 | return sql.Open("postgres", connStr) 13 | } 14 | -------------------------------------------------------------------------------- /utils/validate/validate.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | func email(email string) bool { 8 | Re := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) 9 | return Re.MatchString(email) 10 | } 11 | --------------------------------------------------------------------------------