A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1539 lines
43 KiB

  1. /*
  2. * Copyright © 2018-2021 Musing Studio LLC.
  3. *
  4. * This file is part of WriteFreely.
  5. *
  6. * WriteFreely is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License, included
  8. * in the LICENSE file in this source code package.
  9. */
  10. package writefreely
  11. import (
  12. "encoding/json"
  13. "fmt"
  14. "github.com/mailgun/mailgun-go"
  15. "github.com/writefreely/writefreely/spam"
  16. "html/template"
  17. "net/http"
  18. "regexp"
  19. "strconv"
  20. "strings"
  21. "sync"
  22. "time"
  23. "github.com/gorilla/csrf"
  24. "github.com/gorilla/mux"
  25. "github.com/gorilla/sessions"
  26. "github.com/guregu/null/zero"
  27. "github.com/writeas/impart"
  28. "github.com/writeas/web-core/auth"
  29. "github.com/writeas/web-core/data"
  30. "github.com/writeas/web-core/log"
  31. "github.com/writefreely/writefreely/author"
  32. "github.com/writefreely/writefreely/config"
  33. "github.com/writefreely/writefreely/page"
  34. )
  35. type (
  36. userSettings struct {
  37. Username string `schema:"username" json:"username"`
  38. Email string `schema:"email" json:"email"`
  39. NewPass string `schema:"new-pass" json:"new_pass"`
  40. OldPass string `schema:"current-pass" json:"current_pass"`
  41. IsLogOut bool `schema:"logout" json:"logout"`
  42. }
  43. UserPage struct {
  44. page.StaticPage
  45. PageTitle string
  46. Separator template.HTML
  47. IsAdmin bool
  48. CanInvite bool
  49. CollAlias string
  50. }
  51. )
  52. func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []string) *UserPage {
  53. up := &UserPage{
  54. StaticPage: pageForReq(app, r),
  55. PageTitle: title,
  56. }
  57. up.Username = u.Username
  58. up.Flashes = flashes
  59. up.Path = r.URL.Path
  60. up.IsAdmin = u.IsAdmin()
  61. up.CanInvite = canUserInvite(app.cfg, up.IsAdmin)
  62. return up
  63. }
  64. func canUserInvite(cfg *config.Config, isAdmin bool) bool {
  65. return cfg.App.UserInvites != "" &&
  66. (isAdmin || cfg.App.UserInvites != "admin")
  67. }
  68. func (up *UserPage) SetMessaging(u *User) {
  69. // up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID)
  70. }
  71. const (
  72. loginAttemptExpiration = 3 * time.Second
  73. )
  74. var actuallyUsernameReg = regexp.MustCompile("username is actually ([a-z0-9\\-]+)\\. Please try that, instead")
  75. func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error {
  76. _, err := signup(app, w, r)
  77. return err
  78. }
  79. func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
  80. if app.cfg.App.DisablePasswordAuth {
  81. err := ErrDisabledPasswordAuth
  82. return nil, err
  83. }
  84. reqJSON := IsJSON(r)
  85. // Get params
  86. var ur userRegistration
  87. if reqJSON {
  88. decoder := json.NewDecoder(r.Body)
  89. err := decoder.Decode(&ur)
  90. if err != nil {
  91. log.Error("Couldn't parse signup JSON request: %v\n", err)
  92. return nil, ErrBadJSON
  93. }
  94. } else {
  95. // Check if user is already logged in
  96. u := getUserSession(app, r)
  97. if u != nil {
  98. return &AuthUser{User: u}, nil
  99. }
  100. err := r.ParseForm()
  101. if err != nil {
  102. log.Error("Couldn't parse signup form request: %v\n", err)
  103. return nil, ErrBadFormData
  104. }
  105. err = app.formDecoder.Decode(&ur, r.PostForm)
  106. if err != nil {
  107. log.Error("Couldn't decode signup form request: %v\n", err)
  108. return nil, ErrBadFormData
  109. }
  110. }
  111. return signupWithRegistration(app, ur, w, r)
  112. }
  113. func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
  114. reqJSON := IsJSON(r)
  115. // Validate required params (alias)
  116. if signup.Alias == "" {
  117. return nil, impart.HTTPError{http.StatusBadRequest, "A username is required."}
  118. }
  119. if signup.Pass == "" {
  120. return nil, impart.HTTPError{http.StatusBadRequest, "A password is required."}
  121. }
  122. var desiredUsername string
  123. if signup.Normalize {
  124. // With this option we simply conform the username to what we expect
  125. // without complaining. Since they might've done something funny, like
  126. // enter: write.as/Way Out There, we'll use their raw input for the new
  127. // collection name and sanitize for the slug / username.
  128. desiredUsername = signup.Alias
  129. signup.Alias = getSlug(signup.Alias, "")
  130. }
  131. if !author.IsValidUsername(app.cfg, signup.Alias) {
  132. // Ensure the username is syntactically correct.
  133. return nil, impart.HTTPError{http.StatusPreconditionFailed, "Username is reserved or isn't valid. It must be at least 3 characters long, and can only include letters, numbers, and hyphens."}
  134. }
  135. // Handle empty optional params
  136. hashedPass, err := auth.HashPass([]byte(signup.Pass))
  137. if err != nil {
  138. return nil, impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
  139. }
  140. // Create struct to insert
  141. u := &User{
  142. Username: signup.Alias,
  143. HashedPass: hashedPass,
  144. HasPass: true,
  145. Email: prepareUserEmail(signup.Email, app.keys.EmailKey),
  146. Created: time.Now().Truncate(time.Second).UTC(),
  147. }
  148. // Create actual user
  149. if err := app.db.CreateUser(app.cfg, u, desiredUsername, signup.Description); err != nil {
  150. return nil, err
  151. }
  152. // Log invite if needed
  153. if signup.InviteCode != "" {
  154. err = app.db.CreateInvitedUser(signup.InviteCode, u.ID)
  155. if err != nil {
  156. return nil, err
  157. }
  158. }
  159. // Add back unencrypted data for response
  160. if signup.Email != "" {
  161. u.Email.String = signup.Email
  162. }
  163. resUser := &AuthUser{
  164. User: u,
  165. }
  166. title := signup.Alias
  167. if signup.Normalize {
  168. title = desiredUsername
  169. }
  170. resUser.Collections = &[]Collection{
  171. {
  172. Alias: signup.Alias,
  173. Title: title,
  174. Description: signup.Description,
  175. },
  176. }
  177. var coll *Collection
  178. if signup.Monetization != "" {
  179. if coll == nil {
  180. coll, err = app.db.GetCollection(signup.Alias)
  181. if err != nil {
  182. log.Error("Unable to get new collection '%s' for monetization on signup: %v", signup.Alias, err)
  183. return nil, err
  184. }
  185. }
  186. err = app.db.SetCollectionAttribute(coll.ID, "monetization_pointer", signup.Monetization)
  187. if err != nil {
  188. log.Error("Unable to add monetization on signup: %v", err)
  189. return nil, err
  190. }
  191. coll.Monetization = signup.Monetization
  192. }
  193. var token string
  194. if reqJSON && !signup.Web {
  195. token, err = app.db.GetAccessToken(u.ID)
  196. if err != nil {
  197. return nil, impart.HTTPError{http.StatusInternalServerError, "Could not create access token. Try re-authenticating."}
  198. }
  199. resUser.AccessToken = token
  200. } else {
  201. session, err := app.sessionStore.Get(r, cookieName)
  202. if err != nil {
  203. // The cookie should still save, even if there's an error.
  204. // Source: https://github.com/gorilla/sessions/issues/16#issuecomment-143642144
  205. log.Error("Session: %v; ignoring", err)
  206. }
  207. session.Values[cookieUserVal] = resUser.User.Cookie()
  208. err = session.Save(r, w)
  209. if err != nil {
  210. log.Error("Couldn't save session: %v", err)
  211. return nil, err
  212. }
  213. }
  214. if reqJSON {
  215. return resUser, impart.WriteSuccess(w, resUser, http.StatusCreated)
  216. }
  217. return resUser, nil
  218. }
  219. func viewLogout(app *App, w http.ResponseWriter, r *http.Request) error {
  220. session, err := app.sessionStore.Get(r, cookieName)
  221. if err != nil {
  222. return ErrInternalCookieSession
  223. }
  224. // Ensure user has an email or password set before they go, so they don't
  225. // lose access to their account.
  226. val := session.Values[cookieUserVal]
  227. var u = &User{}
  228. var ok bool
  229. if u, ok = val.(*User); !ok {
  230. log.Error("Error casting user object on logout. Vals: %+v Resetting cookie.", session.Values)
  231. err = session.Save(r, w)
  232. if err != nil {
  233. log.Error("Couldn't save session on logout: %v", err)
  234. return impart.HTTPError{http.StatusInternalServerError, "Unable to save cookie session."}
  235. }
  236. return impart.HTTPError{http.StatusFound, "/"}
  237. }
  238. u, err = app.db.GetUserByID(u.ID)
  239. if err != nil && err != ErrUserNotFound {
  240. return impart.HTTPError{http.StatusInternalServerError, "Unable to fetch user information."}
  241. }
  242. session.Options.MaxAge = -1
  243. err = session.Save(r, w)
  244. if err != nil {
  245. log.Error("Couldn't save session on logout: %v", err)
  246. return impart.HTTPError{http.StatusInternalServerError, "Unable to save cookie session."}
  247. }
  248. return impart.HTTPError{http.StatusFound, "/"}
  249. }
  250. func handleAPILogout(app *App, w http.ResponseWriter, r *http.Request) error {
  251. accessToken := r.Header.Get("Authorization")
  252. if accessToken == "" {
  253. return ErrNoAccessToken
  254. }
  255. t := auth.GetToken(accessToken)
  256. if len(t) == 0 {
  257. return ErrNoAccessToken
  258. }
  259. err := app.db.DeleteToken(t)
  260. if err != nil {
  261. return err
  262. }
  263. return impart.HTTPError{Status: http.StatusNoContent}
  264. }
  265. func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
  266. var earlyError string
  267. oneTimeToken := r.FormValue("with")
  268. if oneTimeToken != "" {
  269. log.Info("Calling login with one-time token.")
  270. err := login(app, w, r)
  271. if err != nil {
  272. log.Info("Received error: %v", err)
  273. earlyError = fmt.Sprintf("%s", err)
  274. }
  275. }
  276. session, err := app.sessionStore.Get(r, cookieName)
  277. if err != nil {
  278. // Ignore this
  279. log.Error("Unable to get session; ignoring: %v", err)
  280. }
  281. p := &struct {
  282. page.StaticPage
  283. *OAuthButtons
  284. To string
  285. Message template.HTML
  286. Flashes []template.HTML
  287. EmailEnabled bool
  288. LoginUsername string
  289. }{
  290. StaticPage: pageForReq(app, r),
  291. OAuthButtons: NewOAuthButtons(app.Config()),
  292. To: r.FormValue("to"),
  293. Message: template.HTML(""),
  294. Flashes: []template.HTML{},
  295. EmailEnabled: app.cfg.Email.Enabled(),
  296. LoginUsername: getTempInfo(app, "login-user", r, w),
  297. }
  298. if earlyError != "" {
  299. p.Flashes = append(p.Flashes, template.HTML(earlyError))
  300. }
  301. // Display any error messages
  302. flashes, _ := getSessionFlashes(app, w, r, session)
  303. for _, flash := range flashes {
  304. p.Flashes = append(p.Flashes, template.HTML(flash))
  305. }
  306. err = pages["login.tmpl"].ExecuteTemplate(w, "base", p)
  307. if err != nil {
  308. log.Error("Unable to render login: %v", err)
  309. return err
  310. }
  311. return nil
  312. }
  313. func webLogin(app *App, w http.ResponseWriter, r *http.Request) error {
  314. err := login(app, w, r)
  315. if err != nil {
  316. username := r.FormValue("alias")
  317. // Login request was unsuccessful; save the error in the session and redirect them
  318. if err, ok := err.(impart.HTTPError); ok {
  319. session, _ := app.sessionStore.Get(r, cookieName)
  320. if session != nil {
  321. session.AddFlash(err.Message)
  322. session.Save(r, w)
  323. }
  324. if m := actuallyUsernameReg.FindStringSubmatch(err.Message); len(m) > 0 {
  325. // Retain fixed username recommendation for the login form
  326. username = m[1]
  327. }
  328. }
  329. // Pass along certain information
  330. saveTempInfo(app, "login-user", username, r, w)
  331. // Retain post-login URL if one was given
  332. redirectTo := "/login"
  333. postLoginRedirect := r.FormValue("to")
  334. if postLoginRedirect != "" {
  335. redirectTo += "?to=" + postLoginRedirect
  336. }
  337. log.Error("Unable to login: %v", err)
  338. return impart.HTTPError{http.StatusTemporaryRedirect, redirectTo}
  339. }
  340. return nil
  341. }
  342. var loginAttemptUsers = sync.Map{}
  343. func login(app *App, w http.ResponseWriter, r *http.Request) error {
  344. reqJSON := IsJSON(r)
  345. oneTimeToken := r.FormValue("with")
  346. verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "")
  347. redirectTo := r.FormValue("to")
  348. if redirectTo == "" {
  349. if app.cfg.App.SingleUser {
  350. redirectTo = "/me/new"
  351. } else {
  352. redirectTo = "/"
  353. }
  354. }
  355. var u *User
  356. var err error
  357. var signin userCredentials
  358. if app.cfg.App.DisablePasswordAuth {
  359. err := ErrDisabledPasswordAuth
  360. return err
  361. }
  362. // Log in with one-time token if one is given
  363. if oneTimeToken != "" {
  364. log.Info("Login: Logging user in via token.")
  365. userID := app.db.GetUserID(oneTimeToken)
  366. if userID == -1 {
  367. log.Error("Login: Got user -1 from token")
  368. err := ErrBadAccessToken
  369. err.Message = "Expired or invalid login code."
  370. return err
  371. }
  372. log.Info("Login: Found user %d.", userID)
  373. u, err = app.db.GetUserByID(userID)
  374. if err != nil {
  375. log.Error("Unable to fetch user on one-time token login: %v", err)
  376. return impart.HTTPError{http.StatusInternalServerError, "There was an error retrieving the user you want."}
  377. }
  378. log.Info("Login: Got user via token")
  379. } else {
  380. // Get params
  381. if reqJSON {
  382. decoder := json.NewDecoder(r.Body)
  383. err := decoder.Decode(&signin)
  384. if err != nil {
  385. log.Error("Couldn't parse signin JSON request: %v\n", err)
  386. return ErrBadJSON
  387. }
  388. } else {
  389. err := r.ParseForm()
  390. if err != nil {
  391. log.Error("Couldn't parse signin form request: %v\n", err)
  392. return ErrBadFormData
  393. }
  394. err = app.formDecoder.Decode(&signin, r.PostForm)
  395. if err != nil {
  396. log.Error("Couldn't decode signin form request: %v\n", err)
  397. return ErrBadFormData
  398. }
  399. }
  400. log.Info("Login: Attempting login for '%s'", signin.Alias)
  401. // Validate required params (all)
  402. if signin.Alias == "" {
  403. msg := "Parameter `alias` required."
  404. if signin.Web {
  405. msg = "A username is required."
  406. }
  407. return impart.HTTPError{http.StatusBadRequest, msg}
  408. }
  409. if !signin.EmailLogin && signin.Pass == "" {
  410. msg := "Parameter `pass` required."
  411. if signin.Web {
  412. msg = "A password is required."
  413. }
  414. return impart.HTTPError{http.StatusBadRequest, msg}
  415. }
  416. // Prevent excessive login attempts on the same account
  417. // Skip this check in dev environment
  418. if !app.cfg.Server.Dev {
  419. now := time.Now()
  420. attemptExp, att := loginAttemptUsers.LoadOrStore(signin.Alias, now.Add(loginAttemptExpiration))
  421. if att {
  422. if attemptExpTime, ok := attemptExp.(time.Time); ok {
  423. if attemptExpTime.After(now) {
  424. // This user attempted previously, and the period hasn't expired yet
  425. return impart.HTTPError{http.StatusTooManyRequests, "You're doing that too much."}
  426. } else {
  427. // This user attempted previously, but the time expired; free up space
  428. loginAttemptUsers.Delete(signin.Alias)
  429. }
  430. } else {
  431. log.Error("Unable to cast expiration to time")
  432. }
  433. }
  434. }
  435. // Retrieve password
  436. u, err = app.db.GetUserForAuth(signin.Alias)
  437. if err != nil {
  438. log.Info("Unable to getUserForAuth on %s: %v", signin.Alias, err)
  439. if strings.IndexAny(signin.Alias, "@") > 0 {
  440. log.Info("Suggesting: %s", ErrUserNotFoundEmail.Message)
  441. return ErrUserNotFoundEmail
  442. }
  443. return err
  444. }
  445. // Authenticate
  446. if u.Email.String == "" {
  447. // User has no email set, so check if they haven't added a password, either,
  448. // so we can return a more helpful error message.
  449. if hasPass, _ := app.db.IsUserPassSet(u.ID); !hasPass {
  450. log.Info("Tried logging into %s, but no password or email.", signin.Alias)
  451. return impart.HTTPError{http.StatusPreconditionFailed, "This user never added a password or email address. Please contact us for help."}
  452. }
  453. }
  454. if len(u.HashedPass) == 0 {
  455. return impart.HTTPError{http.StatusUnauthorized, "This user never set a password. Perhaps try logging in via OAuth?"}
  456. }
  457. if !auth.Authenticated(u.HashedPass, []byte(signin.Pass)) {
  458. return impart.HTTPError{http.StatusUnauthorized, "Incorrect password."}
  459. }
  460. }
  461. if reqJSON && !signin.Web {
  462. var token string
  463. if r.Header.Get("User-Agent") == "" {
  464. // Get last created token when User-Agent is empty
  465. token = app.db.FetchLastAccessToken(u.ID)
  466. if token == "" {
  467. token, err = app.db.GetAccessToken(u.ID)
  468. }
  469. } else {
  470. token, err = app.db.GetAccessToken(u.ID)
  471. }
  472. if err != nil {
  473. log.Error("Login: Unable to create access token: %v", err)
  474. return impart.HTTPError{http.StatusInternalServerError, "Could not create access token. Try re-authenticating."}
  475. }
  476. resUser := getVerboseAuthUser(app, token, u, verbose)
  477. return impart.WriteSuccess(w, resUser, http.StatusOK)
  478. }
  479. session, err := app.sessionStore.Get(r, cookieName)
  480. if err != nil {
  481. // The cookie should still save, even if there's an error.
  482. log.Error("Login: Session: %v; ignoring", err)
  483. }
  484. // Remove unwanted data
  485. session.Values[cookieUserVal] = u.Cookie()
  486. err = session.Save(r, w)
  487. if err != nil {
  488. log.Error("Login: Couldn't save session: %v", err)
  489. // TODO: return error
  490. }
  491. // Send success
  492. if reqJSON {
  493. return impart.WriteSuccess(w, &AuthUser{User: u}, http.StatusOK)
  494. }
  495. log.Info("Login: Redirecting to %s", redirectTo)
  496. w.Header().Set("Location", redirectTo)
  497. w.WriteHeader(http.StatusFound)
  498. return nil
  499. }
  500. func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser {
  501. resUser := &AuthUser{
  502. AccessToken: token,
  503. User: u,
  504. }
  505. // Fetch verbose user data if requested
  506. if verbose {
  507. posts, err := app.db.GetUserPosts(u)
  508. if err != nil {
  509. log.Error("Login: Unable to get user posts: %v", err)
  510. }
  511. colls, err := app.db.GetCollections(u, app.cfg.App.Host)
  512. if err != nil {
  513. log.Error("Login: Unable to get user collections: %v", err)
  514. }
  515. passIsSet, err := app.db.IsUserPassSet(u.ID)
  516. if err != nil {
  517. // TODO: correct error message
  518. log.Error("Login: Unable to get user collections: %v", err)
  519. }
  520. resUser.Posts = posts
  521. resUser.Collections = colls
  522. resUser.User.HasPass = passIsSet
  523. }
  524. return resUser
  525. }
  526. func viewExportOptions(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  527. // Fetch extra user data
  528. p := NewUserPage(app, r, u, "Export", nil)
  529. showUserPage(w, "export", p)
  530. return nil
  531. }
  532. func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
  533. var filename string
  534. var u = &User{}
  535. reqJSON := IsJSON(r)
  536. if reqJSON {
  537. // Use given Authorization header
  538. accessToken := r.Header.Get("Authorization")
  539. if accessToken == "" {
  540. return nil, filename, ErrNoAccessToken
  541. }
  542. userID := app.db.GetUserID(accessToken)
  543. if userID == -1 {
  544. return nil, filename, ErrBadAccessToken
  545. }
  546. var err error
  547. u, err = app.db.GetUserByID(userID)
  548. if err != nil {
  549. return nil, filename, impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve requested user."}
  550. }
  551. } else {
  552. // Use user cookie
  553. session, err := app.sessionStore.Get(r, cookieName)
  554. if err != nil {
  555. // The cookie should still save, even if there's an error.
  556. log.Error("Session: %v; ignoring", err)
  557. }
  558. val := session.Values[cookieUserVal]
  559. var ok bool
  560. if u, ok = val.(*User); !ok {
  561. return nil, filename, ErrNotLoggedIn
  562. }
  563. }
  564. filename = u.Username + "-posts-" + time.Now().Truncate(time.Second).UTC().Format("200601021504")
  565. // Fetch data we're exporting
  566. var err error
  567. var data []byte
  568. posts, err := app.db.GetUserPosts(u)
  569. if err != nil {
  570. return data, filename, err
  571. }
  572. // Export as CSV
  573. if strings.HasSuffix(r.URL.Path, ".csv") {
  574. data = exportPostsCSV(app.cfg.App.Host, u, posts)
  575. return data, filename, err
  576. }
  577. if strings.HasSuffix(r.URL.Path, ".zip") {
  578. data = exportPostsZip(u, posts)
  579. return data, filename, err
  580. }
  581. if r.FormValue("pretty") == "1" {
  582. data, err = json.MarshalIndent(posts, "", "\t")
  583. } else {
  584. data, err = json.Marshal(posts)
  585. }
  586. return data, filename, err
  587. }
  588. func viewExportFull(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
  589. var err error
  590. filename := ""
  591. u := getUserSession(app, r)
  592. if u == nil {
  593. return nil, filename, ErrNotLoggedIn
  594. }
  595. filename = u.Username + "-" + time.Now().Truncate(time.Second).UTC().Format("200601021504")
  596. exportUser := compileFullExport(app, u)
  597. var data []byte
  598. if r.FormValue("pretty") == "1" {
  599. data, err = json.MarshalIndent(exportUser, "", "\t")
  600. } else {
  601. data, err = json.Marshal(exportUser)
  602. }
  603. return data, filename, err
  604. }
  605. func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error {
  606. reqJSON := IsJSON(r)
  607. uObj := struct {
  608. ID int64 `json:"id,omitempty"`
  609. Username string `json:"username,omitempty"`
  610. }{}
  611. var err error
  612. if reqJSON {
  613. _, uObj.Username, err = app.db.GetUserDataFromToken(r.Header.Get("Authorization"))
  614. if err != nil {
  615. return err
  616. }
  617. } else {
  618. u := getUserSession(app, r)
  619. if u == nil {
  620. return impart.WriteSuccess(w, uObj, http.StatusOK)
  621. }
  622. uObj.Username = u.Username
  623. }
  624. return impart.WriteSuccess(w, uObj, http.StatusOK)
  625. }
  626. func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  627. reqJSON := IsJSON(r)
  628. if !reqJSON {
  629. return ErrBadRequestedType
  630. }
  631. isAnonPosts := r.FormValue("anonymous") == "1"
  632. if isAnonPosts {
  633. pageStr := r.FormValue("page")
  634. pg, err := strconv.Atoi(pageStr)
  635. if err != nil {
  636. log.Error("Error parsing page parameter '%s': %s", pageStr, err)
  637. pg = 1
  638. }
  639. p, err := app.db.GetAnonymousPosts(u, pg)
  640. if err != nil {
  641. return err
  642. }
  643. return impart.WriteSuccess(w, p, http.StatusOK)
  644. }
  645. var err error
  646. p := GetPostsCache(u.ID)
  647. if p == nil {
  648. userPostsCache.Lock()
  649. if userPostsCache.users[u.ID].ready == nil {
  650. userPostsCache.users[u.ID] = postsCacheItem{ready: make(chan struct{})}
  651. userPostsCache.Unlock()
  652. p, err = app.db.GetUserPosts(u)
  653. if err != nil {
  654. return err
  655. }
  656. CachePosts(u.ID, p)
  657. } else {
  658. userPostsCache.Unlock()
  659. <-userPostsCache.users[u.ID].ready
  660. p = GetPostsCache(u.ID)
  661. }
  662. }
  663. return impart.WriteSuccess(w, p, http.StatusOK)
  664. }
  665. func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  666. reqJSON := IsJSON(r)
  667. if !reqJSON {
  668. return ErrBadRequestedType
  669. }
  670. p, err := app.db.GetCollections(u, app.cfg.App.Host)
  671. if err != nil {
  672. return err
  673. }
  674. return impart.WriteSuccess(w, p, http.StatusOK)
  675. }
  676. func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  677. p, err := app.db.GetAnonymousPosts(u, 1)
  678. if err != nil {
  679. log.Error("unable to fetch anon posts: %v", err)
  680. }
  681. // nil-out AnonymousPosts slice for easy detection in the template
  682. if p != nil && len(*p) == 0 {
  683. p = nil
  684. }
  685. f, err := getSessionFlashes(app, w, r, nil)
  686. if err != nil {
  687. log.Error("unable to fetch flashes: %v", err)
  688. }
  689. c, err := app.db.GetPublishableCollections(u, app.cfg.App.Host)
  690. if err != nil {
  691. log.Error("unable to fetch collections: %v", err)
  692. }
  693. silenced, err := app.db.IsUserSilenced(u.ID)
  694. if err != nil {
  695. if err == ErrUserNotFound {
  696. return err
  697. }
  698. log.Error("view articles: %v", err)
  699. }
  700. d := struct {
  701. *UserPage
  702. AnonymousPosts *[]PublicPost
  703. Collections *[]Collection
  704. Silenced bool
  705. }{
  706. UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f),
  707. AnonymousPosts: p,
  708. Collections: c,
  709. Silenced: silenced,
  710. }
  711. d.UserPage.SetMessaging(u)
  712. w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
  713. w.Header().Set("Expires", "Thu, 04 Oct 1990 20:00:00 GMT")
  714. showUserPage(w, "articles", d)
  715. return nil
  716. }
  717. func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  718. c, err := app.db.GetCollections(u, app.cfg.App.Host)
  719. if err != nil {
  720. log.Error("unable to fetch collections: %v", err)
  721. return fmt.Errorf("No collections")
  722. }
  723. f, _ := getSessionFlashes(app, w, r, nil)
  724. uc, _ := app.db.GetUserCollectionCount(u.ID)
  725. // TODO: handle any errors
  726. silenced, err := app.db.IsUserSilenced(u.ID)
  727. if err != nil {
  728. if err == ErrUserNotFound {
  729. return err
  730. }
  731. log.Error("view collections: %v", err)
  732. return fmt.Errorf("view collections: %v", err)
  733. }
  734. d := struct {
  735. *UserPage
  736. Collections *[]Collection
  737. UsedCollections, TotalCollections int
  738. NewBlogsDisabled bool
  739. Silenced bool
  740. }{
  741. UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f),
  742. Collections: c,
  743. UsedCollections: int(uc),
  744. NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc),
  745. Silenced: silenced,
  746. }
  747. d.UserPage.SetMessaging(u)
  748. showUserPage(w, "collections", d)
  749. return nil
  750. }
  751. func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  752. vars := mux.Vars(r)
  753. c, err := app.db.GetCollection(vars["collection"])
  754. if err != nil {
  755. return err
  756. }
  757. if c.OwnerID != u.ID {
  758. return ErrCollectionNotFound
  759. }
  760. silenced, err := app.db.IsUserSilenced(u.ID)
  761. if err != nil {
  762. if err == ErrUserNotFound {
  763. return err
  764. }
  765. log.Error("view edit collection %v", err)
  766. return fmt.Errorf("view edit collection: %v", err)
  767. }
  768. flashes, _ := getSessionFlashes(app, w, r, nil)
  769. obj := struct {
  770. *UserPage
  771. *Collection
  772. Silenced bool
  773. config.EmailCfg
  774. LetterReplyTo string
  775. }{
  776. UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
  777. Collection: c,
  778. Silenced: silenced,
  779. EmailCfg: app.cfg.Email,
  780. }
  781. obj.UserPage.CollAlias = c.Alias
  782. if obj.EmailCfg.Enabled() {
  783. obj.LetterReplyTo = app.db.GetCollectionAttribute(c.ID, collAttrLetterReplyTo)
  784. }
  785. showUserPage(w, "collection", obj)
  786. return nil
  787. }
  788. func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error {
  789. reqJSON := IsJSON(r)
  790. var s userSettings
  791. var u *User
  792. var sess *sessions.Session
  793. var err error
  794. if reqJSON {
  795. accessToken := r.Header.Get("Authorization")
  796. if accessToken == "" {
  797. return ErrNoAccessToken
  798. }
  799. u, err = app.db.GetAPIUser(accessToken)
  800. if err != nil {
  801. return ErrBadAccessToken
  802. }
  803. decoder := json.NewDecoder(r.Body)
  804. err := decoder.Decode(&s)
  805. if err != nil {
  806. log.Error("Couldn't parse settings JSON request: %v\n", err)
  807. return ErrBadJSON
  808. }
  809. // Prevent all username updates
  810. // TODO: support changing username via JSON API request
  811. s.Username = ""
  812. } else {
  813. u, sess = getUserAndSession(app, r)
  814. if u == nil {
  815. return ErrNotLoggedIn
  816. }
  817. err := r.ParseForm()
  818. if err != nil {
  819. log.Error("Couldn't parse settings form request: %v\n", err)
  820. return ErrBadFormData
  821. }
  822. err = app.formDecoder.Decode(&s, r.PostForm)
  823. if err != nil {
  824. log.Error("Couldn't decode settings form request: %v\n", err)
  825. return ErrBadFormData
  826. }
  827. }
  828. // Do update
  829. postUpdateReturn := r.FormValue("return")
  830. redirectTo := "/me/settings"
  831. if s.IsLogOut {
  832. redirectTo += "?logout=1"
  833. } else if postUpdateReturn != "" {
  834. redirectTo = postUpdateReturn
  835. }
  836. // Only do updates on values we need
  837. if s.Username != "" && s.Username == u.Username {
  838. // Username hasn't actually changed; blank it out
  839. s.Username = ""
  840. }
  841. err = app.db.ChangeSettings(app, u, &s)
  842. if err != nil {
  843. if reqJSON {
  844. return err
  845. }
  846. if err, ok := err.(impart.HTTPError); ok {
  847. addSessionFlash(app, w, r, err.Message, nil)
  848. }
  849. } else {
  850. // Successful update.
  851. if reqJSON {
  852. return impart.WriteSuccess(w, u, http.StatusOK)
  853. }
  854. if s.IsLogOut {
  855. redirectTo = "/me/logout"
  856. } else {
  857. sess.Values[cookieUserVal] = u.Cookie()
  858. addSessionFlash(app, w, r, "Account updated.", nil)
  859. }
  860. }
  861. w.Header().Set("Location", redirectTo)
  862. w.WriteHeader(http.StatusFound)
  863. return nil
  864. }
  865. func updatePassphrase(app *App, w http.ResponseWriter, r *http.Request) error {
  866. accessToken := r.Header.Get("Authorization")
  867. if accessToken == "" {
  868. return ErrNoAccessToken
  869. }
  870. curPass := r.FormValue("current")
  871. newPass := r.FormValue("new")
  872. // Ensure a new password is given (always required)
  873. if newPass == "" {
  874. return impart.HTTPError{http.StatusBadRequest, "Provide a new password."}
  875. }
  876. userID, sudo := app.db.GetUserIDPrivilege(accessToken)
  877. if userID == -1 {
  878. return ErrBadAccessToken
  879. }
  880. // Ensure a current password is given if the access token doesn't have sudo
  881. // privileges.
  882. if !sudo && curPass == "" {
  883. return impart.HTTPError{http.StatusBadRequest, "Provide current password."}
  884. }
  885. // Hash the new password
  886. hashedPass, err := auth.HashPass([]byte(newPass))
  887. if err != nil {
  888. return impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
  889. }
  890. // Do update
  891. err = app.db.ChangePassphrase(userID, sudo, curPass, hashedPass)
  892. if err != nil {
  893. return err
  894. }
  895. return impart.WriteSuccess(w, struct{}{}, http.StatusOK)
  896. }
  897. func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  898. var c *Collection
  899. var err error
  900. vars := mux.Vars(r)
  901. alias := vars["collection"]
  902. if alias != "" {
  903. c, err = app.db.GetCollection(alias)
  904. if err != nil {
  905. return err
  906. }
  907. if c.OwnerID != u.ID {
  908. return ErrCollectionNotFound
  909. }
  910. c.hostName = app.cfg.App.Host
  911. }
  912. topPosts, err := app.db.GetTopPosts(u, alias, c.hostName)
  913. if err != nil {
  914. log.Error("Unable to get top posts: %v", err)
  915. return err
  916. }
  917. flashes, _ := getSessionFlashes(app, w, r, nil)
  918. titleStats := ""
  919. if c != nil {
  920. titleStats = c.DisplayTitle() + " "
  921. }
  922. silenced, err := app.db.IsUserSilenced(u.ID)
  923. if err != nil {
  924. if err == ErrUserNotFound {
  925. return err
  926. }
  927. log.Error("view stats: %v", err)
  928. return err
  929. }
  930. obj := struct {
  931. *UserPage
  932. VisitsBlog string
  933. Collection *Collection
  934. TopPosts *[]PublicPost
  935. APFollowers int
  936. EmailEnabled bool
  937. EmailSubscribers int
  938. Silenced bool
  939. }{
  940. UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
  941. VisitsBlog: alias,
  942. Collection: c,
  943. TopPosts: topPosts,
  944. EmailEnabled: app.cfg.Email.Enabled(),
  945. Silenced: silenced,
  946. }
  947. obj.UserPage.CollAlias = c.Alias
  948. if app.cfg.App.Federation {
  949. folls, err := app.db.GetAPFollowers(c)
  950. if err != nil {
  951. return err
  952. }
  953. obj.APFollowers = len(*folls)
  954. }
  955. if obj.EmailEnabled {
  956. subs, err := app.db.GetEmailSubscribers(c.ID, true)
  957. if err != nil {
  958. return err
  959. }
  960. obj.EmailSubscribers = len(subs)
  961. }
  962. showUserPage(w, "stats", obj)
  963. return nil
  964. }
  965. func handleViewSubscribers(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  966. vars := mux.Vars(r)
  967. c, err := app.db.GetCollection(vars["collection"])
  968. if err != nil {
  969. return err
  970. }
  971. filter := r.FormValue("filter")
  972. flashes, _ := getSessionFlashes(app, w, r, nil)
  973. obj := struct {
  974. *UserPage
  975. Collection CollectionNav
  976. EmailSubs []*EmailSubscriber
  977. Followers *[]RemoteUser
  978. Silenced bool
  979. Filter string
  980. FederationEnabled bool
  981. CanEmailSub bool
  982. CanAddSubs bool
  983. EmailSubsEnabled bool
  984. }{
  985. UserPage: NewUserPage(app, r, u, c.DisplayTitle()+" Subscribers", flashes),
  986. Collection: CollectionNav{
  987. Collection: c,
  988. Path: r.URL.Path,
  989. SingleUser: app.cfg.App.SingleUser,
  990. },
  991. Silenced: u.IsSilenced(),
  992. Filter: filter,
  993. FederationEnabled: app.cfg.App.Federation,
  994. CanEmailSub: app.cfg.Email.Enabled(),
  995. EmailSubsEnabled: c.EmailSubsEnabled(),
  996. }
  997. obj.Followers, err = app.db.GetAPFollowers(c)
  998. if err != nil {
  999. return err
  1000. }
  1001. obj.EmailSubs, err = app.db.GetEmailSubscribers(c.ID, true)
  1002. if err != nil {
  1003. return err
  1004. }
  1005. if obj.Filter == "" {
  1006. // Set permission to add email subscribers
  1007. //obj.CanAddSubs = app.db.GetUserAttribute(c.OwnerID, userAttrCanAddEmailSubs) == "1"
  1008. }
  1009. showUserPage(w, "subscribers", obj)
  1010. return nil
  1011. }
  1012. func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  1013. fullUser, err := app.db.GetUserByID(u.ID)
  1014. if err != nil {
  1015. if err == ErrUserNotFound {
  1016. return err
  1017. }
  1018. log.Error("Unable to get user for settings: %s", err)
  1019. return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
  1020. }
  1021. passIsSet, err := app.db.IsUserPassSet(u.ID)
  1022. if err != nil {
  1023. log.Error("Unable to get isUserPassSet for settings: %s", err)
  1024. return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
  1025. }
  1026. flashes, _ := getSessionFlashes(app, w, r, nil)
  1027. enableOauthSlack := app.Config().SlackOauth.ClientID != ""
  1028. enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != ""
  1029. enableOauthGitLab := app.Config().GitlabOauth.ClientID != ""
  1030. enableOauthGeneric := app.Config().GenericOauth.ClientID != ""
  1031. enableOauthGitea := app.Config().GiteaOauth.ClientID != ""
  1032. oauthAccounts, err := app.db.GetOauthAccounts(r.Context(), u.ID)
  1033. if err != nil {
  1034. log.Error("Unable to get oauth accounts for settings: %s", err)
  1035. return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
  1036. }
  1037. for idx, oauthAccount := range oauthAccounts {
  1038. switch oauthAccount.Provider {
  1039. case "slack":
  1040. enableOauthSlack = false
  1041. case "write.as":
  1042. enableOauthWriteAs = false
  1043. case "gitlab":
  1044. enableOauthGitLab = false
  1045. case "generic":
  1046. oauthAccounts[idx].DisplayName = app.Config().GenericOauth.DisplayName
  1047. oauthAccounts[idx].AllowDisconnect = app.Config().GenericOauth.AllowDisconnect
  1048. enableOauthGeneric = false
  1049. case "gitea":
  1050. enableOauthGitea = false
  1051. }
  1052. }
  1053. displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGeneric || enableOauthGitea || len(oauthAccounts) > 0
  1054. obj := struct {
  1055. *UserPage
  1056. Email string
  1057. HasPass bool
  1058. IsLogOut bool
  1059. Silenced bool
  1060. CSRFField template.HTML
  1061. OauthSection bool
  1062. OauthAccounts []oauthAccountInfo
  1063. OauthSlack bool
  1064. OauthWriteAs bool
  1065. OauthGitLab bool
  1066. GitLabDisplayName string
  1067. OauthGeneric bool
  1068. OauthGenericDisplayName string
  1069. OauthGitea bool
  1070. GiteaDisplayName string
  1071. }{
  1072. UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
  1073. Email: fullUser.EmailClear(app.keys),
  1074. HasPass: passIsSet,
  1075. IsLogOut: r.FormValue("logout") == "1",
  1076. Silenced: fullUser.IsSilenced(),
  1077. CSRFField: csrf.TemplateField(r),
  1078. OauthSection: displayOauthSection,
  1079. OauthAccounts: oauthAccounts,
  1080. OauthSlack: enableOauthSlack,
  1081. OauthWriteAs: enableOauthWriteAs,
  1082. OauthGitLab: enableOauthGitLab,
  1083. GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName),
  1084. OauthGeneric: enableOauthGeneric,
  1085. OauthGenericDisplayName: config.OrDefaultString(app.Config().GenericOauth.DisplayName, genericOauthDisplayName),
  1086. OauthGitea: enableOauthGitea,
  1087. GiteaDisplayName: config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName),
  1088. }
  1089. showUserPage(w, "settings", obj)
  1090. return nil
  1091. }
  1092. func viewResetPassword(app *App, w http.ResponseWriter, r *http.Request) error {
  1093. token := r.FormValue("t")
  1094. resetting := false
  1095. var userID int64 = 0
  1096. if token != "" {
  1097. // Show new password page
  1098. userID = app.db.GetUserFromPasswordReset(token)
  1099. if userID == 0 {
  1100. return impart.HTTPError{http.StatusNotFound, ""}
  1101. }
  1102. resetting = true
  1103. }
  1104. if r.Method == http.MethodPost {
  1105. newPass := r.FormValue("new-pass")
  1106. if newPass == "" {
  1107. // Send password reset email
  1108. return handleResetPasswordInit(app, w, r)
  1109. }
  1110. // Do actual password reset
  1111. // Assumes token has been validated above
  1112. err := doAutomatedPasswordChange(app, userID, newPass)
  1113. if err != nil {
  1114. return err
  1115. }
  1116. err = app.db.ConsumePasswordResetToken(token)
  1117. if err != nil {
  1118. log.Error("Couldn't consume token %s for user %d!!! %s", token, userID, err)
  1119. }
  1120. addSessionFlash(app, w, r, "Your password was reset. Now you can log in below.", nil)
  1121. return impart.HTTPError{http.StatusFound, "/login"}
  1122. }
  1123. f, _ := getSessionFlashes(app, w, r, nil)
  1124. // Show reset password page
  1125. d := struct {
  1126. page.StaticPage
  1127. Flashes []string
  1128. EmailEnabled bool
  1129. CSRFField template.HTML
  1130. Token string
  1131. IsResetting bool
  1132. IsSent bool
  1133. }{
  1134. StaticPage: pageForReq(app, r),
  1135. Flashes: f,
  1136. EmailEnabled: app.cfg.Email.Enabled(),
  1137. CSRFField: csrf.TemplateField(r),
  1138. Token: token,
  1139. IsResetting: resetting,
  1140. IsSent: r.FormValue("sent") == "1",
  1141. }
  1142. err := pages["reset.tmpl"].ExecuteTemplate(w, "base", d)
  1143. if err != nil {
  1144. log.Error("Unable to render password reset page: %v", err)
  1145. return err
  1146. }
  1147. return err
  1148. }
  1149. func doAutomatedPasswordChange(app *App, userID int64, newPass string) error {
  1150. // Do password reset
  1151. hashedPass, err := auth.HashPass([]byte(newPass))
  1152. if err != nil {
  1153. return impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
  1154. }
  1155. // Do update
  1156. err = app.db.ChangePassphrase(userID, true, "", hashedPass)
  1157. if err != nil {
  1158. return err
  1159. }
  1160. return nil
  1161. }
  1162. func handleResetPasswordInit(app *App, w http.ResponseWriter, r *http.Request) error {
  1163. returnLoc := impart.HTTPError{http.StatusFound, "/reset"}
  1164. if !app.cfg.Email.Enabled() {
  1165. // Email isn't configured, so there's nothing to do; send back to the reset form, where they'll get an explanation
  1166. return returnLoc
  1167. }
  1168. ip := spam.GetIP(r)
  1169. alias := r.FormValue("alias")
  1170. u, err := app.db.GetUserForAuth(alias)
  1171. if err != nil {
  1172. if strings.IndexAny(alias, "@") > 0 {
  1173. addSessionFlash(app, w, r, ErrUserNotFoundEmail.Message, nil)
  1174. return returnLoc
  1175. }
  1176. addSessionFlash(app, w, r, ErrUserNotFound.Message, nil)
  1177. return returnLoc
  1178. }
  1179. if u.IsAdmin() {
  1180. // Prevent any reset emails on admin accounts
  1181. log.Error("Admin reset attempt", `Someone just tried to reset the password for an admin (ID %d - %s). IP address: %s`, u.ID, u.Username, ip)
  1182. return returnLoc
  1183. }
  1184. if u.Email.String == "" {
  1185. err := impart.HTTPError{http.StatusPreconditionFailed, "User doesn't have an email address. Please contact us (" + app.cfg.App.Host + "/contact) to reset your password."}
  1186. addSessionFlash(app, w, r, err.Message, nil)
  1187. return returnLoc
  1188. }
  1189. if isSet, _ := app.db.IsUserPassSet(u.ID); !isSet {
  1190. err = loginViaEmail(app, u.Username, "/me/settings")
  1191. if err != nil {
  1192. return err
  1193. }
  1194. addSessionFlash(app, w, r, "We've emailed you a link to log in with.", nil)
  1195. return returnLoc
  1196. }
  1197. token, err := app.db.CreatePasswordResetToken(u.ID)
  1198. if err != nil {
  1199. log.Error("Error resetting password: %s", err)
  1200. addSessionFlash(app, w, r, ErrInternalGeneral.Message, nil)
  1201. return returnLoc
  1202. }
  1203. err = emailPasswordReset(app, u.EmailClear(app.keys), token)
  1204. if err != nil {
  1205. log.Error("Error emailing password reset: %s", err)
  1206. addSessionFlash(app, w, r, ErrInternalGeneral.Message, nil)
  1207. return returnLoc
  1208. }
  1209. addSessionFlash(app, w, r, "We sent an email to the address associated with this account.", nil)
  1210. returnLoc.Message += "?sent=1"
  1211. return returnLoc
  1212. }
  1213. func emailPasswordReset(app *App, toEmail, token string) error {
  1214. // Send email
  1215. gun := mailgun.NewMailgun(app.cfg.Email.Domain, app.cfg.Email.MailgunPrivate)
  1216. footerPara := "Didn't request this password reset? Your account is still safe, and you can safely ignore this email."
  1217. plainMsg := fmt.Sprintf("We received a request to reset your password on %s. Please click the following link to continue (or copy and paste it into your browser): %s/reset?t=%s\n\n%s", app.cfg.App.SiteName, app.cfg.App.Host, token, footerPara)
  1218. m := mailgun.NewMessage(app.cfg.App.SiteName+" <noreply-password@"+app.cfg.Email.Domain+">", "Reset Your "+app.cfg.App.SiteName+" Password", plainMsg, fmt.Sprintf("<%s>", toEmail))
  1219. m.AddTag("Password Reset")
  1220. m.SetHtml(fmt.Sprintf(`<html>
  1221. <body style="font-family:Lora, 'Palatino Linotype', Palatino, Baskerville, 'Book Antiqua', 'New York', 'DejaVu serif', serif; font-size: 100%%; margin:1em 2em;">
  1222. <div style="margin:0 auto; max-width: 40em; font-size: 1.2em;">
  1223. <h1 style="font-size:1.75em"><a style="text-decoration:none;color:#000;" href="%s">%s</a></h1>
  1224. <p>We received a request to reset your password on %s. Please click the following link to continue:</p>
  1225. <p style="font-size:1.2em;margin-bottom:1.5em;"><a href="%s/reset?t=%s">Reset your password</a></p>
  1226. <p style="font-size: 0.86em;margin:1em auto">%s</p>
  1227. </div>
  1228. </body>
  1229. </html>`, app.cfg.App.Host, app.cfg.App.SiteName, app.cfg.App.SiteName, app.cfg.App.Host, token, footerPara))
  1230. _, _, err := gun.Send(m)
  1231. return err
  1232. }
  1233. func loginViaEmail(app *App, alias, redirectTo string) error {
  1234. if !app.cfg.Email.Enabled() {
  1235. return fmt.Errorf("EMAIL ISN'T CONFIGURED on this server")
  1236. }
  1237. // Make sure user has added an email
  1238. // TODO: create a new func to just get user's email; "ForAuth" doesn't match here
  1239. u, _ := app.db.GetUserForAuth(alias)
  1240. if u == nil {
  1241. if strings.IndexAny(alias, "@") > 0 {
  1242. return ErrUserNotFoundEmail
  1243. }
  1244. return ErrUserNotFound
  1245. }
  1246. if u.Email.String == "" {
  1247. return impart.HTTPError{http.StatusPreconditionFailed, "User doesn't have an email address. Log in with password, instead."}
  1248. }
  1249. // Generate one-time login token
  1250. t, err := app.db.GetTemporaryOneTimeAccessToken(u.ID, 60*15, true)
  1251. if err != nil {
  1252. log.Error("Unable to generate token for email login: %s", err)
  1253. return impart.HTTPError{http.StatusInternalServerError, "Unable to generate token."}
  1254. }
  1255. // Send email
  1256. gun := mailgun.NewMailgun(app.cfg.Email.Domain, app.cfg.Email.MailgunPrivate)
  1257. toEmail := u.EmailClear(app.keys)
  1258. 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."
  1259. 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)
  1260. m := mailgun.NewMessage(app.cfg.App.SiteName+" <noreply-login@"+app.cfg.Email.Domain+">", "Log in to "+app.cfg.App.SiteName, plainMsg, fmt.Sprintf("<%s>", toEmail))
  1261. m.AddTag("Email Login")
  1262. m.SetHtml(fmt.Sprintf(`<html>
  1263. <body style="font-family:Lora, 'Palatino Linotype', Palatino, Baskerville, 'Book Antiqua', 'New York', 'DejaVu serif', serif; font-size: 100%%; margin:1em 2em;">
  1264. <div style="margin:0 auto; max-width: 40em; font-size: 1.2em;">
  1265. <h1 style="font-size:1.75em"><a style="text-decoration:none;color:#000;" href="%s">%s</a></h1>
  1266. <p style="font-size:1.2em;margin-bottom:1.5em;text-align:center"><a href="%s/login?to=%s&with=%s">Log in to %s here</a>.</p>
  1267. <p style="font-size: 0.86em;color:#666;text-align:center;max-width:35em;margin:1em auto">%s</p>
  1268. </div>
  1269. </body>
  1270. </html>`, app.cfg.App.Host, app.cfg.App.SiteName, app.cfg.App.Host, redirectTo, t, app.cfg.App.SiteName, footerPara))
  1271. _, _, err = gun.Send(m)
  1272. return err
  1273. }
  1274. func saveTempInfo(app *App, key, val string, r *http.Request, w http.ResponseWriter) error {
  1275. session, err := app.sessionStore.Get(r, "t")
  1276. if err != nil {
  1277. return ErrInternalCookieSession
  1278. }
  1279. session.Values[key] = val
  1280. err = session.Save(r, w)
  1281. if err != nil {
  1282. log.Error("Couldn't saveTempInfo for key-val (%s:%s): %v", key, val, err)
  1283. }
  1284. return err
  1285. }
  1286. func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) string {
  1287. session, err := app.sessionStore.Get(r, "t")
  1288. if err != nil {
  1289. return ""
  1290. }
  1291. // Get the information
  1292. var s = ""
  1293. var ok bool
  1294. if s, ok = session.Values[key].(string); !ok {
  1295. return ""
  1296. }
  1297. // Delete cookie
  1298. session.Options.MaxAge = -1
  1299. err = session.Save(r, w)
  1300. if err != nil {
  1301. log.Error("Couldn't erase temp data for key %s: %v", key, err)
  1302. }
  1303. // Return value
  1304. return s
  1305. }
  1306. func handleUserDelete(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  1307. if !app.cfg.App.OpenDeletion {
  1308. return impart.HTTPError{http.StatusForbidden, "Open account deletion is disabled on this instance."}
  1309. }
  1310. confirmUsername := r.PostFormValue("confirm-username")
  1311. if u.Username != confirmUsername {
  1312. return impart.HTTPError{http.StatusBadRequest, "Confirmation username must match your username exactly."}
  1313. }
  1314. // Check for account deletion safeguards in place
  1315. if u.IsAdmin() {
  1316. return impart.HTTPError{http.StatusForbidden, "Cannot delete admin."}
  1317. }
  1318. err := app.db.DeleteAccount(u.ID)
  1319. if err != nil {
  1320. log.Error("user delete account: %v", err)
  1321. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not delete account: %v", err)}
  1322. }
  1323. // FIXME: This doesn't ever appear to the user, as (I believe) the value is erased when the session cookie is reset
  1324. _ = addSessionFlash(app, w, r, "Thanks for writing with us! You account was deleted successfully.", nil)
  1325. return impart.HTTPError{http.StatusFound, "/me/logout"}
  1326. }
  1327. func removeOauth(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  1328. provider := r.FormValue("provider")
  1329. clientID := r.FormValue("client_id")
  1330. remoteUserID := r.FormValue("remote_user_id")
  1331. err := app.db.RemoveOauth(r.Context(), u.ID, provider, clientID, remoteUserID)
  1332. if err != nil {
  1333. return impart.HTTPError{Status: http.StatusInternalServerError, Message: err.Error()}
  1334. }
  1335. return impart.HTTPError{Status: http.StatusFound, Message: "/me/settings"}
  1336. }
  1337. func prepareUserEmail(input string, emailKey []byte) zero.String {
  1338. email := zero.NewString("", input != "")
  1339. if len(input) > 0 {
  1340. encEmail, err := data.Encrypt(emailKey, input)
  1341. if err != nil {
  1342. log.Error("Unable to encrypt email: %s\n", err)
  1343. } else {
  1344. email.String = string(encEmail)
  1345. }
  1346. }
  1347. return email
  1348. }