Skip to content

Commit

Permalink
feat: implement invite links (resolve #624)
Browse files Browse the repository at this point in the history
  • Loading branch information
muety committed Mar 28, 2024
1 parent f9edf09 commit 9015e51
Show file tree
Hide file tree
Showing 23 changed files with 4,427 additions and 4,184 deletions.
177 changes: 128 additions & 49 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions config.default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ security:
insecure_cookies: true # should be set to 'false', except when not running with HTTPS (e.g. on localhost)
cookie_max_age: 172800
allow_signup: true
invite_codes: true # whether to enable invite codes for overriding disabled signups
disable_frontpage: false
expose_metrics: false
enable_proxy: false # only intended for production instance at wakapi.dev
Expand Down
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const (
KeyFirstHeartbeat = "first_heartbeat"
KeySubscriptionNotificationSent = "sub_reminder"
KeyNewsbox = "newsbox"
KeyInviteCode = "invite"

SessionKeyDefault = "default"

Expand Down Expand Up @@ -104,6 +105,7 @@ type appConfig struct {

type securityConfig struct {
AllowSignup bool `yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP"`
InviteCodes bool `yaml:"invite_codes" default:"true" env:"WAKAPI_INVITE_CODES"`
ExposeMetrics bool `yaml:"expose_metrics" default:"false" env:"WAKAPI_EXPOSE_METRICS"`
EnableProxy bool `yaml:"enable_proxy" default:"false" env:"WAKAPI_ENABLE_PROXY"` // only intended for production instance at wakapi.dev
DisableFrontpage bool `yaml:"disable_frontpage" default:"false" env:"WAKAPI_DISABLE_FRONTPAGE"`
Expand Down
8,064 changes: 4,041 additions & 4,023 deletions coverage/coverage.out

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func main() {
subscriptionHandler := routes.NewSubscriptionHandler(userService, mailService, keyValueService)
projectsHandler := routes.NewProjectsHandler(userService, heartbeatService)
homeHandler := routes.NewHomeHandler(userService, keyValueService)
loginHandler := routes.NewLoginHandler(userService, mailService)
loginHandler := routes.NewLoginHandler(userService, mailService, keyValueService)
imprintHandler := routes.NewImprintHandler(keyValueService)
leaderboardHandler := condition.TernaryOperator[bool, routes.Handler](config.App.LeaderboardEnabled, routes.NewLeaderboardHandler(userService, leaderboardService), routes.NewNoopHandler())

Expand Down
3 changes: 3 additions & 0 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type User struct {
SubscribedUntil *CustomTime `json:"-" gorm:"swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
SubscriptionRenewal *CustomTime `json:"-" gorm:"swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
StripeCustomerId string `json:"-"`
InvitedBy string `json:"-"`
}

type Login struct {
Expand All @@ -52,6 +53,8 @@ type Signup struct {
Password string `schema:"password"`
PasswordRepeat string `schema:"password_repeat"`
Location string `schema:"location"`
InviteCode string `schema:"invite_code"`
InvitedBy string `schema:"-"`
}

type SetPasswordRequest struct {
Expand Down
1 change: 1 addition & 0 deletions models/view/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type LoginViewModel struct {
Messages
TotalUsers int
AllowSignup bool
InviteCode string
}

type SetPasswordViewModel struct {
Expand Down
2 changes: 2 additions & 0 deletions models/view/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type SettingsViewModel struct {
SupportContact string
LeaderboardEnabled bool
ApiKey string
InvitesEnabled bool
InviteLink string
}

type SettingsVMCombinedAlias struct {
Expand Down
1 change: 1 addition & 0 deletions models/view/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type SummaryViewModel struct {
UserFirstData time.Time
DataRetentionMonths int
LeaderboardEnabled bool
InvitesEnabled bool
}

func (s SummaryViewModel) UserDataExpiring() bool {
Expand Down
1 change: 1 addition & 0 deletions repositories/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
"subscribed_until": user.SubscribedUntil,
"subscription_renewal": user.SubscriptionRenewal,
"stripe_customer_id": user.StripeCustomerId,
"invited_by": user.InvitedBy,
}

result := r.db.Model(user).Updates(updateMap)
Expand Down
59 changes: 43 additions & 16 deletions routes/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@ import (
"github.com/muety/wakapi/utils"
"net/http"
"net/url"
"strings"
"time"
)

type LoginHandler struct {
config *conf.Config
userSrvc services.IUserService
mailSrvc services.IMailService
config *conf.Config
userSrvc services.IUserService
mailSrvc services.IMailService
keyValueSrvc services.IKeyValueService
}

func NewLoginHandler(userService services.IUserService, mailService services.IMailService) *LoginHandler {
func NewLoginHandler(userService services.IUserService, mailService services.IMailService, keyValueService services.IKeyValueService) *LoginHandler {
return &LoginHandler{
config: conf.Get(),
userSrvc: userService,
mailSrvc: mailService,
config: conf.Get(),
userSrvc: userService,
mailSrvc: mailService,
keyValueSrvc: keyValueService,
}
}

Expand Down Expand Up @@ -151,7 +154,19 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
loadTemplates()
}

if !h.config.IsDev() && !h.config.Security.AllowSignup {
var signup models.Signup
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
return
}
if err := signupDecoder.Decode(&signup, r.PostForm); err != nil {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
return
}

if !h.config.IsDev() && !h.config.Security.AllowSignup && (!h.config.Security.InviteCodes || signup.InviteCode == "") {
w.WriteHeader(http.StatusForbidden)
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("registration is disabled on this server"))
return
Expand All @@ -162,18 +177,29 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
return
}

var signup models.Signup
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
return
var invitedBy string
var invitedDate time.Time
var inviteCodeKey = fmt.Sprintf("%s_%s", conf.KeyInviteCode, signup.InviteCode)

if kv, _ := h.keyValueSrvc.GetString(inviteCodeKey); kv != nil && kv.Value != "" {
if parts := strings.Split(kv.Value, ","); len(parts) == 2 {
invitedBy = parts[0]
invitedDate, _ = time.Parse(conf.SimpleDateFormat, parts[1])
}

if err := h.keyValueSrvc.DeleteString(inviteCodeKey); err != nil {
conf.Log().Error("failed to revoke %s", inviteCodeKey)
}
}
if err := signupDecoder.Decode(&signup, r.PostForm); err != nil {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))

if signup.InviteCode != "" && time.Since(invitedDate) > 24*time.Hour {
w.WriteHeader(http.StatusForbidden)
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("invite code invalid or expired"))
return
}

signup.InvitedBy = invitedBy

if !signup.IsValid() {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("invalid parameters"))
Expand Down Expand Up @@ -332,6 +358,7 @@ func (h *LoginHandler) buildViewModel(r *http.Request, w http.ResponseWriter) *v
vm := &view.LoginViewModel{
TotalUsers: int(numUsers),
AllowSignup: h.config.IsDev() || h.config.Security.AllowSignup,
InviteCode: r.URL.Query().Get("invite"),
}
return routeutils.WithSessionMessages(vm, r, w)
}
3 changes: 0 additions & 3 deletions routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"github.com/duke-git/lancet/v2/strutil"
"github.com/muety/wakapi/helpers"
"html/template"
"net/http"
"strings"

"github.com/duke-git/lancet/v2/datetime"
Expand All @@ -14,8 +13,6 @@ import (
"github.com/muety/wakapi/views"
)

type action func(w http.ResponseWriter, r *http.Request) (int, string, string)

var templates map[string]*template.Template

func Init() {
Expand Down
Loading

0 comments on commit 9015e51

Please sign in to comment.