@@ -2,19 +2,170 @@ package api
22
33import (
44 "context"
5+ "encoding/json"
6+ "fmt"
7+ "net/http"
58 "time"
69
710 "github.com/netlify/gotrue/crypto"
811 "github.com/netlify/gotrue/mailer"
912 "github.com/netlify/gotrue/models"
1013 "github.com/netlify/gotrue/storage"
1114 "github.com/pkg/errors"
15+ "github.com/sethvargo/go-password/password"
1216)
1317
1418var (
1519 MaxFrequencyLimitError error = errors .New ("Frequency limit reached" )
20+ configFile = ""
1621)
1722
23+ type GenerateLinkParams struct {
24+ Type string `json:"type"`
25+ Email string `json:"email"`
26+ Password string `json:"password"`
27+ Data map [string ]interface {} `json:"data"`
28+ RedirectTo string `json:"redirect_to"`
29+ }
30+
31+ func (a * API ) GenerateLink (w http.ResponseWriter , r * http.Request ) error {
32+ ctx := r .Context ()
33+ config := a .getConfig (ctx )
34+ mailer := a .Mailer (ctx )
35+ instanceID := getInstanceID (ctx )
36+ adminUser := getAdminUser (ctx )
37+
38+ params := & GenerateLinkParams {}
39+ jsonDecoder := json .NewDecoder (r .Body )
40+
41+ if err := jsonDecoder .Decode (params ); err != nil {
42+ return badRequestError ("Could not read body: %v" , err )
43+ }
44+
45+ if err := a .validateEmail (ctx , params .Email ); err != nil {
46+ return err
47+ }
48+
49+ aud := a .requestAud (ctx , r )
50+ user , err := models .FindUserByEmailAndAudience (a .db , instanceID , params .Email , aud )
51+ if err != nil {
52+ if models .IsNotFoundError (err ) {
53+ if params .Type == "magiclink" {
54+ params .Type = "signup"
55+ params .Password , err = password .Generate (64 , 10 , 0 , false , true )
56+ if err != nil {
57+ return internalServerError ("error creating user" ).WithInternalError (err )
58+ }
59+ } else if params .Type == "recovery" {
60+ return notFoundError (err .Error ())
61+ }
62+ } else {
63+ return internalServerError ("Database error finding user" ).WithInternalError (err )
64+ }
65+ }
66+
67+ var url string
68+ referrer := a .getRedirectURLOrReferrer (r , params .RedirectTo )
69+ now := time .Now ()
70+ err = a .db .Transaction (func (tx * storage.Connection ) error {
71+ var terr error
72+ switch params .Type {
73+ case "magiclink" , "recovery" :
74+ if terr = models .NewAuditLogEntry (tx , instanceID , user , models .UserRecoveryRequestedAction , nil ); terr != nil {
75+ return terr
76+ }
77+ user .RecoveryToken = crypto .SecureToken ()
78+ user .RecoverySentAt = & now
79+ terr = errors .Wrap (tx .UpdateOnly (user , "recovery_token" , "recovery_sent_at" ), "Database error updating user for recovery" )
80+ case "invite" :
81+ if user != nil {
82+ if user .IsConfirmed () {
83+ return unprocessableEntityError (DuplicateEmailMsg )
84+ }
85+ } else {
86+ signupParams := & SignupParams {
87+ Email : params .Email ,
88+ Data : params .Data ,
89+ Provider : "email" ,
90+ Aud : aud ,
91+ }
92+ user , terr = a .signupNewUser (ctx , tx , signupParams )
93+ if terr != nil {
94+ return terr
95+ }
96+ }
97+ if terr = models .NewAuditLogEntry (tx , instanceID , adminUser , models .UserInvitedAction , map [string ]interface {}{
98+ "user_id" : user .ID ,
99+ "user_email" : user .Email ,
100+ }); terr != nil {
101+ return terr
102+ }
103+ user .ConfirmationToken = crypto .SecureToken ()
104+ user .ConfirmationSentAt = & now
105+ user .InvitedAt = & now
106+ terr = errors .Wrap (tx .UpdateOnly (user , "confirmation_token" , "confirmation_sent_at" , "invited_at" ), "Database error updating user for invite" )
107+ case "signup" :
108+ if user != nil {
109+ if user .IsConfirmed () {
110+ return unprocessableEntityError (DuplicateEmailMsg )
111+ }
112+ if err := user .UpdateUserMetaData (tx , params .Data ); err != nil {
113+ return internalServerError ("Database error updating user" ).WithInternalError (err )
114+ }
115+ } else {
116+ if params .Password == "" {
117+ return unprocessableEntityError ("Signup requires a valid password" )
118+ }
119+ if len (params .Password ) < config .PasswordMinLength {
120+ return unprocessableEntityError (fmt .Sprintf ("Password should be at least %d characters" , config .PasswordMinLength ))
121+ }
122+ signupParams := & SignupParams {
123+ Email : params .Email ,
124+ Password : params .Password ,
125+ Data : params .Data ,
126+ Provider : "email" ,
127+ Aud : aud ,
128+ }
129+ user , terr = a .signupNewUser (ctx , tx , signupParams )
130+ if terr != nil {
131+ return terr
132+ }
133+ }
134+ user .ConfirmationToken = crypto .SecureToken ()
135+ user .ConfirmationSentAt = & now
136+ terr = errors .Wrap (tx .UpdateOnly (user , "confirmation_token" , "confirmation_sent_at" ), "Database error updating user for confirmation" )
137+ default :
138+ return badRequestError ("Invalid email action link type requested: %v" , params .Type )
139+ }
140+
141+ if terr != nil {
142+ return terr
143+ }
144+
145+ url , terr = mailer .GetEmailActionLink (user , params .Type , referrer )
146+ if terr != nil {
147+ return terr
148+ }
149+ return nil
150+ })
151+
152+ if err != nil {
153+ return err
154+ }
155+
156+ resp := make (map [string ]interface {})
157+ u , err := json .Marshal (user )
158+ if err != nil {
159+ return internalServerError ("User serialization error" ).WithInternalError (err )
160+ }
161+ if err = json .Unmarshal (u , & resp ); err != nil {
162+ return internalServerError ("User serialization error" ).WithInternalError (err )
163+ }
164+ resp ["action_link" ] = url
165+
166+ return sendJSON (w , http .StatusOK , resp )
167+ }
168+
18169func sendConfirmation (tx * storage.Connection , u * models.User , mailer mailer.Mailer , maxFrequency time.Duration , referrerURL string ) error {
19170 if u .ConfirmationSentAt != nil && ! u .ConfirmationSentAt .Add (maxFrequency ).Before (time .Now ()) {
20171 return MaxFrequencyLimitError
0 commit comments