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:
parent
895dcf5a30
commit
2722e8482d
@ -453,6 +453,14 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
||||
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 {
|
||||
// length := len(app.telegram.verifiedTokens)
|
||||
// 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")
|
||||
_, 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)
|
||||
}
|
||||
|
||||
@ -510,7 +527,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||
// @Summary Generate and send a new PIN to a specified Matrix user.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param invCode path string true "invite Code"
|
||||
@ -526,9 +543,18 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||
var req MatrixSendPINDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.UserID == "" {
|
||||
respond(400, "errorNoUserID", gc)
|
||||
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)
|
||||
if !ok {
|
||||
respondBool(500, false, gc)
|
||||
|
49
api-users.go
49
api-users.go
@ -130,6 +130,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
success = false
|
||||
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)
|
||||
if err != nil {
|
||||
f = func(gc *gin.Context) {
|
||||
@ -164,6 +176,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
success = false
|
||||
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
|
||||
matrixUser = *user.User
|
||||
|
||||
@ -195,6 +219,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
success = false
|
||||
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 {
|
||||
@ -445,11 +481,22 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
||||
gc.JSON(200, validation)
|
||||
return
|
||||
}
|
||||
if emailEnabled && app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") {
|
||||
if emailEnabled {
|
||||
if app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") {
|
||||
app.info.Printf("%s: New user failed: Email Required", req.Code)
|
||||
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)
|
||||
if !success {
|
||||
f(gc)
|
||||
|
@ -502,6 +502,14 @@
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"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,
|
||||
"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": {
|
||||
"name": "API Token",
|
||||
"required": false,
|
||||
@ -745,6 +761,14 @@
|
||||
"value": false,
|
||||
"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": {
|
||||
"name": "API Token",
|
||||
"required": false,
|
||||
@ -801,6 +825,14 @@
|
||||
"value": false,
|
||||
"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": {
|
||||
"name": "Home Server URL",
|
||||
"required": false,
|
||||
|
160
daemon.go
Normal file
160
daemon.go
Normal 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)
|
||||
}
|
50
invdaemon.go
50
invdaemon.go
@ -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)
|
||||
}
|
@ -24,6 +24,8 @@
|
||||
"notifications": {
|
||||
"errorUserExists": "User already exists.",
|
||||
"errorInvalidCode": "Invalid invite code.",
|
||||
"errorAccountLinked": "Account already in use.",
|
||||
"errorEmailLinked": "Email already in use.",
|
||||
"errorTelegramVerification": "Telegram verification required.",
|
||||
"errorDiscordVerification": "Discord verification required.",
|
||||
"errorMatrixVerification": "Matrix verification required.",
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
type Storage struct {
|
||||
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
|
||||
users map[string]time.Time
|
||||
users map[string]time.Time // Map of Jellyfin User IDs to their expiry times.
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
|
10
ts/form.ts
10
ts/form.ts
@ -61,6 +61,9 @@ if (window.telegramEnabled) {
|
||||
window.telegramModal.close();
|
||||
window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
|
||||
return;
|
||||
} else if (req.status == 400) {
|
||||
window.telegramModal.close();
|
||||
window.notifications.customError("accountLinkedError", window.messages["errorAccountLinked"]);
|
||||
} else if (req.status == 200) {
|
||||
if (req.response["success"] as boolean) {
|
||||
telegramVerified = true;
|
||||
@ -124,6 +127,9 @@ if (window.discordEnabled) {
|
||||
window.discordModal.close();
|
||||
window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
|
||||
return;
|
||||
} else if (req.status == 400) {
|
||||
window.discordModal.close();
|
||||
window.notifications.customError("accountLinkedError", window.messages["errorAccountLinked"]);
|
||||
} else if (req.status == 200) {
|
||||
if (req.response["success"] as boolean) {
|
||||
discordVerified = true;
|
||||
@ -165,6 +171,10 @@ if (window.matrixEnabled) {
|
||||
};
|
||||
_post("/invite/" + window.code + "/matrix/user", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 400 && req.response["error"] == "errorAccountLinked") {
|
||||
window.matrixModal.close();
|
||||
window.notifications.customError("accountLinkedError", window.messages["errorAccountLinked"]);
|
||||
}
|
||||
removeLoader(submitButton);
|
||||
userID = input.value;
|
||||
if (req.status != 200) {
|
||||
|
Loading…
Reference in New Issue
Block a user