1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 09:00:10 +00:00

Invites: unique email/ID requirement

"Require unique ..." Settings (`require_unique` in relevant sections of
config.ini) are now available for email/discord/telegram/matrix. An
error is shown on the invite form if a non-unique address/ID is used.
This was on my kanban without a link to an issue, so i'm guessing it was
requested on Discord.
This commit is contained in:
Harvey Tindall 2023-02-01 15:11:10 +00:00
parent 895dcf5a30
commit 2722e8482d
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
8 changed files with 284 additions and 57 deletions

View File

@ -453,6 +453,14 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
break break
} }
} }
if app.config.Section("telegram").Key("require_unique").MustBool(false) {
for _, u := range app.storage.telegram {
if app.telegram.verifiedTokens[tokenIndex].Username == u.Username {
respondBool(400, false, gc)
return
}
}
}
// if tokenIndex != -1 { // if tokenIndex != -1 {
// length := len(app.telegram.verifiedTokens) // length := len(app.telegram.verifiedTokens)
// app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1] // app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1]
@ -477,6 +485,15 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
} }
pin := gc.Param("pin") pin := gc.Param("pin")
_, ok := app.discord.verifiedTokens[pin] _, ok := app.discord.verifiedTokens[pin]
if app.config.Section("discord").Key("require_unique").MustBool(false) {
for _, u := range app.storage.discord {
if app.discord.verifiedTokens[pin].ID == u.ID {
delete(app.discord.verifiedTokens, pin)
respondBool(400, false, gc)
return
}
}
}
respondBool(200, ok, gc) respondBool(200, ok, gc)
} }
@ -510,7 +527,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) {
// @Summary Generate and send a new PIN to a specified Matrix user. // @Summary Generate and send a new PIN to a specified Matrix user.
// @Produce json // @Produce json
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse // @Failure 400 {object} stringResponse
// @Failure 401 {object} boolResponse // @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Param invCode path string true "invite Code" // @Param invCode path string true "invite Code"
@ -526,9 +543,18 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
var req MatrixSendPINDTO var req MatrixSendPINDTO
gc.BindJSON(&req) gc.BindJSON(&req)
if req.UserID == "" { if req.UserID == "" {
respondBool(400, false, gc) respond(400, "errorNoUserID", gc)
return return
} }
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
for _, u := range app.storage.matrix {
if req.UserID == u.UserID {
respondBool(400, false, gc)
return
}
}
}
ok := app.matrix.SendStart(req.UserID) ok := app.matrix.SendStart(req.UserID)
if !ok { if !ok {
respondBool(500, false, gc) respondBool(500, false, gc)

View File

@ -130,6 +130,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false success = false
return return
} }
if app.config.Section("discord").Key("require_unique").MustBool(false) {
for _, u := range app.storage.discord {
if discordUser.ID == u.ID {
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Discord user already linked", req.Code)
respond(400, "errorAccountLinked", gc)
}
success = false
return
}
}
}
err := app.discord.ApplyRole(discordUser.ID) err := app.discord.ApplyRole(discordUser.ID)
if err != nil { if err != nil {
f = func(gc *gin.Context) { f = func(gc *gin.Context) {
@ -164,6 +176,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false success = false
return return
} }
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
for _, u := range app.storage.matrix {
if user.User.UserID == u.UserID {
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Matrix user already linked", req.Code)
respond(400, "errorAccountLinked", gc)
}
success = false
return
}
}
}
matrixVerified = user.Verified matrixVerified = user.Verified
matrixUser = *user.User matrixUser = *user.User
@ -195,6 +219,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false success = false
return return
} }
if app.config.Section("telegram").Key("require_unique").MustBool(false) {
for _, u := range app.storage.telegram {
if app.telegram.verifiedTokens[telegramTokenIndex].Username == u.Username {
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Telegram user already linked", req.Code)
respond(400, "errorAccountLinked", gc)
}
success = false
return
}
}
}
} }
} }
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed { if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed {
@ -445,10 +481,21 @@ func (app *appContext) NewUser(gc *gin.Context) {
gc.JSON(200, validation) gc.JSON(200, validation)
return return
} }
if emailEnabled && app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") { if emailEnabled {
app.info.Printf("%s: New user failed: Email Required", req.Code) if app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") {
respond(400, "errorNoEmail", gc) app.info.Printf("%s: New user failed: Email Required", req.Code)
return respond(400, "errorNoEmail", gc)
return
}
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" {
for _, email := range app.storage.emails {
if req.Email == email.Addr {
app.info.Printf("%s: New user failed: Email already in use", req.Code)
respond(400, "errorEmailLinked", gc)
return
}
}
}
} }
f, success := app.newUser(req, false) f, success := app.newUser(req, false)
if !success { if !success {

View File

@ -502,6 +502,14 @@
"type": "bool", "type": "bool",
"value": false, "value": false,
"description": "Require an email address on sign-up." "description": "Require an email address on sign-up."
},
"require_unique": {
"name": "Require unique address",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Disables using the same address on multiple accounts."
} }
} }
}, },
@ -641,6 +649,14 @@
"value": false, "value": false,
"description": "Require Discord connection on sign-up. See the jfa-go wiki for info on setting this up." "description": "Require Discord connection on sign-up. See the jfa-go wiki for info on setting this up."
}, },
"require_unique": {
"name": "Require unique user",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Disables using the same user on multiple Jellyfin accounts."
},
"token": { "token": {
"name": "API Token", "name": "API Token",
"required": false, "required": false,
@ -745,6 +761,14 @@
"value": false, "value": false,
"description": "Require telegram connection on sign-up." "description": "Require telegram connection on sign-up."
}, },
"require_unique": {
"name": "Require unique user",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Disables using the same user on multiple Jellyfin accounts."
},
"token": { "token": {
"name": "API Token", "name": "API Token",
"required": false, "required": false,
@ -801,6 +825,14 @@
"value": false, "value": false,
"description": "Require Matrix connection on sign-up." "description": "Require Matrix connection on sign-up."
}, },
"require_unique": {
"name": "Require unique user",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Disables using the same user on multiple Jellyfin accounts."
},
"homeserver": { "homeserver": {
"name": "Home Server URL", "name": "Home Server URL",
"required": false, "required": false,

160
daemon.go Normal file
View File

@ -0,0 +1,160 @@
package main
import "time"
// clearEmails removes stored emails for users which no longer exist.
// meant to be called with other such housekeeping functions, so assumes
// the user cache is fresh.
func (app *appContext) clearEmails() {
app.debug.Println("Housekeeping: removing unused email addresses")
users, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil || len(users) == 0 {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
return
}
// Rebuild email storage to from existing users to reduce time complexity
emails := map[string]EmailAddress{}
for _, user := range users {
if email, ok := app.storage.emails[user.ID]; ok {
emails[user.ID] = email
}
}
app.storage.emails = emails
app.storage.storeEmails()
}
// clearDiscord does the same as clearEmails, but for Discord Users.
func (app *appContext) clearDiscord() {
app.debug.Println("Housekeeping: removing unused Discord IDs")
users, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil || len(users) == 0 {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
return
}
// Rebuild discord storage to from existing users to reduce time complexity
dcUsers := map[string]DiscordUser{}
for _, user := range users {
if dcUser, ok := app.storage.discord[user.ID]; ok {
dcUsers[user.ID] = dcUser
}
}
app.storage.discord = dcUsers
app.storage.storeDiscordUsers()
}
// clearMatrix does the same as clearEmails, but for Matrix Users.
func (app *appContext) clearMatrix() {
app.debug.Println("Housekeeping: removing unused Matrix IDs")
users, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil || len(users) == 0 {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
return
}
// Rebuild matrix storage to from existing users to reduce time complexity
mxUsers := map[string]MatrixUser{}
for _, user := range users {
if mxUser, ok := app.storage.matrix[user.ID]; ok {
mxUsers[user.ID] = mxUser
}
}
app.storage.matrix = mxUsers
app.storage.storeMatrixUsers()
}
// clearTelegram does the same as clearEmails, but for Telegram Users.
func (app *appContext) clearTelegram() {
app.debug.Println("Housekeeping: removing unused Telegram IDs")
users, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil || len(users) == 0 {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
return
}
// Rebuild telegram storage to from existing users to reduce time complexity
tgUsers := map[string]TelegramUser{}
for _, user := range users {
if tgUser, ok := app.storage.telegram[user.ID]; ok {
tgUsers[user.ID] = tgUser
}
}
app.storage.telegram = tgUsers
app.storage.storeTelegramUsers()
}
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
type housekeepingDaemon struct {
Stopped bool
ShutdownChannel chan string
Interval time.Duration
period time.Duration
jobs []func(app *appContext)
app *appContext
}
func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemon {
daemon := housekeepingDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
Interval: interval,
period: interval,
app: app,
}
daemon.jobs = []func(app *appContext){func(app *appContext) {
app.debug.Println("Housekeeping: Checking for expired invites")
app.checkInvites()
}}
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
clearTelegram := app.config.Section("telegram").Key("require_unique").MustBool(false)
clearMatrix := app.config.Section("matrix").Key("require_unique").MustBool(false)
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
}
if clearEmail {
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearEmails() })
}
if clearDiscord {
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearDiscord() })
}
if clearTelegram {
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearTelegram() })
}
if clearMatrix {
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
}
return &daemon
}
func (rt *housekeepingDaemon) run() {
rt.app.info.Println("Invite daemon started")
for {
select {
case <-rt.ShutdownChannel:
rt.ShutdownChannel <- "Down"
return
case <-time.After(rt.period):
break
}
started := time.Now()
rt.app.storage.loadInvites()
for _, job := range rt.jobs {
job(rt.app)
}
finished := time.Now()
duration := finished.Sub(started)
rt.period = rt.Interval - duration
}
}
func (rt *housekeepingDaemon) Shutdown() {
rt.Stopped = true
rt.ShutdownChannel <- "Down"
<-rt.ShutdownChannel
close(rt.ShutdownChannel)
}

