mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
users: huge cleanup/dedupe, interface-based third-party services
shared "newUser" method is now "NewUserPostVerification", and is shared between all routes which create a jellyfin account. The new "NewUserFromInvite", "NewUserFromAdmin" and "NewUserFromConfirmationKey" are smaller as a result. Discord, Telegram, and Matrix now implement the "ContactMethodLinker" and "ContactMethodUser" interfaces, meaning code is shared a lot between them in the NewUser methods, and the specifics are now in their own files. Ombi/Jellyseerr similarly implement a simpler interface "ThirdPartyService", which simply has ImportUser and AddContactMethod routes. Note these new interface methods are only used for user creation as of yet, but could likely be used in other places.
This commit is contained in:
parent
711394232b
commit
54e4a51a7f
@ -1,10 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -97,3 +99,67 @@ func (app *appContext) DeleteJellyseerrProfile(gc *gin.Context) {
|
|||||||
app.storage.SetProfileKey(profileName, profile)
|
app.storage.SetProfileKey(profileName, profile)
|
||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JellyseerrWrapper struct {
|
||||||
|
*jellyseerr.Jellyseerr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *JellyseerrWrapper) ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool) {
|
||||||
|
// Gets existing user (not possible) or imports the given user.
|
||||||
|
_, err = js.MustGetUser(jellyfinID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ok = true
|
||||||
|
err = js.ApplyTemplateToUser(jellyfinID, profile.Jellyseerr.User)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, jellyfinID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = js.ApplyNotificationsTemplateToUser(jellyfinID, profile.Jellyseerr.Notifications)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, jellyfinID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *JellyseerrWrapper) AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error) {
|
||||||
|
_, err = js.MustGetUser(jellyfinID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contactMethods := map[jellyseerr.NotificationsField]any{}
|
||||||
|
if emailEnabled {
|
||||||
|
err = js.ModifyMainUserSettings(jellyfinID, jellyseerr.MainUserSettings{Email: req.Email})
|
||||||
|
if err != nil {
|
||||||
|
// FIXME: This is a little ugly, considering all other errors are unformatted
|
||||||
|
err = fmt.Errorf(lm.FailedSetEmailAddress, lm.Jellyseerr, jellyfinID, err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
contactMethods[jellyseerr.FieldEmailEnabled] = req.EmailContact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if discordEnabled && discord != nil {
|
||||||
|
contactMethods[jellyseerr.FieldDiscord] = discord.ID
|
||||||
|
contactMethods[jellyseerr.FieldDiscordEnabled] = req.DiscordContact
|
||||||
|
}
|
||||||
|
if telegramEnabled && discord != nil {
|
||||||
|
contactMethods[jellyseerr.FieldTelegram] = telegram.ChatID
|
||||||
|
contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact
|
||||||
|
}
|
||||||
|
if len(contactMethods) > 0 {
|
||||||
|
err = js.ModifyNotifications(jellyfinID, contactMethods)
|
||||||
|
if err != nil {
|
||||||
|
// app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *JellyseerrWrapper) Name() string { return lm.Jellyseerr }
|
||||||
|
|
||||||
|
func (js *JellyseerrWrapper) Enabled(app *appContext, profile *Profile) bool {
|
||||||
|
return profile != nil && profile.Jellyseerr.Enabled && app.config.Section("jellyseerr").Key("enabled").MustBool(false)
|
||||||
|
}
|
||||||
|
@ -431,7 +431,7 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
|||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
token, ok := app.telegram.TokenVerified(pin)
|
token, ok := app.telegram.TokenVerified(pin)
|
||||||
if ok && app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
|
if ok && app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
|
||||||
app.discord.DeleteVerifiedUser(pin)
|
app.discord.DeleteVerifiedToken(pin)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -454,7 +454,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
user, ok := app.discord.UserVerified(pin)
|
user, ok := app.discord.UserVerified(pin)
|
||||||
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.ID) {
|
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.MethodID().(string)) {
|
||||||
delete(app.discord.verifiedTokens, pin)
|
delete(app.discord.verifiedTokens, pin)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
@ -472,7 +472,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
|||||||
// @Router /invite/{invCode}/discord/invite [get]
|
// @Router /invite/{invCode}/discord/invite [get]
|
||||||
// @tags Other
|
// @tags Other
|
||||||
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||||
if app.discord.inviteChannelName == "" {
|
if app.discord.InviteChannel.Name == "" {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
89
api-ombi.go
89
api-ombi.go
@ -1,19 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
|
"github.com/hrfee/jfa-go/ombi"
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
|
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
|
||||||
ombiUsers, code, err := app.ombi.GetUsers()
|
|
||||||
if err != nil || code != 200 {
|
|
||||||
return nil, code, err
|
|
||||||
}
|
|
||||||
jfUser, code, err := app.jf.UserByID(jfID, false)
|
jfUser, code, err := app.jf.UserByID(jfID, false)
|
||||||
if err != nil || code != 200 {
|
if err != nil || code != 200 {
|
||||||
return nil, code, err
|
return nil, code, err
|
||||||
@ -23,6 +22,14 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
|
|||||||
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
||||||
email = e.Addr
|
email = e.Addr
|
||||||
}
|
}
|
||||||
|
return app.ombi.getUser(username, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]interface{}, int, error) {
|
||||||
|
ombiUsers, code, err := ombi.GetUsers()
|
||||||
|
if err != nil || code != 200 {
|
||||||
|
return nil, code, err
|
||||||
|
}
|
||||||
for _, ombiUser := range ombiUsers {
|
for _, ombiUser := range ombiUsers {
|
||||||
ombiAddr := ""
|
ombiAddr := ""
|
||||||
if a, ok := ombiUser["emailAddress"]; ok && a != nil {
|
if a, ok := ombiUser["emailAddress"]; ok && a != nil {
|
||||||
@ -32,13 +39,13 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
|
|||||||
return ombiUser, code, err
|
return ombiUser, code, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, 400, fmt.Errorf("couldn't find user")
|
return nil, 400, errors.New(lm.NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a user with the given name who has been imported from Jellyfin/Emby by Ombi
|
// Returns a user with the given name who has been imported from Jellyfin/Emby by Ombi
|
||||||
func (app *appContext) getOmbiImportedUser(name string) (map[string]interface{}, int, error) {
|
func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, int, error) {
|
||||||
// Ombi User Types: 3/4 = Emby, 5 = Jellyfin
|
// Ombi User Types: 3/4 = Emby, 5 = Jellyfin
|
||||||
ombiUsers, code, err := app.ombi.GetUsers()
|
ombiUsers, code, err := ombi.GetUsers()
|
||||||
if err != nil || code != 200 {
|
if err != nil || code != 200 {
|
||||||
return nil, code, err
|
return nil, code, err
|
||||||
}
|
}
|
||||||
@ -136,7 +143,11 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
|
|||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) applyOmbiProfile(user map[string]interface{}, profile map[string]interface{}) (status int, err error) {
|
type OmbiWrapper struct {
|
||||||
|
*ombi.Ombi
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[string]interface{}) (status int, err error) {
|
||||||
for k, v := range profile {
|
for k, v := range profile {
|
||||||
switch v.(type) {
|
switch v.(type) {
|
||||||
case map[string]interface{}, []interface{}:
|
case map[string]interface{}, []interface{}:
|
||||||
@ -147,6 +158,66 @@ func (app *appContext) applyOmbiProfile(user map[string]interface{}, profile map
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
status, err = app.ombi.ModifyUser(user)
|
status, err = ombi.ModifyUser(user)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ombi *OmbiWrapper) ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool) {
|
||||||
|
errors, code, err := ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
|
||||||
|
var ombiUser map[string]interface{}
|
||||||
|
var status int
|
||||||
|
if err != nil || code != 200 {
|
||||||
|
// Check if on the off chance, Ombi's user importer has already added the account.
|
||||||
|
ombiUser, status, err = ombi.getImportedUser(req.Username)
|
||||||
|
if status == 200 && err == nil {
|
||||||
|
// app.info.Println(lm.Ombi + " " + lm.UserExists)
|
||||||
|
profile.Ombi["password"] = req.Password
|
||||||
|
status, err = ombi.applyProfile(ombiUser, profile.Ombi)
|
||||||
|
if status != 200 || err != nil {
|
||||||
|
err = fmt.Errorf(lm.FailedApplyProfile, lm.Ombi, req.Username, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(errors) != 0 {
|
||||||
|
err = fmt.Errorf("%v, %s", err, strings.Join(errors, ", "))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ombi *OmbiWrapper) AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error) {
|
||||||
|
var ombiUser map[string]interface{}
|
||||||
|
var status int
|
||||||
|
ombiUser, status, err = ombi.getUser(req.Username, req.Email)
|
||||||
|
if status != 200 || err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if discordEnabled || telegramEnabled {
|
||||||
|
dID := ""
|
||||||
|
tUser := ""
|
||||||
|
if discord != nil {
|
||||||
|
dID = discord.ID
|
||||||
|
}
|
||||||
|
if telegram != nil {
|
||||||
|
tUser = telegram.Username
|
||||||
|
}
|
||||||
|
var resp string
|
||||||
|
var status int
|
||||||
|
resp, status, err = ombi.SetNotificationPrefs(ombiUser, dID, tUser)
|
||||||
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
|
if resp != "" {
|
||||||
|
err = fmt.Errorf("%v, %s", err, resp)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ombi *OmbiWrapper) Name() string { return lm.Ombi }
|
||||||
|
|
||||||
|
func (ombi *OmbiWrapper) Enabled(app *appContext, profile *Profile) bool {
|
||||||
|
return profile != nil && profile.Ombi != nil && len(profile.Ombi) != 0 && app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||||
|
}
|
||||||
|
@ -292,7 +292,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags User Page
|
// @tags User Page
|
||||||
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
||||||
if app.discord.inviteChannelName == "" {
|
if app.discord.InviteChannel.Name == "" {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -340,7 +340,7 @@ func (app *appContext) GetMyPIN(gc *gin.Context) {
|
|||||||
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
|
dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
|
||||||
app.discord.DeleteVerifiedUser(pin)
|
app.discord.DeleteVerifiedToken(pin)
|
||||||
if !ok {
|
if !ok {
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
|
657
api-users.go
657
api-users.go
@ -23,7 +23,7 @@ import (
|
|||||||
// @Router /users [post]
|
// @Router /users [post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Users
|
// @tags Users
|
||||||
func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
func (app *appContext) NewUserFromAdmin(gc *gin.Context) {
|
||||||
respondUser := func(code int, user, email bool, msg string, gc *gin.Context) {
|
respondUser := func(code int, user, email bool, msg string, gc *gin.Context) {
|
||||||
resp := newUserResponse{
|
resp := newUserResponse{
|
||||||
User: user,
|
User: user,
|
||||||
@ -35,235 +35,118 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
var req newUserDTO
|
var req newUserDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
existingUser, _, _ := app.jf.UserByName(req.Username, false)
|
|
||||||
if existingUser.Name != "" {
|
|
||||||
msg := lm.UserExists
|
|
||||||
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg)
|
|
||||||
respondUser(401, false, false, msg, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user, status, err := app.jf.NewUser(req.Username, req.Password)
|
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
|
||||||
app.err.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, err)
|
|
||||||
respondUser(401, false, false, err.Error(), gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id := user.ID
|
|
||||||
|
|
||||||
// Record activity
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
|
||||||
Type: ActivityCreation,
|
|
||||||
UserID: id,
|
|
||||||
SourceType: ActivityAdmin,
|
|
||||||
Source: gc.GetString("jfId"),
|
|
||||||
Value: user.Name,
|
|
||||||
Time: time.Now(),
|
|
||||||
}, gc, false)
|
|
||||||
|
|
||||||
profile := app.storage.GetDefaultProfile()
|
profile := app.storage.GetDefaultProfile()
|
||||||
if req.Profile != "" && req.Profile != "none" {
|
nu := app.NewUserPostVerification(NewUserParams{
|
||||||
if p, ok := app.storage.GetProfileKey(req.Profile); ok {
|
Req: req,
|
||||||
profile = p
|
SourceType: ActivityAdmin,
|
||||||
} else {
|
Source: gc.GetString("jfId"),
|
||||||
app.debug.Printf(lm.FailedGetProfile+lm.FallbackToDefault, req.Profile)
|
ContextForIPLogging: gc,
|
||||||
|
Profile: &profile,
|
||||||
|
})
|
||||||
|
if !nu.Success {
|
||||||
|
nu.Log()
|
||||||
}
|
}
|
||||||
|
|
||||||
status, err = app.jf.SetPolicy(id, profile.Policy)
|
welcomeMessageSentIfNecessary := true
|
||||||
if !(status == 200 || status == 204 || err == nil) {
|
if nu.Created {
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, req.Username, err)
|
welcomeMessageSentIfNecessary = app.WelcomeNewUser(nu.User, time.Time{})
|
||||||
}
|
}
|
||||||
status, err = app.jf.SetConfiguration(id, profile.Configuration)
|
|
||||||
if (status == 200 || status == 204) && err == nil {
|
respondUser(nu.Status, nu.Created, welcomeMessageSentIfNecessary, nu.Message, gc)
|
||||||
status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs)
|
|
||||||
}
|
|
||||||
if !((status == 200 || status == 204) && err == nil) {
|
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, req.Username, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app.jf.CacheExpiry = time.Now()
|
|
||||||
if emailEnabled {
|
|
||||||
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
|
|
||||||
}
|
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
|
||||||
if profile.Ombi == nil {
|
|
||||||
profile.Ombi = map[string]interface{}{}
|
|
||||||
}
|
|
||||||
errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
|
|
||||||
if err != nil || code != 200 {
|
|
||||||
app.err.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err)
|
|
||||||
app.debug.Printf(lm.AdditionalErrors, lm.Ombi, strings.Join(errors, ", "))
|
|
||||||
} else {
|
|
||||||
app.info.Printf(lm.CreateUser, lm.Ombi, req.Username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
|
||||||
// Gets existing user (not possible) or imports the given user.
|
|
||||||
_, err := app.js.MustGetUser(id)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedCreateUser, lm.Jellyseerr, req.Username, err)
|
|
||||||
} else {
|
|
||||||
app.info.Printf(lm.CreateUser, lm.Jellyseerr, req.Username)
|
|
||||||
}
|
|
||||||
err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, req.Username, err)
|
|
||||||
}
|
|
||||||
err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, req.Username, err)
|
|
||||||
}
|
|
||||||
if emailEnabled {
|
|
||||||
err = app.js.ModifyUser(id, map[jellyseerr.UserField]any{jellyseerr.FieldEmail: req.Email})
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, id, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" {
|
|
||||||
msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedConstructWelcomeMessage, id, err)
|
|
||||||
respondUser(500, true, false, err.Error(), gc)
|
|
||||||
return
|
|
||||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
|
||||||
app.err.Printf(lm.FailedSendWelcomeMessage, req.Username, req.Email, err)
|
|
||||||
respondUser(500, true, false, err.Error(), gc)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
app.info.Printf(lm.SentWelcomeMessage, req.Username, req.Email)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
respondUser(200, true, true, "", gc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type errorFunc func(gc *gin.Context)
|
// @Summary Creates a new Jellyfin user via invite code
|
||||||
|
// @Produce json
|
||||||
|
// @Param newUserDTO body newUserDTO true "New user request object"
|
||||||
|
// @Success 200 {object} PasswordValidation
|
||||||
|
// @Failure 400 {object} PasswordValidation
|
||||||
|
// @Router /newUser [post]
|
||||||
|
// @tags Users
|
||||||
|
func (app *appContext) NewUserFromInvite(gc *gin.Context) {
|
||||||
|
/*
|
||||||
|
-- STEPS --
|
||||||
|
- Validate CAPTCHA
|
||||||
|
- Validate Invite
|
||||||
|
- Validate Password
|
||||||
|
- a) Discord (Require, Verify, ExistingUser, ApplyRole)
|
||||||
|
b) Telegram (Require, Verify, ExistingUser)
|
||||||
|
c) Matrix (Require, Verify, ExistingUser)
|
||||||
|
d) Email (Require, Verify, ExistingUser) (only occurs here, PWRs are sent by mail so do this on their own, kinda)
|
||||||
|
*/
|
||||||
|
var req newUserDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
|
||||||
// Used on the form & when a users email has been confirmed.
|
// Validate CAPTCHA
|
||||||
func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) (f errorFunc, success bool) {
|
if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText, false) {
|
||||||
existingUser, _, _ := app.jf.UserByName(req.Username, false)
|
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, lm.IncorrectCaptcha)
|
||||||
if existingUser.Name != "" {
|
respond(400, "errorCaptcha", gc)
|
||||||
f = func(gc *gin.Context) {
|
|
||||||
msg := lm.UserExists
|
|
||||||
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg)
|
|
||||||
respond(401, "errorUserExists", gc)
|
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var discordUser DiscordUser
|
// Validate Invite
|
||||||
discordVerified := false
|
if !app.checkInvite(req.Code, false, "") {
|
||||||
if discordEnabled {
|
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, fmt.Sprintf(lm.InvalidInviteCode, req.Code))
|
||||||
if req.DiscordPIN == "" {
|
respond(401, "errorInvalidCode", gc)
|
||||||
if app.config.Section("discord").Key("required").MustBool(false) {
|
return
|
||||||
f = func(gc *gin.Context) {
|
|
||||||
app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, lm.AccountUnverified)
|
|
||||||
respond(401, "errorDiscordVerification", gc)
|
|
||||||
}
|
}
|
||||||
success = false
|
// Validate Password
|
||||||
|
validation := app.validator.validate(req.Password)
|
||||||
|
valid := true
|
||||||
|
for _, val := range validation {
|
||||||
|
if !val {
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
// 200 bcs idk what i did in js
|
||||||
|
gc.JSON(200, validation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completeContactMethods := make([]struct {
|
||||||
|
Verified bool
|
||||||
|
PIN string
|
||||||
|
User ContactMethodUser
|
||||||
|
}, len(app.contactMethods))
|
||||||
|
for i, cm := range app.contactMethods {
|
||||||
|
completeContactMethods[i].PIN = cm.PIN(req)
|
||||||
|
if completeContactMethods[i].PIN == "" {
|
||||||
|
if cm.Required() {
|
||||||
|
app.info.Printf(lm.FailedLinkUser, cm.Name(), "?", req.Code, lm.AccountUnverified)
|
||||||
|
respond(401, fmt.Sprintf("error%sVerification", cm.Name()), gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
discordUser, discordVerified = app.discord.UserVerified(req.DiscordPIN)
|
completeContactMethods[i].User, completeContactMethods[i].Verified = cm.UserVerified(completeContactMethods[i].PIN)
|
||||||
if !discordVerified {
|
if !completeContactMethods[i].Verified {
|
||||||
f = func(gc *gin.Context) {
|
app.info.Printf(lm.FailedLinkUser, cm.Name(), "?", req.Code, fmt.Sprintf(lm.InvalidPIN, completeContactMethods[i].PIN))
|
||||||
app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, fmt.Sprintf(lm.InvalidPIN, req.DiscordPIN))
|
|
||||||
respond(401, "errorInvalidPIN", gc)
|
respond(401, "errorInvalidPIN", gc)
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(discordUser.ID) {
|
if cm.UniqueRequired() && cm.Exists(completeContactMethods[i].User) {
|
||||||
f = func(gc *gin.Context) {
|
app.debug.Printf(lm.FailedLinkUser, cm.Name(), completeContactMethods[i].User.Name(), req.Code, lm.AccountLinked)
|
||||||
app.debug.Printf(lm.FailedLinkUser, lm.Discord, discordUser.ID, req.Code, lm.AccountLinked)
|
|
||||||
respond(400, "errorAccountLinked", gc)
|
respond(400, "errorAccountLinked", gc)
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := app.discord.ApplyRole(discordUser.ID)
|
if err := cm.PostVerificationTasks(completeContactMethods[i].PIN, completeContactMethods[i].User); err != nil {
|
||||||
if err != nil {
|
app.err.Printf(lm.FailedLinkUser, cm.Name(), completeContactMethods[i].User.Name(), req.Code, err)
|
||||||
f = func(gc *gin.Context) {
|
|
||||||
app.err.Printf(lm.FailedLinkUser, lm.Discord, discordUser.ID, req.Code, fmt.Sprintf(lm.FailedSetDiscordMemberRole, err))
|
|
||||||
respond(401, "error", gc)
|
|
||||||
}
|
}
|
||||||
success = false
|
}
|
||||||
|
}
|
||||||
|
// Email (FIXME: Potentially make this a ContactMethodLinker)
|
||||||
|
if emailEnabled {
|
||||||
|
// Require
|
||||||
|
if app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") {
|
||||||
|
respond(400, "errorNoEmail", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
// ExistingUser
|
||||||
}
|
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" && app.EmailAddressExists(req.Email) {
|
||||||
var matrixUser MatrixUser
|
respond(400, "errorEmailLinked", gc)
|
||||||
matrixVerified := false
|
|
||||||
if matrixEnabled {
|
|
||||||
if req.MatrixPIN == "" {
|
|
||||||
if app.config.Section("matrix").Key("required").MustBool(false) {
|
|
||||||
f = func(gc *gin.Context) {
|
|
||||||
app.info.Printf(lm.FailedLinkUser, lm.Matrix, "?", req.Code, lm.AccountUnverified)
|
|
||||||
respond(401, "errorMatrixVerification", gc)
|
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
// Verify
|
||||||
user, ok := app.matrix.tokens[req.MatrixPIN]
|
if app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
||||||
if !ok || !user.Verified {
|
|
||||||
matrixVerified = false
|
|
||||||
f = func(gc *gin.Context) {
|
|
||||||
uid := ""
|
|
||||||
if ok {
|
|
||||||
uid = user.User.UserID
|
|
||||||
}
|
|
||||||
app.info.Printf(lm.FailedLinkUser, lm.Matrix, uid, req.Code, fmt.Sprintf(lm.InvalidPIN, req.MatrixPIN))
|
|
||||||
respond(401, "errorInvalidPIN", gc)
|
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if app.config.Section("matrix").Key("require_unique").MustBool(false) && app.matrix.UserExists(user.User.UserID) {
|
|
||||||
f = func(gc *gin.Context) {
|
|
||||||
app.debug.Printf(lm.FailedLinkUser, lm.Matrix, user.User.UserID, req.Code, lm.AccountLinked)
|
|
||||||
respond(400, "errorAccountLinked", gc)
|
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
matrixVerified = user.Verified
|
|
||||||
matrixUser = *user.User
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var tgToken TelegramVerifiedToken
|
|
||||||
telegramVerified := false
|
|
||||||
if telegramEnabled {
|
|
||||||
if req.TelegramPIN == "" {
|
|
||||||
if app.config.Section("telegram").Key("required").MustBool(false) {
|
|
||||||
f = func(gc *gin.Context) {
|
|
||||||
app.info.Printf(lm.FailedLinkUser, lm.Telegram, "?", req.Code, lm.AccountUnverified)
|
|
||||||
respond(401, "errorTelegramVerification", gc)
|
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tgToken, telegramVerified = app.telegram.TokenVerified(req.TelegramPIN)
|
|
||||||
if !telegramVerified {
|
|
||||||
f = func(gc *gin.Context) {
|
|
||||||
app.info.Printf(lm.FailedLinkUser, lm.Telegram, tgToken.Username, req.Code, fmt.Sprintf(lm.InvalidPIN, req.TelegramPIN))
|
|
||||||
respond(401, "errorInvalidPIN", gc)
|
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(tgToken.Username) {
|
|
||||||
f = func(gc *gin.Context) {
|
|
||||||
app.debug.Printf(lm.FailedLinkUser, lm.Telegram, tgToken.Username, req.Code, lm.AccountLinked)
|
|
||||||
respond(400, "errorAccountLinked", gc)
|
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,
|
||||||
@ -273,11 +156,8 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
|
|||||||
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")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f = func(gc *gin.Context) {
|
app.err.Printf(lm.FailedSignJWT, err)
|
||||||
app.info.Printf(lm.FailedSignJWT, err)
|
|
||||||
respond(500, "errorUnknown", gc)
|
respond(500, "errorUnknown", gc)
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.ConfirmationKeys == nil {
|
if app.ConfirmationKeys == nil {
|
||||||
@ -291,7 +171,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
|
|||||||
app.confirmationKeysLock.Lock()
|
app.confirmationKeysLock.Lock()
|
||||||
app.ConfirmationKeys[req.Code] = cKeys
|
app.ConfirmationKeys[req.Code] = cKeys
|
||||||
app.confirmationKeysLock.Unlock()
|
app.confirmationKeysLock.Unlock()
|
||||||
f = func(gc *gin.Context) {
|
|
||||||
app.debug.Printf(lm.EmailConfirmationRequired, req.Username)
|
app.debug.Printf(lm.EmailConfirmationRequired, req.Username)
|
||||||
respond(401, "confirmEmail", gc)
|
respond(401, "confirmEmail", gc)
|
||||||
msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app, false)
|
msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app, false)
|
||||||
@ -302,25 +182,76 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
|
|||||||
} else {
|
} else {
|
||||||
app.err.Printf(lm.SentConfirmationEmail, req.Code, req.Email)
|
app.err.Printf(lm.SentConfirmationEmail, req.Code, req.Email)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
success = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
user, status, err := app.jf.NewUser(req.Username, req.Password)
|
invite, _ := app.storage.GetInvitesKey(req.Code)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
|
||||||
f = func(gc *gin.Context) {
|
sourceType, source := invite.Source()
|
||||||
app.err.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, err)
|
|
||||||
respond(401, app.storage.lang.Admin[app.storage.lang.chosenAdminLang].Notifications.get("errorUnknown"), gc)
|
var profile *Profile = nil
|
||||||
|
if invite.Profile != "" {
|
||||||
|
p, ok := app.storage.GetProfileKey(invite.Profile)
|
||||||
|
if !ok {
|
||||||
|
app.debug.Printf(lm.FailedGetProfile+lm.FallbackToDefault, invite.Profile)
|
||||||
|
p = app.storage.GetDefaultProfile()
|
||||||
}
|
}
|
||||||
success = false
|
profile = &p
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Use NewUserPostVerification
|
||||||
|
nu := app.NewUserPostVerification(NewUserParams{
|
||||||
|
Req: req,
|
||||||
|
SourceType: sourceType,
|
||||||
|
Source: source,
|
||||||
|
ContextForIPLogging: gc,
|
||||||
|
Profile: profile,
|
||||||
|
})
|
||||||
|
if !nu.Success {
|
||||||
|
nu.Log()
|
||||||
|
}
|
||||||
|
if !nu.Created {
|
||||||
|
respond(nu.Status, nu.Message, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
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) {
|
|
||||||
|
nonEmailContactMethodEnabled := false
|
||||||
|
for i, c := range completeContactMethods {
|
||||||
|
if c.Verified {
|
||||||
|
c.User.SetAllowContactFromDTO(req)
|
||||||
|
if c.User.AllowContact() {
|
||||||
|
nonEmailContactMethodEnabled = true
|
||||||
|
}
|
||||||
|
app.contactMethods[i].DeleteVerifiedToken(c.PIN)
|
||||||
|
c.User.Store(&(app.storage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
referralsEnabled := profile != nil && profile.ReferralTemplateKey != "" && app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false)
|
||||||
|
|
||||||
|
if (emailEnabled && req.Email != "") || invite.UserLabel != "" || referralsEnabled {
|
||||||
|
emailStore := EmailAddress{
|
||||||
|
Addr: req.Email,
|
||||||
|
Contact: (req.Email != ""),
|
||||||
|
Label: invite.UserLabel,
|
||||||
|
ReferralTemplateKey: profile.ReferralTemplateKey,
|
||||||
|
}
|
||||||
|
/// Ensures at least one contact method is enabled.
|
||||||
|
if nonEmailContactMethodEnabled {
|
||||||
|
emailStore.Contact = req.EmailContact
|
||||||
|
}
|
||||||
|
app.storage.SetEmailsKey(nu.User.ID, emailStore)
|
||||||
|
}
|
||||||
|
if emailEnabled {
|
||||||
|
if app.config.Section("notifications").Key("enabled").MustBool(false) {
|
||||||
for address, settings := range invite.Notify {
|
for address, settings := range invite.Notify {
|
||||||
if settings["notify-creation"] {
|
if !settings["notify-creation"] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// FIXME: Forgot how "go" works, but these might get killed if not finished
|
||||||
|
// before we do?
|
||||||
go func(addr string) {
|
go func(addr string) {
|
||||||
msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false)
|
msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -342,275 +273,62 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
id := user.ID
|
|
||||||
|
|
||||||
// Record activity
|
if referralsEnabled {
|
||||||
sourceType := ActivityAnon
|
|
||||||
source := ""
|
|
||||||
if invite.ReferrerJellyfinID != "" {
|
|
||||||
sourceType = ActivityUser
|
|
||||||
source = invite.ReferrerJellyfinID
|
|
||||||
}
|
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
|
||||||
Type: ActivityCreation,
|
|
||||||
UserID: id,
|
|
||||||
SourceType: sourceType,
|
|
||||||
Source: source,
|
|
||||||
InviteCode: invite.Code,
|
|
||||||
Value: user.Name,
|
|
||||||
Time: time.Now(),
|
|
||||||
}, gc, true)
|
|
||||||
|
|
||||||
emailStore := EmailAddress{
|
|
||||||
Addr: req.Email,
|
|
||||||
Contact: (req.Email != ""),
|
|
||||||
}
|
|
||||||
// Only allow disabling of email contact if some other method is available.
|
|
||||||
if req.DiscordContact || req.TelegramContact || req.MatrixContact {
|
|
||||||
emailStore.Contact = req.EmailContact
|
|
||||||
}
|
|
||||||
|
|
||||||
if invite.UserLabel != "" {
|
|
||||||
emailStore.Label = invite.UserLabel
|
|
||||||
}
|
|
||||||
|
|
||||||
var profile Profile
|
|
||||||
if invite.Profile != "" {
|
|
||||||
app.debug.Printf(lm.ApplyProfile, invite.Profile)
|
|
||||||
var ok bool
|
|
||||||
profile, ok = app.storage.GetProfileKey(invite.Profile)
|
|
||||||
if !ok {
|
|
||||||
profile = app.storage.GetDefaultProfile()
|
|
||||||
}
|
|
||||||
status, err = app.jf.SetPolicy(id, profile.Policy)
|
|
||||||
if !((status == 200 || status == 204) && err == nil) {
|
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, id, err)
|
|
||||||
}
|
|
||||||
status, err = app.jf.SetConfiguration(id, profile.Configuration)
|
|
||||||
if (status == 200 || status == 204) && err == nil {
|
|
||||||
status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs)
|
|
||||||
}
|
|
||||||
if !((status == 200 || status == 204) && err == nil) {
|
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, id, err)
|
|
||||||
}
|
|
||||||
if app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false) && profile.ReferralTemplateKey != "" {
|
|
||||||
emailStore.ReferralTemplateKey = profile.ReferralTemplateKey
|
|
||||||
// Store here, just incase email are disabled (whether this is even possible, i don't know)
|
|
||||||
app.storage.SetEmailsKey(id, emailStore)
|
|
||||||
|
|
||||||
// If UseReferralExpiry is enabled, create the ref now so the clock starts ticking
|
|
||||||
refInv := Invite{}
|
refInv := Invite{}
|
||||||
err = app.storage.db.Get(profile.ReferralTemplateKey, &refInv)
|
/*err := */ app.storage.db.Get(profile.ReferralTemplateKey, &refInv)
|
||||||
|
// If UseReferralExpiry is enabled, create the ref now so the clock starts ticking
|
||||||
if refInv.UseReferralExpiry {
|
if refInv.UseReferralExpiry {
|
||||||
refInv.Code = GenerateInviteCode()
|
refInv.Code = GenerateInviteCode()
|
||||||
expiryDelta := refInv.ValidTill.Sub(refInv.Created)
|
expiryDelta := refInv.ValidTill.Sub(refInv.Created)
|
||||||
refInv.Created = time.Now()
|
refInv.Created = time.Now()
|
||||||
refInv.ValidTill = refInv.Created.Add(expiryDelta)
|
refInv.ValidTill = refInv.Created.Add(expiryDelta)
|
||||||
refInv.IsReferral = true
|
refInv.IsReferral = true
|
||||||
refInv.ReferrerJellyfinID = id
|
refInv.ReferrerJellyfinID = nu.User.ID
|
||||||
app.storage.SetInvitesKey(refInv.Code, refInv)
|
app.storage.SetInvitesKey(refInv.Code, refInv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
|
||||||
if req.Email != "" || invite.UserLabel != "" {
|
|
||||||
app.storage.SetEmailsKey(id, emailStore)
|
|
||||||
}
|
|
||||||
expiry := time.Time{}
|
expiry := time.Time{}
|
||||||
if invite.UserExpiry {
|
if invite.UserExpiry {
|
||||||
expiry = time.Now().AddDate(0, invite.UserMonths, invite.UserDays).Add(time.Duration((60*invite.UserHours)+invite.UserMinutes) * time.Minute)
|
expiry = time.Now().AddDate(0, invite.UserMonths, invite.UserDays).Add(time.Duration((60*invite.UserHours)+invite.UserMinutes) * time.Minute)
|
||||||
app.storage.SetUserExpiryKey(id, UserExpiry{Expiry: expiry})
|
app.storage.SetUserExpiryKey(nu.User.ID, UserExpiry{Expiry: expiry})
|
||||||
}
|
}
|
||||||
if discordVerified {
|
|
||||||
discordUser.Contact = req.DiscordContact
|
|
||||||
if app.storage.deprecatedDiscord == nil {
|
|
||||||
app.storage.deprecatedDiscord = discordStore{}
|
|
||||||
}
|
|
||||||
// Note we don't log an activity here, since it's part of creating a user.
|
|
||||||
app.storage.SetDiscordKey(user.ID, discordUser)
|
|
||||||
delete(app.discord.verifiedTokens, req.DiscordPIN)
|
|
||||||
}
|
|
||||||
if telegramVerified {
|
|
||||||
tgUser := TelegramUser{
|
|
||||||
ChatID: tgToken.ChatID,
|
|
||||||
Username: tgToken.Username,
|
|
||||||
Contact: req.TelegramContact,
|
|
||||||
}
|
|
||||||
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
|
|
||||||
tgUser.Lang = lang
|
|
||||||
}
|
|
||||||
if app.storage.deprecatedTelegram == nil {
|
|
||||||
app.storage.deprecatedTelegram = telegramStore{}
|
|
||||||
}
|
|
||||||
app.telegram.DeleteVerifiedToken(req.TelegramPIN)
|
|
||||||
app.storage.SetTelegramKey(user.ID, tgUser)
|
|
||||||
}
|
|
||||||
if invite.Profile != "" && app.config.Section("ombi").Key("enabled").MustBool(false) {
|
|
||||||
if profile.Ombi != nil && len(profile.Ombi) != 0 {
|
|
||||||
template := profile.Ombi
|
|
||||||
errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, template)
|
|
||||||
accountExists := false
|
|
||||||
var ombiUser map[string]interface{}
|
|
||||||
if err != nil || code != 200 {
|
|
||||||
// Check if on the off chance, Ombi's user importer has already added the account.
|
|
||||||
ombiUser, status, err = app.getOmbiImportedUser(req.Username)
|
|
||||||
if status == 200 && err == nil {
|
|
||||||
app.info.Println(lm.Ombi + " " + lm.UserExists)
|
|
||||||
accountExists = true
|
|
||||||
template["password"] = req.Password
|
|
||||||
status, err = app.applyOmbiProfile(ombiUser, template)
|
|
||||||
if status != 200 || err != nil {
|
|
||||||
app.err.Printf(lm.FailedApplyProfile, lm.Ombi, req.Username, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
app.info.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err)
|
|
||||||
app.debug.Printf(lm.AdditionalErrors, lm.Ombi, strings.Join(errors, ", "))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ombiUser, status, err = app.getOmbiUser(id)
|
|
||||||
if status != 200 || err != nil {
|
|
||||||
app.err.Printf(lm.FailedGetUser, id, lm.Ombi, err)
|
|
||||||
} else {
|
|
||||||
app.info.Println(lm.CreateUser, lm.Ombi, id)
|
|
||||||
accountExists = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if accountExists {
|
|
||||||
if discordVerified || telegramVerified {
|
|
||||||
dID := ""
|
|
||||||
tUser := ""
|
|
||||||
if discordVerified {
|
|
||||||
dID = discordUser.ID
|
|
||||||
}
|
|
||||||
if telegramVerified {
|
|
||||||
u, _ := app.storage.GetTelegramKey(user.ID)
|
|
||||||
tUser = u.Username
|
|
||||||
}
|
|
||||||
resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser)
|
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Ombi, err)
|
|
||||||
app.debug.Printf(lm.AdditionalErrors, lm.Ombi, resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if invite.Profile != "" && app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
|
||||||
if profile.Jellyseerr.Enabled {
|
|
||||||
// Gets existing user (not possible) or imports the given user.
|
|
||||||
_, err := app.js.MustGetUser(id)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedCreateUser, lm.Jellyseerr, id, err)
|
|
||||||
} else {
|
|
||||||
app.info.Printf(lm.CreateUser, lm.Jellyseerr, id)
|
|
||||||
}
|
|
||||||
err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, id, err)
|
|
||||||
}
|
|
||||||
err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, id, err)
|
|
||||||
}
|
|
||||||
contactMethods := map[jellyseerr.NotificationsField]any{}
|
|
||||||
if emailEnabled {
|
|
||||||
err = app.js.ModifyMainUserSettings(id, jellyseerr.MainUserSettings{Email: req.Email})
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, id, err)
|
|
||||||
} else {
|
|
||||||
contactMethods[jellyseerr.FieldEmailEnabled] = req.EmailContact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if discordVerified {
|
|
||||||
contactMethods[jellyseerr.FieldDiscord] = discordUser.ID
|
|
||||||
contactMethods[jellyseerr.FieldDiscordEnabled] = req.DiscordContact
|
|
||||||
}
|
|
||||||
if telegramVerified {
|
|
||||||
u, _ := app.storage.GetTelegramKey(user.ID)
|
|
||||||
contactMethods[jellyseerr.FieldTelegram] = u.ChatID
|
|
||||||
contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact
|
|
||||||
}
|
|
||||||
if emailEnabled || discordVerified || telegramVerified {
|
|
||||||
err := app.js.ModifyNotifications(id, contactMethods)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if matrixVerified {
|
|
||||||
matrixUser.Contact = req.MatrixContact
|
|
||||||
delete(app.matrix.tokens, req.MatrixPIN)
|
|
||||||
if app.storage.deprecatedMatrix == nil {
|
|
||||||
app.storage.deprecatedMatrix = matrixStore{}
|
|
||||||
}
|
|
||||||
app.storage.SetMatrixKey(user.ID, matrixUser)
|
|
||||||
}
|
|
||||||
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramVerified || discordVerified || matrixVerified {
|
|
||||||
name := app.getAddressOrName(user.ID)
|
|
||||||
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedConstructWelcomeMessage, id, err)
|
|
||||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
|
||||||
app.err.Printf(lm.FailedSendWelcomeMessage, id, req.Email, err)
|
|
||||||
} else {
|
|
||||||
app.info.Printf(lm.SentWelcomeMessage, id, req.Email)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app.jf.CacheExpiry = time.Now()
|
|
||||||
success = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Creates a new Jellyfin user via invite code
|
var discordUser *DiscordUser = nil
|
||||||
// @Produce json
|
var telegramUser *TelegramUser = nil
|
||||||
// @Param newUserDTO body newUserDTO true "New user request object"
|
if app.ombi.Enabled(app, profile) || app.js.Enabled(app, profile) {
|
||||||
// @Success 200 {object} PasswordValidation
|
// FIXME: figure these out in a nicer way? this relies on the current ordering,
|
||||||
// @Failure 400 {object} PasswordValidation
|
// which may not be fixed.
|
||||||
// @Router /newUser [post]
|
if discordEnabled {
|
||||||
// @tags Users
|
discordUser = completeContactMethods[0].User.(*DiscordUser)
|
||||||
func (app *appContext) NewUser(gc *gin.Context) {
|
if telegramEnabled {
|
||||||
var req newUserDTO
|
telegramUser = completeContactMethods[1].User.(*TelegramUser)
|
||||||
gc.BindJSON(&req)
|
|
||||||
if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText, false) {
|
|
||||||
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, lm.IncorrectCaptcha)
|
|
||||||
respond(400, "errorCaptcha", gc)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if !app.checkInvite(req.Code, false, "") {
|
} else if telegramEnabled {
|
||||||
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, fmt.Sprintf(lm.InvalidInviteCode, req.Code))
|
telegramUser = completeContactMethods[0].User.(*TelegramUser)
|
||||||
respond(401, "errorInvalidCode", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
validation := app.validator.validate(req.Password)
|
|
||||||
valid := true
|
|
||||||
for _, val := range validation {
|
|
||||||
if !val {
|
|
||||||
valid = false
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !valid {
|
|
||||||
// 200 bcs idk what i did in js
|
for _, tps := range app.thirdPartyServices {
|
||||||
gc.JSON(200, validation)
|
if !tps.Enabled(app, profile) {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
if emailEnabled {
|
// User already created, now we can link contact methods
|
||||||
if app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") {
|
err := tps.AddContactMethods(nu.User.ID, req, discordUser, telegramUser)
|
||||||
respond(400, "errorNoEmail", gc)
|
if err != nil {
|
||||||
return
|
app.err.Printf(lm.FailedSyncContactMethods, tps.Name(), err)
|
||||||
}
|
|
||||||
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" && app.EmailAddressExists(req.Email) {
|
|
||||||
respond(400, "errorEmailLinked", gc)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f, success := app.newUser(req, false, gc)
|
|
||||||
|
app.WelcomeNewUser(nu.User, expiry)
|
||||||
|
|
||||||
|
/*responseFunc, logFunc, success := app.NewUser(req, false, gc)
|
||||||
if !success {
|
if !success {
|
||||||
f(gc)
|
logFunc()
|
||||||
|
responseFunc(gc)
|
||||||
return
|
return
|
||||||
}
|
}*/
|
||||||
code := 200
|
code := 200
|
||||||
for _, val := range validation {
|
for _, val := range validation {
|
||||||
if !val {
|
if !val {
|
||||||
@ -954,7 +672,7 @@ func (app *appContext) Announce(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.SentAnnouncementMessage, userID, "?")
|
// app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
|
||||||
} else {
|
} else {
|
||||||
msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
|
msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -966,8 +684,9 @@ func (app *appContext) Announce(gc *gin.Context) {
|
|||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
|
// app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
|
||||||
}
|
}
|
||||||
|
app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1455,7 +1174,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
// newUser["userName"] = user["userName"]
|
// newUser["userName"] = user["userName"]
|
||||||
// newUser["alias"] = user["alias"]
|
// newUser["alias"] = user["alias"]
|
||||||
// newUser["emailAddress"] = user["emailAddress"]
|
// newUser["emailAddress"] = user["emailAddress"]
|
||||||
status, err = app.applyOmbiProfile(user, ombi)
|
status, err = app.ombi.applyProfile(user, ombi)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
errorString += fmt.Sprintf("Apply %d: %v ", status, err)
|
errorString += fmt.Sprintf("Apply %d: %v ", status, err)
|
||||||
}
|
}
|
||||||
|
79
discord.go
79
discord.go
@ -17,7 +17,7 @@ type DiscordDaemon struct {
|
|||||||
username string
|
username string
|
||||||
tokens map[string]VerifToken // Map of pins to tokens.
|
tokens map[string]VerifToken // Map of pins to tokens.
|
||||||
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
|
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
|
||||||
channelID, channelName, inviteChannelID, inviteChannelName string
|
Channel, InviteChannel struct{ ID, Name string }
|
||||||
guildID string
|
guildID string
|
||||||
serverChannelName, serverName string
|
serverChannelName, serverName string
|
||||||
users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start.
|
users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start.
|
||||||
@ -120,12 +120,12 @@ func (d *DiscordDaemon) run() {
|
|||||||
d.serverChannelName = guild.Name
|
d.serverChannelName = guild.Name
|
||||||
d.serverName = guild.Name
|
d.serverName = guild.Name
|
||||||
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
|
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
|
||||||
d.channelName = channel
|
d.Channel.Name = channel
|
||||||
d.serverChannelName += "/" + channel
|
d.serverChannelName += "/" + channel
|
||||||
}
|
}
|
||||||
if d.app.config.Section("discord").Key("provide_invite").MustBool(false) {
|
if d.app.config.Section("discord").Key("provide_invite").MustBool(false) {
|
||||||
if invChannel := d.app.config.Section("discord").Key("invite_channel").String(); invChannel != "" {
|
if invChannel := d.app.config.Section("discord").Key("invite_channel").String(); invChannel != "" {
|
||||||
d.inviteChannelName = invChannel
|
d.InviteChannel.Name = invChannel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = d.bot.UpdateGameStatus(0, "/"+d.app.config.Section("discord").Key("start_command").MustString("start"))
|
err = d.bot.UpdateGameStatus(0, "/"+d.app.config.Section("discord").Key("start_command").MustString("start"))
|
||||||
@ -171,11 +171,11 @@ func (d *DiscordDaemon) ApplyRole(userID string) error {
|
|||||||
func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) {
|
func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) {
|
||||||
var inv *dg.Invite
|
var inv *dg.Invite
|
||||||
var err error
|
var err error
|
||||||
if d.inviteChannelName == "" {
|
if d.InviteChannel.Name == "" {
|
||||||
d.app.err.Println(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty)
|
d.app.err.Println(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if d.inviteChannelID == "" {
|
if d.InviteChannel.ID == "" {
|
||||||
channels, err := d.bot.GuildChannels(d.guildID)
|
channels, err := d.bot.GuildChannels(d.guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordChannels, err)
|
d.app.err.Printf(lm.FailedGetDiscordChannels, err)
|
||||||
@ -188,14 +188,14 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
|
|||||||
// d.app.err.Printf(lm.FailedGetDiscordChannel, ch.ID, err)
|
// d.app.err.Printf(lm.FailedGetDiscordChannel, ch.ID, err)
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
if channel.Name == d.inviteChannelName {
|
if channel.Name == d.InviteChannel.Name {
|
||||||
d.inviteChannelID = channel.ID
|
d.InviteChannel.ID = channel.ID
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordChannel, d.InviteChannelName, lm.NotFound)
|
d.app.err.Printf(lm.FailedGetDiscordChannel, d.InviteChannel.Name, lm.NotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,7 +204,7 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
|
|||||||
// d.app.err.Printf(lm.FailedGetDiscordChannel, d.inviteChannelID, err)
|
// d.app.err.Printf(lm.FailedGetDiscordChannel, d.inviteChannelID, err)
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
inv, err = d.bot.ChannelInviteCreate(d.inviteChannelID, dg.Invite{
|
inv, err = d.bot.ChannelInviteCreate(d.InviteChannel.ID, dg.Invite{
|
||||||
// Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1],
|
// Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1],
|
||||||
// Channel: channel,
|
// Channel: channel,
|
||||||
// Inviter: d.bot.State.User,
|
// Inviter: d.bot.State.User,
|
||||||
@ -451,19 +451,19 @@ func (d *DiscordDaemon) UpdateCommands() {
|
|||||||
|
|
||||||
func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
|
func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
|
||||||
if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok {
|
if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok {
|
||||||
if i.GuildID != "" && d.channelName != "" {
|
if i.GuildID != "" && d.Channel.Name != "" {
|
||||||
if d.channelID == "" {
|
if d.Channel.ID == "" {
|
||||||
channel, err := s.Channel(i.ChannelID)
|
channel, err := s.Channel(i.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordChannel, i.ChannelID, err)
|
d.app.err.Printf(lm.FailedGetDiscordChannel, i.ChannelID, err)
|
||||||
d.app.err.Println(lm.MonitorAllDiscordChannels)
|
d.app.err.Println(lm.MonitorAllDiscordChannels)
|
||||||
d.channelName = ""
|
d.Channel.Name = ""
|
||||||
}
|
}
|
||||||
if channel.Name == d.channelName {
|
if channel.Name == d.Channel.Name {
|
||||||
d.channelID = channel.ID
|
d.Channel.ID = channel.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if d.channelID != i.ChannelID {
|
if d.Channel.ID != i.ChannelID {
|
||||||
d.app.debug.Printf(lm.IgnoreOutOfChannelMessage, lm.Discord)
|
d.app.debug.Printf(lm.IgnoreOutOfChannelMessage, lm.Discord)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -739,10 +739,10 @@ func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserVerified returns whether or not a token with the given PIN has been verified, and the user itself.
|
// UserVerified returns whether or not a token with the given PIN has been verified, and the user itself.
|
||||||
func (d *DiscordDaemon) UserVerified(pin string) (user DiscordUser, ok bool) {
|
func (d *DiscordDaemon) UserVerified(pin string) (ContactMethodUser, bool) {
|
||||||
user, ok = d.verifiedTokens[pin]
|
u, ok := d.verifiedTokens[pin]
|
||||||
// delete(d.verifiedTokens, pin)
|
// delete(d.verifiedTokens, pin)
|
||||||
return
|
return &u, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssignedUserVerified returns whether or not a user with the given PIN has been verified, and the token itself.
|
// AssignedUserVerified returns whether or not a user with the given PIN has been verified, and the token itself.
|
||||||
@ -762,7 +762,44 @@ func (d *DiscordDaemon) UserExists(id string) bool {
|
|||||||
return err != nil || c > 0
|
return err != nil || c > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteVerifiedUser removes the token with the given PIN.
|
// Exists returns whether or not the given user exists.
|
||||||
func (d *DiscordDaemon) DeleteVerifiedUser(pin string) {
|
func (d *DiscordDaemon) Exists(user ContactMethodUser) bool {
|
||||||
delete(d.verifiedTokens, pin)
|
return d.UserExists(user.MethodID().(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteVerifiedToken removes the token with the given PIN.
|
||||||
|
func (d *DiscordDaemon) DeleteVerifiedToken(PIN string) {
|
||||||
|
delete(d.verifiedTokens, PIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) PIN(req newUserDTO) string { return req.DiscordPIN }
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) Name() string { return lm.Discord }
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) Required() bool {
|
||||||
|
return d.app.config.Section("discord").Key("required").MustBool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) UniqueRequired() bool {
|
||||||
|
return d.app.config.Section("discord").Key("require_unique").MustBool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) PostVerificationTasks(PIN string, u ContactMethodUser) error {
|
||||||
|
err := d.ApplyRole(u.MethodID().(string))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(lm.FailedSetDiscordMemberRole, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordUser) Name() string { return RenderDiscordUsername(*d) }
|
||||||
|
func (d *DiscordUser) SetMethodID(id any) { d.ID = id.(string) }
|
||||||
|
func (d *DiscordUser) MethodID() any { return d.ID }
|
||||||
|
func (d *DiscordUser) SetJellyfin(id string) { d.JellyfinID = id }
|
||||||
|
func (d *DiscordUser) Jellyfin() string { return d.JellyfinID }
|
||||||
|
func (d *DiscordUser) SetAllowContactFromDTO(req newUserDTO) { d.Contact = req.DiscordContact }
|
||||||
|
func (d *DiscordUser) SetAllowContact(contact bool) { d.Contact = contact }
|
||||||
|
func (d *DiscordUser) AllowContact() bool { return d.Contact }
|
||||||
|
func (d *DiscordUser) Store(st *Storage) {
|
||||||
|
st.SetDiscordKey(d.Jellyfin(), *d)
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ func (app *appContext) clearActivities() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == badger.ErrTxnTooBig {
|
if err == badger.ErrTxnTooBig {
|
||||||
app.debug.Printf(lm.AcitivityLogTxnTooBig)
|
app.debug.Printf(lm.ActivityLogTxnTooBig)
|
||||||
list := []Activity{}
|
list := []Activity{}
|
||||||
if errorSource == 0 {
|
if errorSource == 0 {
|
||||||
app.storage.db.Find(&list, badgerhold.Where("Time").Lt(minAge))
|
app.storage.db.Find(&list, badgerhold.Where("Time").Lt(minAge))
|
||||||
|
0
images/jfa-go-icon.png
Executable file → Normal file
0
images/jfa-go-icon.png
Executable file → Normal file
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
0
images/jfa-go-icon.svg
Executable file → Normal file
0
images/jfa-go-icon.svg
Executable file → Normal file
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
@ -11,7 +11,7 @@ import (
|
|||||||
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||||
user, imported, err := app.js.GetOrImportUser(jfID)
|
user, imported, err := app.js.GetOrImportUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Printf(lm.FailedMustGetJellyseerrUser, jfID, err)
|
app.debug.Printf(lm.FailedImportUser, lm.Jellyseerr, jfID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if imported {
|
if imported {
|
||||||
|
@ -115,7 +115,7 @@ const (
|
|||||||
FailedGetJellyseerrNotificationPrefs = "Failed to get user \"%s\"'s notification prefs from " + Jellyseerr + ": %v"
|
FailedGetJellyseerrNotificationPrefs = "Failed to get user \"%s\"'s notification prefs from " + Jellyseerr + ": %v"
|
||||||
FailedSyncContactMethods = "Failed to sync contact methods with %s: %v"
|
FailedSyncContactMethods = "Failed to sync contact methods with %s: %v"
|
||||||
ImportJellyseerrUser = "Triggered import for " + Jellyseerr + " user \"%s\" (New ID: %d)"
|
ImportJellyseerrUser = "Triggered import for " + Jellyseerr + " user \"%s\" (New ID: %d)"
|
||||||
FailedMustGetJellyseerrUser = "Failed to get or trigger import for " + Jellyseerr + " user \"%s\": %v"
|
FailedImportUser = "Failed to get or trigger import for %s user \"%s\": %v"
|
||||||
|
|
||||||
// api-messages.go
|
// api-messages.go
|
||||||
FailedGetCustomMessage = "Failed to get custom message \"%s\""
|
FailedGetCustomMessage = "Failed to get custom message \"%s\""
|
||||||
|
44
main.go
44
main.go
@ -102,8 +102,9 @@ type appContext struct {
|
|||||||
// 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 *OmbiWrapper
|
||||||
js *jellyseerr.Jellyseerr
|
js *JellyseerrWrapper
|
||||||
|
thirdPartyServices []ThirdPartyService
|
||||||
datePattern string
|
datePattern string
|
||||||
timePattern string
|
timePattern string
|
||||||
storage Storage
|
storage Storage
|
||||||
@ -112,6 +113,7 @@ type appContext struct {
|
|||||||
telegram *TelegramDaemon
|
telegram *TelegramDaemon
|
||||||
discord *DiscordDaemon
|
discord *DiscordDaemon
|
||||||
matrix *MatrixDaemon
|
matrix *MatrixDaemon
|
||||||
|
contactMethods []ContactMethodLinker
|
||||||
info, debug, err *logger.Logger
|
info, debug, err *logger.Logger
|
||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
@ -349,27 +351,32 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
||||||
|
|
||||||
|
// NOTE: As of writing this, the order in app.thirdPartServices doesn't matter,
|
||||||
|
// but in future it might (like app.contactMethods does), so append to the end!
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
|
app.ombi = &OmbiWrapper{}
|
||||||
app.debug.Printf(lm.UsingOmbi)
|
app.debug.Printf(lm.UsingOmbi)
|
||||||
ombiServer := app.config.Section("ombi").Key("server").String()
|
ombiServer := app.config.Section("ombi").Key("server").String()
|
||||||
app.ombi = ombi.NewOmbi(
|
app.ombi.Ombi = ombi.NewOmbi(
|
||||||
ombiServer,
|
ombiServer,
|
||||||
app.config.Section("ombi").Key("api_key").String(),
|
app.config.Section("ombi").Key("api_key").String(),
|
||||||
common.NewTimeoutHandler("Ombi", ombiServer, true),
|
common.NewTimeoutHandler("Ombi", ombiServer, true),
|
||||||
)
|
)
|
||||||
|
app.thirdPartyServices = append(app.thirdPartyServices, app.ombi)
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||||
|
app.js = &JellyseerrWrapper{}
|
||||||
app.debug.Printf(lm.UsingJellyseerr)
|
app.debug.Printf(lm.UsingJellyseerr)
|
||||||
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
|
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
|
||||||
app.js = jellyseerr.NewJellyseerr(
|
app.js.Jellyseerr = jellyseerr.NewJellyseerr(
|
||||||
jellyseerrServer,
|
jellyseerrServer,
|
||||||
app.config.Section("jellyseerr").Key("api_key").String(),
|
app.config.Section("jellyseerr").Key("api_key").String(),
|
||||||
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
|
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
|
||||||
)
|
)
|
||||||
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
|
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
|
||||||
// app.js.LogRequestBodies = true
|
// app.js.LogRequestBodies = true
|
||||||
|
app.thirdPartyServices = append(app.thirdPartyServices, app.js)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -511,17 +518,8 @@ func start(asDaemon, firstCall bool) {
|
|||||||
defer backupDaemon.Shutdown()
|
defer backupDaemon.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
if telegramEnabled {
|
// NOTE: The order in which these are placed in app.contactMethods matters.
|
||||||
app.telegram, err = newTelegramDaemon(app)
|
// Add new ones to the end.
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedInitTelegram, err)
|
|
||||||
telegramEnabled = false
|
|
||||||
} else {
|
|
||||||
app.debug.Println(lm.InitTelegram)
|
|
||||||
go app.telegram.run()
|
|
||||||
defer app.telegram.Shutdown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if discordEnabled {
|
if discordEnabled {
|
||||||
app.discord, err = newDiscordDaemon(app)
|
app.discord, err = newDiscordDaemon(app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -531,6 +529,19 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.debug.Println(lm.InitDiscord)
|
app.debug.Println(lm.InitDiscord)
|
||||||
go app.discord.run()
|
go app.discord.run()
|
||||||
defer app.discord.Shutdown()
|
defer app.discord.Shutdown()
|
||||||
|
app.contactMethods = append(app.contactMethods, app.discord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if telegramEnabled {
|
||||||
|
app.telegram, err = newTelegramDaemon(app)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf(lm.FailedInitTelegram, err)
|
||||||
|
telegramEnabled = false
|
||||||
|
} else {
|
||||||
|
app.debug.Println(lm.InitTelegram)
|
||||||
|
go app.telegram.run()
|
||||||
|
defer app.telegram.Shutdown()
|
||||||
|
app.contactMethods = append(app.contactMethods, app.telegram)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if matrixEnabled {
|
if matrixEnabled {
|
||||||
@ -542,6 +553,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.debug.Println(lm.InitMatrix)
|
app.debug.Println(lm.InitMatrix)
|
||||||
go app.matrix.run()
|
go app.matrix.run()
|
||||||
defer app.matrix.Shutdown()
|
defer app.matrix.Shutdown()
|
||||||
|
app.contactMethods = append(app.contactMethods, app.matrix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
60
matrix.go
60
matrix.go
@ -32,15 +32,6 @@ type UnverifiedUser struct {
|
|||||||
User *MatrixUser
|
User *MatrixUser
|
||||||
}
|
}
|
||||||
|
|
||||||
type MatrixUser struct {
|
|
||||||
RoomID string
|
|
||||||
Encrypted bool
|
|
||||||
UserID string
|
|
||||||
Lang string
|
|
||||||
Contact bool
|
|
||||||
JellyfinID string `badgerhold:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var matrixFilter = mautrix.Filter{
|
var matrixFilter = mautrix.Filter{
|
||||||
Room: mautrix.RoomFilter{
|
Room: mautrix.RoomFilter{
|
||||||
Timeline: mautrix.FilterPart{
|
Timeline: mautrix.FilterPart{
|
||||||
@ -277,6 +268,57 @@ func (d *MatrixDaemon) UserExists(userID string) bool {
|
|||||||
return err != nil || c > 0
|
return err != nil || c > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exists returns whether or not the given user exists.
|
||||||
|
func (d *MatrixDaemon) Exists(user ContactMethodUser) bool {
|
||||||
|
return d.UserExists(user.Name())
|
||||||
|
}
|
||||||
|
|
||||||
// User enters ID on sign-up, a PIN is sent to them. They enter it on sign-up.
|
// User enters ID on sign-up, a PIN is sent to them. They enter it on sign-up.
|
||||||
|
|
||||||
// Message the user first, to avoid E2EE by default
|
// Message the user first, to avoid E2EE by default
|
||||||
|
|
||||||
|
func (d *MatrixDaemon) PIN(req newUserDTO) string { return req.MatrixPIN }
|
||||||
|
|
||||||
|
func (d *MatrixDaemon) Name() string { return lm.Matrix }
|
||||||
|
|
||||||
|
func (d *MatrixDaemon) Required() bool {
|
||||||
|
return d.app.config.Section("telegram").Key("required").MustBool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MatrixDaemon) UniqueRequired() bool {
|
||||||
|
return d.app.config.Section("telegram").Key("require_unique").MustBool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenVerified returns whether or not a token with the given PIN has been verified, and the token itself.
|
||||||
|
func (d *MatrixDaemon) TokenVerified(pin string) (token UnverifiedUser, ok bool) {
|
||||||
|
token, ok = d.tokens[pin]
|
||||||
|
// delete(t.verifiedTokens, pin)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteVerifiedToken removes the token with the given PIN.
|
||||||
|
func (d *MatrixDaemon) DeleteVerifiedToken(PIN string) {
|
||||||
|
delete(d.tokens, PIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MatrixDaemon) UserVerified(PIN string) (ContactMethodUser, bool) {
|
||||||
|
token, ok := d.TokenVerified(PIN)
|
||||||
|
if !ok {
|
||||||
|
return &MatrixUser{}, false
|
||||||
|
}
|
||||||
|
return token.User, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *MatrixDaemon) PostVerificationTasks(string, ContactMethodUser) error { return nil }
|
||||||
|
|
||||||
|
func (m *MatrixUser) Name() string { return m.UserID }
|
||||||
|
func (m *MatrixUser) SetMethodID(id any) { m.UserID = id.(string) }
|
||||||
|
func (m *MatrixUser) MethodID() any { return m.UserID }
|
||||||
|
func (m *MatrixUser) SetJellyfin(id string) { m.JellyfinID = id }
|
||||||
|
func (m *MatrixUser) Jellyfin() string { return m.JellyfinID }
|
||||||
|
func (m *MatrixUser) SetAllowContactFromDTO(req newUserDTO) { m.Contact = req.MatrixContact }
|
||||||
|
func (m *MatrixUser) SetAllowContact(contact bool) { m.Contact = contact }
|
||||||
|
func (m *MatrixUser) AllowContact() bool { return m.Contact }
|
||||||
|
func (m *MatrixUser) Store(st *Storage) {
|
||||||
|
st.SetMatrixKey(m.Jellyfin(), *m)
|
||||||
|
}
|
||||||
|
@ -31,7 +31,7 @@ type newUserDTO struct {
|
|||||||
|
|
||||||
type newUserResponse struct {
|
type newUserResponse struct {
|
||||||
User bool `json:"user" binding:"required"` // Whether user was created successfully
|
User bool `json:"user" binding:"required"` // Whether user was created successfully
|
||||||
Email bool `json:"email"` // Whether welcome email was successfully sent (always true if feature is disabled
|
Email bool `json:"email"` // Whether welcome email was successfully sent (always true if feature is disabled)
|
||||||
Error string `json:"error"` // Optional error message.
|
Error string `json:"error"` // Optional error message.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
router.GET(p+"/lang/:page/:file", app.ServeLang)
|
router.GET(p+"/lang/:page/:file", app.ServeLang)
|
||||||
router.GET(p+"/token/login", app.getTokenLogin)
|
router.GET(p+"/token/login", app.getTokenLogin)
|
||||||
router.GET(p+"/token/refresh", app.getTokenRefresh)
|
router.GET(p+"/token/refresh", app.getTokenRefresh)
|
||||||
router.POST(p+"/newUser", app.NewUser)
|
router.POST(p+"/newUser", app.NewUserFromInvite)
|
||||||
router.Use(static.Serve(p+"/invite/", app.webFS))
|
router.Use(static.Serve(p+"/invite/", app.webFS))
|
||||||
router.GET(p+"/invite/:invCode", app.InviteProxy)
|
router.GET(p+"/invite/:invCode", app.InviteProxy)
|
||||||
if app.config.Section("captcha").Key("enabled").MustBool(false) {
|
if app.config.Section("captcha").Key("enabled").MustBool(false) {
|
||||||
@ -182,7 +182,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
router.POST(p+"/logout", app.Logout)
|
router.POST(p+"/logout", app.Logout)
|
||||||
api.DELETE(p+"/users", app.DeleteUsers)
|
api.DELETE(p+"/users", app.DeleteUsers)
|
||||||
api.GET(p+"/users", app.GetUsers)
|
api.GET(p+"/users", app.GetUsers)
|
||||||
api.POST(p+"/users", app.NewUserAdmin)
|
api.POST(p+"/users", app.NewUserFromAdmin)
|
||||||
api.POST(p+"/users/extend", app.ExtendExpiry)
|
api.POST(p+"/users/extend", app.ExtendExpiry)
|
||||||
api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
|
api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
|
||||||
api.POST(p+"/users/enable", app.EnableDisableUsers)
|
api.POST(p+"/users/enable", app.EnableDisableUsers)
|
||||||
|
72
storage.go
72
storage.go
@ -509,6 +509,15 @@ func (st *Storage) GetDefaultProfile() Profile {
|
|||||||
return defaultProfile
|
return defaultProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustGetProfileKey returns the profile at key k, or if missing, the default profile.
|
||||||
|
func (st *Storage) MustGetProfileKey(k string) Profile {
|
||||||
|
p, ok := st.GetProfileKey(k)
|
||||||
|
if !ok {
|
||||||
|
p = st.GetDefaultProfile()
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
// GetCustomContent returns a copy of the store.
|
// GetCustomContent returns a copy of the store.
|
||||||
func (st *Storage) GetCustomContent() []CustomContent {
|
func (st *Storage) GetCustomContent() []CustomContent {
|
||||||
result := []CustomContent{}
|
result := []CustomContent{}
|
||||||
@ -584,12 +593,35 @@ func (st *Storage) DeleteActivityKey(k string) {
|
|||||||
st.db.Delete(k, Activity{})
|
st.db.Delete(k, Activity{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type TelegramUser struct {
|
type ThirdPartyService interface {
|
||||||
JellyfinID string `badgerhold:"key"`
|
// ok implies user imported, err can be any issue that occurs during
|
||||||
ChatID int64 `badgerhold:"index"`
|
ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool)
|
||||||
Username string `badgerhold:"index"`
|
AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error)
|
||||||
Lang string
|
Enabled(app *appContext, profile *Profile) bool
|
||||||
Contact bool // Whether to contact through telegram or not
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContactMethodLinker interface {
|
||||||
|
PIN(req newUserDTO) string
|
||||||
|
Name() string
|
||||||
|
Required() bool
|
||||||
|
UniqueRequired() bool
|
||||||
|
UserVerified(PIN string) (ContactMethodUser, bool)
|
||||||
|
PostVerificationTasks(PIN string, u ContactMethodUser) error
|
||||||
|
DeleteVerifiedToken(PIN string)
|
||||||
|
Exists(ContactMethodUser) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContactMethodUser interface {
|
||||||
|
SetMethodID(id any)
|
||||||
|
MethodID() any
|
||||||
|
Name() string
|
||||||
|
SetJellyfin(id string)
|
||||||
|
Jellyfin() string
|
||||||
|
SetAllowContactFromDTO(req newUserDTO)
|
||||||
|
SetAllowContact(contact bool)
|
||||||
|
AllowContact() bool
|
||||||
|
Store(st *Storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiscordUser struct {
|
type DiscordUser struct {
|
||||||
@ -602,6 +634,24 @@ type DiscordUser struct {
|
|||||||
JellyfinID string `json:"-" badgerhold:"key"`
|
JellyfinID string `json:"-" badgerhold:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TelegramUser struct {
|
||||||
|
TelegramVerifiedToken
|
||||||
|
JellyfinID string `badgerhold:"key"`
|
||||||
|
ChatID int64 `badgerhold:"index"`
|
||||||
|
Username string `badgerhold:"index"`
|
||||||
|
Lang string
|
||||||
|
Contact bool // Whether to contact through telegram or not
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatrixUser struct {
|
||||||
|
RoomID string
|
||||||
|
Encrypted bool
|
||||||
|
UserID string
|
||||||
|
Lang string
|
||||||
|
Contact bool
|
||||||
|
JellyfinID string `badgerhold:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
type EmailAddress struct {
|
type EmailAddress struct {
|
||||||
Addr string `badgerhold:"index"`
|
Addr string `badgerhold:"index"`
|
||||||
Label string // User Label.
|
Label string // User Label.
|
||||||
@ -686,6 +736,16 @@ type Invite struct {
|
|||||||
UseReferralExpiry bool `json:"use_referral_expiry"`
|
UseReferralExpiry bool `json:"use_referral_expiry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (invite Invite) Source() (ActivitySource, string) {
|
||||||
|
sourceType := ActivityAnon
|
||||||
|
source := ""
|
||||||
|
if invite.ReferrerJellyfinID != "" {
|
||||||
|
sourceType = ActivityUser
|
||||||
|
source = invite.ReferrerJellyfinID
|
||||||
|
}
|
||||||
|
return sourceType, source
|
||||||
|
}
|
||||||
|
|
||||||
type Captcha struct {
|
type Captcha struct {
|
||||||
Answer string
|
Answer string
|
||||||
Image []byte // image/png
|
Image []byte // image/png
|
||||||
|
62
telegram.go
62
telegram.go
@ -16,9 +16,27 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TelegramVerifiedToken struct {
|
type TelegramVerifiedToken struct {
|
||||||
ChatID int64
|
JellyfinID string `badgerhold:"key"` // optional, for ensuring a user-requested change is only accessed by them.
|
||||||
Username string
|
ChatID int64 `badgerhold:"index"`
|
||||||
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
|
Username string `badgerhold:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tv TelegramVerifiedToken) ToUser() *TelegramUser {
|
||||||
|
return &TelegramUser{
|
||||||
|
TelegramVerifiedToken: tv,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TelegramVerifiedToken) Name() string { return t.Username }
|
||||||
|
func (t *TelegramVerifiedToken) SetMethodID(id any) { t.ChatID = id.(int64) }
|
||||||
|
func (t *TelegramVerifiedToken) MethodID() any { return t.ChatID }
|
||||||
|
func (t *TelegramVerifiedToken) SetJellyfin(id string) { t.JellyfinID = id }
|
||||||
|
func (t *TelegramVerifiedToken) Jellyfin() string { return t.JellyfinID }
|
||||||
|
func (t *TelegramUser) SetAllowContactFromDTO(req newUserDTO) { t.Contact = req.TelegramContact }
|
||||||
|
func (t *TelegramUser) SetAllowContact(contact bool) { t.Contact = contact }
|
||||||
|
func (t *TelegramUser) AllowContact() bool { return t.Contact }
|
||||||
|
func (t *TelegramUser) Store(st *Storage) {
|
||||||
|
st.SetTelegramKey(t.Jellyfin(), *t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifToken stores details about a pending user verification token.
|
// VerifToken stores details about a pending user verification token.
|
||||||
@ -274,7 +292,39 @@ func (t *TelegramDaemon) UserExists(username string) bool {
|
|||||||
return err != nil || c > 0
|
return err != nil || c > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteVerifiedToken removes the token with the given PIN.
|
// Exists returns whether or not the given user exists.
|
||||||
func (t *TelegramDaemon) DeleteVerifiedToken(pin string) {
|
func (t *TelegramDaemon) Exists(user ContactMethodUser) bool {
|
||||||
delete(t.verifiedTokens, pin)
|
return t.UserExists(user.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteVerifiedToken removes the token with the given PIN.
|
||||||
|
func (t *TelegramDaemon) DeleteVerifiedToken(PIN string) {
|
||||||
|
delete(t.verifiedTokens, PIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TelegramDaemon) PIN(req newUserDTO) string { return req.TelegramPIN }
|
||||||
|
|
||||||
|
func (t *TelegramDaemon) Name() string { return lm.Telegram }
|
||||||
|
|
||||||
|
func (t *TelegramDaemon) Required() bool {
|
||||||
|
return t.app.config.Section("telegram").Key("required").MustBool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TelegramDaemon) UniqueRequired() bool {
|
||||||
|
return t.app.config.Section("telegram").Key("require_unique").MustBool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TelegramDaemon) UserVerified(PIN string) (ContactMethodUser, bool) {
|
||||||
|
token, ok := t.TokenVerified(PIN)
|
||||||
|
if !ok {
|
||||||
|
return &TelegramUser{}, false
|
||||||
|
}
|
||||||
|
tu := token.ToUser()
|
||||||
|
if lang, ok := t.languages[tu.ChatID]; ok {
|
||||||
|
tu.Lang = lang
|
||||||
|
}
|
||||||
|
|
||||||
|
return tu, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TelegramDaemon) PostVerificationTasks(string, ContactMethodUser) error { return nil }
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"lib": ["dom", "es2017"],
|
"lib": ["dom", "es2017"],
|
||||||
"typeRoots": ["./typings", "../node_modules/@types"],
|
"typeRoots": ["./typings", "../node_modules/@types"],
|
||||||
"moduleResolution": "nodenext",
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
180
users.go
Normal file
180
users.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
|
"github.com/hrfee/mediabrowser"
|
||||||
|
"github.com/lithammer/shortuuid/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReponseFunc responds to the user, generally by HTTP response
|
||||||
|
// The cases when more than this occurs are given below.
|
||||||
|
type ResponseFunc func(gc *gin.Context)
|
||||||
|
|
||||||
|
// LogFunc prints a log line once called.
|
||||||
|
type LogFunc func()
|
||||||
|
|
||||||
|
type ContactMethodConf struct {
|
||||||
|
Email, Discord, Telegram, Matrix bool
|
||||||
|
}
|
||||||
|
type ContactMethodUsers struct {
|
||||||
|
Email emailStore
|
||||||
|
Discord DiscordUser
|
||||||
|
Telegram TelegramVerifiedToken
|
||||||
|
Matrix MatrixUser
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContactMethodValidation struct {
|
||||||
|
Verified ContactMethodConf
|
||||||
|
Users ContactMethodUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewUserParams struct {
|
||||||
|
Req newUserDTO
|
||||||
|
SourceType ActivitySource
|
||||||
|
Source string
|
||||||
|
ContextForIPLogging *gin.Context
|
||||||
|
Profile *Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewUserData struct {
|
||||||
|
Created bool
|
||||||
|
Success bool
|
||||||
|
User mediabrowser.User
|
||||||
|
Message string
|
||||||
|
Status int
|
||||||
|
Log func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: First load of steps are going in NewUserFromInvite, because they're only used there.
|
||||||
|
// Make an interface{} for Require/Verify/ExistingUser which all contact daemons respect, then loop through!
|
||||||
|
/*
|
||||||
|
-- STEPS --
|
||||||
|
- Validate Invite
|
||||||
|
- Validate CAPTCHA
|
||||||
|
- Validate Password
|
||||||
|
- a) Discord (Require, Verify, ExistingUser, ApplyRole)
|
||||||
|
b) Telegram (Require, Verify, ExistingUser)
|
||||||
|
c) Matrix (Require, Verify, ExistingUser)
|
||||||
|
d) Email (Require, Verify, ExistingUser)
|
||||||
|
* Check for existing user
|
||||||
|
* Generate JF user
|
||||||
|
- Delete Invite
|
||||||
|
* Store Activity
|
||||||
|
* Store Email
|
||||||
|
- Store Discord/Telegram/Matrix/Label
|
||||||
|
- Notify Admin (Doesn't really matter when this happens)
|
||||||
|
* Apply Profile
|
||||||
|
* Generate JS, Ombi Users, apply profiles
|
||||||
|
* Send Welcome Email
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData) {
|
||||||
|
// Some helper functions which will behave as our app.info/error/debug
|
||||||
|
deferLogInfo := func(s string, args ...any) {
|
||||||
|
out.Log = func() {
|
||||||
|
app.info.Printf(s, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* deferLogDebug := func(s string, args ...any) {
|
||||||
|
out.Log = func() {
|
||||||
|
app.debug.Printf(s, args)
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
deferLogError := func(s string, args ...any) {
|
||||||
|
out.Log = func() {
|
||||||
|
app.err.Printf(s, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existingUser, _, _ := app.jf.UserByName(p.Req.Username, false)
|
||||||
|
if existingUser.Name != "" {
|
||||||
|
out.Message = lm.UserExists
|
||||||
|
deferLogInfo(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
||||||
|
out.Status = 401
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var status int
|
||||||
|
var err error
|
||||||
|
out.User, status, err = app.jf.NewUser(p.Req.Username, p.Req.Password)
|
||||||
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
|
out.Message = err.Error()
|
||||||
|
deferLogError(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
||||||
|
out.Status = 401
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.Created = true
|
||||||
|
// Invalidate Cache to be safe
|
||||||
|
app.jf.CacheExpiry = time.Now()
|
||||||
|
|
||||||
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
|
Type: ActivityCreation,
|
||||||
|
UserID: out.User.ID,
|
||||||
|
SourceType: p.SourceType,
|
||||||
|
Source: p.Source,
|
||||||
|
InviteCode: p.Req.Code, // Left blank when an admin does this
|
||||||
|
Value: out.User.Name,
|
||||||
|
Time: time.Now(),
|
||||||
|
}, p.ContextForIPLogging, (p.SourceType != ActivityAdmin))
|
||||||
|
|
||||||
|
if p.Profile != nil {
|
||||||
|
status, err = app.jf.SetPolicy(out.User.ID, p.Profile.Policy)
|
||||||
|
if !((status == 200 || status == 204) && err == nil) {
|
||||||
|
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, out.User.ID, err)
|
||||||
|
}
|
||||||
|
status, err = app.jf.SetConfiguration(out.User.ID, p.Profile.Configuration)
|
||||||
|
if (status == 200 || status == 204) && err == nil {
|
||||||
|
status, err = app.jf.SetDisplayPreferences(out.User.ID, p.Profile.Displayprefs)
|
||||||
|
}
|
||||||
|
if !((status == 200 || status == 204) && err == nil) {
|
||||||
|
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, out.User.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tps := range app.thirdPartyServices {
|
||||||
|
if !tps.Enabled(app, p.Profile) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// When ok and err != nil, its a non-fatal failure that we lot without the "FailedImportUser".
|
||||||
|
err, ok := tps.ImportUser(out.User.ID, p.Req, *p.Profile)
|
||||||
|
if !ok {
|
||||||
|
app.err.Printf(lm.FailedImportUser, tps.Name(), p.Req.Username, err)
|
||||||
|
} else if err != nil {
|
||||||
|
app.info.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Welcome email is sent by each user of this method separately..
|
||||||
|
|
||||||
|
out.Status = 200
|
||||||
|
out.Success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) WelcomeNewUser(user mediabrowser.User, expiry time.Time) (failed bool) {
|
||||||
|
if !app.config.Section("welcome_email").Key("enabled").MustBool(false) {
|
||||||
|
// we didn't "fail", we "politely declined"
|
||||||
|
// failed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
failed = true
|
||||||
|
name := app.getAddressOrName(user.ID)
|
||||||
|
if name == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg, err := app.email.constructWelcome(user.Name, expiry, app, false)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf(lm.FailedConstructWelcomeMessage, user.ID, err)
|
||||||
|
} else if err := app.sendByID(msg, user.ID); err != nil {
|
||||||
|
app.err.Printf(lm.FailedSendWelcomeMessage, user.ID, name, err)
|
||||||
|
} else {
|
||||||
|
app.info.Printf(lm.SentWelcomeMessage, user.ID, name)
|
||||||
|
failed = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
111
views.go
111
views.go
@ -237,7 +237,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
|
|||||||
"server_channel": app.discord.serverChannelName,
|
"server_channel": app.discord.serverChannelName,
|
||||||
}))
|
}))
|
||||||
data["discordServerName"] = app.discord.serverName
|
data["discordServerName"] = app.discord.serverName
|
||||||
data["discordInviteLink"] = app.discord.inviteChannelName != ""
|
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
|
||||||
}
|
}
|
||||||
if data["linkResetEnabled"].(bool) {
|
if data["linkResetEnabled"].(bool) {
|
||||||
data["resetPasswordUsername"] = app.config.Section("user_page").Key("allow_pwr_username").MustBool(true)
|
data["resetPasswordUsername"] = app.config.Section("user_page").Key("allow_pwr_username").MustBool(true)
|
||||||
@ -616,23 +616,7 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) InviteProxy(gc *gin.Context) {
|
func (app *appContext) NewUserFromConfirmationKey(invite Invite, key string, lang string, gc *gin.Context) {
|
||||||
app.pushResources(gc, FormPage)
|
|
||||||
code := gc.Param("invCode")
|
|
||||||
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.GetInvitesKey(code)
|
|
||||||
if !ok {
|
|
||||||
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
|
||||||
"urlBase": app.getURLBase(gc),
|
|
||||||
"cssClass": app.cssClass,
|
|
||||||
"cssVersion": cssVersion,
|
|
||||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
|
||||||
fail := func() {
|
fail := func() {
|
||||||
gcHTML(gc, 404, "404.html", gin.H{
|
gcHTML(gc, 404, "404.html", gin.H{
|
||||||
"urlBase": app.getURLBase(gc),
|
"urlBase": app.getURLBase(gc),
|
||||||
@ -647,7 +631,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
invKeys, ok := app.ConfirmationKeys[code]
|
invKeys, ok := app.ConfirmationKeys[invite.Code]
|
||||||
if !ok {
|
if !ok {
|
||||||
fail()
|
fail()
|
||||||
return
|
return
|
||||||
@ -665,18 +649,41 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
expiry := time.Unix(int64(claims["exp"].(float64)), 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())) {
|
if !(ok && token.Valid && claims["invite"].(string) == invite.Code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
||||||
fail()
|
fail()
|
||||||
app.debug.Printf(lm.InvalidJWT)
|
app.debug.Printf(lm.InvalidJWT)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f, success := app.newUser(req, true, gc)
|
|
||||||
if !success {
|
sourceType, source := invite.Source()
|
||||||
// Not meant for us. Calling this is bad but at least gives us log output.
|
|
||||||
f(gc)
|
var profile *Profile = nil
|
||||||
|
if invite.Profile != "" {
|
||||||
|
p, ok := app.storage.GetProfileKey(invite.Profile)
|
||||||
|
if !ok {
|
||||||
|
app.debug.Printf(lm.FailedGetProfile+lm.FallbackToDefault, invite.Profile)
|
||||||
|
p = app.storage.GetDefaultProfile()
|
||||||
|
}
|
||||||
|
profile = &p
|
||||||
|
}
|
||||||
|
|
||||||
|
nu := app.NewUserPostVerification(NewUserParams{
|
||||||
|
Req: req,
|
||||||
|
SourceType: sourceType,
|
||||||
|
Source: source,
|
||||||
|
ContextForIPLogging: gc,
|
||||||
|
Profile: profile,
|
||||||
|
})
|
||||||
|
if !nu.Success {
|
||||||
|
nu.Log()
|
||||||
|
}
|
||||||
|
if !nu.Created {
|
||||||
|
respond(nu.Status, nu.Message, gc)
|
||||||
fail()
|
fail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
app.checkInvite(req.Code, true, req.Username)
|
||||||
|
|
||||||
jfLink := app.config.Section("ui").Key("redirect_url").String()
|
jfLink := app.config.Section("ui").Key("redirect_url").String()
|
||||||
if app.config.Section("ui").Key("auto_redirect").MustBool(false) {
|
if app.config.Section("ui").Key("auto_redirect").MustBool(false) {
|
||||||
gc.Redirect(301, jfLink)
|
gc.Redirect(301, jfLink)
|
||||||
@ -691,16 +698,42 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
"jfLink": jfLink,
|
"jfLink": jfLink,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
delete(invKeys, key)
|
|
||||||
app.confirmationKeysLock.Lock()
|
app.confirmationKeysLock.Lock()
|
||||||
app.ConfirmationKeys[code] = invKeys
|
// Re-fetch invKeys just incase an update occurred
|
||||||
app.confirmationKeysLock.Unlock()
|
invKeys, ok = app.ConfirmationKeys[invite.Code]
|
||||||
|
if !ok {
|
||||||
|
fail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
email := ""
|
delete(invKeys, key)
|
||||||
if invite, ok := app.storage.GetInvitesKey(code); ok {
|
app.ConfirmationKeys[invite.Code] = invKeys
|
||||||
email = invite.SendTo
|
app.confirmationKeysLock.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||||
|
app.pushResources(gc, FormPage)
|
||||||
|
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, "") {
|
||||||
|
invite, ok := app.storage.GetInvitesKey(gc.Param("invCode"))
|
||||||
|
if !ok {
|
||||||
|
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
||||||
|
"urlBase": app.getURLBase(gc),
|
||||||
|
"cssClass": app.cssClass,
|
||||||
|
"cssVersion": cssVersion,
|
||||||
|
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
||||||
|
app.NewUserFromConfirmationKey(invite, key, lang, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
email := invite.SendTo
|
||||||
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
|
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
|
||||||
email = ""
|
email = ""
|
||||||
}
|
}
|
||||||
@ -715,8 +748,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
userPageAddress += "/my/account"
|
userPageAddress += "/my/account"
|
||||||
|
|
||||||
fromUser := ""
|
fromUser := ""
|
||||||
if inv.ReferrerJellyfinID != "" {
|
if invite.ReferrerJellyfinID != "" {
|
||||||
sender, status, err := app.jf.UserByID(inv.ReferrerJellyfinID, false)
|
sender, status, err := app.jf.UserByID(invite.ReferrerJellyfinID, false)
|
||||||
if status == 200 && err == nil {
|
if status == 200 && err == nil {
|
||||||
fromUser = sender.Name
|
fromUser = sender.Name
|
||||||
}
|
}
|
||||||
@ -738,13 +771,13 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
"strings": app.storage.lang.User[lang].Strings,
|
"strings": app.storage.lang.User[lang].Strings,
|
||||||
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
|
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
|
||||||
"notifications": app.storage.lang.User[lang].notificationsJSON,
|
"notifications": app.storage.lang.User[lang].notificationsJSON,
|
||||||
"code": code,
|
"code": invite.Code,
|
||||||
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
|
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
|
||||||
"userExpiry": inv.UserExpiry,
|
"userExpiry": invite.UserExpiry,
|
||||||
"userExpiryMonths": inv.UserMonths,
|
"userExpiryMonths": invite.UserMonths,
|
||||||
"userExpiryDays": inv.UserDays,
|
"userExpiryDays": invite.UserDays,
|
||||||
"userExpiryHours": inv.UserHours,
|
"userExpiryHours": invite.UserHours,
|
||||||
"userExpiryMinutes": inv.UserMinutes,
|
"userExpiryMinutes": invite.UserMinutes,
|
||||||
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
|
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
|
||||||
"langName": lang,
|
"langName": lang,
|
||||||
"passwordReset": false,
|
"passwordReset": false,
|
||||||
@ -779,7 +812,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
"server_channel": app.discord.serverChannelName,
|
"server_channel": app.discord.serverChannelName,
|
||||||
}))
|
}))
|
||||||
data["discordServerName"] = app.discord.serverName
|
data["discordServerName"] = app.discord.serverName
|
||||||
data["discordInviteLink"] = app.discord.inviteChannelName != ""
|
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
|
||||||
}
|
}
|
||||||
if msg, ok := app.storage.GetCustomContentKey("PostSignupCard"); ok && msg.Enabled {
|
if msg, ok := app.storage.GetCustomContentKey("PostSignupCard"); ok && msg.Enabled {
|
||||||
data["customSuccessCard"] = true
|
data["customSuccessCard"] = true
|
||||||
|
Loading…
Reference in New Issue
Block a user