From e2fde518ca986f21f98352359436d5243faf3ca1 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 25 Sep 2023 18:18:01 -0400 Subject: [PATCH 1/2] Fix GetTemporaryOneTimeAccessToken query for SQLite --- database.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/database.go b/database.go index f1e9cfa..85cce90 100644 --- a/database.go +++ b/database.go @@ -174,6 +174,13 @@ func (db *datastore) upsert(indexedCols ...string) string { return "ON DUPLICATE KEY UPDATE" } +func (db *datastore) dateAdd(l int, unit string) string { + if db.driverName == driverSQLite { + return fmt.Sprintf("DATETIME('now', '%d %s')", l, unit) + } + return fmt.Sprintf("DATE_ADD(NOW(), INTERVAL %d %s)", l, unit) +} + func (db *datastore) dateSub(l int, unit string) string { if db.driverName == driverSQLite { return fmt.Sprintf("DATETIME('now', '-%d %s')", l, unit) @@ -567,7 +574,7 @@ func (db *datastore) GetTemporaryOneTimeAccessToken(userID int64, validSecs int, expirationVal := "NULL" if validSecs > 0 { - expirationVal = fmt.Sprintf("DATE_ADD("+db.now()+", INTERVAL %d SECOND)", validSecs) + expirationVal = db.dateAdd(validSecs, "SECOND") } _, err = db.Exec("INSERT INTO accesstokens (token, user_id, one_time, expires) VALUES (?, ?, ?, "+expirationVal+")", string(binTok), userID, oneTime) From 7dda53146dd82ad59098b2179f71331c7f3adb04 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 25 Sep 2023 18:21:20 -0400 Subject: [PATCH 2/2] Add function for logging in via emailed link This doesn't add any user-facing behavior, but provides the basic functionality to generate a one-time use token and email it to a user, so they can log in with a link instead of a password. --- account.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/account.go b/account.go index ecd02e3..ecc39b4 100644 --- a/account.go +++ b/account.go @@ -13,6 +13,7 @@ package writefreely import ( "encoding/json" "fmt" + "github.com/mailgun/mailgun-go" "html/template" "net/http" "regexp" @@ -1237,6 +1238,54 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err return nil } +func loginViaEmail(app *App, alias, redirectTo string) error { + if !app.cfg.Email.Enabled() { + return fmt.Errorf("EMAIL ISN'T CONFIGURED on this server") + } + + // Make sure user has added an email + // TODO: create a new func to just get user's email; "ForAuth" doesn't match here + u, _ := app.db.GetUserForAuth(alias) + if u == nil { + if strings.IndexAny(alias, "@") > 0 { + return ErrUserNotFoundEmail + } + return ErrUserNotFound + } + if u.Email.String == "" { + return impart.HTTPError{http.StatusPreconditionFailed, "User doesn't have an email address. Log in with password, instead."} + } + + // Generate one-time login token + t, err := app.db.GetTemporaryOneTimeAccessToken(u.ID, 60*15, true) + if err != nil { + log.Error("Unable to generate token for email login: %s", err) + return impart.HTTPError{http.StatusInternalServerError, "Unable to generate token."} + } + + // Send email + gun := mailgun.NewMailgun(app.cfg.Email.Domain, app.cfg.Email.MailgunPrivate) + toEmail := u.EmailClear(app.keys) + footerPara := "This link will only work once and expires in 15 minutes. Didn't ask us to log in? You can safely ignore this email." + + plainMsg := fmt.Sprintf("Log in to %s here: %s/login?to=%s&with=%s\n\n%s", app.cfg.App.SiteName, app.cfg.App.Host, redirectTo, t, footerPara) + m := mailgun.NewMessage(app.cfg.App.SiteName+" ", "Log in to "+app.cfg.App.SiteName, plainMsg, fmt.Sprintf("<%s>", toEmail)) + m.AddTag("Email Login") + + m.SetHtml(fmt.Sprintf(` + +
+

%s

+

Log in to %s here.

+

%s

+
+ +`, app.cfg.App.Host, app.cfg.App.SiteName, app.cfg.App.Host, redirectTo, t, app.cfg.App.SiteName, footerPara)) + _, _, err = gun.Send(m) + + return err +} + func saveTempInfo(app *App, key, val string, r *http.Request, w http.ResponseWriter) error { session, err := app.sessionStore.Get(r, "t") if err != nil {