@@ -5,10 +5,12 @@ import (
5
5
"errors"
6
6
"fmt"
7
7
"log"
8
+ "math/rand"
8
9
"net/http"
9
10
"strings"
10
11
"time"
11
12
13
+ "github.com/staticbackendhq/core/config"
12
14
"github.com/staticbackendhq/core/internal"
13
15
"github.com/staticbackendhq/core/middleware"
14
16
@@ -64,34 +66,12 @@ func (m *membership) login(w http.ResponseWriter, r *http.Request) {
64
66
return
65
67
}
66
68
67
- token := fmt .Sprintf ("%s|%s" , tok .ID , tok .Token )
68
-
69
- // get their JWT
70
- jwtBytes , err := m .getJWT (token )
69
+ jwtBytes , err := m .getAuthToken (tok , conf )
71
70
if err != nil {
72
71
http .Error (w , err .Error (), http .StatusInternalServerError )
73
72
return
74
73
}
75
74
76
- auth := internal.Auth {
77
- AccountID : tok .AccountID ,
78
- UserID : tok .ID ,
79
- Email : tok .Email ,
80
- Role : tok .Role ,
81
- Token : tok .Token ,
82
- }
83
-
84
- //TODO: find a good way to find all occurences of those two
85
- // and make them easily callable via a shared function
86
- if err := volatile .SetTyped (token , auth ); err != nil {
87
- http .Error (w , err .Error (), http .StatusInternalServerError )
88
- return
89
- }
90
- if err := volatile .SetTyped ("base:" + token , conf ); err != nil {
91
- http .Error (w , err .Error (), http .StatusInternalServerError )
92
- return
93
- }
94
-
95
75
respond (w , http .StatusOK , string (jwtBytes ))
96
76
}
97
77
@@ -163,6 +143,35 @@ func (m *membership) register(w http.ResponseWriter, r *http.Request) {
163
143
respond (w , http .StatusOK , token )
164
144
}
165
145
146
+ func (m * membership ) getAuthToken (tok internal.Token , conf internal.BaseConfig ) (jwtBytes []byte , err error ) {
147
+ token := fmt .Sprintf ("%s|%s" , tok .ID , tok .Token )
148
+
149
+ // get their JWT
150
+ jwtBytes , err = m .getJWT (token )
151
+ if err != nil {
152
+ return
153
+ }
154
+
155
+ auth := internal.Auth {
156
+ AccountID : tok .AccountID ,
157
+ UserID : tok .ID ,
158
+ Email : tok .Email ,
159
+ Role : tok .Role ,
160
+ Token : tok .Token ,
161
+ }
162
+
163
+ //TODO: find a good way to find all occurences of those two
164
+ // and make them easily callable via a shared function
165
+ if err = volatile .SetTyped (token , auth ); err != nil {
166
+ return
167
+ }
168
+ if err = volatile .SetTyped ("base:" + token , conf ); err != nil {
169
+ return
170
+ }
171
+
172
+ return
173
+ }
174
+
166
175
func (m * membership ) createAccountAndUser (dbName , email , password string , role int ) ([]byte , internal.Token , error ) {
167
176
acctID , err := datastore .CreateUserAccount (dbName , email )
168
177
if err != nil {
@@ -411,3 +420,102 @@ func (m *membership) me(w http.ResponseWriter, r *http.Request) {
411
420
412
421
respond (w , http .StatusOK , auth )
413
422
}
423
+
424
+ func (m * membership ) magicLink (w http.ResponseWriter , r * http.Request ) {
425
+ conf , _ , err := middleware .Extract (r , false )
426
+ if err != nil {
427
+ http .Error (w , "invalid StaticBackend key" , http .StatusUnauthorized )
428
+ return
429
+ }
430
+
431
+ if r .Method == http .MethodGet {
432
+ // we use GET to validate magic link code
433
+ email := r .URL .Query ().Get ("email" )
434
+ code := r .URL .Query ().Get ("code" )
435
+
436
+ val , err := volatile .Get ("ml-" + email )
437
+ if err != nil {
438
+ http .Error (w , err .Error (), http .StatusInternalServerError )
439
+ return
440
+ }
441
+
442
+ parts := strings .Split (val , " " )
443
+ if len (parts ) != 2 {
444
+ http .Error (w , "invalid data" , http .StatusBadRequest )
445
+ return
446
+ }
447
+
448
+ // if the code isn't what was set we make sure they're not trying to
449
+ // "brute force" random code.
450
+ if parts [0 ] != code {
451
+ if len (parts [1 ]) >= 10 {
452
+ http .Error (w , "maximum retry reched" , http .StatusTooManyRequests )
453
+ return
454
+ }
455
+
456
+ if err := volatile .Set ("ml-" + email , val + "a" ); err != nil {
457
+ http .Error (w , err .Error (), http .StatusInternalServerError )
458
+ return
459
+ }
460
+
461
+ respond (w , http .StatusBadRequest , false )
462
+ return
463
+ }
464
+
465
+ // they got the right code, return a session token
466
+
467
+ tok , err := datastore .FindTokenByEmail (conf .Name , email )
468
+ if err != nil {
469
+ http .Error (w , err .Error (), http .StatusInternalServerError )
470
+ return
471
+ }
472
+
473
+ jwtBytes , err := m .getAuthToken (tok , conf )
474
+ if err != nil {
475
+ http .Error (w , err .Error (), http .StatusInternalServerError )
476
+ return
477
+ }
478
+
479
+ respond (w , http .StatusOK , string (jwtBytes ))
480
+ return
481
+ }
482
+
483
+ data := new (struct {
484
+ FromEmail string `json:"fromEmail"`
485
+ FromName string `json:"fromName"`
486
+ Email string `json:"email"`
487
+ Subject string `json:"subject"`
488
+ Body string `json:"body"`
489
+ MagicLink string `json:"link"`
490
+ })
491
+ if err := parseBody (r .Body , & data ); err != nil {
492
+ http .Error (w , err .Error (), http .StatusBadRequest )
493
+ return
494
+ }
495
+
496
+ code := rand .Intn (987654 ) + 123456
497
+ // to accomodate unit test, we hard code a magic link code in dev mode
498
+ if config .Current .AppEnv == AppEnvDev {
499
+ code = 666333
500
+ }
501
+ data .MagicLink += fmt .Sprintf ("?code=%d&email=%s" , code , data .Email )
502
+
503
+ if err := volatile .Set ("ml-" + data .Email , fmt .Sprintf ("%d a" , code )); err != nil {
504
+ http .Error (w , err .Error (), http .StatusInternalServerError )
505
+ return
506
+ }
507
+
508
+ mail := internal.SendMailData {
509
+ From : data .FromEmail ,
510
+ FromName : data .FromName ,
511
+ To : data .Email ,
512
+ Subject : data .Subject ,
513
+ HTMLBody : strings .Replace (data .Body , "[link]" , data .MagicLink , - 1 ),
514
+ }
515
+ if err := emailer .Send (mail ); err != nil {
516
+ http .Error (w , err .Error (), http .StatusInternalServerError )
517
+ return
518
+ }
519
+
520
+ respond (w , http .StatusOK , true )
521
+ }
0 commit comments