1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-09-19 10:50:11 +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:
Harvey Tindall 2024-08-03 21:23:59 +01:00
parent 711394232b
commit 54e4a51a7f
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
20 changed files with 947 additions and 676 deletions

View File

@ -1,10 +1,12 @@
package main
import (
"fmt"
"net/url"
"strconv"
"github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/jellyseerr"
lm "github.com/hrfee/jfa-go/logmessages"
)
@ -97,3 +99,67 @@ func (app *appContext) DeleteJellyseerrProfile(gc *gin.Context) {
app.storage.SetProfileKey(profileName, profile)
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)
}

View File

@ -431,7 +431,7 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
pin := gc.Param("pin")
token, ok := app.telegram.TokenVerified(pin)
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)
return
}
@ -454,7 +454,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
}
pin := gc.Param("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)
respondBool(400, false, gc)
return
@ -472,7 +472,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
// @Router /invite/{invCode}/discord/invite [get]
// @tags Other
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
if app.discord.inviteChannelName == "" {
if app.discord.InviteChannel.Name == "" {
respondBool(400, false, gc)
return
}

View File

@ -1,19 +1,18 @@
package main
import (
"errors"
"fmt"
"net/url"
"strings"
"github.com/gin-gonic/gin"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/jfa-go/ombi"
"github.com/hrfee/mediabrowser"
)
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)
if err != nil || code != 200 {
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 {
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 {
ombiAddr := ""
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 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
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
ombiUsers, code, err := app.ombi.GetUsers()
ombiUsers, code, err := ombi.GetUsers()
if err != nil || code != 200 {
return nil, code, err
}
@ -136,7 +143,11 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
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 {
switch v.(type) {
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
}
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)
}

View File

@ -292,7 +292,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
// @Security Bearer
// @tags User Page
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
if app.discord.inviteChannelName == "" {
if app.discord.InviteChannel.Name == "" {
respondBool(400, false, gc)
return
}
@ -340,7 +340,7 @@ func (app *appContext) GetMyPIN(gc *gin.Context) {
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
pin := gc.Param("pin")
dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
app.discord.DeleteVerifiedUser(pin)
app.discord.DeleteVerifiedToken(pin)
if !ok {
respondBool(200, false, gc)
return

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)
}

View File

@ -11,21 +11,21 @@ import (
)
type DiscordDaemon struct {
Stopped bool
ShutdownChannel chan string
bot *dg.Session
username string
tokens map[string]VerifToken // Map of pins to tokens.
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
channelID, channelName, inviteChannelID, inviteChannelName string
guildID 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.
roleID string
app *appContext
commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string)
commandIDs []string
commandDescriptions []*dg.ApplicationCommand
Stopped bool
ShutdownChannel chan string
bot *dg.Session
username string
tokens map[string]VerifToken // Map of pins to tokens.
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
Channel, InviteChannel struct{ ID, Name string }
guildID 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.
roleID string
app *appContext
commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string)
commandIDs []string
commandDescriptions []*dg.ApplicationCommand
}
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
@ -120,12 +120,12 @@ func (d *DiscordDaemon) run() {
d.serverChannelName = guild.Name
d.serverName = guild.Name
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
d.channelName = channel
d.Channel.Name = channel
d.serverChannelName += "/" + channel
}
if d.app.config.Section("discord").Key("provide_invite").MustBool(false) {
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"))
@ -171,11 +171,11 @@ func (d *DiscordDaemon) ApplyRole(userID string) error {
func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) {
var inv *dg.Invite
var err error
if d.inviteChannelName == "" {
if d.InviteChannel.Name == "" {
d.app.err.Println(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty)
return
}
if d.inviteChannelID == "" {
if d.InviteChannel.ID == "" {
channels, err := d.bot.GuildChannels(d.guildID)
if err != nil {
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)
// return
// }
if channel.Name == d.inviteChannelName {
d.inviteChannelID = channel.ID
if channel.Name == d.InviteChannel.Name {
d.InviteChannel.ID = channel.ID
found = true
break
}
}
if !found {
d.app.err.Printf(lm.FailedGetDiscordChannel, d.InviteChannelName, lm.NotFound)
d.app.err.Printf(lm.FailedGetDiscordChannel, d.InviteChannel.Name, lm.NotFound)
return
}
}
@ -204,7 +204,7 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
// d.app.err.Printf(lm.FailedGetDiscordChannel, d.inviteChannelID, err)
// 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],
// Channel: channel,
// Inviter: d.bot.State.User,
@ -451,19 +451,19 @@ func (d *DiscordDaemon) UpdateCommands() {
func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok {
if i.GuildID != "" && d.channelName != "" {
if d.channelID == "" {
if i.GuildID != "" && d.Channel.Name != "" {
if d.Channel.ID == "" {
channel, err := s.Channel(i.ChannelID)
if err != nil {
d.app.err.Printf(lm.FailedGetDiscordChannel, i.ChannelID, err)
d.app.err.Println(lm.MonitorAllDiscordChannels)
d.channelName = ""
d.Channel.Name = ""
}
if channel.Name == d.channelName {
d.channelID = channel.ID
if channel.Name == d.Channel.Name {
d.Channel.ID = channel.ID
}
}
if d.channelID != i.ChannelID {
if d.Channel.ID != i.ChannelID {
d.app.debug.Printf(lm.IgnoreOutOfChannelMessage, lm.Discord)
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.
func (d *DiscordDaemon) UserVerified(pin string) (user DiscordUser, ok bool) {
user, ok = d.verifiedTokens[pin]
func (d *DiscordDaemon) UserVerified(pin string) (ContactMethodUser, bool) {
u, ok := 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.
@ -762,7 +762,44 @@ func (d *DiscordDaemon) UserExists(id string) bool {
return err != nil || c > 0
}
// DeleteVerifiedUser removes the token with the given PIN.
func (d *DiscordDaemon) DeleteVerifiedUser(pin string) {
delete(d.verifiedTokens, pin)
// Exists returns whether or not the given user exists.
func (d *DiscordDaemon) Exists(user ContactMethodUser) bool {
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)
}

View File

@ -104,7 +104,7 @@ func (app *appContext) clearActivities() {
}
}
if err == badger.ErrTxnTooBig {
app.debug.Printf(lm.AcitivityLogTxnTooBig)
app.debug.Printf(lm.ActivityLogTxnTooBig)
list := []Activity{}
if errorSource == 0 {
app.storage.db.Find(&list, badgerhold.Where("Time").Lt(minAge))

0
images/jfa-go-icon.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

0
images/jfa-go-icon.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -11,7 +11,7 @@ import (
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
user, imported, err := app.js.GetOrImportUser(jfID)
if err != nil {
app.debug.Printf(lm.FailedMustGetJellyseerrUser, jfID, err)
app.debug.Printf(lm.FailedImportUser, lm.Jellyseerr, jfID, err)
return
}
if imported {

View File

@ -115,7 +115,7 @@ const (
FailedGetJellyseerrNotificationPrefs = "Failed to get user \"%s\"'s notification prefs from " + Jellyseerr + ": %v"
FailedSyncContactMethods = "Failed to sync contact methods with %s: %v"
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
FailedGetCustomMessage = "Failed to get custom message \"%s\""

44
main.go
View File

@ -102,8 +102,9 @@ type appContext struct {
// Keeping jf name because I can't think of a better one
jf *mediabrowser.MediaBrowser
authJf *mediabrowser.MediaBrowser
ombi *ombi.Ombi
js *jellyseerr.Jellyseerr
ombi *OmbiWrapper
js *JellyseerrWrapper
thirdPartyServices []ThirdPartyService
datePattern string
timePattern string
storage Storage
@ -112,6 +113,7 @@ type appContext struct {
telegram *TelegramDaemon
discord *DiscordDaemon
matrix *MatrixDaemon
contactMethods []ContactMethodLinker
info, debug, err *logger.Logger
host string
port int
@ -349,27 +351,32 @@ func start(asDaemon, firstCall bool) {
}
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) {
app.ombi = &OmbiWrapper{}
app.debug.Printf(lm.UsingOmbi)
ombiServer := app.config.Section("ombi").Key("server").String()
app.ombi = ombi.NewOmbi(
app.ombi.Ombi = ombi.NewOmbi(
ombiServer,
app.config.Section("ombi").Key("api_key").String(),
common.NewTimeoutHandler("Ombi", ombiServer, true),
)
app.thirdPartyServices = append(app.thirdPartyServices, app.ombi)
}
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
app.js = &JellyseerrWrapper{}
app.debug.Printf(lm.UsingJellyseerr)
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
app.js = jellyseerr.NewJellyseerr(
app.js.Jellyseerr = jellyseerr.NewJellyseerr(
jellyseerrServer,
app.config.Section("jellyseerr").Key("api_key").String(),
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
)
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
// app.js.LogRequestBodies = true
app.thirdPartyServices = append(app.thirdPartyServices, app.js)
}
@ -511,17 +518,8 @@ func start(asDaemon, firstCall bool) {
defer backupDaemon.Shutdown()
}
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()
}
}
// NOTE: The order in which these are placed in app.contactMethods matters.
// Add new ones to the end.
if discordEnabled {
app.discord, err = newDiscordDaemon(app)
if err != nil {
@ -531,6 +529,19 @@ func start(asDaemon, firstCall bool) {
app.debug.Println(lm.InitDiscord)
go app.discord.run()
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 {
@ -542,6 +553,7 @@ func start(asDaemon, firstCall bool) {
app.debug.Println(lm.InitMatrix)
go app.matrix.run()
defer app.matrix.Shutdown()
app.contactMethods = append(app.contactMethods, app.matrix)
}
}
} else {

View File

@ -32,15 +32,6 @@ type UnverifiedUser struct {
User *MatrixUser
}
type MatrixUser struct {
RoomID string
Encrypted bool
UserID string
Lang string
Contact bool
JellyfinID string `badgerhold:"key"`
}
var matrixFilter = mautrix.Filter{
Room: mautrix.RoomFilter{
Timeline: mautrix.FilterPart{
@ -277,6 +268,57 @@ func (d *MatrixDaemon) UserExists(userID string) bool {
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.
// 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)
}

View File

@ -31,7 +31,7 @@ type newUserDTO struct {
type newUserResponse struct {
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.
}

View File

@ -135,7 +135,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.GET(p+"/lang/:page/:file", app.ServeLang)
router.GET(p+"/token/login", app.getTokenLogin)
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.GET(p+"/invite/:invCode", app.InviteProxy)
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)
api.DELETE(p+"/users", app.DeleteUsers)
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.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
api.POST(p+"/users/enable", app.EnableDisableUsers)

View File

@ -509,6 +509,15 @@ func (st *Storage) GetDefaultProfile() Profile {
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.
func (st *Storage) GetCustomContent() []CustomContent {
result := []CustomContent{}
@ -584,12 +593,35 @@ func (st *Storage) DeleteActivityKey(k string) {
st.db.Delete(k, Activity{})
}
type TelegramUser struct {
JellyfinID string `badgerhold:"key"`
ChatID int64 `badgerhold:"index"`
Username string `badgerhold:"index"`
Lang string
Contact bool // Whether to contact through telegram or not
type ThirdPartyService interface {
// ok implies user imported, err can be any issue that occurs during
ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool)
AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error)
Enabled(app *appContext, profile *Profile) bool
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 {
@ -602,6 +634,24 @@ type DiscordUser struct {
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 {
Addr string `badgerhold:"index"`
Label string // User Label.
@ -686,6 +736,16 @@ type Invite struct {
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 {
Answer string
Image []byte // image/png

View File

@ -16,9 +16,27 @@ const (
)
type TelegramVerifiedToken struct {
ChatID int64
Username string
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
JellyfinID string `badgerhold:"key"` // optional, for ensuring a user-requested change is only accessed by them.
ChatID int64 `badgerhold:"index"`
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.
@ -274,7 +292,39 @@ func (t *TelegramDaemon) UserExists(username string) bool {
return err != nil || c > 0
}
// DeleteVerifiedToken removes the token with the given PIN.
func (t *TelegramDaemon) DeleteVerifiedToken(pin string) {
delete(t.verifiedTokens, pin)
// Exists returns whether or not the given user exists.
func (t *TelegramDaemon) Exists(user ContactMethodUser) bool {
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 }

View File

@ -4,7 +4,8 @@
"target": "es2017",
"lib": ["dom", "es2017"],
"typeRoots": ["./typings", "../node_modules/@types"],
"moduleResolution": "nodenext",
"module": "esnext",
"moduleResolution": "bundler",
"esModuleInterop": true
}
}

180
users.go Normal file
View 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
}

189
views.go
View File

@ -237,7 +237,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
"server_channel": app.discord.serverChannelName,
}))
data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.inviteChannelName != ""
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
}
if data["linkResetEnabled"].(bool) {
data["resetPasswordUsername"] = app.config.Section("user_page").Key("allow_pwr_username").MustBool(true)
@ -616,13 +616,108 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
return
}
func (app *appContext) NewUserFromConfirmationKey(invite Invite, key string, lang string, gc *gin.Context) {
fail := func() {
gcHTML(gc, 404, "404.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
var req newUserDTO
if app.ConfirmationKeys == nil {
fail()
return
}
invKeys, ok := app.ConfirmationKeys[invite.Code]
if !ok {
fail()
return
}
req, ok = invKeys[key]
if !ok {
fail()
return
}
token, err := jwt.Parse(key, checkToken)
if err != nil {
fail()
app.debug.Printf(lm.FailedParseJWT, err)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
if !(ok && token.Valid && claims["invite"].(string) == invite.Code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
fail()
app.debug.Printf(lm.InvalidJWT)
return
}
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()
}
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()
return
}
app.checkInvite(req.Code, true, req.Username)
jfLink := app.config.Section("ui").Key("redirect_url").String()
if app.config.Section("ui").Key("auto_redirect").MustBool(false) {
gc.Redirect(301, jfLink)
} else {
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"strings": app.storage.lang.User[lang].Strings,
"successMessage": app.config.Section("ui").Key("success_message").String(),
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"jfLink": jfLink,
})
}
app.confirmationKeysLock.Lock()
// Re-fetch invKeys just incase an update occurred
invKeys, ok = app.ConfirmationKeys[invite.Code]
if !ok {
fail()
return
}
delete(invKeys, key)
app.ConfirmationKeys[invite.Code] = invKeys
app.confirmationKeysLock.Unlock()
return
}
func (app *appContext) InviteProxy(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)
invite, ok := app.storage.GetInvitesKey(gc.Param("invCode"))
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
@ -632,75 +727,13 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
})
return
}
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
fail := func() {
gcHTML(gc, 404, "404.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
var req newUserDTO
if app.ConfirmationKeys == nil {
fail()
return
}
invKeys, ok := app.ConfirmationKeys[code]
if !ok {
fail()
return
}
req, ok = invKeys[key]
if !ok {
fail()
return
}
token, err := jwt.Parse(key, checkToken)
if err != nil {
fail()
app.debug.Printf(lm.FailedParseJWT, err)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
fail()
app.debug.Printf(lm.InvalidJWT)
return
}
f, success := app.newUser(req, true, gc)
if !success {
// Not meant for us. Calling this is bad but at least gives us log output.
f(gc)
fail()
return
}
jfLink := app.config.Section("ui").Key("redirect_url").String()
if app.config.Section("ui").Key("auto_redirect").MustBool(false) {
gc.Redirect(301, jfLink)
} else {
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"strings": app.storage.lang.User[lang].Strings,
"successMessage": app.config.Section("ui").Key("success_message").String(),
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"jfLink": jfLink,
})
}
delete(invKeys, key)
app.confirmationKeysLock.Lock()
app.ConfirmationKeys[code] = invKeys
app.confirmationKeysLock.Unlock()
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
app.NewUserFromConfirmationKey(invite, key, lang, gc)
return
}
email := ""
if invite, ok := app.storage.GetInvitesKey(code); ok {
email = invite.SendTo
}
email := invite.SendTo
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
email = ""
}
@ -715,8 +748,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
userPageAddress += "/my/account"
fromUser := ""
if inv.ReferrerJellyfinID != "" {
sender, status, err := app.jf.UserByID(inv.ReferrerJellyfinID, false)
if invite.ReferrerJellyfinID != "" {
sender, status, err := app.jf.UserByID(invite.ReferrerJellyfinID, false)
if status == 200 && err == nil {
fromUser = sender.Name
}
@ -738,13 +771,13 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"strings": app.storage.lang.User[lang].Strings,
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
"notifications": app.storage.lang.User[lang].notificationsJSON,
"code": code,
"code": invite.Code,
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
"userExpiry": inv.UserExpiry,
"userExpiryMonths": inv.UserMonths,
"userExpiryDays": inv.UserDays,
"userExpiryHours": inv.UserHours,
"userExpiryMinutes": inv.UserMinutes,
"userExpiry": invite.UserExpiry,
"userExpiryMonths": invite.UserMonths,
"userExpiryDays": invite.UserDays,
"userExpiryHours": invite.UserHours,
"userExpiryMinutes": invite.UserMinutes,
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang,
"passwordReset": false,
@ -779,7 +812,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"server_channel": app.discord.serverChannelName,
}))
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 {
data["customSuccessCard"] = true