2024-08-03 20:23:59 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-08-24 13:21:15 +00:00
|
|
|
"fmt"
|
|
|
|
"strings"
|
2024-08-20 20:33:43 +00:00
|
|
|
"sync"
|
2024-08-03 20:23:59 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2024-08-24 13:21:15 +00:00
|
|
|
"github.com/hrfee/jfa-go/logger"
|
2024-08-03 20:23:59 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2024-08-04 12:57:42 +00:00
|
|
|
// Called after a new-user-creating route has done pre-steps (veryfing contact methods for example).
|
2024-08-20 20:33:43 +00:00
|
|
|
func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData, pendingTasks *sync.WaitGroup) {
|
|
|
|
pendingTasks = &sync.WaitGroup{}
|
2024-08-03 20:23:59 +00:00
|
|
|
// Some helper functions which will behave as our app.info/error/debug
|
2024-08-24 13:21:15 +00:00
|
|
|
// And make sure we capture the correct caller location.
|
2024-08-03 20:23:59 +00:00
|
|
|
deferLogInfo := func(s string, args ...any) {
|
2024-08-24 13:21:15 +00:00
|
|
|
loc := logger.Lshortfile(2)
|
2024-08-03 20:23:59 +00:00
|
|
|
out.Log = func() {
|
2024-08-24 13:21:15 +00:00
|
|
|
app.info.PrintfNoFile(loc+" "+s, args...)
|
2024-08-03 20:23:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
/* deferLogDebug := func(s string, args ...any) {
|
|
|
|
out.Log = func() {
|
|
|
|
app.debug.Printf(s, args)
|
|
|
|
}
|
|
|
|
} */
|
|
|
|
deferLogError := func(s string, args ...any) {
|
2024-08-24 13:21:15 +00:00
|
|
|
loc := logger.Lshortfile(2)
|
2024-08-03 20:23:59 +00:00
|
|
|
out.Log = func() {
|
2024-08-24 13:21:15 +00:00
|
|
|
app.err.PrintfNoFile(loc+" "+s, args...)
|
2024-08-03 20:23:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-24 13:21:15 +00:00
|
|
|
if strings.ContainsRune(p.Req.Username, '+') {
|
|
|
|
deferLogError(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, fmt.Sprintf(lm.InvalidChar, '+'))
|
|
|
|
out.Status = 400
|
|
|
|
out.Message = "errorSpecialSymbols"
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-08-06 13:48:31 +00:00
|
|
|
existingUser, _ := app.jf.UserByName(p.Req.Username, false)
|
2024-08-03 20:23:59 +00:00
|
|
|
if existingUser.Name != "" {
|
|
|
|
out.Message = lm.UserExists
|
|
|
|
deferLogInfo(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
|
|
|
out.Status = 401
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
2024-08-06 13:48:31 +00:00
|
|
|
out.User, err = app.jf.NewUser(p.Req.Username, p.Req.Password)
|
|
|
|
if err != nil {
|
2024-08-03 20:23:59 +00:00
|
|
|
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 {
|
2024-08-06 13:48:31 +00:00
|
|
|
err = app.jf.SetPolicy(out.User.ID, p.Profile.Policy)
|
|
|
|
if err != nil {
|
2024-08-03 20:23:59 +00:00
|
|
|
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, out.User.ID, err)
|
|
|
|
}
|
2024-08-06 13:48:31 +00:00
|
|
|
err = app.jf.SetConfiguration(out.User.ID, p.Profile.Configuration)
|
|
|
|
if err == nil {
|
|
|
|
err = app.jf.SetDisplayPreferences(out.User.ID, p.Profile.Displayprefs)
|
2024-08-03 20:23:59 +00:00
|
|
|
}
|
2024-08-06 13:48:31 +00:00
|
|
|
if err != nil {
|
2024-08-03 20:23:59 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-20 20:33:43 +00:00
|
|
|
webhookURIs := app.config.Section("webhooks").Key("created").StringsWithShadows("|")
|
|
|
|
if len(webhookURIs) != 0 {
|
|
|
|
summary := app.userSummary(out.User)
|
|
|
|
for _, uri := range webhookURIs {
|
|
|
|
go func() {
|
|
|
|
pendingTasks.Add(1)
|
|
|
|
app.webhooks.Send(uri, summary)
|
|
|
|
pendingTasks.Done()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Welcome email is sent by each user of this method separately.
|
2024-08-03 20:23:59 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2024-08-04 14:14:51 +00:00
|
|
|
|
|
|
|
func (app *appContext) SetUserDisabled(user mediabrowser.User, disabled bool) (err error, change bool, activityType ActivityType) {
|
|
|
|
activityType = ActivityEnabled
|
|
|
|
if disabled {
|
|
|
|
activityType = ActivityDisabled
|
|
|
|
}
|
|
|
|
change = user.Policy.IsDisabled != disabled
|
|
|
|
user.Policy.IsDisabled = disabled
|
|
|
|
|
2024-08-06 13:48:31 +00:00
|
|
|
err = app.jf.SetPolicy(user.ID, user.Policy)
|
2024-08-04 14:14:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if app.discord != nil && app.config.Section("discord").Key("disable_enable_role").MustBool(false) {
|
2024-08-04 14:37:01 +00:00
|
|
|
cmUser, ok := app.storage.GetDiscordKey(user.ID)
|
|
|
|
if ok {
|
|
|
|
if err := app.discord.SetRoleDisabled(cmUser.MethodID().(string), disabled); err != nil {
|
|
|
|
app.err.Printf(lm.FailedSetDiscordMemberRole, err)
|
|
|
|
}
|
|
|
|
}
|
2024-08-04 14:14:51 +00:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bool) {
|
|
|
|
if app.ombi != nil {
|
|
|
|
var tpUser map[string]any
|
2024-08-06 13:48:31 +00:00
|
|
|
tpUser, err = app.getOmbiUser(user.ID)
|
|
|
|
if err == nil {
|
2024-08-04 14:14:51 +00:00
|
|
|
if id, ok := tpUser["id"]; ok {
|
2024-08-06 13:48:31 +00:00
|
|
|
err = app.ombi.DeleteUser(id.(string))
|
2024-08-04 14:14:51 +00:00
|
|
|
if err != nil {
|
|
|
|
app.err.Printf(lm.FailedDeleteUser, lm.Ombi, user.ID, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if app.discord != nil && app.config.Section("discord").Key("disable_enable_role").MustBool(false) {
|
2024-08-04 14:37:01 +00:00
|
|
|
cmUser, ok := app.storage.GetDiscordKey(user.ID)
|
|
|
|
if ok {
|
|
|
|
if err := app.discord.RemoveRole(cmUser.MethodID().(string)); err != nil {
|
|
|
|
app.err.Printf(lm.FailedSetDiscordMemberRole, err)
|
|
|
|
}
|
|
|
|
}
|
2024-08-04 14:14:51 +00:00
|
|
|
}
|
|
|
|
|
2024-08-06 13:48:31 +00:00
|
|
|
err = app.jf.DeleteUser(user.ID)
|
2024-08-04 14:14:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
deleted = true
|
|
|
|
return
|
|
|
|
}
|