/
app.go
331 lines (284 loc) · 7.66 KB
/
app.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
"github.com/q1t/movielist/db"
"github.com/q1t/movielist/middleware"
"html/template"
valid "github.com/asaskevich/govalidator"
"github.com/dgrijalva/jwt-go"
"github.com/jmoiron/sqlx"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
"github.com/lib/pq"
)
const Bearer = "Bearer"
type App struct {
IndexTempl *template.Template
SigningKey []byte
E *echo.Echo
DB *sqlx.DB
}
func NewApp(key []byte, dbConn *sqlx.DB) *App {
app := &App{
SigningKey: key,
E: echo.New(),
DB: dbConn,
}
// default middleware
app.E.Use(mw.Logger())
app.E.Use(mw.Recover())
// serve the static assets
app.E.Static("/public", "./public/")
// API routes
api := app.E.Group("/api")
validUserInp := BindAndValidate(userData{})
login := api.Group("/login")
login.Use(validUserInp)
register := api.Group("/register")
register.Use(validUserInp)
login.Post("", app.userLogin)
register.Post("", app.registerNewAcc)
// Private API routes
auth := middleware.JWTAuth(string(app.SigningKey))
p_api := api.Group("/private")
p_api.Use(auth)
user := p_api.Group("/user")
user.Get("", app.UserEverything)
user.Get("/lists", app.UserLists)
user.Post("/lists/new", app.NewList)
user.Delete("/lists/:id", app.DeleteList)
app.E.Get("/proxy/*", app.proxyImg)
// user routing is on the client side
app.E.ServeFile("/*", "./public/index.html")
return app
}
// FIXME reuse user
func (app *App) DeleteList(c *echo.Context) error {
claims := app.claims(c)
username := claims["username"].(string)
user, err := FindUserByUsername(app.DB, username)
if err != nil {
return err
}
lid, _ := strconv.Atoi(c.Param("id"))
log.Println(lid)
if err = user.DeleteList(app.DB, int64(lid)); err != nil {
return err
}
return c.NoContent(http.StatusOK)
}
// FIXME reuse user
func (app *App) UserLists(c *echo.Context) error {
claims := app.claims(c)
username := claims["username"].(string)
user, err := FindUserByUsername(app.DB, username)
if err != nil {
return err
}
return c.JSON(http.StatusOK, user.ListsAll())
}
// FIXME reuse user
func (app *App) NewList(c *echo.Context) error {
nl := List{}
err := c.Bind(&nl)
if err != nil {
return err
}
claims := app.claims(c)
username := claims["username"].(string)
user, err := FindUserByUsername(app.DB, username)
if err != nil {
return err
}
err = user.NewList(app.DB, nl)
if err != nil {
return err
}
return nil
}
func (app *App) claims(c *echo.Context) map[string]interface{} {
return c.Get("claims").(map[string]interface{})
}
// TODO reduce complexity
func BindAndValidate(data interface{}) echo.MiddlewareFunc {
return func(h echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
// FIXME
input := data.(userData)
err := c.Bind(&input)
if err != nil {
err := fmt.Errorf("Struct binding error %s", err)
return err
}
c.Set("userInput", input)
var response struct {
Errors []string `json:"errors,omitempty"`
}
validated, err := valid.ValidateStruct(input)
if err != nil {
errs := err.(valid.Errors)
for _, v := range errs.Errors() {
// TODO format error
e := strings.Join(strings.Split(v.Error(), ":")[:1], "")
errorMsg := fmt.Sprintf("%s is not valid, min length 4", e)
response.Errors = append(response.Errors, errorMsg)
}
// log.Printf("Validation error: %#v", err)
}
if validated {
if err := h(c); err != nil {
c.Error(err)
}
return nil
}
return c.JSON(http.StatusOK, response)
}
}
}
func hashPwd(s string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(s), bcrypt.DefaultCost)
}
// no spaces among the validatons params
// "email, required" is not correct
// "email,required" is correct
// this struct is used in register and login handlers
type userData struct {
Username string `valid:"stringlength(4|64),required"`
Password string `valid:"stringlength(4|64),required"`
Email string `valid:"email"`
}
func (app *App) UserEverything(c *echo.Context) error {
claims := c.Get("claims").(map[string]interface{})
uid := int64(claims["uid"].(float64))
user, _ := FindUserByID(app.DB, uid)
return c.JSON(http.StatusOK, user)
}
// FIXME too many returns
func (app *App) registerNewAcc(c *echo.Context) error {
var response struct {
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
Bearer string `json:"bearer,omitempty"`
// ID int64 `json:"user_id,omitempty"`
}
// handle casting error
userData := c.Get("userInput").(userData)
hashedPwd, err := hashPwd(userData.Password)
if err != nil {
return err
}
user := User{
Password: string(hashedPwd),
Email: userData.Email,
Username: userData.Username,
}
id, err := app.newUser(user)
user.ID = id
if err != nil {
if ExpectedError(err) {
// return error to the user
// FIXME
response.Error = "UniqueViolation"
return c.JSON(http.StatusOK, response)
} else {
// unexpected error, return code 500
log.Println("Error while creating a user ", err)
return err
}
}
// }
response.Bearer = user.GenSignedString(app.SigningKey)
response.Timestamp = time.Now().Unix()
// response.ID = id
return c.JSON(http.StatusOK, response)
}
func ExpectedError(err error) bool {
if err, ok := err.(*pq.Error); ok {
// TODO make known errors as a map where you just can look
// up for an existing key
if database.UniqueViolationErr == string(err.Code) {
return true
}
}
return false
}
func (app *App) indexSinglePage(c *echo.Context) error {
// add some info to the template
tmpl := app.IndexTempl.Delims("[[", "]]")
err := tmpl.Execute(c.Response(), nil)
return err
}
func (app *App) userLogin(c *echo.Context) error {
userData := c.Get("userInput").(userData)
var response struct {
Bearer string `json:"bearer,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
Error string `json:"error,omitempty"`
}
// Handle credentials here
// If everything is alright (pass is correct)
// proceed further
user, err := FindUserByUsername(app.DB, userData.Username)
if err == nil {
if err := bcrypt.CompareHashAndPassword(
[]byte(user.Password),
[]byte(userData.Password),
); err == nil {
response.Bearer = user.GenSignedString(app.SigningKey)
response.Timestamp = time.Now().Unix()
// user is found and password is correct
return c.JSON(http.StatusOK, response)
}
}
response.Error = "Incorrect credentials"
return c.JSON(http.StatusOK, response)
}
func (app *App) newUser(u User) (int64, error) {
return NewUser(app.DB, u)
}
func (app *App) GenTokenString(uid int64) string {
token := jwt.New(jwt.SigningMethodHS256)
duration := time.Now().Add(time.Hour * 96).Unix()
// Set a header and a claim
token.Header["typ"] = "JWT"
token.Claims["exp"] = duration
token.Claims["auth"] = true
token.Claims["uid"] = uid
// Generate encoded token
t, err := token.SignedString([]byte(app.SigningKey))
if err != nil {
log.Printf("Error generating signed string out of token: %s", err)
// return invalid token [temporarily]
return ""
}
return t
}
// TODO: Enable cache
var cache map[string]*http.Response
func retriveFromCache(destUrl string) *http.Response {
r, err := http.Get(destUrl)
if err != nil {
log.Printf("Error making req for img to proxy it: %s", err)
return nil
}
return r
}
// proxy and cache images from imdb
func (app *App) proxyImg(c *echo.Context) error {
resp := retriveFromCache(c.Param("_*"))
data, _ := ioutil.ReadAll(resp.Body)
for k, v := range resp.Header {
if len(v) > 0 {
c.Response().Header().Add(k, v[0])
}
}
c.Response().Write(data)
return nil
}