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.
 
 
 
 
 

1073 lines
27 KiB

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