View File

@ -1,50 +0,0 @@
package main
import "time"
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
type inviteDaemon struct {
Stopped bool
ShutdownChannel chan string
Interval time.Duration
period time.Duration
app *appContext
}
func newInviteDaemon(interval time.Duration, app *appContext) *inviteDaemon {
return &inviteDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
Interval: interval,
period: interval,
app: app,
}
}
func (rt *inviteDaemon) run() {
rt.app.info.Println("Invite daemon started")
for {
select {
case <-rt.ShutdownChannel:
rt.ShutdownChannel <- "Down"
return
case <-time.After(rt.period):
break
}
started := time.Now()
rt.app.storage.loadInvites()
rt.app.debug.Println("Daemon: Checking invites")
rt.app.checkInvites()
finished := time.Now()
duration := finished.Sub(started)
rt.period = rt.Interval - duration
}
}
func (rt *inviteDaemon) Shutdown() {
rt.Stopped = true
rt.ShutdownChannel <- "Down"
<-rt.ShutdownChannel
close(rt.ShutdownChannel)
}

View File

@ -24,6 +24,8 @@
"notifications": { "notifications": {
"errorUserExists": "User already exists.", "errorUserExists": "User already exists.",
"errorInvalidCode": "Invalid invite code.", "errorInvalidCode": "Invalid invite code.",
"errorAccountLinked": "Account already in use.",
"errorEmailLinked": "Email already in use.",
"errorTelegramVerification": "Telegram verification required.", "errorTelegramVerification": "Telegram verification required.",
"errorDiscordVerification": "Discord verification required.", "errorDiscordVerification": "Discord verification required.",
"errorMatrixVerification": "Matrix verification required.", "errorMatrixVerification": "Matrix verification required.",

View File

@ -18,7 +18,7 @@ import (
type Storage struct { type Storage struct {
timePattern string timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path string invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path string
users map[string]time.Time users map[string]time.Time // Map of Jellyfin User IDs to their expiry times.
invites Invites invites Invites
profiles map[string]Profile profiles map[string]Profile
defaultProfile string defaultProfile string

View File

@ -61,6 +61,9 @@ if (window.telegramEnabled) {
window.telegramModal.close(); window.telegramModal.close();
window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]); window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
return; return;
} else if (req.status == 400) {
window.telegramModal.close();
window.notifications.customError("accountLinkedError", window.messages["errorAccountLinked"]);
} else if (req.status == 200) { } else if (req.status == 200) {
if (req.response["success"] as boolean) { if (req.response["success"] as boolean) {
telegramVerified = true; telegramVerified = true;
@ -124,6 +127,9 @@ if (window.discordEnabled) {
window.discordModal.close(); window.discordModal.close();
window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]); window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
return; return;
} else if (req.status == 400) {
window.discordModal.close();
window.notifications.customError("accountLinkedError", window.messages["errorAccountLinked"]);
} else if (req.status == 200) { } else if (req.status == 200) {
if (req.response["success"] as boolean) { if (req.response["success"] as boolean) {
discordVerified = true; discordVerified = true;
@ -165,6 +171,10 @@ if (window.matrixEnabled) {
}; };
_post("/invite/" + window.code + "/matrix/user", send, (req: XMLHttpRequest) => { _post("/invite/" + window.code + "/matrix/user", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status == 400 && req.response["error"] == "errorAccountLinked") {
window.matrixModal.close();
window.notifications.customError("accountLinkedError", window.messages["errorAccountLinked"]);
}
removeLoader(submitButton); removeLoader(submitButton);
userID = input.value; userID = input.value;
if (req.status != 200) { if (req.status != 200) {