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.
 
 
 
 
 

207 lines
4.7 KiB

  1. /*
  2. * Copyright © 2019-2021 Musing Studio LLC.
  3. *
  4. * This file is part of WriteFreely.
  5. *
  6. * WriteFreely is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License, included
  8. * in the LICENSE file in this source code package.
  9. */
  10. package writefreely
  11. import (
  12. "database/sql"
  13. "html/template"
  14. "net/http"
  15. "strconv"
  16. "time"
  17. "github.com/gorilla/mux"
  18. "github.com/writeas/impart"
  19. "github.com/writeas/web-core/id"
  20. "github.com/writeas/web-core/log"
  21. "github.com/writefreely/writefreely/page"
  22. )
  23. type Invite struct {
  24. ID string
  25. MaxUses sql.NullInt64
  26. Created time.Time
  27. Expires *time.Time
  28. Inactive bool
  29. uses int64
  30. }
  31. func (i Invite) Uses() int64 {
  32. return i.uses
  33. }
  34. func (i Invite) Expired() bool {
  35. return i.Expires != nil && i.Expires.Before(time.Now())
  36. }
  37. func (i Invite) Active(db *datastore) bool {
  38. if i.Expired() {
  39. return false
  40. }
  41. if i.MaxUses.Valid && i.MaxUses.Int64 > 0 {
  42. if c := db.GetUsersInvitedCount(i.ID); c >= i.MaxUses.Int64 {
  43. return false
  44. }
  45. }
  46. return true
  47. }
  48. func (i Invite) ExpiresFriendly() string {
  49. return i.Expires.Format("January 2, 2006, 3:04 PM")
  50. }
  51. func handleViewUserInvites(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  52. // Don't show page if instance doesn't allow it
  53. if !(app.cfg.App.UserInvites != "" && (u.IsAdmin() || app.cfg.App.UserInvites != "admin")) {
  54. return impart.HTTPError{http.StatusNotFound, ""}
  55. }
  56. f, _ := getSessionFlashes(app, w, r, nil)
  57. p := struct {
  58. *UserPage
  59. Invites *[]Invite
  60. Silenced bool
  61. }{
  62. UserPage: NewUserPage(app, r, u, "Invite People", f),
  63. }
  64. var err error
  65. p.Silenced, err = app.db.IsUserSilenced(u.ID)
  66. if err != nil {
  67. if err == ErrUserNotFound {
  68. return err
  69. }
  70. log.Error("view invites: %v", err)
  71. }
  72. p.Invites, err = app.db.GetUserInvites(u.ID)
  73. if err != nil {
  74. return err
  75. }
  76. for i := range *p.Invites {
  77. (*p.Invites)[i].uses = app.db.GetUsersInvitedCount((*p.Invites)[i].ID)
  78. }
  79. showUserPage(w, "invite", p)
  80. return nil
  81. }
  82. func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  83. muVal := r.FormValue("uses")
  84. expVal := r.FormValue("expires")
  85. if u.IsSilenced() {
  86. return ErrUserSilenced
  87. }
  88. var err error
  89. var maxUses int
  90. if muVal != "0" {
  91. maxUses, err = strconv.Atoi(muVal)
  92. if err != nil {
  93. return impart.HTTPError{http.StatusBadRequest, "Invalid value for 'max_uses'"}
  94. }
  95. }
  96. var expDate *time.Time
  97. var expires int
  98. if expVal != "0" {
  99. expires, err = strconv.Atoi(expVal)
  100. if err != nil {
  101. return impart.HTTPError{http.StatusBadRequest, "Invalid value for 'expires'"}
  102. }
  103. ed := time.Now().Add(time.Duration(expires) * time.Minute)
  104. expDate = &ed
  105. }
  106. inviteID := id.GenerateRandomString("0123456789BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz", 6)
  107. err = app.db.CreateUserInvite(inviteID, u.ID, maxUses, expDate)
  108. if err != nil {
  109. return err
  110. }
  111. return impart.HTTPError{http.StatusFound, "/me/invites"}
  112. }
  113. func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error {
  114. inviteCode := mux.Vars(r)["code"]
  115. i, err := app.db.GetUserInvite(inviteCode)
  116. if err != nil {
  117. return err
  118. }
  119. expired := i.Expired()
  120. if !expired && i.MaxUses.Valid && i.MaxUses.Int64 > 0 {
  121. // Invite has a max-use number, so check if we're past that limit
  122. i.uses = app.db.GetUsersInvitedCount(inviteCode)
  123. expired = i.uses >= i.MaxUses.Int64
  124. }
  125. if u := getUserSession(app, r); u != nil {
  126. // check if invite belongs to another user
  127. // error can be ignored as not important in this case
  128. if ownInvite, _ := app.db.IsUsersInvite(inviteCode, u.ID); !ownInvite {
  129. addSessionFlash(app, w, r, "You're already registered and logged in.", nil)
  130. // show homepage
  131. return impart.HTTPError{http.StatusFound, "/me/settings"}
  132. }
  133. // show invite instructions
  134. p := struct {
  135. *UserPage
  136. Invite *Invite
  137. Expired bool
  138. }{
  139. UserPage: NewUserPage(app, r, u, "Invite to "+app.cfg.App.SiteName, nil),
  140. Invite: i,
  141. Expired: expired,
  142. }
  143. showUserPage(w, "invite-help", p)
  144. return nil
  145. }
  146. p := struct {
  147. page.StaticPage
  148. *OAuthButtons
  149. Error string
  150. Flashes []template.HTML
  151. Invite string
  152. }{
  153. StaticPage: pageForReq(app, r),
  154. OAuthButtons: NewOAuthButtons(app.cfg),
  155. Invite: inviteCode,
  156. }
  157. if expired {
  158. p.Error = "This invite link has expired."
  159. }
  160. // Tell search engines not to index invite links
  161. w.Header().Set("X-Robots-Tag", "noindex")
  162. // Get error messages
  163. session, err := app.sessionStore.Get(r, cookieName)
  164. if err != nil {
  165. // Ignore this
  166. log.Error("Unable to get session in handleViewInvite; ignoring: %v", err)
  167. }
  168. flashes, _ := getSessionFlashes(app, w, r, session)
  169. for _, flash := range flashes {
  170. p.Flashes = append(p.Flashes, template.HTML(flash))
  171. }
  172. // Show landing page
  173. return renderPage(w, "signup.tmpl", p)
  174. }