├── README.md ├── LICENSE ├── api.md └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # muffin-rest-api 2 | REST API with JWT on Golang for my pet project 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 devpew 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | # Muffin API 2 | 3 | ## Authentication 4 | 5 | ### POST 6 | 7 | All the Merhods in the API protected by JSON Web Tokens (JTW). 8 | 9 | We must send field "Token" in Header. 10 | 11 | The value of the token shoud be generated based on the passphrase. The passphrase placed in the REST API server. 12 | 13 | To get authenticate token we should use this request 14 | 15 | ``` 16 | /login 17 | ``` 18 | 19 | and send Username and Password, like that: 20 | 21 | ``` 22 | { 23 | "Username": "user", 24 | "Password": "pass" 25 | } 26 | ``` 27 | 28 | If user and password are correct we will get 200 OK and reqponse with our key, and after that we should save it in localStorage: 29 | 30 | ``` 31 | HTTP/1.1 200 OK 32 | Access-Control-Allow-Origin: * 33 | Content-Type: application/json 34 | Date: Fri, 10 Jul 2020 20:42:19 GMT 35 | Content-Length: 163 36 | Connection: close 37 | 38 | "eykjlskdfijiI3djk3kd1idjkjfd.eysljFJejejf9DJlePppdjf8fkdjjdks.Dlfklksf183oowlsfid9edjoksrjf" 39 | ``` 40 | 41 | If user or pass is not correct we will get 200 OK with the error: 42 | 43 | ``` 44 | HTTP/1.1 200 OK 45 | Access-Control-Allow-Origin: * 46 | Content-Type: application/json 47 | Date: Sun, 12 Jul 2020 15:20:25 GMT 48 | Content-Length: 8 49 | Connection: close 50 | 51 | "error" 52 | ``` 53 | 54 | 55 | ## Funds 56 | 57 | ### GET 58 | 59 | GET shares rub - `/funds/rub/shares` 60 | 61 | GET bonds rub - `/funds/rub/bonds` 62 | 63 | GET shares use - `/funds/usd/shares` 64 | 65 | GET bonds usd - `/funds/usd/bonds` 66 | 67 | http://127.0.0.1:8000/funds/rub/shares 68 | 69 | responce: 70 | 71 | ``` 72 | [ 73 | { 74 | "id": 131, 75 | "name": "AGRO", 76 | "ticker": "AGRO", 77 | "amount": 32, 78 | "priceperitem": 103.48, 79 | "purchaseprice": 31360, 80 | "pricecurrent": 33113.6, 81 | "percentchanges": 5.5918367346938735, 82 | "yearlyinvestment": 10.327319061645275, 83 | "clearmoney": 1721.3631999999986, 84 | "datepurchase": "2019-12-09T13:59:53.66277+03:00", 85 | "datelastupdate": "2020-06-30T18:00:49+03:00", 86 | "type": "share" 87 | }, 88 | { 89 | "id": 145, 90 | "name": "Детский мир", 91 | "ticker": "DSKY", 92 | "amount": 450, 93 | "priceperitem": 103.48, 94 | "purchaseprice": 41885, 95 | "pricecurrent": 46566, 96 | "percentchanges": 11.1758386057061, 97 | "yearlyinvestment": 40.81456061148672, 98 | "clearmoney": 4636.7745, 99 | "datepurchase": "2020-03-13T13:59:53.66277+03:00", 100 | "datelastupdate": "2020-06-19T23:54:26.655833+03:00", 101 | "type": "share" 102 | }, 103 | { 104 | "id": 138, 105 | "name": "Газпром", 106 | "ticker": "GAZP", 107 | "amount": 300, 108 | "priceperitem": 195.81, 109 | "purchaseprice": 68100, 110 | "pricecurrent": 58743, 111 | "percentchanges": -13.740088105726873, 112 | "yearlyinvestment": -35.809394273127744, 113 | "clearmoney": -9420.4215, 114 | "datepurchase": "2020-01-31T13:59:53.66277+03:00", 115 | "datelastupdate": "2020-06-19T23:59:27.189249+03:00", 116 | "type": "share" 117 | } 118 | ] 119 | ``` 120 | 121 | ### POST 122 | 123 | `POST /funds/rub` – to add new fund 124 | 125 | Need to send: 126 | 127 | ``` 128 | { 129 | "name": "РусАгро", 130 | "ticker": "AGRO", 131 | "amount": 32, 132 | "priceperitem": 660, 133 | "datepurchase": "2020-06-19T23:59:27.189249+03:00", 134 | "type": "bond" 135 | } 136 | ``` 137 | 138 | ### EDIT 139 | 140 | `PUT /funds/rub/{id}` 141 | 142 | `PUT /funds/usd/{id}` 143 | 144 | Example URL – http://127.0.0.1:8000/funds/rub/131 145 | 146 | ``` 147 | { 148 | "Id": 131, 149 | "name": "РусАгро", 150 | "ticker": "AGRO", 151 | "amount": 32, 152 | "priceperitem": 660, 153 | "datepurchase": "2020-06-19T23:59:27.189249+03:00", 154 | "type": "share" 155 | } 156 | ``` 157 | 158 | It's necessary to send field Id in the body. If the Id field in the body and {id} in the URL is not equal, it will not work. 159 | 160 | It's necessary to pass TYPE field. In other way we will not understand how to save our data. 161 | 162 | Fields `name`, `ticker`, `amount`, `priceperitem`, `datepurchase` can be send by one. 163 | 164 | ### DELETE 165 | 166 | ``` 167 | /funds/rub/{id} 168 | ``` 169 | 170 | ``` 171 | /funds/usd/{id} 172 | ``` 173 | 174 | http://127.0.0.1:8000/funds/rub/239 – URL 175 | 176 | In BODY we need to send: 177 | 178 | ``` 179 | { 180 | "id": 239 181 | } 182 | ``` 183 | 184 | In will not work if `{id}` in URL and `"Id"` in body is not the same. 185 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/dgrijalva/jwt-go" 12 | "github.com/gorilla/mux" 13 | _ "github.com/joho/godotenv" 14 | _ "github.com/lib/pq" 15 | "github.com/shopspring/decimal" 16 | ) 17 | 18 | var mySigningKey = []byte("johenews") 19 | 20 | type Funds struct { 21 | Id int `json:"id"` 22 | Name string `json:"name"` 23 | Ticker string `json:"ticker"` 24 | Amount int64 `json:"amount"` 25 | PricePerItem decimal.Decimal `json:"priceperitem"` 26 | PurchasePrice decimal.Decimal `json:"purchaseprice"` 27 | PriceCurrent decimal.Decimal `json:"pricecurrent"` 28 | PercentChanges decimal.Decimal `json:"percentchanges"` 29 | YearlyInvestment decimal.Decimal `json:"yearlyinvestment"` 30 | ClearMoney decimal.Decimal `json:"clearmoney"` 31 | DatePurchase time.Time `json:"datepurchase"` 32 | DateLastUpdate time.Time `json:"datelastupdate"` 33 | Type string `json:"type"` 34 | } 35 | 36 | type User struct { 37 | Username string `json:"username"` 38 | Password string `json:"password"` 39 | } 40 | 41 | var user = User{ 42 | Username: "1", 43 | Password: "1", 44 | } 45 | 46 | func isAuthorized(endpoint func(http.ResponseWriter, *http.Request)) http.Handler { 47 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 48 | 49 | w.Header().Set("Connection", "close") 50 | defer r.Body.Close() 51 | 52 | if r.Header["Token"] != nil { 53 | token, err := jwt.Parse(r.Header["Token"][0], func(token *jwt.Token) (interface{}, error) { 54 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 55 | return nil, fmt.Errorf("There was an error") 56 | } 57 | return mySigningKey, nil 58 | }) 59 | 60 | if err != nil { 61 | w.WriteHeader(http.StatusForbidden) 62 | w.Header().Add("Content-Type", "application/json") 63 | return 64 | } 65 | 66 | if token.Valid { 67 | endpoint(w, r) 68 | } 69 | 70 | } else { 71 | fmt.Fprintf(w, "Not Authorized") 72 | } 73 | }) 74 | } 75 | 76 | func main() { 77 | fmt.Println("My Simple Server") 78 | 79 | r := mux.NewRouter() 80 | 81 | ////////////////////////////////////////////////// 82 | //////////////////// FUNDS RUB /////////////////// 83 | ////////////////////////////////////////////////// 84 | 85 | //GET 86 | r.Handle("/funds/rub/shares", isAuthorized(getRUBFundsShares)).Methods("GET") 87 | r.Handle("/funds/rub/bonds", isAuthorized(getRUBFundsBonds)).Methods("GET") 88 | 89 | r.Handle("/funds/usd/shares", isAuthorized(getUSDFundsShares)).Methods("GET") 90 | r.Handle("/funds/usd/bonds", isAuthorized(getUSDFundsBonds)).Methods("GET") 91 | 92 | //POST 93 | r.Handle("/funds/rub", isAuthorized(createFund)).Methods("POST") 94 | 95 | ////////////////////////////////////////////////// 96 | ////////////////////// Login ///////////////////// 97 | ////////////////////////////////////////////////// 98 | 99 | //POST 100 | r.HandleFunc("/login", login).Methods("POST") 101 | 102 | log.Fatal(http.ListenAndServe(":8000", r)) 103 | } 104 | 105 | func getRUBFundsShares(w http.ResponseWriter, r *http.Request) { 106 | w.Header().Set("Access-Control-Allow-Origin", "*") 107 | w.Header().Set("Content-Type", "application/json") 108 | var ArrShares = myRUBCurrentFunds("share") 109 | json.NewEncoder(w).Encode(ArrShares) 110 | } 111 | 112 | func getRUBFundsBonds(w http.ResponseWriter, r *http.Request) { 113 | w.Header().Set("Access-Control-Allow-Origin", "*") 114 | w.Header().Set("Content-Type", "application/json") 115 | var ArrShares = myRUBCurrentFunds("bond") 116 | json.NewEncoder(w).Encode(ArrShares) 117 | } 118 | 119 | func getUSDFundsShares(w http.ResponseWriter, r *http.Request) { 120 | w.Header().Set("Access-Control-Allow-Origin", "*") 121 | w.Header().Set("Content-Type", "application/json") 122 | var ArrShares = myUSDCurrentFunds("share") 123 | json.NewEncoder(w).Encode(ArrShares) 124 | } 125 | 126 | func getUSDFundsBonds(w http.ResponseWriter, r *http.Request) { 127 | w.Header().Set("Access-Control-Allow-Origin", "*") 128 | w.Header().Set("Content-Type", "application/json") 129 | var ArrShares = myUSDCurrentFunds("bond") 130 | json.NewEncoder(w).Encode(ArrShares) 131 | } 132 | 133 | func myRUBCurrentFunds(fundType string) []Funds { 134 | var amountShares []Funds 135 | 136 | db, err := sql.Open("postgres", "postgres://postgres:1234@localhost/fin?sslmode=disable") 137 | 138 | if err != nil { 139 | log.Fatal(err) 140 | } 141 | 142 | rows, err := db.Query("SELECT * FROM funds WHERE type = $1 ORDER BY ticker ASC", fundType) 143 | 144 | for rows.Next() { 145 | bk := Funds{} 146 | err = rows.Scan(&bk.Id, &bk.Name, &bk.Ticker, &bk.Amount, &bk.PricePerItem, &bk.PurchasePrice, &bk.PriceCurrent, &bk.PercentChanges, &bk.YearlyInvestment, &bk.ClearMoney, &bk.DatePurchase, &bk.DateLastUpdate, &bk.Type) 147 | if err != nil { 148 | fmt.Println(err) 149 | } 150 | amountShares = append(amountShares, bk) 151 | } 152 | 153 | defer rows.Close() 154 | 155 | return amountShares 156 | } 157 | 158 | func myUSDCurrentFunds(fundType string) []Funds { 159 | var amountShares []Funds 160 | db, err := sql.Open("postgres", "postgres://postgres:1234@localhost/fin?sslmode=disable") 161 | 162 | if err != nil { 163 | log.Fatal(err) 164 | } 165 | 166 | rows, err := db.Query("SELECT * FROM fundsusd WHERE type = $1 ORDER BY ticker ASC", fundType) 167 | 168 | for rows.Next() { 169 | bk := Funds{} 170 | err = rows.Scan(&bk.Id, &bk.Name, &bk.Ticker, &bk.Amount, &bk.PricePerItem, &bk.PurchasePrice, &bk.PriceCurrent, &bk.PercentChanges, &bk.YearlyInvestment, &bk.ClearMoney, &bk.DatePurchase, &bk.DateLastUpdate, &bk.Type) 171 | if err != nil { 172 | fmt.Println(err) 173 | } 174 | amountShares = append(amountShares, bk) 175 | } 176 | 177 | defer rows.Close() 178 | 179 | return amountShares 180 | } 181 | 182 | func createFund(w http.ResponseWriter, r *http.Request) { 183 | w.Header().Set("Access-Control-Allow-Origin", "*") 184 | w.Header().Set("Content-Type", "application/json") 185 | var fund Funds 186 | json.NewDecoder(r.Body).Decode(&fund) 187 | addNewFunds(fund) 188 | } 189 | 190 | func addNewFunds(data Funds) { 191 | 192 | db, err := sql.Open("postgres", "postgres://postgres:1234@localhost/fin?sslmode=disable") 193 | 194 | if err != nil { 195 | log.Fatal(err) 196 | } 197 | 198 | _, err = db.Exec("INSERT INTO funds (name, ticker, amount, priceperitem, purchaseprice, pricecurrent, percentchanges, yearlyinvestment, clearmoney, datePurchase, dateLastUpdate, type) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)", 199 | data.Name, 200 | data.Ticker, 201 | data.Amount, 202 | data.PricePerItem, 203 | data.PurchasePrice, 204 | data.PriceCurrent, 205 | data.PercentChanges, 206 | data.YearlyInvestment, 207 | data.ClearMoney, 208 | data.DatePurchase, 209 | time.Now(), 210 | data.Type) 211 | 212 | if err != nil { 213 | fmt.Println(err.Error()) 214 | fmt.Println("addNewF crashed") 215 | return 216 | } 217 | } 218 | 219 | func login(w http.ResponseWriter, r *http.Request) { 220 | w.Header().Set("Access-Control-Allow-Origin", "*") 221 | w.Header().Set("Content-Type", "application/json") 222 | var u User 223 | json.NewDecoder(r.Body).Decode(&u) 224 | checkLogin(u) 225 | } 226 | 227 | func checkLogin(u User) string { 228 | 229 | if user.Username != u.Username || user.Password != u.Password { 230 | fmt.Println("NOT CORRECT") 231 | err := "error" 232 | return err 233 | } 234 | 235 | validToken, err := GenerateJWT() 236 | //fmt.Println(validToken) 237 | 238 | if err != nil { 239 | fmt.Println(err) 240 | } 241 | 242 | return validToken 243 | } 244 | 245 | func GenerateJWT() (string, error) { 246 | token := jwt.New(jwt.SigningMethodHS256) 247 | 248 | claims := token.Claims.(jwt.MapClaims) 249 | 250 | claims["authorized"] = true 251 | claims["user"] = "Elliot Forbes" 252 | claims["exp"] = time.Now().Add(time.Hour * 2160).Unix() 253 | 254 | tokenString, err := token.SignedString(mySigningKey) 255 | 256 | if err != nil { 257 | fmt.Errorf("Something went wrong: %s", err.Error()) 258 | } 259 | 260 | return tokenString, nil 261 | } 262 | --------------------------------------------------------------------------------