├── .env ├── README.md ├── app ├── authentication.go └── error.go ├── controllers ├── authController.go └── todoController.go ├── main.go ├── models ├── accounts.go ├── db.go ├── todo.go └── viewModels │ └── getTodo.go └── utils └── util.go /.env: -------------------------------------------------------------------------------- 1 | db_name = restful 2 | db_pass = password 3 | db_user = postgres 4 | db_type = postgres 5 | db_host = localhost 6 | db_port = 5434 7 | token_password = something 8 | database_url = postgres://postgres:man@localhost:5432/restful 9 | PORT=9090 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golang-restful 2 | 3 | Todo app with RESTful API service using golang. 4 | 5 | 6 | Features 7 | 8 | * GORM 9 | * JWT Access 10 | * PostreSQL 11 | 12 | 13 | Repository will be constantly updated. 14 | -------------------------------------------------------------------------------- /app/authentication.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/dgrijalva/jwt-go" 7 | "golang-todo/models" 8 | u "golang-todo/utils" 9 | "net/http" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | var JwtAuthentication = func(next http.Handler) http.Handler { 15 | 16 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | 18 | notAuth := []string{"/api/user/new", "/api/user/login"} //List of endpoints that doesn't require auth 19 | requestPath := r.URL.Path //current request path 20 | 21 | //check if request does not need authentication, serve the request if it doesn't need it 22 | for _, value := range notAuth { 23 | 24 | if value == requestPath { 25 | next.ServeHTTP(w, r) 26 | return 27 | } 28 | } 29 | 30 | response := make(map[string] interface{}) 31 | tokenHeader := r.Header.Get("Authorization") //Grab the token from the header 32 | 33 | if tokenHeader == "" { //Token is missing, returns with error code 403 Unauthorized 34 | response = u.Message(false, "Missing auth token") 35 | w.WriteHeader(http.StatusForbidden) 36 | w.Header().Add("Content-Type", "application/json") 37 | u.Respond(w, response) 38 | return 39 | } 40 | 41 | splitted := strings.Split(tokenHeader, " ") //The token normally comes in format `Bearer {token-body}`, we check if the retrieved token matched this requirement 42 | if len(splitted) != 2 { 43 | response = u.Message(false, "Invalid/Malformed auth token") 44 | w.WriteHeader(http.StatusForbidden) 45 | w.Header().Add("Content-Type", "application/json") 46 | u.Respond(w, response) 47 | return 48 | } 49 | 50 | tokenPart := splitted[1] //Grab the token part, what we are truly interested in 51 | tk := &models.Token{} 52 | 53 | token, err := jwt.ParseWithClaims(tokenPart, tk, func(token *jwt.Token) (interface{}, error) { 54 | return []byte(os.Getenv("token_password")), nil 55 | }) 56 | 57 | if err != nil { //Malformed token, returns with http code 403 as usual 58 | response = u.Message(false, "Malformed authentication token") 59 | w.WriteHeader(http.StatusForbidden) 60 | w.Header().Add("Content-Type", "application/json") 61 | u.Respond(w, response) 62 | return 63 | } 64 | 65 | if !token.Valid { //Token is invalid, maybe not signed on this server 66 | response = u.Message(false, "Token is not valid.") 67 | w.WriteHeader(http.StatusForbidden) 68 | w.Header().Add("Content-Type", "application/json") 69 | u.Respond(w, response) 70 | return 71 | } 72 | 73 | //Everything went well, proceed with the request and set the caller to the user retrieved from the parsed token 74 | fmt.Sprintf("User %", tk.UserId) //Useful for monitoring 75 | ctx := context.WithValue(r.Context(), "user", tk.UserId) 76 | r = r.WithContext(ctx) 77 | next.ServeHTTP(w, r) //proceed in the middleware chain! 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /app/error.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | u "golang-todo/utils" 5 | "net/http" 6 | ) 7 | 8 | var NotFoundHandler = func(next http.Handler) http.Handler { 9 | 10 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 | w.WriteHeader(http.StatusNotFound) 12 | u.Respond(w, u.Message(false, "This resources was not found on our server")) 13 | next.ServeHTTP(w, r) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /controllers/authController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | u "golang-todo/utils" 6 | "golang-todo/models" 7 | "encoding/json" 8 | ) 9 | 10 | var CreateAccount = func(w http.ResponseWriter, r *http.Request) { 11 | 12 | account := &models.Account{} 13 | err := json.NewDecoder(r.Body).Decode(account) 14 | if err != nil { 15 | u.Respond(w, u.Message(false, "Invalid request")) 16 | return 17 | } 18 | 19 | resp := account.Create() 20 | u.Respond(w, resp) 21 | } 22 | 23 | var Authenticate = func(w http.ResponseWriter, r *http.Request) { 24 | 25 | account := &models.Account{} 26 | err := json.NewDecoder(r.Body).Decode(account) 27 | if err != nil { 28 | u.Respond(w, u.Message(false, "Invalid request")) 29 | return 30 | } 31 | 32 | resp := models.Login(account.Email, account.Password) 33 | u.Respond(w, resp) 34 | } 35 | -------------------------------------------------------------------------------- /controllers/todoController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "golang-todo/models" 6 | u "golang-todo/utils" 7 | "net/http" 8 | ) 9 | 10 | 11 | 12 | 13 | type GetTodoViewModel struct{ 14 | ID uint `json:"id"` 15 | } 16 | 17 | var CreateTodo = func(w http.ResponseWriter, r *http.Request) { 18 | 19 | todo := &models.Todo{} 20 | 21 | err := json.NewDecoder(r.Body).Decode(todo) 22 | if err != nil { 23 | u.Respond(w, u.Message(false, "Invalid request")) 24 | return 25 | } 26 | resp := todo.Create() 27 | u.Respond(w, resp) 28 | } 29 | 30 | var GetTodo = func(w http.ResponseWriter, r *http.Request) { 31 | 32 | info := &GetTodoViewModel{} 33 | 34 | err := json.NewDecoder(r.Body).Decode(info) 35 | 36 | if err != nil { 37 | u.Respond(w, u.Message(false, "Invalid request")) 38 | return 39 | } 40 | 41 | data := models.GetTodo(info.ID) 42 | resp := u.Message(true, "success") 43 | resp["data"] = data 44 | u.Respond(w, resp) 45 | 46 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gorilla/mux" 6 | "golang-todo/app" 7 | "golang-todo/controllers" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | func main() { 13 | 14 | router := mux.NewRouter() 15 | 16 | router.HandleFunc("/api/user/new", controllers.CreateAccount).Methods("POST") 17 | router.HandleFunc("/api/user/login", controllers.Authenticate).Methods("POST") 18 | router.HandleFunc("/api/todo/new", controllers.CreateTodo).Methods("POST") 19 | router.HandleFunc("/api/todo", controllers.GetTodo).Methods("GET") // user/2/contacts 20 | 21 | router.Use(app.JwtAuthentication) 22 | 23 | //router.NotFoundHandler = app.NotFoundHandler 24 | 25 | port := os.Getenv("PORT") 26 | if port == "" { 27 | port = "8000" //localhost 28 | } 29 | 30 | fmt.Println(port) 31 | 32 | err := http.ListenAndServe(":" + port, router) //Launch the app, visit localhost:8000/api 33 | if err != nil { 34 | fmt.Print(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /models/accounts.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/dgrijalva/jwt-go" 5 | "github.com/jinzhu/gorm" 6 | u "golang-todo/utils" 7 | "golang.org/x/crypto/bcrypt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | /* 13 | JWT claims struct 14 | */ 15 | type Token struct { 16 | UserId uint 17 | jwt.StandardClaims 18 | } 19 | 20 | //a struct to rep user account 21 | type Account struct { 22 | gorm.Model 23 | Email string `json:"email"` 24 | Password string `json:"password"` 25 | Token string `json:"token";sql:"-"` 26 | } 27 | 28 | //Validate incoming user details... 29 | func (account *Account) Validate() (map[string] interface{}, bool) { 30 | 31 | if !strings.Contains(account.Email, "@") { 32 | return u.Message(false, "Email address is required"), false 33 | } 34 | 35 | if len(account.Password) < 6 { 36 | return u.Message(false, "Password is required"), false 37 | } 38 | 39 | //Email must be unique 40 | temp := &Account{} 41 | 42 | //check for errors and duplicate emails 43 | err := GetDB().Table("accounts").Where("email = ?", account.Email).First(temp).Error 44 | if err != nil && err != gorm.ErrRecordNotFound { 45 | return u.Message(false, "Connection error. Please retry"), false 46 | } 47 | if temp.Email != "" { 48 | return u.Message(false, "Email address already in use by another user."), false 49 | } 50 | 51 | return u.Message(false, "Requirement passed"), true 52 | } 53 | 54 | func (account *Account) Create() (map[string] interface{}) { 55 | 56 | if resp, ok := account.Validate(); !ok { 57 | return resp 58 | } 59 | 60 | hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost) 61 | account.Password = string(hashedPassword) 62 | 63 | GetDB().Create(account) 64 | 65 | if account.ID <= 0 { 66 | return u.Message(false, "Failed to create account, connection error.") 67 | } 68 | 69 | //Create new JWT token for the newly registered account 70 | tk := &Token{UserId: account.ID} 71 | token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), tk) 72 | tokenString, _ := token.SignedString([]byte(os.Getenv("token_password"))) 73 | account.Token = tokenString 74 | 75 | account.Password = "" 76 | 77 | response := u.Message(true, "Account has been created") 78 | response["account"] = account 79 | return response 80 | } 81 | 82 | func Login(email, password string) (map[string]interface{}) { 83 | 84 | account := &Account{} 85 | err := GetDB().Table("accounts").Where("email = ?", email).First(account).Error 86 | if err != nil { 87 | if err == gorm.ErrRecordNotFound { 88 | return u.Message(false, "Email address not found") 89 | } 90 | return u.Message(false, "Connection error. Please retry") 91 | } 92 | 93 | err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password)) 94 | if err != nil && err == bcrypt.ErrMismatchedHashAndPassword { //Password does not match! 95 | return u.Message(false, "Invalid login credentials. Please try again") 96 | } 97 | //Worked! Logged In 98 | account.Password = "" 99 | 100 | //Create JWT token 101 | tk := &Token{UserId: account.ID} 102 | token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), tk) 103 | tokenString, _ := token.SignedString([]byte(os.Getenv("token_password"))) 104 | account.Token = tokenString //Store the token in the response 105 | 106 | resp := u.Message(true, "Logged In") 107 | resp["account"] = account 108 | return resp 109 | } 110 | 111 | func GetUser(u uint) *Account { 112 | 113 | acc := &Account{} 114 | GetDB().Table("accounts").Where("id = ?", u).First(acc) 115 | if acc.Email == "" { //User not found! 116 | return nil 117 | } 118 | 119 | acc.Password = "" 120 | return acc 121 | } 122 | -------------------------------------------------------------------------------- /models/db.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jinzhu/gorm" 6 | _ "github.com/jinzhu/gorm/dialects/postgres" 7 | "github.com/joho/godotenv" 8 | "os" 9 | ) 10 | 11 | var db *gorm.DB 12 | func init() { 13 | 14 | e := godotenv.Load() 15 | if e != nil { 16 | fmt.Print(e) 17 | } 18 | 19 | username := os.Getenv("db_user") 20 | password := os.Getenv("db_pass") 21 | dbName := os.Getenv("db_name") 22 | dbHost := os.Getenv("db_host") 23 | 24 | 25 | dbUri := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable password=%s", dbHost, username, dbName, password) 26 | fmt.Println(dbUri) 27 | 28 | conn, err := gorm.Open("postgres", dbUri) 29 | if err != nil { 30 | fmt.Print(err) 31 | } 32 | 33 | db = conn 34 | db.Debug().AutoMigrate(&Account{}, &Todo{}) 35 | } 36 | 37 | func GetDB() *gorm.DB { 38 | return db 39 | } 40 | -------------------------------------------------------------------------------- /models/todo.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jinzhu/gorm" 6 | u "golang-todo/utils" 7 | ) 8 | 9 | type Todo struct { 10 | gorm.Model 11 | Title string `json:"title",gorm:"not null"` 12 | UserId uint `json:"user_id"` 13 | } 14 | 15 | func (todo *Todo) Validate() (map[string] interface{}, bool) { 16 | fmt.Print(todo) 17 | if todo.Title == "" { 18 | return u.Message(false, "Title is required!"), false 19 | } 20 | 21 | if todo.UserId <= 0 { 22 | return u.Message(false, "User is not defined"), false 23 | } 24 | 25 | //All the required parameters are present 26 | return u.Message(true, "success"), true 27 | } 28 | 29 | func (todo *Todo) Create() (map[string] interface{}) { 30 | 31 | if resp, ok := todo.Validate(); !ok { 32 | return resp 33 | } 34 | 35 | GetDB().Create(todo) 36 | 37 | resp := u.Message(true, "success") 38 | resp["todo"] = todo 39 | return resp 40 | } 41 | 42 | func GetTodo(id uint) (*Todo) { 43 | 44 | todo := &Todo{} 45 | err := GetDB().Table("todos").Where("id = ?", id).First(todo).Error 46 | if err != nil { 47 | return nil 48 | } 49 | return todo 50 | } 51 | 52 | func GetTodos(user uint) ([]*Todo) { 53 | 54 | todos := make([]*Todo, 0) 55 | err := GetDB().Table("contacts").Where("user_id = ?", user).Find(&todos).Error 56 | if err != nil { 57 | fmt.Println(err) 58 | return nil 59 | } 60 | 61 | return todos 62 | } 63 | 64 | -------------------------------------------------------------------------------- /models/viewModels/getTodo.go: -------------------------------------------------------------------------------- 1 | package viewModels 2 | 3 | type getTodo struct { 4 | id int 5 | } -------------------------------------------------------------------------------- /utils/util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func Message(status bool, message string) (map[string]interface{}) { 9 | return map[string]interface{} {"status" : status, "message" : message} 10 | } 11 | 12 | func Respond(w http.ResponseWriter, data map[string] interface{}) { 13 | w.Header().Add("Content-Type", "application/json") 14 | json.NewEncoder(w).Encode(data) 15 | } --------------------------------------------------------------------------------