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.
 
 
 
 
 

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