This repository has been archived by the owner on Mar 3, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathhandler.go
339 lines (287 loc) · 9.5 KB
/
handler.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
332
333
334
335
336
337
338
339
/*
Copyright (c) 2017 Bitnami
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/gorilla/mux"
"github.com/kubeapps/ratesvc/response"
log "github.com/sirupsen/logrus"
jwt "github.com/dgrijalva/jwt-go"
"github.com/globalsign/mgo/bson"
)
// Params a key-value map of path params
type Params map[string]string
// WithParams can be used to wrap handlers to take an extra arg for path params
type WithParams func(http.ResponseWriter, *http.Request, Params)
func (h WithParams) ServeHTTP(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
h(w, req, vars)
}
const itemCollection = "items"
type item struct {
// Instead of bson.ObjectID, we use a human-friendly identifier (e.g. "stable/wordpress")
ID string `json:"id" bson:"_id,omitempty"`
// Type could be "chart", "function", etc.
Type string `json:"type"`
// List of IDs of Stargazers that will be stored in the database
StargazersIDs []bson.ObjectId `json:"-" bson:"stargazers_ids"`
// Count of the Stargazers which is only exposed in the JSON response
StargazersCount int `json:"stargazers_count" bson:"-"`
// Whether the current user has starred the item, only exposed in the JSON response
HasStarred bool `json:"has_starred" bson:"-"`
// Comments collection
Comments []comment `json:"-"`
}
// User represents user info
type User struct {
ID bson.ObjectId `json:"id" bson:"_id"`
Name string `json:"name"`
Email string `json:"-"`
AvatarURL string `json:"avatar_url" bson:"-"`
}
// Defines a comment object
type comment struct {
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
Text string `json:"text"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
Author *User `json:"author"`
}
// GetStars returns a list of starred items
func GetStars(w http.ResponseWriter, req *http.Request) {
db, closer := dbSession.DB()
defer closer()
var items []*item
if err := db.C(itemCollection).Find(nil).All(&items); err != nil {
log.WithError(err).Error("could not fetch all items")
response.NewErrorResponse(http.StatusInternalServerError, "could not fetch all items").Write(w)
return
}
currentUser, _ := getCurrentUser(req)
for _, it := range items {
it.StargazersCount = len(it.StargazersIDs)
if currentUser != nil {
it.HasStarred = hasStarred(it, currentUser)
}
}
response.NewDataResponse(items).Write(w)
}
// UpdateStar updates the HasStarred attribute on an item
func UpdateStar(w http.ResponseWriter, req *http.Request) {
db, closer := dbSession.DB()
defer closer()
currentUser, err := getCurrentUser(req)
if err != nil {
response.NewErrorResponse(http.StatusUnauthorized, "unauthorized").Write(w)
return
}
// Params validation
var params *item
if err := json.NewDecoder(req.Body).Decode(¶ms); err != nil {
log.WithError(err).Error("could not parse request body")
response.NewErrorResponse(http.StatusBadRequest, "could not parse request body").Write(w)
return
}
if params.ID == "" {
response.NewErrorResponse(http.StatusBadRequest, "id missing in request body").Write(w)
return
}
if params.Type == "" {
params.Type = "chart"
}
var it item
err = db.C(itemCollection).FindId(params.ID).One(&it)
if err != nil {
// Create the item if inexistant
it = *params
if params.HasStarred {
it.StargazersIDs = []bson.ObjectId{currentUser.ID}
}
if err := db.C(itemCollection).Insert(it); err != nil {
log.WithError(err).Error("could not insert item")
response.NewErrorResponse(http.StatusInternalServerError, "internal server error").Write(w)
return
}
} else {
// Otherwise we just need to update the database
op := "$pull"
if params.HasStarred {
// no-op if item is already starred by user
if hasStarred(&it, currentUser) {
response.NewDataResponse(it).WithCode(http.StatusOK).Write(w)
return
}
op = "$push"
}
if err := db.C(itemCollection).UpdateId(it.ID, bson.M{op: bson.M{"stargazers_ids": currentUser.ID}}); err != nil {
log.WithError(err).Error("could not update item")
response.NewErrorResponse(http.StatusInternalServerError, "internal server error").Write(w)
return
}
}
response.NewDataResponse(it).WithCode(http.StatusCreated).Write(w)
}
// GetComments returns a list of comments
func GetComments(w http.ResponseWriter, req *http.Request, params Params) {
db, closer := dbSession.DB()
defer closer()
var it item
itemID := params["repo"] + "/" + params["chartName"]
if err := db.C(itemCollection).FindId(itemID).One(&it); err != nil {
response.NewDataResponse([]int64{}).Write(w)
return
}
for _, cm := range it.Comments {
h := md5.New()
io.WriteString(h, cm.Author.Email)
cm.Author.AvatarURL = fmt.Sprintf("https://s.gravatar.com/avatar/%x", h.Sum(nil))
}
response.NewDataResponse(it.Comments).Write(w)
}
// CreateComment creates a comment and appends the comment to the item.Comments array
func CreateComment(w http.ResponseWriter, req *http.Request, params Params) {
db, closer := dbSession.DB()
defer closer()
currentUser, err := getCurrentUser(req)
if err != nil {
response.NewErrorResponse(http.StatusUnauthorized, "unauthorized").Write(w)
return
}
// Params validation
var cm comment
if err := json.NewDecoder(req.Body).Decode(&cm); err != nil {
log.WithError(err).Error("could not parse request body")
response.NewErrorResponse(http.StatusBadRequest, "could not parse request body").Write(w)
return
}
if cm.Text == "" {
response.NewErrorResponse(http.StatusBadRequest, "text missing in request body").Write(w)
return
}
cm.ID = getNewObjectID()
cm.CreatedAt = getTimestamp()
cm.Author = currentUser
var it item
itemID := params["repo"] + "/" + params["chartName"]
if err = db.C(itemCollection).FindId(itemID).One(&it); err != nil {
// Create the item if inexistant
it.Type = "chart"
it.ID = itemID
it.Comments = []comment{cm}
if err := db.C(itemCollection).Insert(it); err != nil {
log.WithError(err).Error("could not insert item")
response.NewErrorResponse(http.StatusInternalServerError, "internal server error").Write(w)
return
}
} else {
// Append comment to collection
if err = db.C(itemCollection).UpdateId(it.ID, bson.M{"$push": bson.M{"comments": cm}}); err != nil {
log.WithError(err).Error("could not update item")
response.NewErrorResponse(http.StatusInternalServerError, "internal server error").Write(w)
return
}
}
// update avatar_url in response object
h := md5.New()
io.WriteString(h, cm.Author.Email)
cm.Author.AvatarURL = fmt.Sprintf("https://s.gravatar.com/avatar/%x", h.Sum(nil))
response.NewDataResponse(cm).WithCode(http.StatusCreated).Write(w)
}
// DeleteComment delete's an existing comment
func DeleteComment(w http.ResponseWriter, req *http.Request, params Params) {
db, closer := dbSession.DB()
defer closer()
itemID := params["repo"] + "/" + params["chartName"]
commentID := bson.ObjectIdHex(params["commentId"])
currentUser, err := getCurrentUser(req)
if err != nil {
response.NewErrorResponse(http.StatusUnauthorized, "unauthorized").Write(w)
return
}
var it item
if err := db.C(itemCollection).FindId(itemID).One(&it); err != nil {
response.NewErrorResponse(http.StatusNotFound, "comment not found").Write(w)
return
}
var cm comment
for _, c := range it.Comments {
if commentID == c.ID {
cm = c
break
}
}
if cm == (comment{}) {
response.NewErrorResponse(http.StatusNotFound, "comment not found").Write(w)
return
}
// Users can only delete their own comments
if cm.Author.ID != currentUser.ID {
response.NewErrorResponse(http.StatusUnauthorized, "not authorized to delete this comment").Write(w)
return
}
if err = db.C(itemCollection).UpdateId(it.ID, bson.M{"$pull": bson.M{"comments": cm}}); err != nil {
response.NewErrorResponse(http.StatusInternalServerError, "internal server error").Write(w)
return
}
response.NewDataResponse(cm).WithCode(http.StatusAccepted).Write(w)
}
type userClaims struct {
*User
Email string
jwt.StandardClaims
}
var getCurrentUser = func(req *http.Request) (*User, error) {
jwtKey, ok := os.LookupEnv("JWT_KEY")
if !ok {
return nil, errors.New("JWT_KEY not set")
}
cookie, err := req.Cookie("ka_auth")
if err != nil {
return nil, err
}
token, err := jwt.ParseWithClaims(cookie.Value, &userClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(jwtKey), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*userClaims); ok && token.Valid {
claims.User.Email = claims.Email
return claims.User, nil
}
return nil, errors.New("invalid token")
}
var getNewObjectID = func() bson.ObjectId {
return bson.NewObjectId()
}
var getTimestamp = func() time.Time {
return time.Now()
}
// hasStarred returns true if item is starred by the user
func hasStarred(it *item, currentUser *User) bool {
for _, id := range it.StargazersIDs {
if id == currentUser.ID {
return true
}
}
return false
}