diff --git a/api-invites.go b/api-invites.go index a41bd57..268fdac 100644 --- a/api-invites.go +++ b/api-invites.go @@ -16,7 +16,7 @@ func (app *appContext) checkInvites() { currentTime := time.Now() app.storage.loadInvites() changed := false - for code, data := range app.storage.invites { + for code, data := range app.storage.GetInvites() { expiry := data.ValidTill if !currentTime.After(expiry) { continue @@ -54,7 +54,7 @@ func (app *appContext) checkInvites() { wait.Wait() } changed = true - delete(app.storage.invites, code) + app.storage.DeleteInvitesKey(code) } if changed { app.storage.storeInvites() @@ -65,7 +65,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool currentTime := time.Now() app.storage.loadInvites() changed := false - inv, match := app.storage.invites[code] + inv, match := app.storage.GetInvitesKey(code) if !match { return false } @@ -105,21 +105,21 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool } changed = true match = false - delete(app.storage.invites, code) + app.storage.DeleteInvitesKey(code) } else if used { changed = true del := false newInv := inv if newInv.RemainingUses == 1 { del = true - delete(app.storage.invites, code) + app.storage.DeleteInvitesKey(code) } else if newInv.RemainingUses != 0 { // 0 means infinite i guess? newInv.RemainingUses-- } newInv.UsedBy = append(newInv.UsedBy, []string{username, strconv.FormatInt(currentTime.Unix(), 10)}) if !del { - app.storage.invites[code] = newInv + app.storage.SetInvitesKey(code, newInv) } } if changed { @@ -219,7 +219,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { invite.Profile = "Default" } } - app.storage.invites[inviteCode] = invite + app.storage.SetInvitesKey(inviteCode, invite) app.storage.storeInvites() respondBool(200, true, gc) } @@ -236,7 +236,7 @@ func (app *appContext) GetInvites(gc *gin.Context) { app.storage.loadInvites() app.checkInvites() var invites []inviteDTO - for code, inv := range app.storage.invites { + for code, inv := range app.storage.GetInvites() { _, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime) invite := inviteDTO{ Code: code, @@ -335,9 +335,9 @@ func (app *appContext) SetProfile(gc *gin.Context) { respond(500, "Profile not found", gc) return } - inv := app.storage.invites[req.Invite] + inv, _ := app.storage.GetInvitesKey(req.Invite) inv.Profile = req.Profile - app.storage.invites[req.Invite] = inv + app.storage.SetInvitesKey(req.Invite, inv) app.storage.storeInvites() respondBool(200, true, gc) } @@ -359,7 +359,7 @@ func (app *appContext) SetNotify(gc *gin.Context) { app.debug.Printf("%s: Notification settings change requested", code) app.storage.loadInvites() app.storage.loadEmails() - invite, ok := app.storage.invites[code] + invite, ok := app.storage.GetInvitesKey(code) if !ok { app.err.Printf("%s Notification setting change failed: Invalid code", code) respond(400, "Invalid invite code", gc) @@ -398,7 +398,7 @@ func (app *appContext) SetNotify(gc *gin.Context) { changed = true } if changed { - app.storage.invites[code] = invite + app.storage.SetInvitesKey(code, invite) } } if changed { @@ -419,9 +419,9 @@ func (app *appContext) DeleteInvite(gc *gin.Context) { gc.BindJSON(&req) app.debug.Printf("%s: Deletion requested", req.Code) var ok bool - _, ok = app.storage.invites[req.Code] + _, ok = app.storage.GetInvitesKey(req.Code) if ok { - delete(app.storage.invites, req.Code) + app.storage.DeleteInvitesKey(req.Code) app.storage.storeInvites() app.info.Printf("%s: Invite deleted", req.Code) respondBool(200, true, gc) diff --git a/api-messages.go b/api-messages.go index d4ec8ff..c571eb7 100644 --- a/api-messages.go +++ b/api-messages.go @@ -460,7 +460,7 @@ func (app *appContext) TelegramVerified(gc *gin.Context) { // @tags Other func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) { code := gc.Param("invCode") - if _, ok := app.storage.invites[code]; !ok { + if _, ok := app.storage.GetInvitesKey(code); !ok { respondBool(401, false, gc) return } @@ -484,7 +484,7 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) { // @tags Other func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) { code := gc.Param("invCode") - if _, ok := app.storage.invites[code]; !ok { + if _, ok := app.storage.GetInvitesKey(code); !ok { respondBool(401, false, gc) return } @@ -513,7 +513,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) { return } code := gc.Param("invCode") - if _, ok := app.storage.invites[code]; !ok { + if _, ok := app.storage.GetInvitesKey(code); !ok { respondBool(401, false, gc) return } @@ -537,7 +537,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) { // @tags Other func (app *appContext) MatrixSendPIN(gc *gin.Context) { code := gc.Param("invCode") - if _, ok := app.storage.invites[code]; !ok { + if _, ok := app.storage.GetInvitesKey(code); !ok { respondBool(401, false, gc) return } @@ -575,7 +575,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) { // @tags Other func (app *appContext) MatrixCheckPIN(gc *gin.Context) { code := gc.Param("invCode") - if _, ok := app.storage.invites[code]; !ok { + if _, ok := app.storage.GetInvitesKey(code); !ok { app.debug.Println("Matrix: Invite code was invalid") respondBool(401, false, gc) return diff --git a/api-users.go b/api-users.go index 4a54e01..3b2e934 100644 --- a/api-users.go +++ b/api-users.go @@ -227,14 +227,10 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc } if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed { claims := jwt.MapClaims{ - "valid": true, - "invite": req.Code, - "email": req.Email, - "username": req.Username, - "password": req.Password, - "telegramPIN": req.TelegramPIN, - "exp": time.Now().Add(time.Hour * 12).Unix(), - "type": "confirmation", + "valid": true, + "invite": req.Code, + "exp": time.Now().Add(30 * time.Minute).Unix(), + "type": "confirmation", } tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) @@ -246,10 +242,12 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc success = false return } - inv := app.storage.invites[req.Code] - inv.Keys = append(inv.Keys, key) - app.storage.invites[req.Code] = inv - app.storage.storeInvites() + inv, _ := app.storage.GetInvitesKey(req.Code) + if inv.ConfirmationKeys == nil { + inv.ConfirmationKeys = map[string]newUserDTO{} + } + inv.ConfirmationKeys[key] = req + app.storage.SetInvitesKey(req.Code, inv) f = func(gc *gin.Context) { app.debug.Printf("%s: Email confirmation required", req.Code) respond(401, "confirmEmail", gc) @@ -276,7 +274,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc return } app.storage.loadProfiles() - invite := app.storage.invites[req.Code] + invite, _ := app.storage.GetInvitesKey(req.Code) app.checkInvite(req.Code, true, req.Username) if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) { for address, settings := range invite.Notify { diff --git a/storage.go b/storage.go index 5d66d0b..415c798 100644 --- a/storage.go +++ b/storage.go @@ -167,6 +167,39 @@ func (st *Storage) DeleteMatrixKey(k string) { st.matrixLock.Unlock() } +// GetInvites returns a copy of the store. +func (st *Storage) GetInvites() Invites { + if st.invites == nil { + st.invites = Invites{} + } + return st.invites +} + +// GetInvitesKey returns the value stored in the store's key. +func (st *Storage) GetInvitesKey(k string) (Invite, bool) { + v, ok := st.invites[k] + return v, ok +} + +// SetInvitesKey stores value v in key k. +func (st *Storage) SetInvitesKey(k string, v Invite) { + st.invitesLock.Lock() + if st.invites == nil { + st.invites = Invites{} + } + st.invites[k] = v + st.storeInvites() + st.invitesLock.Unlock() +} + +// DeleteInvitesKey deletes value at key k. +func (st *Storage) DeleteInvitesKey(k string) { + st.invitesLock.Lock() + delete(st.invites, k) + st.storeInvites() + st.invitesLock.Unlock() +} + type TelegramUser struct { ChatID int64 Username string @@ -241,12 +274,12 @@ type Invite struct { UserMinutes int `json:"user-minutes,omitempty"` SendTo string `json:"email"` // Used to be stored as formatted time, now as Unix. - UsedBy [][]string `json:"used-by"` - Notify map[string]map[string]bool `json:"notify"` - Profile string `json:"profile"` - Label string `json:"label,omitempty"` - Keys []string `json:"keys,omitempty"` - Captchas map[string]*captcha.Data // Map of Captcha IDs to answers + UsedBy [][]string `json:"used-by"` + Notify map[string]map[string]bool `json:"notify"` + Profile string `json:"profile"` + Label string `json:"label,omitempty"` + ConfirmationKeys map[string]newUserDTO `json:"-"` // map of JWT confirmation keys to their original requests + Captchas map[string]*captcha.Data // Map of Captcha IDs to answers } type Lang struct { @@ -932,14 +965,10 @@ func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error { type Invites map[string]Invite func (st *Storage) loadInvites() error { - st.invitesLock.Lock() - defer st.invitesLock.Unlock() return loadJSON(st.invite_path, &st.invites) } func (st *Storage) storeInvites() error { - st.invitesLock.Lock() - defer st.invitesLock.Unlock() return storeJSON(st.invite_path, st.invites) } diff --git a/views.go b/views.go index 6c145e2..8ce557b 100644 --- a/views.go +++ b/views.go @@ -342,7 +342,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) { func (app *appContext) GetCaptcha(gc *gin.Context) { code := gc.Param("invCode") captchaID := gc.Param("captchaID") - inv, ok := app.storage.invites[code] + inv, ok := app.storage.GetInvitesKey(code) if !ok { gcHTML(gc, 404, "invalidCode.html", gin.H{ "cssClass": app.cssClass, @@ -376,7 +376,7 @@ func (app *appContext) GetCaptcha(gc *gin.Context) { // @tags Users func (app *appContext) GenCaptcha(gc *gin.Context) { code := gc.Param("invCode") - inv, ok := app.storage.invites[code] + inv, ok := app.storage.GetInvitesKey(code) if !ok { gcHTML(gc, 404, "invalidCode.html", gin.H{ "cssClass": app.cssClass, @@ -395,8 +395,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) { } captchaID := genAuthToken() inv.Captchas[captchaID] = capt - app.storage.invites[code] = inv - app.storage.storeInvites() + app.storage.SetInvitesKey(code, inv) gc.JSON(200, genCaptchaDTO{captchaID}) return } @@ -405,7 +404,7 @@ func (app *appContext) verifyCaptcha(code, id, text string) bool { reCAPTCHA := app.config.Section("captcha").Key("recaptcha").MustBool(false) if !reCAPTCHA { // internal CAPTCHA - inv, ok := app.storage.invites[code] + inv, ok := app.storage.GetInvitesKey(code) if !ok || inv.Captchas == nil { app.debug.Printf("Couldn't find invite \"%s\"", code) return false @@ -472,7 +471,7 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) { code := gc.Param("invCode") captchaID := gc.Param("captchaID") text := gc.Param("text") - inv, ok := app.storage.invites[code] + inv, ok := app.storage.GetInvitesKey(code) if !ok { gcHTML(gc, 404, "invalidCode.html", gin.H{ "cssClass": app.cssClass, @@ -503,7 +502,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { lang := app.getLang(gc, FormPage, app.storage.lang.chosenUserLang) /* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */ // if app.checkInvite(code, false, "") { - inv, ok := app.storage.invites[code] + inv, ok := app.storage.GetInvitesKey(code) if !ok { gcHTML(gc, 404, "invalidCode.html", gin.H{ "cssClass": app.cssClass, @@ -513,15 +512,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { return } if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) { - validKey := false - keyIndex := -1 - for i, k := range inv.Keys { - if k == key { - validKey = true - keyIndex = i - break - } - } + req, ok := inv.ConfirmationKeys[key] fail := func() { gcHTML(gc, 404, "404.html", gin.H{ "cssClass": app.cssClass, @@ -529,7 +520,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { "contactMessage": app.config.Section("ui").Key("contact_message").String(), }) } - if !validKey { + if !ok { fail() return } @@ -540,24 +531,12 @@ func (app *appContext) InviteProxy(gc *gin.Context) { return } claims, ok := token.Claims.(jwt.MapClaims) - expiryUnix := int64(claims["exp"].(float64)) - if err != nil { - fail() - app.err.Printf("Failed to parse key expiry: %s", err) - return - } - expiry := time.Unix(expiryUnix, 0) + expiry := time.Unix(int64(claims["exp"].(float64)), 0) if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) { fail() app.debug.Printf("Invalid key") return } - req := newUserDTO{ - Email: claims["email"].(string), - Username: claims["username"].(string), - Password: claims["password"].(string), - Code: claims["invite"].(string), - } _, success := app.newUser(req, true) if !success { fail() @@ -575,15 +554,17 @@ func (app *appContext) InviteProxy(gc *gin.Context) { "jfLink": jfLink, }) } - inv, ok := app.storage.invites[code] + inv, ok := app.storage.GetInvitesKey(code) if ok { - l := len(inv.Keys) - inv.Keys[l-1], inv.Keys[keyIndex] = inv.Keys[keyIndex], inv.Keys[l-1] - app.storage.invites[code] = inv + delete(inv.ConfirmationKeys, key) + app.storage.SetInvitesKey(code, inv) } return } - email := app.storage.invites[code].SendTo + email := "" + if invite, ok := app.storage.GetInvitesKey(code); ok { + email = invite.SendTo + } if strings.Contains(email, "Failed") || !strings.Contains(email, "@") { email = "" }