-
Notifications
You must be signed in to change notification settings - Fork 0
/
invitations.go
467 lines (414 loc) · 19.5 KB
/
invitations.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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
package main
import (
"bytes"
"database/sql"
"encoding/json"
"log"
"net/http"
"os"
"strconv"
"text/template"
"time"
"github.com/dchest/uniuri"
"github.com/gorilla/mux"
"github.com/lib/pq"
)
// Invite is a struct that models the invitations to a collection
type Invite struct {
InvitationID int64 `json:"invitation_id" db:"invitation_id"`
InviteeEmail string `json:"invitee_email" db:"invitee_email"`
InviteeName string `json:"invitee_name"`
AdminInvite bool `json:"admin_invite" db:"admin_invite"`
InviteSent *time.Time `json:"invite_sent" db:"invite_sent"`
Message string `json:"message"`
}
// AcceptInvite is a struct that is sent to a user confirming an invitation
type AcceptInvite struct {
Email string `json:"email"`
CollectionID int64 `json:"collection_id"`
CollectionName string `json:"collection_name"`
InviterName string `json:"inviter_name"`
InviterEmail string `json:"inviter_email"`
Administrator bool `json:"administrator"`
}
// PendingInvite is a struct that is sent to a logged in user checking their pending invitations
type PendingInvite struct {
InvitationID int64 `json:"invitation_id"`
CollectionName string `json:"collection_name"`
InviterName string `json:"inviter_name"`
InviterEmail string `json:"inviter_email"`
Administrator bool `json:"administrator"`
Token string `json:"token"`
}
// InvitationsHandler handles accepting invitations
func InvitationsHandler(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session")
if err != nil {
log.Printf("Invitations handler - Unable to get session: %v\n", err)
SendError(w, SERVER_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
if r.Method == "GET" {
var token string = r.URL.Query().Get("token")
if token == "" {
SendError(w, `{"error": "No token provided."}`, http.StatusBadRequest)
return
}
// Get invitation from database
var invite AcceptInvite
var inviterID, invitationID int64
var retracted bool
if err := db.QueryRow("SELECT invitation_id, invitee_email, admin_invite, collection_id, inviter_id, retracted FROM invitations WHERE token = $1", token).Scan(&invitationID, &invite.Email, &invite.Administrator, &invite.CollectionID, &inviterID, &retracted); err != nil {
if err == sql.ErrNoRows {
log.Printf("Invitations GET - Attempted to accept invitation with invalid token: %v\n", token)
SendError(w, `{"error": "Invitation not found."}`, http.StatusNotFound)
} else {
log.Printf("Invitations GET - Unable to get invitation from database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
}
return
}
// Check if invitation has been retracted
if retracted {
log.Printf("Invitations POST - User %d attempted to get a retracted invitation %d\n", session.Values["user_id"], invitationID)
SendError(w, `{"error": "This invitation has been retracted and is no longer valid.", "code": "retracted"}`, http.StatusForbidden)
return
}
// Check if the correct user is logged in
if invite.Email != session.Values["email"] {
log.Printf("Invitation GET - User %s logged in to accept invitation for %s.\n", session.Values["email"], invite.Email)
SendError(w, `{"error": "You cannot accept this invitation. Please log out and try again.", "code": "wrong_user"}`, http.StatusForbidden)
return
}
// Get collection from database
if err := db.QueryRow("SELECT name FROM collections WHERE collection_id = $1", invite.CollectionID).Scan(&invite.CollectionName); err != nil {
log.Printf("Invitations GET - Unable to get invitation's collection name from database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
// Get inviter from database
if err := db.QueryRow("SELECT name, email FROM users WHERE user_id = $1", inviterID).Scan(&invite.InviterName, &invite.InviterEmail); err != nil {
log.Printf("Invitations GET - Unable to get invitation's inviter from database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(invite)
return
} else if r.Method == "POST" {
var accept struct {
Token string `json:"token"`
}
err := json.NewDecoder(r.Body).Decode(&accept)
if err != nil {
// If there is something wrong with the request body, return a 400 status
log.Printf("Invitations POST - Unable to parse request body: %v\n", err)
SendError(w, `{"error": "Unable to parse request body."}`, http.StatusBadRequest)
return
}
// Get invitation from database
var collectionID, invitationID int64
var adminInvite, retracted bool
if err := db.QueryRow("SELECT admin_invite, collection_id, retracted, invitation_id FROM invitations WHERE token = $1", accept.Token).Scan(&adminInvite, &collectionID, &retracted, &invitationID); err != nil {
if err == sql.ErrNoRows {
log.Printf("Invitations POST - Attempted to accept invitation with invalid token: %v\n", accept.Token)
SendError(w, `{"error": "Invitation not found."}`, http.StatusNotFound)
} else {
log.Printf("Invitations POST - Unable to accept invitation from database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
}
return
}
// Check if invitation has been retracted
if retracted {
log.Printf("Invitations POST - User %d accepted a retracted invitation %d\n", session.Values["user_id"], invitationID)
SendError(w, `{"error": "This invitation has been retracted and is no longer valid."}`, http.StatusForbidden)
return
}
// Start db transaction
tx, err := db.Begin()
if err != nil {
log.Printf("Invitations POST - Unable to begin database transaction: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
}
// Add the user to this collection
if _, err = tx.Exec("INSERT INTO collection_members VALUES ($1, $2, $3)", session.Values["user_id"], collectionID, adminInvite); err != nil {
log.Printf("Invitations POST - Unable to add user to collection: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
// Delete the invite
if _, err = tx.Exec("DELETE FROM invitations WHERE token = $1", accept.Token); err != nil {
log.Printf("Invitations POST - Remove invite from database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
// Save changes
if err = tx.Commit(); err != nil {
log.Printf("Invitations POST - Unable to commit database transaction: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
}
// Make the new collection available for this user's session
session.Values["ids"] = append(session.Values["ids"].([]int64), collectionID)
if err := session.Save(r, w); err != nil {
log.Printf("Invitations POST - Unable to save session state: %v\n", err)
SendError(w, SERVER_ERROR_MESSAGE, http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
}
// CollectionInvitationsHandler handles inviting new members, managing invitations, and revoking invitations to a collection.
func CollectionInvitationsHandler(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session")
if err != nil {
SendError(w, SERVER_ERROR_MESSAGE, http.StatusInternalServerError)
log.Printf("Invitations handler - Unable to get session: %v\n", err)
return
}
var collectionID int64
// Get URL parameter
collectionID, err = strconv.ParseInt(mux.Vars(r)["collection_id"], 10, 64)
if err != nil {
log.Printf("Invitations handler - Unable to parse collection id: %v\n", err)
SendError(w, URL_ERROR_MESSAGE, http.StatusBadRequest)
return
}
if r.Method == "GET" {
// Get all invitations from this user for this collection
rows, err := db.Query("SELECT invitation_id, invitee_email, admin_invite, invite_sent FROM invitations WHERE inviter_id = $1 AND collection_id = $2 AND retracted = false AND invite_sent > CURRENT_TIMESTAMP - INTERVAL '7 days'", session.Values["user_id"], collectionID)
if err != nil {
log.Printf("Invitations GET - Unable to retrieve invitations from database for user %v: %v\n", session.Values["user_id"], err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
defer rows.Close()
// Retrieve rows from database
invitations := make([]Invite, 0)
for rows.Next() {
var invite Invite
if err := rows.Scan(&invite.InvitationID, &invite.InviteeEmail, &invite.AdminInvite, &invite.InviteSent); err != nil {
log.Printf("Unable to retrieve row from database result: %v\n", err)
}
invitations = append(invitations, invite)
}
// Check for errors from iterating over rows.
if err := rows.Err(); err != nil {
log.Printf("Error retrieving collection invitations from database result: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
// Send response
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(invitations)
return
} else if r.Method == "POST" {
var invite Invite
err := json.NewDecoder(r.Body).Decode(&invite)
if err != nil {
// If there is something wrong with the request body, return a 400 status
log.Printf("Invitations POST - Unable to parse request body: %v\n", err)
SendError(w, `{"error": "Unable to parse request body."}`, http.StatusBadRequest)
return
}
// Check if user is allowed to send invitations
if session.Values["restricted"].(bool) {
log.Printf("Invitations POST - Restricted user '%s' attempted to send an invitation to '%s'\n", session.Values["email"], invite.InviteeEmail)
SendError(w, PERMISSION_ERROR_MESSAGE, http.StatusForbidden)
return
}
// Check if the user's account has been verified
if !session.Values["verified"].(bool) {
log.Printf("Invitations POST - User '%s' attempted to send an invitation to '%s' without verifying their account.\n", session.Values["email"], invite.InviteeEmail)
SendError(w, `{"error": "You must verify your account first before you can perform this action.", "code": "unverified"}`, http.StatusForbidden)
return
}
// Check if user is an administrator on this collection
var admin bool
if err := db.QueryRow("SELECT admin FROM collection_members WHERE collection_id = $1 AND user_id = $2", collectionID, session.Values["user_id"]).Scan(&admin); err != nil {
log.Printf("Invitations POST - Unable to get collection member from database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
if !admin {
log.Printf("Invitations POST - Non-admin user %d attempted to send invite to %s\n", session.Values["user_id"], invite.InviteeEmail)
SendError(w, `{"error": "You are not authorized to perfrom this action for this collection."}`, http.StatusForbidden)
return
}
// Check for existing invitations
var invitationID int64
var sent time.Time
var retracted bool
if err := db.QueryRow("SELECT invitation_id, invite_sent, retracted FROM invitations WHERE collection_id = $1 AND inviter_id = $2 AND invitee_email = $3", collectionID, session.Values["user_id"], invite.InviteeEmail).Scan(&invitationID, &sent, &retracted); err != nil {
// There was a database error executing the SQL statement
if err != sql.ErrNoRows {
log.Printf("Invitations POST - Unable to look up invitation for user")
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
// The error returned was sql.ErrNoRows, so there is no existing invitation.
// This new invitation can be safely committed to the database
} else {
// Invitation exists.
log.Printf("Invitations POST - Existing invitation %d sent at %v. Retracted: %v\n", invitationID, sent, retracted)
// Check if invitations is expired or retracted
if sent.Before(time.Now().AddDate(0, 0, -7)) || retracted {
// Delete existing expired invitation from database
if _, err = db.Exec("DELETE FROM invitations WHERE inviter_id = $1 AND invitee_email = $2 AND collection_id = $3", session.Values["user_id"], invite.InviteeEmail, collectionID); err != nil {
log.Printf("Invitations POST - Unable to delete expired and/or retracted invite from database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
log.Printf("Invitations POST - Expired and/or retracted invitation deleted.")
}
// There was an invitation that has not expired yet.
// Continue to the next step, which will fail because of the
// UNIQUE constraint on the database table. That will
// cause an appropriate "already invited" message to be
// sent to the user.
}
// Add new invitation to database
token := uniuri.NewLen(64)
if _, err = db.Exec("INSERT INTO invitations (inviter_id, invitee_email, admin_invite, collection_id, token) VALUES ($1, $2, $3, $4, $5)", session.Values["user_id"], invite.InviteeEmail, invite.AdminInvite, collectionID, token); err != nil {
if pgerr, ok := err.(*pq.Error); ok {
if pgerr.Code == "23505" {
log.Printf("Invitations POST - User %d attempted to re-invite user %s\n", session.Values["user_id"], invite.InviteeEmail)
SendError(w, `{"error": "There is already a pending invitation for this user."}`, http.StatusConflict)
} else {
log.Printf("Invitations POST - Unable to create invite in database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
}
} else {
log.Printf("Invitations POST - Unable to create invite in database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
}
return
}
// Get collection name from database
var collectionName string
if err := db.QueryRow("SELECT name FROM collections WHERE collection_id = $1", collectionID).Scan(&collectionName); err != nil {
log.Printf("Invitations POST - Unable to get collection name from database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
// Create email template
htmlTemplate := template.Must(template.New("invite_email.html").ParseFiles("email_templates/invite_email.html"))
textTemplate := template.Must(template.New("invite_email.txt").ParseFiles("email_templates/invite_email.txt"))
var htmlBuffer, textBuffer bytes.Buffer
url := "https://" + os.Getenv("HOST") + "/accept_invite.html?token=" + token + "&email=" + invite.InviteeEmail
data := struct {
Href string
InviteeName string
InviterName string
InviterEmail string
CollectionName string
Message string
}{
url,
invite.InviteeName,
session.Values["name"].(string),
session.Values["email"].(string),
collectionName,
invite.Message,
}
if err := htmlTemplate.Execute(&htmlBuffer, data); err != nil {
log.Printf("Invitation - Unable to execute html template: %v\n", err)
SendError(w, SERVER_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
if err := textTemplate.Execute(&textBuffer, data); err != nil {
log.Printf("Invitation - Unable to execute text template: %v\n", err)
SendError(w, SERVER_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
// Send email
if err := SendEmail(invite.InviteeName, invite.InviteeEmail, "Sheet Music Organizer Invitation", htmlBuffer.String(), textBuffer.String()); err != nil {
log.Printf("Invitation - Failed to invitation email: %v\n", err)
SendError(w, `{"error": "Unable to send invitation email."}`, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
return
} else if r.Method == "DELETE" {
// Get invitation_id from URL
invitationID, err := strconv.ParseInt(mux.Vars(r)["invitation_id"], 10, 64)
if err != nil {
log.Printf("Invitation DELETE - Unable to parse invitation id: %v\n", err)
SendError(w, URL_ERROR_MESSAGE, http.StatusBadRequest)
return
}
// Check if user actually sent this invitation
var inviterID int64
if err := db.QueryRow("SELECT inviter_id FROM invitations WHERE collection_id = $1 AND invitation_id = $2", collectionID, invitationID).Scan(&inviterID); err != nil {
if err == sql.ErrNoRows {
log.Printf("Invitation DELETE - User %d attempted to retract a non-existant invitation %d.\n", session.Values["user_id"], invitationID)
SendError(w, `{"error": "Invitation not found."}`, http.StatusNotFound)
} else {
log.Printf("Invitation DELETE - Unable to get collection member from database: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
}
return
}
if inviterID != session.Values["user_id"] {
log.Printf("Invitation DELETE - User %d attempted to retract invitation %d owned by user %d.\n", session.Values["user_id"], invitationID, inviterID)
SendError(w, PERMISSION_ERROR_MESSAGE, http.StatusForbidden)
return
}
// Mark invitation as retracted
if _, err = db.Exec("UPDATE invitations SET retracted = true WHERE invitation_id = $1", invitationID); err != nil {
log.Printf("Invitation DELETE - Unable to retract invitation %d: %v\n", invitationID, err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
return
}
}
// UserInvitationsHandler handles checking for pending invitation for the current
func UserInvitationsHandler(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session")
if err != nil {
log.Printf("User Invitations handler - Unable to get session: %v\n", err)
SendError(w, SERVER_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
if r.Method == "GET" {
// Get all invitations for this user
rows, err := db.Query(`
SELECT invitation_id, collections.name, users.name, users.email, admin_invite, token
FROM invitations
JOIN users ON invitations.inviter_id = users.user_id
JOIN collections on invitations.collection_id = collections.collection_id
WHERE invitee_email = $1
AND retracted = false
AND invite_sent > CURRENT_TIMESTAMP - INTERVAL '7 days'`,
session.Values["email"])
if err != nil {
log.Printf("User Invitations GET - Unable to retrieve invitations from database for user %v: %v\n", session.Values["user_email"], err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
defer rows.Close()
// Retrieve rows from database
invitations := make([]PendingInvite, 0)
for rows.Next() {
var invite PendingInvite
if err := rows.Scan(&invite.InvitationID, &invite.CollectionName, &invite.InviterName, &invite.InviterEmail, &invite.Administrator, &invite.Token); err != nil {
log.Printf("User Invitations GET - Unable to retrieve row from database result: %v\n", err)
}
invitations = append(invitations, invite)
}
// Check for errors from iterating over rows.
if err := rows.Err(); err != nil {
log.Printf("User Invitations GET - Error retrieving user invitations from database result: %v\n", err)
SendError(w, DATABASE_ERROR_MESSAGE, http.StatusInternalServerError)
return
}
// Send response
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(invitations)
return
}
}