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.
 
 
 
 
 

1210 lines
32 KiB

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