1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-29 12:30:11 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
8113f794ab
form: fix confirmation success page css 2023-06-21 21:22:05 +01:00
14c18bd668
form: rework email confirmation
realized half the info from the signup form wasnt being stored in the JWT
used to create the account after email confirmation, and instead of
adding them, the -whole request- from the browser is stored temporarily
by the server, indexed by a smaller JWT that only includes the invite
code. Someone complained on reddit about me storing the password in the
JWT a while back, and although security-wise that isn't an issue (only
the server can decrypt the token), it doesn't happen anymore. Happy?
2023-06-21 21:14:41 +01:00
f779f0345e
storage: Use familiar api for invite access
An almost identical set of functions to the discord/telegram/matrix
storage ones is now used for accessing invites. No more
parallelism-related issues, yay. Need to do this for everything
eventually.
2023-06-21 20:39:16 +01:00
7 changed files with 133 additions and 97 deletions

View File

@ -16,7 +16,7 @@ func (app *appContext) checkInvites() {
currentTime := time.Now() currentTime := time.Now()
app.storage.loadInvites() app.storage.loadInvites()
changed := false changed := false
for code, data := range app.storage.invites { for code, data := range app.storage.GetInvites() {
expiry := data.ValidTill expiry := data.ValidTill
if !currentTime.After(expiry) { if !currentTime.After(expiry) {
continue continue
@ -54,7 +54,7 @@ func (app *appContext) checkInvites() {
wait.Wait() wait.Wait()
} }
changed = true changed = true
delete(app.storage.invites, code) app.storage.DeleteInvitesKey(code)
} }
if changed { if changed {
app.storage.storeInvites() app.storage.storeInvites()
@ -65,7 +65,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
currentTime := time.Now() currentTime := time.Now()
app.storage.loadInvites() app.storage.loadInvites()
changed := false changed := false
inv, match := app.storage.invites[code] inv, match := app.storage.GetInvitesKey(code)
if !match { if !match {
return false return false
} }
@ -105,21 +105,21 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
} }
changed = true changed = true
match = false match = false
delete(app.storage.invites, code) app.storage.DeleteInvitesKey(code)
} else if used { } else if used {
changed = true changed = true
del := false del := false
newInv := inv newInv := inv
if newInv.RemainingUses == 1 { if newInv.RemainingUses == 1 {
del = true del = true
delete(app.storage.invites, code) app.storage.DeleteInvitesKey(code)
} else if newInv.RemainingUses != 0 { } else if newInv.RemainingUses != 0 {
// 0 means infinite i guess? // 0 means infinite i guess?
newInv.RemainingUses-- newInv.RemainingUses--
} }
newInv.UsedBy = append(newInv.UsedBy, []string{username, strconv.FormatInt(currentTime.Unix(), 10)}) newInv.UsedBy = append(newInv.UsedBy, []string{username, strconv.FormatInt(currentTime.Unix(), 10)})
if !del { if !del {
app.storage.invites[code] = newInv app.storage.SetInvitesKey(code, newInv)
} }
} }
if changed { if changed {
@ -219,7 +219,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
invite.Profile = "Default" invite.Profile = "Default"
} }
} }
app.storage.invites[inviteCode] = invite app.storage.SetInvitesKey(inviteCode, invite)
app.storage.storeInvites() app.storage.storeInvites()
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -236,7 +236,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
app.storage.loadInvites() app.storage.loadInvites()
app.checkInvites() app.checkInvites()
var invites []inviteDTO 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) _, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
invite := inviteDTO{ invite := inviteDTO{
Code: code, Code: code,
@ -335,9 +335,9 @@ func (app *appContext) SetProfile(gc *gin.Context) {
respond(500, "Profile not found", gc) respond(500, "Profile not found", gc)
return return
} }
inv := app.storage.invites[req.Invite] inv, _ := app.storage.GetInvitesKey(req.Invite)
inv.Profile = req.Profile inv.Profile = req.Profile
app.storage.invites[req.Invite] = inv app.storage.SetInvitesKey(req.Invite, inv)
app.storage.storeInvites() app.storage.storeInvites()
respondBool(200, true, gc) 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.debug.Printf("%s: Notification settings change requested", code)
app.storage.loadInvites() app.storage.loadInvites()
app.storage.loadEmails() app.storage.loadEmails()
invite, ok := app.storage.invites[code] invite, ok := app.storage.GetInvitesKey(code)
if !ok { if !ok {
app.err.Printf("%s Notification setting change failed: Invalid code", code) app.err.Printf("%s Notification setting change failed: Invalid code", code)
respond(400, "Invalid invite code", gc) respond(400, "Invalid invite code", gc)
@ -398,7 +398,7 @@ func (app *appContext) SetNotify(gc *gin.Context) {
changed = true changed = true
} }
if changed { if changed {
app.storage.invites[code] = invite app.storage.SetInvitesKey(code, invite)
} }
} }
if changed { if changed {
@ -419,9 +419,9 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
gc.BindJSON(&req) gc.BindJSON(&req)
app.debug.Printf("%s: Deletion requested", req.Code) app.debug.Printf("%s: Deletion requested", req.Code)
var ok bool var ok bool
_, ok = app.storage.invites[req.Code] _, ok = app.storage.GetInvitesKey(req.Code)
if ok { if ok {
delete(app.storage.invites, req.Code) app.storage.DeleteInvitesKey(req.Code)
app.storage.storeInvites() app.storage.storeInvites()
app.info.Printf("%s: Invite deleted", req.Code) app.info.Printf("%s: Invite deleted", req.Code)
respondBool(200, true, gc) respondBool(200, true, gc)

View File

@ -460,7 +460,7 @@ func (app *appContext) TelegramVerified(gc *gin.Context) {
// @tags Other // @tags Other
func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) { func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok { if _, ok := app.storage.GetInvitesKey(code); !ok {
respondBool(401, false, gc) respondBool(401, false, gc)
return return
} }
@ -484,7 +484,7 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
// @tags Other // @tags Other
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) { func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok { if _, ok := app.storage.GetInvitesKey(code); !ok {
respondBool(401, false, gc) respondBool(401, false, gc)
return return
} }
@ -513,7 +513,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) {
return return
} }
code := gc.Param("invCode") code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok { if _, ok := app.storage.GetInvitesKey(code); !ok {
respondBool(401, false, gc) respondBool(401, false, gc)
return return
} }
@ -537,7 +537,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) {
// @tags Other // @tags Other
func (app *appContext) MatrixSendPIN(gc *gin.Context) { func (app *appContext) MatrixSendPIN(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok { if _, ok := app.storage.GetInvitesKey(code); !ok {
respondBool(401, false, gc) respondBool(401, false, gc)
return return
} }
@ -575,7 +575,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
// @tags Other // @tags Other
func (app *appContext) MatrixCheckPIN(gc *gin.Context) { func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
code := gc.Param("invCode") 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") app.debug.Println("Matrix: Invite code was invalid")
respondBool(401, false, gc) respondBool(401, false, gc)
return return

View File

@ -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 { if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed {
claims := jwt.MapClaims{ claims := jwt.MapClaims{
"valid": true, "valid": true,
"invite": req.Code, "invite": req.Code,
"email": req.Email, "exp": time.Now().Add(30 * time.Minute).Unix(),
"username": req.Username, "type": "confirmation",
"password": req.Password,
"telegramPIN": req.TelegramPIN,
"exp": time.Now().Add(time.Hour * 12).Unix(),
"type": "confirmation",
} }
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
@ -246,10 +242,17 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false success = false
return return
} }
inv := app.storage.invites[req.Code] if app.ConfirmationKeys == nil {
inv.Keys = append(inv.Keys, key) app.ConfirmationKeys = map[string]map[string]newUserDTO{}
app.storage.invites[req.Code] = inv }
app.storage.storeInvites() cKeys, ok := app.ConfirmationKeys[req.Code]
if !ok {
cKeys = map[string]newUserDTO{}
}
cKeys[key] = req
app.confirmationKeysLock.Lock()
app.ConfirmationKeys[req.Code] = cKeys
app.confirmationKeysLock.Unlock()
f = func(gc *gin.Context) { f = func(gc *gin.Context) {
app.debug.Printf("%s: Email confirmation required", req.Code) app.debug.Printf("%s: Email confirmation required", req.Code)
respond(401, "confirmEmail", gc) respond(401, "confirmEmail", gc)
@ -276,7 +279,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
return return
} }
app.storage.loadProfiles() app.storage.loadProfiles()
invite := app.storage.invites[req.Code] invite, _ := app.storage.GetInvitesKey(req.Code)
app.checkInvite(req.Code, true, req.Username) app.checkInvite(req.Code, true, req.Username)
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) { if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) {
for address, settings := range invite.Notify { for address, settings := range invite.Notify {

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}"> <html lang="en" class="{{ .cssClass }}">
<head> <head>
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css"> <link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }} {{ template "header.html" . }}
<title>Invalid Code - jfa-go</title> <title>Invalid Code - jfa-go</title>
</head> </head>

45
main.go
View File

@ -17,6 +17,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"syscall" "syscall"
"time" "time"
@ -90,27 +91,29 @@ type appContext struct {
adminUsers []User adminUsers []User
invalidTokens []string invalidTokens []string
// Keeping jf name because I can't think of a better one // Keeping jf name because I can't think of a better one
jf *mediabrowser.MediaBrowser jf *mediabrowser.MediaBrowser
authJf *mediabrowser.MediaBrowser authJf *mediabrowser.MediaBrowser
ombi *ombi.Ombi ombi *ombi.Ombi
datePattern string datePattern string
timePattern string timePattern string
storage Storage storage Storage
validator Validator validator Validator
email *Emailer email *Emailer
telegram *TelegramDaemon telegram *TelegramDaemon
discord *DiscordDaemon discord *DiscordDaemon
matrix *MatrixDaemon matrix *MatrixDaemon
info, debug, err *logger.Logger info, debug, err *logger.Logger
host string host string
port int port int
version string version string
URLBase string URLBase string
updater *Updater updater *Updater
newUpdate bool // Whether whatever's in update is new. newUpdate bool // Whether whatever's in update is new.
tag Tag tag Tag
update Update update Update
internalPWRs map[string]InternalPWR internalPWRs map[string]InternalPWR
ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request
confirmationKeysLock sync.Mutex
} }
func generateSecret(length int) (string, error) { func generateSecret(length int) (string, error) {

View File

@ -167,6 +167,39 @@ func (st *Storage) DeleteMatrixKey(k string) {
st.matrixLock.Unlock() 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 { type TelegramUser struct {
ChatID int64 ChatID int64
Username string Username string
@ -245,7 +278,6 @@ type Invite struct {
Notify map[string]map[string]bool `json:"notify"` Notify map[string]map[string]bool `json:"notify"`
Profile string `json:"profile"` Profile string `json:"profile"`
Label string `json:"label,omitempty"` Label string `json:"label,omitempty"`
Keys []string `json:"keys,omitempty"`
Captchas map[string]*captcha.Data // Map of Captcha IDs to answers Captchas map[string]*captcha.Data // Map of Captcha IDs to answers
} }
@ -932,14 +964,10 @@ func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error {
type Invites map[string]Invite type Invites map[string]Invite
func (st *Storage) loadInvites() error { func (st *Storage) loadInvites() error {
st.invitesLock.Lock()
defer st.invitesLock.Unlock()
return loadJSON(st.invite_path, &st.invites) return loadJSON(st.invite_path, &st.invites)
} }
func (st *Storage) storeInvites() error { func (st *Storage) storeInvites() error {
st.invitesLock.Lock()
defer st.invitesLock.Unlock()
return storeJSON(st.invite_path, st.invites) return storeJSON(st.invite_path, st.invites)
} }

View File

@ -342,9 +342,10 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
func (app *appContext) GetCaptcha(gc *gin.Context) { func (app *appContext) GetCaptcha(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
captchaID := gc.Param("captchaID") captchaID := gc.Param("captchaID")
inv, ok := app.storage.invites[code] inv, ok := app.storage.GetInvitesKey(code)
if !ok { if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{ gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
"cssVersion": cssVersion, "cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
@ -376,9 +377,10 @@ func (app *appContext) GetCaptcha(gc *gin.Context) {
// @tags Users // @tags Users
func (app *appContext) GenCaptcha(gc *gin.Context) { func (app *appContext) GenCaptcha(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
inv, ok := app.storage.invites[code] inv, ok := app.storage.GetInvitesKey(code)
if !ok { if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{ gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
"cssVersion": cssVersion, "cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
@ -395,8 +397,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
} }
captchaID := genAuthToken() captchaID := genAuthToken()
inv.Captchas[captchaID] = capt inv.Captchas[captchaID] = capt
app.storage.invites[code] = inv app.storage.SetInvitesKey(code, inv)
app.storage.storeInvites()
gc.JSON(200, genCaptchaDTO{captchaID}) gc.JSON(200, genCaptchaDTO{captchaID})
return return
} }
@ -405,7 +406,7 @@ func (app *appContext) verifyCaptcha(code, id, text string) bool {
reCAPTCHA := app.config.Section("captcha").Key("recaptcha").MustBool(false) reCAPTCHA := app.config.Section("captcha").Key("recaptcha").MustBool(false)
if !reCAPTCHA { if !reCAPTCHA {
// internal CAPTCHA // internal CAPTCHA
inv, ok := app.storage.invites[code] inv, ok := app.storage.GetInvitesKey(code)
if !ok || inv.Captchas == nil { if !ok || inv.Captchas == nil {
app.debug.Printf("Couldn't find invite \"%s\"", code) app.debug.Printf("Couldn't find invite \"%s\"", code)
return false return false
@ -472,9 +473,10 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
captchaID := gc.Param("captchaID") captchaID := gc.Param("captchaID")
text := gc.Param("text") text := gc.Param("text")
inv, ok := app.storage.invites[code] inv, ok := app.storage.GetInvitesKey(code)
if !ok { if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{ gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
"cssVersion": cssVersion, "cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
@ -503,9 +505,10 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
lang := app.getLang(gc, FormPage, app.storage.lang.chosenUserLang) 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. */ /* 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, "") { // if app.checkInvite(code, false, "") {
inv, ok := app.storage.invites[code] inv, ok := app.storage.GetInvitesKey(code)
if !ok { if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{ gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
"cssVersion": cssVersion, "cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
@ -513,23 +516,27 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
return return
} }
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) { 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
}
}
fail := func() { fail := func() {
gcHTML(gc, 404, "404.html", gin.H{ gcHTML(gc, 404, "404.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
"cssVersion": cssVersion, "cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
}) })
} }
if !validKey { var req newUserDTO
if app.ConfirmationKeys == nil {
fail()
return
}
invKeys, ok := app.ConfirmationKeys[code]
if !ok {
fail()
return
}
req, ok = invKeys[key]
if !ok {
fail() fail()
return return
} }
@ -540,26 +547,17 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
return return
} }
claims, ok := token.Claims.(jwt.MapClaims) claims, ok := token.Claims.(jwt.MapClaims)
expiryUnix := int64(claims["exp"].(float64)) expiry := time.Unix(int64(claims["exp"].(float64)), 0)
if err != nil {
fail()
app.err.Printf("Failed to parse key expiry: %s", err)
return
}
expiry := time.Unix(expiryUnix, 0)
if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) { if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
fail() fail()
app.debug.Printf("Invalid key") app.debug.Printf("Invalid key")
return return
} }
req := newUserDTO{ f, success := app.newUser(req, true)
Email: claims["email"].(string),
Username: claims["username"].(string),
Password: claims["password"].(string),
Code: claims["invite"].(string),
}
_, success := app.newUser(req, true)
if !success { if !success {
app.err.Printf("Failed to create new user")
// Not meant for us. Calling this will be a mess, but at least it might give us some information.
f(gc)
fail() fail()
return return
} }
@ -568,22 +566,25 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
gc.Redirect(301, jfLink) gc.Redirect(301, jfLink)
} else { } else {
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{ gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
"cssVersion": cssVersion,
"strings": app.storage.lang.User[lang].Strings, "strings": app.storage.lang.User[lang].Strings,
"successMessage": app.config.Section("ui").Key("success_message").String(), "successMessage": app.config.Section("ui").Key("success_message").String(),
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
"jfLink": jfLink, "jfLink": jfLink,
}) })
} }
inv, ok := app.storage.invites[code] delete(invKeys, key)
if ok { app.confirmationKeysLock.Lock()
l := len(inv.Keys) app.ConfirmationKeys[code] = invKeys
inv.Keys[l-1], inv.Keys[keyIndex] = inv.Keys[keyIndex], inv.Keys[l-1] app.confirmationKeysLock.Unlock()
app.storage.invites[code] = inv
}
return 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, "@") { if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
email = "" email = ""
} }
@ -657,6 +658,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
func (app *appContext) NoRouteHandler(gc *gin.Context) { func (app *appContext) NoRouteHandler(gc *gin.Context) {
app.pushResources(gc, OtherPage) app.pushResources(gc, OtherPage)
gcHTML(gc, 404, "404.html", gin.H{ gcHTML(gc, 404, "404.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
"cssVersion": cssVersion, "cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),