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.
 
 
 
 
 

1183 lines
31 KiB

  1. /*
  2. * Copyright © 2018-2020 A Bunch Tell 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. "html/template"
  15. "net/http"
  16. "regexp"
  17. "strings"
  18. "sync"
  19. "time"
  20. "github.com/gorilla/mux"
  21. "github.com/gorilla/sessions"
  22. "github.com/guregu/null/zero"
  23. "github.com/writeas/impart"
  24. "github.com/writeas/web-core/auth"
  25. "github.com/writeas/web-core/data"
  26. "github.com/writeas/web-core/log"
  27. "github.com/writeas/writefreely/author"
  28. "github.com/writeas/writefreely/config"
  29. "github.com/writeas/writefreely/page"
  30. )
  31. type (
  32. userSettings struct {
  33. Username string `schema:"username" json:"username"`
  34. Email string `schema:"email" json:"email"`
  35. NewPass string `schema:"new-pass" json:"new_pass"`
  36. OldPass string `schema:"current-pass" json:"current_pass"`
  37. IsLogOut bool `schema:"logout" json:"logout"`
  38. }
  39. UserPage struct {
  40. page.StaticPage
  41. PageTitle string
  42. Separator template.HTML
  43. IsAdmin bool
  44. CanInvite bool
  45. CollAlias string
  46. }
  47. )
  48. func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []string) *UserPage {
  49. up := &UserPage{
  50. StaticPage: pageForReq(app, r),
  51. PageTitle: title,
  52. }
  53. up.Username = u.Username
  54. up.Flashes = flashes
  55. up.Path = r.URL.Path
  56. up.IsAdmin = u.IsAdmin()
  57. up.CanInvite = canUserInvite(app.cfg, up.IsAdmin)
  58. return up
  59. }
  60. func canUserInvite(cfg *config.Config, isAdmin bool) bool {
  61. return cfg.App.UserInvites != "" &&
  62. (isAdmin || cfg.App.UserInvites != "admin")
  63. }
  64. func (up *UserPage) SetMessaging(u *User) {
  65. // up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID)
  66. }
  67. const (
  68. loginAttemptExpiration = 3 * time.Second
  69. )
  70. var actuallyUsernameReg = regexp.MustCompile("username is actually ([a-z0-9\\-]+)\\. Please try that, instead")
  71. func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error {
  72. _, err := signup(app, w, r)
  73. return err
  74. }
  75. func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
  76. if app.cfg.App.DisablePasswordAuth {
  77. err := ErrDisabledPasswordAuth
  78. return nil, err
  79. }
  80. reqJSON := IsJSON(r)
  81. // Get params
  82. var ur userRegistration
  83. if reqJSON {
  84. decoder := json.NewDecoder(r.Body)
  85. err := decoder.Decode(&ur)
  86. if err != nil {
  87. log.Error("Couldn't parse signup JSON request: %v\n", err)
  88. return nil, ErrBadJSON
  89. }
  90. } else {
  91. // Check if user is already logged in
  92. u := getUserSession(app, r)
  93. if u != nil {
  94. return &AuthUser{User: u}, nil
  95. }
  96. err := r.ParseForm()
  97. if err != nil {
  98. log.Error("Couldn't parse signup form request: %v\n", err)
  99. return nil, ErrBadFormData
  100. }
  101. err = app.formDecoder.Decode(&ur, r.PostForm)
  102. if err != nil {
  103. log.Error("Couldn't decode signup form request: %v\n", err)
  104. return nil, ErrBadFormData
  105. }
  106. }
  107. return signupWithRegistration(app, ur, w, r)
  108. }
  109. func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
  110. reqJSON := IsJSON(r)
  111. // Validate required params (alias)
  112. if signup.Alias == "" {
  113. return nil, impart.HTTPError{http.StatusBadRequest, "A username is required."}
  114. }
  115. if signup.Pass == "" {
  116. return nil, impart.HTTPError{http.StatusBadRequest, "A password is required."}
  117. }
  118. var desiredUsername string
  119. if signup.Normalize {
  120. // With this option we simply conform the username to what we expect
  121. // without complaining. Since they might've done something funny, like
  122. // enter: write.as/Way Out There, we'll use their raw input for the new
  123. // collection name and sanitize for the slug / username.
  124. desiredUsername = signup.Alias
  125. signup.Alias = getSlug(signup.Alias, "")
  126. }
  127. if !author.IsValidUsername(app.cfg, signup.Alias) {
  128. // Ensure the username is syntactically correct.
  129. 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."}
  130. }
  131. // Handle empty optional params
  132. // TODO: remove this var
  133. createdWithPass := true
  134. hashedPass, err := auth.HashPass([]byte(signup.Pass))
  135. if err != nil {
  136. return nil, impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
  137. }
  138. // Create struct to insert
  139. u := &User{
  140. Username: signup.Alias,
  141. HashedPass: hashedPass,
  142. HasPass: createdWithPass,
  143. Email: prepareUserEmail(signup.Email, app.keys.EmailKey),
  144. Created: time.Now().Truncate(time.Second).UTC(),
  145. }
  146. // Create actual user
  147. if err := app.db.CreateUser(app.cfg, u, desiredUsername); err != nil {
  148. return nil, err
  149. }
  150. // Log invite if needed
  151. if signup.InviteCode != "" {
  152. err = app.db.CreateInvitedUser(signup.InviteCode, u.ID)
  153. if err != nil {
  154. return nil, err
  155. }
  156. }
  157. // Add back unencrypted data for response
  158. if signup.Email != "" {
  159. u.Email.String = signup.Email
  160. }
  161. resUser := &AuthUser{
  162. User: u,
  163. }
  164. if !createdWithPass {
  165. resUser.Password = signup.Pass
  166. }
  167. title := signup.Alias
  168. if signup.Normalize {
  169. title = desiredUsername
  170. }
  171. resUser.Collections = &[]Collection{
  172. {
  173. Alias: signup.Alias,
  174. Title: title,
  175. },
  176. }
  177. var token string
  178. if reqJSON && !signup.Web {
  179. token, err = app.db.GetAccessToken(u.ID)
  180. if err != nil {
  181. return nil, impart.HTTPError{http.StatusInternalServerError, "Could not create access token. Try re-authenticating."}
  182. }
  183. resUser.AccessToken = token
  184. } else {
  185. session, err := app.sessionStore.Get(r, cookieName)
  186. if err != nil {
  187. // The cookie should still save, even if there's an error.
  188. // Source: https://github.com/gorilla/sessions/issues/16#issuecomment-143642144
  189. log.Error("Session: %v; ignoring", err)
  190. }
  191. session.Values[cookieUserVal] = resUser.User.Cookie()
  192. err = session.Save(r, w)
  193. if err != nil {
  194. log.Error("Couldn't save session: %v", err)
  195. return nil, err
  196. }
  197. }
  198. if reqJSON {
  199. return resUser, impart.WriteSuccess(w, resUser, http.StatusCreated)
  200. }
  201. return resUser, nil
  202. }
  203. func viewLogout(app *App, w http.ResponseWriter, r *http.Request) error {
  204. session, err := app.sessionStore.Get(r, cookieName)
  205. if err != nil {
  206. return ErrInternalCookieSession
  207. }
  208. // Ensure user has an email or password set before they go, so they don't
  209. // lose access to their account.
  210. val := session.Values[cookieUserVal]
  211. var u = &User{}
  212. var ok bool
  213. if u, ok = val.(*User); !ok {
  214. log.Error("Error casting user object on logout. Vals: %+v Resetting cookie.", session.Values)
  215. err = session.Save(r, w)
  216. if err != nil {
  217. log.Error("Couldn't save session on logout: %v", err)
  218. return impart.HTTPError{http.StatusInternalServerError, "Unable to save cookie session."}
  219. }
  220. return impart.HTTPError{http.StatusFound, "/"}
  221. }
  222. u, err = app.db.GetUserByID(u.ID)
  223. if err != nil && err != ErrUserNotFound {
  224. return impart.HTTPError{http.StatusInternalServerError, "Unable to fetch user information."}
  225. }
  226. session.Options.MaxAge = -1
  227. err = session.Save(r, w)
  228. if err != nil {
  229. log.Error("Couldn't save session on logout: %v", err)
  230. return impart.HTTPError{http.StatusInternalServerError, "Unable to save cookie session."}
  231. }
  232. return impart.HTTPError{http.StatusFound, "/"}
  233. }
  234. func handleAPILogout(app *App, w http.ResponseWriter, r *http.Request) error {
  235. accessToken := r.Header.Get("Authorization")
  236. if accessToken == "" {
  237. return ErrNoAccessToken
  238. }
  239. t := auth.GetToken(accessToken)
  240. if len(t) == 0 {
  241. return ErrNoAccessToken
  242. }
  243. err := app.db.DeleteToken(t)
  244. if err != nil {
  245. return err
  246. }
  247. return impart.HTTPError{Status: http.StatusNoContent}
  248. }
  249. func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
  250. var earlyError string
  251. oneTimeToken := r.FormValue("with")
  252. if oneTimeToken != "" {
  253. log.Info("Calling login with one-time token.")
  254. err := login(app, w, r)
  255. if err != nil {
  256. log.Info("Received error: %v", err)
  257. earlyError = fmt.Sprintf("%s", err)
  258. }
  259. }
  260. session, err := app.sessionStore.Get(r, cookieName)
  261. if err != nil {
  262. // Ignore this
  263. log.Error("Unable to get session; ignoring: %v", err)
  264. }
  265. p := &struct {
  266. page.StaticPage
  267. *OAuthButtons
  268. To string
  269. Message template.HTML
  270. Flashes []template.HTML
  271. LoginUsername string
  272. }{
  273. StaticPage: pageForReq(app, r),
  274. OAuthButtons: NewOAuthButtons(app.Config()),
  275. To: r.FormValue("to"),
  276. Message: template.HTML(""),
  277. Flashes: []template.HTML{},
  278. LoginUsername: getTempInfo(app, "login-user", r, w),
  279. }
  280. if earlyError != "" {
  281. p.Flashes = append(p.Flashes, template.HTML(earlyError))
  282. }
  283. // Display any error messages
  284. flashes, _ := getSessionFlashes(app, w, r, session)
  285. for _, flash := range flashes {
  286. p.Flashes = append(p.Flashes, template.HTML(flash))
  287. }
  288. err = pages["login.tmpl"].ExecuteTemplate(w, "base", p)
  289. if err != nil {
  290. log.Error("Unable to render login: %v", err)
  291. return err
  292. }
  293. return nil
  294. }
  295. func webLogin(app *App, w http.ResponseWriter, r *http.Request) error {
  296. err := login(app, w, r)
  297. if err != nil {
  298. username := r.FormValue("alias")
  299. // Login request was unsuccessful; save the error in the session and redirect them
  300. if err, ok := err.(impart.HTTPError); ok {
  301. session, _ := app.sessionStore.Get(r, cookieName)
  302. if session != nil {
  303. session.AddFlash(err.Message)
  304. session.Save(r, w)
  305. }
  306. if m := actuallyUsernameReg.FindStringSubmatch(err.Message); len(m) > 0 {
  307. // Retain fixed username recommendation for the login form
  308. username = m[1]
  309. }
  310. }
  311. // Pass along certain information
  312. saveTempInfo(app, "login-user", username, r, w)
  313. // Retain post-login URL if one was given
  314. redirectTo := "/login"
  315. postLoginRedirect := r.FormValue("to")
  316. if postLoginRedirect != "" {
  317. redirectTo += "?to=" + postLoginRedirect
  318. }
  319. log.Error("Unable to login: %v", err)
  320. return impart.HTTPError{http.StatusTemporaryRedirect, redirectTo}
  321. }
  322. return nil
  323. }
  324. var loginAttemptUsers = sync.Map{}
  325. func login(app *App, w http.ResponseWriter, r *http.Request) error {
  326. reqJSON := IsJSON(r)
  327. oneTimeToken := r.FormValue("with")
  328. verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "")
  329. redirectTo := r.FormValue("to")
  330. if redirectTo == "" {
  331. if app.cfg.App.SingleUser {
  332. redirectTo = "/me/new"
  333. } else {
  334. redirectTo = "/"
  335. }
  336. }
  337. var u *User
  338. var err error
  339. var signin userCredentials
  340. if app.cfg.App.DisablePasswordAuth {
  341. err := ErrDisabledPasswordAuth
  342. return err
  343. }
  344. // Log in with one-time token if one is given
  345. if oneTimeToken != "" {
  346. log.Info("Login: Logging user in via token.")
  347. userID := app.db.GetUserID(oneTimeToken)
  348. if userID == -1 {
  349. log.Error("Login: Got user -1 from token")
  350. err := ErrBadAccessToken
  351. err.Message = "Expired or invalid login code."
  352. return err
  353. }
  354. log.Info("Login: Found user %d.", userID)
  355. u, err = app.db.GetUserByID(userID)
  356. if err != nil {
  357. log.Error("Unable to fetch user on one-time token login: %v", err)
  358. return impart.HTTPError{http.StatusInternalServerError, "There was an error retrieving the user you want."}
  359. }
  360. log.Info("Login: Got user via token")
  361. } else {
  362. // Get params
  363. if reqJSON {
  364. decoder := json.NewDecoder(r.Body)
  365. err := decoder.Decode(&signin)
  366. if err != nil {
  367. log.Error("Couldn't parse signin JSON request: %v\n", err)
  368. return ErrBadJSON
  369. }
  370. } else {
  371. err := r.ParseForm()
  372. if err != nil {
  373. log.Error("Couldn't parse signin form request: %v\n", err)
  374. return ErrBadFormData
  375. }
  376. err = app.formDecoder.Decode(&signin, r.PostForm)
  377. if err != nil {
  378. log.Error("Couldn't decode signin form request: %v\n", err)
  379. return ErrBadFormData
  380. }
  381. }
  382. log.Info("Login: Attempting login for '%s'", signin.Alias)
  383. // Validate required params (all)
  384. if signin.Alias == "" {
  385. msg := "Parameter `alias` required."
  386. if signin.Web {
  387. msg = "A username is required."
  388. }
  389. return impart.HTTPError{http.StatusBadRequest, msg}
  390. }
  391. if !signin.EmailLogin && signin.Pass == "" {
  392. msg := "Parameter `pass` required."
  393. if signin.Web {
  394. msg = "A password is required."
  395. }
  396. return impart.HTTPError{http.StatusBadRequest, msg}
  397. }
  398. // Prevent excessive login attempts on the same account
  399. // Skip this check in dev environment
  400. if !app.cfg.Server.Dev {
  401. now := time.Now()
  402. attemptExp, att := loginAttemptUsers.LoadOrStore(signin.Alias, now.Add(loginAttemptExpiration))
  403. if att {
  404. if attemptExpTime, ok := attemptExp.(time.Time); ok {
  405. if attemptExpTime.After(now) {
  406. // This user attempted previously, and the period hasn't expired yet
  407. return impart.HTTPError{http.StatusTooManyRequests, "You're doing that too much."}
  408. } else {
  409. // This user attempted previously, but the time expired; free up space
  410. loginAttemptUsers.Delete(signin.Alias)
  411. }
  412. } else {
  413. log.Error("Unable to cast expiration to time")
  414. }
  415. }
  416. }
  417. // Retrieve password
  418. u, err = app.db.GetUserForAuth(signin.Alias)
  419. if err != nil {
  420. log.Info("Unable to getUserForAuth on %s: %v", signin.Alias, err)
  421. if strings.IndexAny(signin.Alias, "@") > 0 {
  422. log.Info("Suggesting: %s", ErrUserNotFoundEmail.Message)
  423. return ErrUserNotFoundEmail
  424. }
  425. return err
  426. }
  427. // Authenticate
  428. if u.Email.String == "" {
  429. // User has no email set, so check if they haven't added a password, either,
  430. // so we can return a more helpful error message.
  431. if hasPass, _ := app.db.IsUserPassSet(u.ID); !hasPass {
  432. log.Info("Tried logging in to %s, but no password or email.", signin.Alias)
  433. return impart.HTTPError{http.StatusPreconditionFailed, "This user never added a password or email address. Please contact us for help."}
  434. }
  435. }
  436. if len(u.HashedPass) == 0 {
  437. return impart.HTTPError{http.StatusUnauthorized, "This user never set a password. Perhaps try logging in via OAuth?"}
  438. }
  439. if !auth.Authenticated(u.HashedPass, []byte(signin.Pass)) {
  440. return impart.HTTPError{http.StatusUnauthorized, "Incorrect password."}
  441. }
  442. }
  443. if reqJSON && !signin.Web {
  444. var token string
  445. if r.Header.Get("User-Agent") == "" {
  446. // Get last created token when User-Agent is empty
  447. token = app.db.FetchLastAccessToken(u.ID)
  448. if token == "" {
  449. token, err = app.db.GetAccessToken(u.ID)
  450. }
  451. } else {
  452. token, err = app.db.GetAccessToken(u.ID)
  453. }
  454. if err != nil {
  455. log.Error("Login: Unable to create access token: %v", err)
  456. return impart.HTTPError{http.StatusInternalServerError, "Could not create access token. Try re-authenticating."}
  457. }
  458. resUser := getVerboseAuthUser(app, token, u, verbose)
  459. return impart.WriteSuccess(w, resUser, http.StatusOK)
  460. }
  461. session, err := app.sessionStore.Get(r, cookieName)
  462. if err != nil {
  463. // The cookie should still save, even if there's an error.
  464. log.Error("Login: Session: %v; ignoring", err)
  465. }
  466. // Remove unwanted data
  467. session.Values[cookieUserVal] = u.Cookie()
  468. err = session.Save(r, w)
  469. if err != nil {
  470. log.Error("Login: Couldn't save session: %v", err)
  471. // TODO: return error
  472. }
  473. // Send success
  474. if reqJSON {
  475. return impart.WriteSuccess(w, &AuthUser{User: u}, http.StatusOK)
  476. }
  477. log.Info("Login: Redirecting to %s", redirectTo)
  478. w.Header().Set("Location", redirectTo)
  479. w.WriteHeader(http.StatusFound)
  480. return nil
  481. }
  482. func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser {
  483. resUser := &AuthUser{
  484. AccessToken: token,
  485. User: u,
  486. }
  487. // Fetch verbose user data if requested
  488. if verbose {
  489. posts, err := app.db.GetUserPosts(u)
  490. if err != nil {
  491. log.Error("Login: Unable to get user posts: %v", err)
  492. }
  493. colls, err := app.db.GetCollections(u, app.cfg.App.Host)
  494. if err != nil {
  495. log.Error("Login: Unable to get user collections: %v", err)
  496. }
  497. passIsSet, err := app.db.IsUserPassSet(u.ID)
  498. if err != nil {
  499. // TODO: correct error meesage
  500. log.Error("Login: Unable to get user collections: %v", err)
  501. }
  502. resUser.Posts = posts
  503. resUser.Collections = colls
  504. resUser.User.HasPass = passIsSet
  505. }
  506. return resUser
  507. }
  508. func viewExportOptions(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  509. // Fetch extra user data
  510. p := NewUserPage(app, r, u, "Export", nil)
  511. showUserPage(w, "export", p)
  512. return nil
  513. }
  514. func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
  515. var filename string
  516. var u = &User{}
  517. reqJSON := IsJSON(r)
  518. if reqJSON {
  519. // Use given Authorization header
  520. accessToken := r.Header.Get("Authorization")
  521. if accessToken == "" {
  522. return nil, filename, ErrNoAccessToken
  523. }
  524. userID := app.db.GetUserID(accessToken)
  525. if userID == -1 {
  526. return nil, filename, ErrBadAccessToken
  527. }
  528. var err error
  529. u, err = app.db.GetUserByID(userID)
  530. if err != nil {
  531. return nil, filename, impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve requested user."}
  532. }
  533. } else {
  534. // Use user cookie
  535. session, err := app.sessionStore.Get(r, cookieName)
  536. if err != nil {
  537. // The cookie should still save, even if there's an error.
  538. log.Error("Session: %v; ignoring", err)
  539. }
  540. val := session.Values[cookieUserVal]
  541. var ok bool
  542. if u, ok = val.(*User); !ok {
  543. return nil, filename, ErrNotLoggedIn
  544. }
  545. }
  546. filename = u.Username + "-posts-" + time.Now().Truncate(time.Second).UTC().Format("200601021504")
  547. // Fetch data we're exporting
  548. var err error
  549. var data []byte
  550. posts, err := app.db.GetUserPosts(u)
  551. if err != nil {
  552. return data, filename, err
  553. }
  554. // Export as CSV
  555. if strings.HasSuffix(r.URL.Path, ".csv") {
  556. data = exportPostsCSV(app.cfg.App.Host, u, posts)
  557. return data, filename, err
  558. }
  559. if strings.HasSuffix(r.URL.Path, ".zip") {
  560. data = exportPostsZip(u, posts)
  561. return data, filename, err
  562. }
  563. if r.FormValue("pretty") == "1" {
  564. data, err = json.MarshalIndent(posts, "", "\t")
  565. } else {
  566. data, err = json.Marshal(posts)
  567. }
  568. return data, filename, err
  569. }
  570. func viewExportFull(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
  571. var err error
  572. filename := ""
  573. u := getUserSession(app, r)
  574. if u == nil {
  575. return nil, filename, ErrNotLoggedIn
  576. }
  577. filename = u.Username + "-" + time.Now().Truncate(time.Second).UTC().Format("200601021504")
  578. exportUser := compileFullExport(app, u)
  579. var data []byte
  580. if r.FormValue("pretty") == "1" {
  581. data, err = json.MarshalIndent(exportUser, "", "\t")
  582. } else {
  583. data, err = json.Marshal(exportUser)
  584. }
  585. return data, filename, err
  586. }
  587. func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error {
  588. reqJSON := IsJSON(r)
  589. uObj := struct {
  590. ID int64 `json:"id,omitempty"`
  591. Username string `json:"username,omitempty"`
  592. }{}
  593. var err error
  594. if reqJSON {
  595. _, uObj.Username, err = app.db.GetUserDataFromToken(r.Header.Get("Authorization"))
  596. if err != nil {
  597. return err
  598. }
  599. } else {
  600. u := getUserSession(app, r)
  601. if u == nil {
  602. return impart.WriteSuccess(w, uObj, http.StatusOK)
  603. }
  604. uObj.Username = u.Username
  605. }
  606. return impart.WriteSuccess(w, uObj, http.StatusOK)
  607. }
  608. func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  609. reqJSON := IsJSON(r)
  610. if !reqJSON {
  611. return ErrBadRequestedType
  612. }
  613. var err error
  614. p := GetPostsCache(u.ID)
  615. if p == nil {
  616. userPostsCache.Lock()
  617. if userPostsCache.users[u.ID].ready == nil {
  618. userPostsCache.users[u.ID] = postsCacheItem{ready: make(chan struct{})}
  619. userPostsCache.Unlock()
  620. p, err = app.db.GetUserPosts(u)
  621. if err != nil {
  622. return err
  623. }
  624. CachePosts(u.ID, p)
  625. } else {
  626. userPostsCache.Unlock()
  627. <-userPostsCache.users[u.ID].ready
  628. p = GetPostsCache(u.ID)
  629. }
  630. }
  631. return impart.WriteSuccess(w, p, http.StatusOK)
  632. }
  633. func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  634. reqJSON := IsJSON(r)
  635. if !reqJSON {
  636. return ErrBadRequestedType
  637. }
  638. p, err := app.db.GetCollections(u, app.cfg.App.Host)
  639. if err != nil {
  640. return err
  641. }
  642. return impart.WriteSuccess(w, p, http.StatusOK)
  643. }
  644. func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  645. p, err := app.db.GetAnonymousPosts(u)
  646. if err != nil {
  647. log.Error("unable to fetch anon posts: %v", err)
  648. }
  649. // nil-out AnonymousPosts slice for easy detection in the template
  650. if p != nil && len(*p) == 0 {
  651. p = nil
  652. }
  653. f, err := getSessionFlashes(app, w, r, nil)
  654. if err != nil {
  655. log.Error("unable to fetch flashes: %v", err)
  656. }
  657. c, err := app.db.GetPublishableCollections(u, app.cfg.App.Host)
  658. if err != nil {
  659. log.Error("unable to fetch collections: %v", err)
  660. }
  661. silenced, err := app.db.IsUserSilenced(u.ID)
  662. if err != nil {
  663. log.Error("view articles: %v", err)
  664. }
  665. d := struct {
  666. *UserPage
  667. AnonymousPosts *[]PublicPost
  668. Collections *[]Collection
  669. Silenced bool
  670. }{
  671. UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f),
  672. AnonymousPosts: p,
  673. Collections: c,
  674. Silenced: silenced,
  675. }
  676. d.UserPage.SetMessaging(u)
  677. w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
  678. w.Header().Set("Expires", "Thu, 04 Oct 1990 20:00:00 GMT")
  679. showUserPage(w, "articles", d)
  680. return nil
  681. }
  682. func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  683. c, err := app.db.GetCollections(u, app.cfg.App.Host)
  684. if err != nil {
  685. log.Error("unable to fetch collections: %v", err)
  686. return fmt.Errorf("No collections")
  687. }
  688. f, _ := getSessionFlashes(app, w, r, nil)
  689. uc, _ := app.db.GetUserCollectionCount(u.ID)
  690. // TODO: handle any errors
  691. silenced, err := app.db.IsUserSilenced(u.ID)
  692. if err != nil {
  693. log.Error("view collections %v", err)
  694. return fmt.Errorf("view collections: %v", err)
  695. }
  696. d := struct {
  697. *UserPage
  698. Collections *[]Collection
  699. UsedCollections, TotalCollections int
  700. NewBlogsDisabled bool
  701. Silenced bool
  702. }{
  703. UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f),
  704. Collections: c,
  705. UsedCollections: int(uc),
  706. NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc),
  707. Silenced: silenced,
  708. }
  709. d.UserPage.SetMessaging(u)
  710. showUserPage(w, "collections", d)
  711. return nil
  712. }
  713. func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  714. vars := mux.Vars(r)
  715. c, err := app.db.GetCollection(vars["collection"])
  716. if err != nil {
  717. return err
  718. }
  719. if c.OwnerID != u.ID {
  720. return ErrCollectionNotFound
  721. }
  722. silenced, err := app.db.IsUserSilenced(u.ID)
  723. if err != nil {
  724. log.Error("view edit collection %v", err)
  725. return fmt.Errorf("view edit collection: %v", err)
  726. }
  727. flashes, _ := getSessionFlashes(app, w, r, nil)
  728. obj := struct {
  729. *UserPage
  730. *Collection
  731. Silenced bool
  732. }{
  733. UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
  734. Collection: c,
  735. Silenced: silenced,
  736. }
  737. obj.UserPage.CollAlias = c.Alias
  738. showUserPage(w, "collection", obj)
  739. return nil
  740. }
  741. func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error {
  742. reqJSON := IsJSON(r)
  743. var s userSettings
  744. var u *User
  745. var sess *sessions.Session
  746. var err error
  747. if reqJSON {
  748. accessToken := r.Header.Get("Authorization")
  749. if accessToken == "" {
  750. return ErrNoAccessToken
  751. }
  752. u, err = app.db.GetAPIUser(accessToken)
  753. if err != nil {
  754. return ErrBadAccessToken
  755. }
  756. decoder := json.NewDecoder(r.Body)
  757. err := decoder.Decode(&s)
  758. if err != nil {
  759. log.Error("Couldn't parse settings JSON request: %v\n", err)
  760. return ErrBadJSON
  761. }
  762. // Prevent all username updates
  763. // TODO: support changing username via JSON API request
  764. s.Username = ""
  765. } else {
  766. u, sess = getUserAndSession(app, r)
  767. if u == nil {
  768. return ErrNotLoggedIn
  769. }
  770. err := r.ParseForm()
  771. if err != nil {
  772. log.Error("Couldn't parse settings form request: %v\n", err)
  773. return ErrBadFormData
  774. }
  775. err = app.formDecoder.Decode(&s, r.PostForm)
  776. if err != nil {
  777. log.Error("Couldn't decode settings form request: %v\n", err)
  778. return ErrBadFormData
  779. }
  780. }
  781. // Do update
  782. postUpdateReturn := r.FormValue("return")
  783. redirectTo := "/me/settings"
  784. if s.IsLogOut {
  785. redirectTo += "?logout=1"
  786. } else if postUpdateReturn != "" {
  787. redirectTo = postUpdateReturn
  788. }
  789. // Only do updates on values we need
  790. if s.Username != "" && s.Username == u.Username {
  791. // Username hasn't actually changed; blank it out
  792. s.Username = ""
  793. }
  794. err = app.db.ChangeSettings(app, u, &s)
  795. if err != nil {
  796. if reqJSON {
  797. return err
  798. }
  799. if err, ok := err.(impart.HTTPError); ok {
  800. addSessionFlash(app, w, r, err.Message, nil)
  801. }
  802. } else {
  803. // Successful update.
  804. if reqJSON {
  805. return impart.WriteSuccess(w, u, http.StatusOK)
  806. }
  807. if s.IsLogOut {
  808. redirectTo = "/me/logout"
  809. } else {
  810. sess.Values[cookieUserVal] = u.Cookie()
  811. addSessionFlash(app, w, r, "Account updated.", nil)
  812. }
  813. }
  814. w.Header().Set("Location", redirectTo)
  815. w.WriteHeader(http.StatusFound)
  816. return nil
  817. }
  818. func updatePassphrase(app *App, w http.ResponseWriter, r *http.Request) error {
  819. accessToken := r.Header.Get("Authorization")
  820. if accessToken == "" {
  821. return ErrNoAccessToken
  822. }
  823. curPass := r.FormValue("current")
  824. newPass := r.FormValue("new")
  825. // Ensure a new password is given (always required)
  826. if newPass == "" {
  827. return impart.HTTPError{http.StatusBadRequest, "Provide a new password."}
  828. }
  829. userID, sudo := app.db.GetUserIDPrivilege(accessToken)
  830. if userID == -1 {
  831. return ErrBadAccessToken
  832. }
  833. // Ensure a current password is given if the access token doesn't have sudo
  834. // privileges.
  835. if !sudo && curPass == "" {
  836. return impart.HTTPError{http.StatusBadRequest, "Provide current password."}
  837. }
  838. // Hash the new password
  839. hashedPass, err := auth.HashPass([]byte(newPass))
  840. if err != nil {
  841. return impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
  842. }
  843. // Do update
  844. err = app.db.ChangePassphrase(userID, sudo, curPass, hashedPass)
  845. if err != nil {
  846. return err
  847. }
  848. return impart.WriteSuccess(w, struct{}{}, http.StatusOK)
  849. }
  850. func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  851. var c *Collection
  852. var err error
  853. vars := mux.Vars(r)
  854. alias := vars["collection"]
  855. if alias != "" {
  856. c, err = app.db.GetCollection(alias)
  857. if err != nil {
  858. return err
  859. }
  860. if c.OwnerID != u.ID {
  861. return ErrCollectionNotFound
  862. }
  863. }
  864. topPosts, err := app.db.GetTopPosts(u, alias)
  865. if err != nil {
  866. log.Error("Unable to get top posts: %v", err)
  867. return err
  868. }
  869. flashes, _ := getSessionFlashes(app, w, r, nil)
  870. titleStats := ""
  871. if c != nil {
  872. titleStats = c.DisplayTitle() + " "
  873. }
  874. silenced, err := app.db.IsUserSilenced(u.ID)
  875. if err != nil {
  876. log.Error("view stats: %v", err)
  877. return err
  878. }
  879. obj := struct {
  880. *UserPage
  881. VisitsBlog string
  882. Collection *Collection
  883. TopPosts *[]PublicPost
  884. APFollowers int
  885. Silenced bool
  886. }{
  887. UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
  888. VisitsBlog: alias,
  889. Collection: c,
  890. TopPosts: topPosts,
  891. Silenced: silenced,
  892. }
  893. obj.UserPage.CollAlias = c.Alias
  894. if app.cfg.App.Federation {
  895. folls, err := app.db.GetAPFollowers(c)
  896. if err != nil {
  897. return err
  898. }
  899. obj.APFollowers = len(*folls)
  900. }
  901. showUserPage(w, "stats", obj)
  902. return nil
  903. }
  904. func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  905. fullUser, err := app.db.GetUserByID(u.ID)
  906. if err != nil {
  907. log.Error("Unable to get user for settings: %s", err)
  908. return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
  909. }
  910. passIsSet, err := app.db.IsUserPassSet(u.ID)
  911. if err != nil {
  912. log.Error("Unable to get isUserPassSet for settings: %s", err)
  913. return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
  914. }
  915. flashes, _ := getSessionFlashes(app, w, r, nil)
  916. enableOauthSlack := app.Config().SlackOauth.ClientID != ""
  917. enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != ""
  918. enableOauthGitLab := app.Config().GitlabOauth.ClientID != ""
  919. enableOauthGeneric := app.Config().GenericOauth.ClientID != ""
  920. enableOauthGitea := app.Config().GiteaOauth.ClientID != ""
  921. oauthAccounts, err := app.db.GetOauthAccounts(r.Context(), u.ID)
  922. if err != nil {
  923. log.Error("Unable to get oauth accounts for settings: %s", err)
  924. return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
  925. }
  926. for idx, oauthAccount := range oauthAccounts {
  927. switch oauthAccount.Provider {
  928. case "slack":
  929. enableOauthSlack = false
  930. case "write.as":
  931. enableOauthWriteAs = false
  932. case "gitlab":
  933. enableOauthGitLab = false
  934. case "generic":
  935. oauthAccounts[idx].DisplayName = app.Config().GenericOauth.DisplayName
  936. oauthAccounts[idx].AllowDisconnect = app.Config().GenericOauth.AllowDisconnect
  937. enableOauthGeneric = false
  938. case "gitea":
  939. enableOauthGitea = false
  940. }
  941. }
  942. displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGeneric || enableOauthGitea || len(oauthAccounts) > 0
  943. obj := struct {
  944. *UserPage
  945. Email string
  946. HasPass bool
  947. IsLogOut bool
  948. Silenced bool
  949. OauthSection bool
  950. OauthAccounts []oauthAccountInfo
  951. OauthSlack bool
  952. OauthWriteAs bool
  953. OauthGitLab bool
  954. GitLabDisplayName string
  955. OauthGeneric bool
  956. OauthGenericDisplayName string
  957. OauthGitea bool
  958. GiteaDisplayName string
  959. }{
  960. UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
  961. Email: fullUser.EmailClear(app.keys),
  962. HasPass: passIsSet,
  963. IsLogOut: r.FormValue("logout") == "1",
  964. Silenced: fullUser.IsSilenced(),
  965. OauthSection: displayOauthSection,
  966. OauthAccounts: oauthAccounts,
  967. OauthSlack: enableOauthSlack,
  968. OauthWriteAs: enableOauthWriteAs,
  969. OauthGitLab: enableOauthGitLab,
  970. GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName),
  971. OauthGeneric: enableOauthGeneric,
  972. OauthGenericDisplayName: config.OrDefaultString(app.Config().GenericOauth.DisplayName, genericOauthDisplayName),
  973. OauthGitea: enableOauthGitea,
  974. GiteaDisplayName: config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName),
  975. }
  976. showUserPage(w, "settings", obj)
  977. return nil
  978. }
  979. func saveTempInfo(app *App, key, val string, r *http.Request, w http.ResponseWriter) error {
  980. session, err := app.sessionStore.Get(r, "t")
  981. if err != nil {
  982. return ErrInternalCookieSession
  983. }
  984. session.Values[key] = val
  985. err = session.Save(r, w)
  986. if err != nil {
  987. log.Error("Couldn't saveTempInfo for key-val (%s:%s): %v", key, val, err)
  988. }
  989. return err
  990. }
  991. func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) string {
  992. session, err := app.sessionStore.Get(r, "t")
  993. if err != nil {
  994. return ""
  995. }
  996. // Get the information
  997. var s = ""
  998. var ok bool
  999. if s, ok = session.Values[key].(string); !ok {
  1000. return ""
  1001. }
  1002. // Delete cookie
  1003. session.Options.MaxAge = -1
  1004. err = session.Save(r, w)
  1005. if err != nil {
  1006. log.Error("Couldn't erase temp data for key %s: %v", key, err)
  1007. }
  1008. // Return value
  1009. return s
  1010. }
  1011. func removeOauth(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  1012. provider := r.FormValue("provider")
  1013. clientID := r.FormValue("client_id")
  1014. remoteUserID := r.FormValue("remote_user_id")
  1015. err := app.db.RemoveOauth(r.Context(), u.ID, provider, clientID, remoteUserID)
  1016. if err != nil {
  1017. return impart.HTTPError{Status: http.StatusInternalServerError, Message: err.Error()}
  1018. }
  1019. return impart.HTTPError{Status: http.StatusFound, Message: "/me/settings"}
  1020. }
  1021. func prepareUserEmail(input string, emailKey []byte) zero.String {
  1022. email := zero.NewString("", input != "")
  1023. if len(input) > 0 {
  1024. encEmail, err := data.Encrypt(emailKey, input)
  1025. if err != nil {
  1026. log.Error("Unable to encrypt email: %s\n", err)
  1027. } else {
  1028. email.String = string(encEmail)
  1029. }
  1030. }
  1031. return email
  1032. }