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:
Harvey Tindall
2024-08-03 21:23:59 +01:00
parent 711394232b
commit 54e4a51a7f
20 changed files with 947 additions and 676 deletions
+224 -505
View File
@@ -23,7 +23,7 @@ import (
// @Router /users [post]
// @Security Bearer
// @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) {
resp := newUserResponse{
User: user,
@@ -35,263 +35,143 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
}
var req newUserDTO
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()
if req.Profile != "" && req.Profile != "none" {
if p, ok := app.storage.GetProfileKey(req.Profile); ok {
profile = p
} else {
app.debug.Printf(lm.FailedGetProfile+lm.FallbackToDefault, req.Profile)
}
nu := app.NewUserPostVerification(NewUserParams{
Req: req,
SourceType: ActivityAdmin,
Source: gc.GetString("jfId"),
ContextForIPLogging: gc,
Profile: &profile,
})
if !nu.Success {
nu.Log()
}
status, err = app.jf.SetPolicy(id, profile.Policy)
if !(status == 200 || status == 204 || err == nil) {
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, req.Username, 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, req.Username, err)
}
welcomeMessageSentIfNecessary := true
if nu.Created {
welcomeMessageSentIfNecessary = app.WelcomeNewUser(nu.User, time.Time{})
}
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)
respondUser(nu.Status, nu.Created, welcomeMessageSentIfNecessary, nu.Message, 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.
func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) (f errorFunc, success bool) {
existingUser, _, _ := app.jf.UserByName(req.Username, false)
if existingUser.Name != "" {
f = func(gc *gin.Context) {
msg := lm.UserExists
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg)
respond(401, "errorUserExists", gc)
}
success = false
// Validate CAPTCHA
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
}
var discordUser DiscordUser
discordVerified := false
if discordEnabled {
if req.DiscordPIN == "" {
if app.config.Section("discord").Key("required").MustBool(false) {
f = func(gc *gin.Context) {
app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, lm.AccountUnverified)
respond(401, "errorDiscordVerification", gc)
}
success = false
// Validate Invite
if !app.checkInvite(req.Code, false, "") {
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, fmt.Sprintf(lm.InvalidInviteCode, req.Code))
respond(401, "errorInvalidCode", gc)
return
}
// 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
}
} else {
discordUser, discordVerified = app.discord.UserVerified(req.DiscordPIN)
if !discordVerified {
f = func(gc *gin.Context) {
app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, fmt.Sprintf(lm.InvalidPIN, req.DiscordPIN))
respond(401, "errorInvalidPIN", gc)
}
success = false
completeContactMethods[i].User, completeContactMethods[i].Verified = cm.UserVerified(completeContactMethods[i].PIN)
if !completeContactMethods[i].Verified {
app.info.Printf(lm.FailedLinkUser, cm.Name(), "?", req.Code, fmt.Sprintf(lm.InvalidPIN, completeContactMethods[i].PIN))
respond(401, "errorInvalidPIN", gc)
return
}
if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(discordUser.ID) {
f = func(gc *gin.Context) {
app.debug.Printf(lm.FailedLinkUser, lm.Discord, discordUser.ID, req.Code, lm.AccountLinked)
respond(400, "errorAccountLinked", gc)
}
success = false
if cm.UniqueRequired() && cm.Exists(completeContactMethods[i].User) {
app.debug.Printf(lm.FailedLinkUser, cm.Name(), completeContactMethods[i].User.Name(), req.Code, lm.AccountLinked)
respond(400, "errorAccountLinked", gc)
return
}
err := app.discord.ApplyRole(discordUser.ID)
if err != nil {
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
return
if err := cm.PostVerificationTasks(completeContactMethods[i].PIN, completeContactMethods[i].User); err != nil {
app.err.Printf(lm.FailedLinkUser, cm.Name(), completeContactMethods[i].User.Name(), req.Code, err)
}
}
}
var matrixUser MatrixUser
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
}
} else {
user, ok := app.matrix.tokens[req.MatrixPIN]
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{
"valid": true,
"invite": req.Code,
"exp": time.Now().Add(30 * time.Minute).Unix(),
"type": "confirmation",
}
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
if err != nil {
f = func(gc *gin.Context) {
app.info.Printf(lm.FailedSignJWT, err)
respond(500, "errorUnknown", 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
}
if app.ConfirmationKeys == nil {
app.ConfirmationKeys = map[string]map[string]newUserDTO{}
// ExistingUser
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" && app.EmailAddressExists(req.Email) {
respond(400, "errorEmailLinked", gc)
return
}
cKeys, ok := app.ConfirmationKeys[req.Code]
if !ok {
cKeys = map[string]newUserDTO{}
}
cKeys[key] = req
app.confirmationKeysLock.Lock()
app.ConfirmationKeys[req.Code] = cKeys
app.confirmationKeysLock.Unlock()
f = func(gc *gin.Context) {
// Verify
if app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
claims := jwt.MapClaims{
"valid": true,
"invite": req.Code,
"exp": time.Now().Add(30 * time.Minute).Unix(),
"type": "confirmation",
}
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
if err != nil {
app.err.Printf(lm.FailedSignJWT, err)
respond(500, "errorUnknown", gc)
return
}
if app.ConfirmationKeys == nil {
app.ConfirmationKeys = map[string]map[string]newUserDTO{}
}
cKeys, ok := app.ConfirmationKeys[req.Code]
if !ok {
cKeys = map[string]newUserDTO{}
}
cKeys[key] = req
app.confirmationKeysLock.Lock()
app.ConfirmationKeys[req.Code] = cKeys
app.confirmationKeysLock.Unlock()
app.debug.Printf(lm.EmailConfirmationRequired, req.Username)
respond(401, "confirmEmail", gc)
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 {
app.err.Printf(lm.SentConfirmationEmail, req.Code, req.Email)
}
return
}
success = false
return
}
user, status, err := app.jf.NewUser(req.Username, req.Password)
if !(status == 200 || status == 204) || err != nil {
f = func(gc *gin.Context) {
app.err.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, err)
respond(401, app.storage.lang.Admin[app.storage.lang.chosenAdminLang].Notifications.get("errorUnknown"), gc)
invite, _ := app.storage.GetInvitesKey(req.Code)
sourceType, source := invite.Source()
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
}
invite, _ := app.storage.GetInvitesKey(req.Code)
app.checkInvite(req.Code, true, req.Username)
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) {
for address, settings := range invite.Notify {
if settings["notify-creation"] {
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 {
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) {
msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false)
if err != nil {
@@ -342,275 +273,62 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
}
}
}
id := user.ID
// Record activity
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{}
err = app.storage.db.Get(profile.ReferralTemplateKey, &refInv)
if refInv.UseReferralExpiry {
refInv.Code = GenerateInviteCode()
expiryDelta := refInv.ValidTill.Sub(refInv.Created)
refInv.Created = time.Now()
refInv.ValidTill = refInv.Created.Add(expiryDelta)
refInv.IsReferral = true
refInv.ReferrerJellyfinID = id
app.storage.SetInvitesKey(refInv.Code, refInv)
}
if referralsEnabled {
refInv := Invite{}
/*err := */ app.storage.db.Get(profile.ReferralTemplateKey, &refInv)
// If UseReferralExpiry is enabled, create the ref now so the clock starts ticking
if refInv.UseReferralExpiry {
refInv.Code = GenerateInviteCode()
expiryDelta := refInv.ValidTill.Sub(refInv.Created)
refInv.Created = time.Now()
refInv.ValidTill = refInv.Created.Add(expiryDelta)
refInv.IsReferral = true
refInv.ReferrerJellyfinID = nu.User.ID
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{}
if invite.UserExpiry {
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
// @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) NewUser(gc *gin.Context) {
var req newUserDTO
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, "") {
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, fmt.Sprintf(lm.InvalidInviteCode, req.Code))
respond(401, "errorInvalidCode", gc)
return
}
validation := app.validator.validate(req.Password)
valid := true
for _, val := range validation {
if !val {
valid = false
break
var discordUser *DiscordUser = nil
var telegramUser *TelegramUser = nil
if app.ombi.Enabled(app, profile) || app.js.Enabled(app, profile) {
// FIXME: figure these out in a nicer way? this relies on the current ordering,
// which may not be fixed.
if discordEnabled {
discordUser = completeContactMethods[0].User.(*DiscordUser)
if telegramEnabled {
telegramUser = completeContactMethods[1].User.(*TelegramUser)
}
} else if telegramEnabled {
telegramUser = completeContactMethods[0].User.(*TelegramUser)
}
}
if !valid {
// 200 bcs idk what i did in js
gc.JSON(200, validation)
return
}
if emailEnabled {
if app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") {
respond(400, "errorNoEmail", gc)
return
for _, tps := range app.thirdPartyServices {
if !tps.Enabled(app, profile) {
continue
}
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" && app.EmailAddressExists(req.Email) {
respond(400, "errorEmailLinked", gc)
return
// User already created, now we can link contact methods
err := tps.AddContactMethods(nu.User.ID, req, discordUser, telegramUser)
if err != nil {
app.err.Printf(lm.FailedSyncContactMethods, tps.Name(), err)
}
}
f, success := app.newUser(req, false, gc)
app.WelcomeNewUser(nu.User, expiry)
/*responseFunc, logFunc, success := app.NewUser(req, false, gc)
if !success {
f(gc)
logFunc()
responseFunc(gc)
return
}
}*/
code := 200
for _, val := range validation {
if !val {
@@ -954,7 +672,7 @@ func (app *appContext) Announce(gc *gin.Context) {
return
}
}
app.info.Printf(lm.SentAnnouncementMessage, userID, "?")
// app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
} else {
msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
if err != nil {
@@ -966,8 +684,9 @@ func (app *appContext) Announce(gc *gin.Context) {
respondBool(500, false, gc)
return
}
app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
// app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
}
app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
respondBool(200, true, gc)
}
@@ -1455,7 +1174,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
// newUser["userName"] = user["userName"]
// newUser["alias"] = user["alias"]
// newUser["emailAddress"] = user["emailAddress"]
status, err = app.applyOmbiProfile(user, ombi)
status, err = app.ombi.applyProfile(user, ombi)
if status != 200 || err != nil {
errorString += fmt.Sprintf("Apply %d: %v ", status, err)
}