mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-11-09 20:00:12 +00:00
users: huge cleanup/dedupe, interface-based third-party services
shared "newUser" method is now "NewUserPostVerification", and is shared between all routes which create a jellyfin account. The new "NewUserFromInvite", "NewUserFromAdmin" and "NewUserFromConfirmationKey" are smaller as a result. Discord, Telegram, and Matrix now implement the "ContactMethodLinker" and "ContactMethodUser" interfaces, meaning code is shared a lot between them in the NewUser methods, and the specifics are now in their own files. Ombi/Jellyseerr similarly implement a simpler interface "ThirdPartyService", which simply has ImportUser and AddContactMethod routes. Note these new interface methods are only used for user creation as of yet, but could likely be used in other places.
This commit is contained in:
parent
711394232b
commit
54e4a51a7f
@ -1,10 +1,12 @@
|
||||
package main
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
89
api-ombi.go
89
api-ombi.go
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
729
api-users.go
729
api-users.go
@ -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)
|
||||
}
|
||||
|
107
discord.go
107
discord.go
@ -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)
|
||||
}
|
||||
|
@ -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
0
images/jfa-go-icon.png
Executable file → Normal file
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
0
images/jfa-go-icon.svg
Executable file → Normal file
0
images/jfa-go-icon.svg
Executable file → Normal file
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
@ -11,7 +11,7 @@ import (
|
||||
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||
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 {
|
||||
|
@ -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
44
main.go
@ -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 {
|
||||
|
60
matrix.go
60
matrix.go
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
72
storage.go
72
storage.go
@ -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
|
||||
|
62
telegram.go
62
telegram.go
@ -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 }
|
||||
|
@ -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
180
users.go
Normal file
@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
)
|
||||
|
||||
// ReponseFunc responds to the user, generally by HTTP response
|
||||
// The cases when more than this occurs are given below.
|
||||
type ResponseFunc func(gc *gin.Context)
|
||||
|
||||
// LogFunc prints a log line once called.
|
||||
type LogFunc func()
|
||||
|
||||
type ContactMethodConf struct {
|
||||
Email, Discord, Telegram, Matrix bool
|
||||
}
|
||||
type ContactMethodUsers struct {
|
||||
Email emailStore
|
||||
Discord DiscordUser
|
||||
Telegram TelegramVerifiedToken
|
||||
Matrix MatrixUser
|
||||
}
|
||||
|
||||
type ContactMethodValidation struct {
|
||||
Verified ContactMethodConf
|
||||
Users ContactMethodUsers
|
||||
}
|
||||
|
||||
type NewUserParams struct {
|
||||
Req newUserDTO
|
||||
SourceType ActivitySource
|
||||
Source string
|
||||
ContextForIPLogging *gin.Context
|
||||
Profile *Profile
|
||||
}
|
||||
|
||||
type NewUserData struct {
|
||||
Created bool
|
||||
Success bool
|
||||
User mediabrowser.User
|
||||
Message string
|
||||
Status int
|
||||
Log func()
|
||||
}
|
||||
|
||||
// FIXME: First load of steps are going in NewUserFromInvite, because they're only used there.
|
||||
// Make an interface{} for Require/Verify/ExistingUser which all contact daemons respect, then loop through!
|
||||
/*
|
||||
-- STEPS --
|
||||
- Validate Invite
|
||||
- Validate CAPTCHA
|
||||
- Validate Password
|
||||
- a) Discord (Require, Verify, ExistingUser, ApplyRole)
|
||||
b) Telegram (Require, Verify, ExistingUser)
|
||||
c) Matrix (Require, Verify, ExistingUser)
|
||||
d) Email (Require, Verify, ExistingUser)
|
||||
* Check for existing user
|
||||
* Generate JF user
|
||||
- Delete Invite
|
||||
* Store Activity
|
||||
* Store Email
|
||||
- Store Discord/Telegram/Matrix/Label
|
||||
- Notify Admin (Doesn't really matter when this happens)
|
||||
* Apply Profile
|
||||
* Generate JS, Ombi Users, apply profiles
|
||||
* Send Welcome Email
|
||||
|
||||
|
||||
*/
|
||||
|
||||
func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData) {
|
||||
// Some helper functions which will behave as our app.info/error/debug
|
||||
deferLogInfo := func(s string, args ...any) {
|
||||
out.Log = func() {
|
||||
app.info.Printf(s, args)
|
||||
}
|
||||
}
|
||||
/* deferLogDebug := func(s string, args ...any) {
|
||||
out.Log = func() {
|
||||
app.debug.Printf(s, args)
|
||||
}
|
||||
} */
|
||||
deferLogError := func(s string, args ...any) {
|
||||
out.Log = func() {
|
||||
app.err.Printf(s, args)
|
||||
}
|
||||
}
|
||||
|
||||
existingUser, _, _ := app.jf.UserByName(p.Req.Username, false)
|
||||
if existingUser.Name != "" {
|
||||
out.Message = lm.UserExists
|
||||
deferLogInfo(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
||||
out.Status = 401
|
||||
return
|
||||
}
|
||||
|
||||
var status int
|
||||
var err error
|
||||
out.User, status, err = app.jf.NewUser(p.Req.Username, p.Req.Password)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
out.Message = err.Error()
|
||||
deferLogError(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
||||
out.Status = 401
|
||||
return
|
||||
}
|
||||
out.Created = true
|
||||
// Invalidate Cache to be safe
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityCreation,
|
||||
UserID: out.User.ID,
|
||||
SourceType: p.SourceType,
|
||||
Source: p.Source,
|
||||
InviteCode: p.Req.Code, // Left blank when an admin does this
|
||||
Value: out.User.Name,
|
||||
Time: time.Now(),
|
||||
}, p.ContextForIPLogging, (p.SourceType != ActivityAdmin))
|
||||
|
||||
if p.Profile != nil {
|
||||
status, err = app.jf.SetPolicy(out.User.ID, p.Profile.Policy)
|
||||
if !((status == 200 || status == 204) && err == nil) {
|
||||
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, out.User.ID, err)
|
||||
}
|
||||
status, err = app.jf.SetConfiguration(out.User.ID, p.Profile.Configuration)
|
||||
if (status == 200 || status == 204) && err == nil {
|
||||
status, err = app.jf.SetDisplayPreferences(out.User.ID, p.Profile.Displayprefs)
|
||||
}
|
||||
if !((status == 200 || status == 204) && err == nil) {
|
||||
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, out.User.ID, err)
|
||||
}
|
||||
|
||||
for _, tps := range app.thirdPartyServices {
|
||||
if !tps.Enabled(app, p.Profile) {
|
||||
continue
|
||||
}
|
||||
// When ok and err != nil, its a non-fatal failure that we lot without the "FailedImportUser".
|
||||
err, ok := tps.ImportUser(out.User.ID, p.Req, *p.Profile)
|
||||
if !ok {
|
||||
app.err.Printf(lm.FailedImportUser, tps.Name(), p.Req.Username, err)
|
||||
} else if err != nil {
|
||||
app.info.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Welcome email is sent by each user of this method separately..
|
||||
|
||||
out.Status = 200
|
||||
out.Success = true
|
||||
return
|
||||
}
|
||||
|
||||
func (app *appContext) WelcomeNewUser(user mediabrowser.User, expiry time.Time) (failed bool) {
|
||||
if !app.config.Section("welcome_email").Key("enabled").MustBool(false) {
|
||||
// we didn't "fail", we "politely declined"
|
||||
// failed = true
|
||||
return
|
||||
}
|
||||
failed = true
|
||||
name := app.getAddressOrName(user.ID)
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
msg, err := app.email.constructWelcome(user.Name, expiry, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedConstructWelcomeMessage, user.ID, err)
|
||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
||||
app.err.Printf(lm.FailedSendWelcomeMessage, user.ID, name, err)
|
||||
} else {
|
||||
app.info.Printf(lm.SentWelcomeMessage, user.ID, name)
|
||||
failed = false
|
||||
}
|
||||
return
|
||||
}
|
189
views.go
189
views.go
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user