1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-09-19 19:00:11 +00:00
jfa-go/api-userpage.go
Harvey Tindall 54e4a51a7f
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.
2024-08-03 21:27:46 +01:00

809 lines
24 KiB
Go

package main
import (
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/hrfee/jfa-go/jellyseerr"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
)
const (
REFERRAL_EXPIRY_DAYS = 90
)
// @Summary Returns the logged-in user's Jellyfin ID & Username, and other details.
// @Produce json
// @Success 200 {object} MyDetailsDTO
// @Router /my/details [get]
// @tags User Page
func (app *appContext) MyDetails(gc *gin.Context) {
resp := MyDetailsDTO{
Id: gc.GetString("jfId"),
}
user, status, err := app.jf.UserByID(resp.Id, false)
if status != 200 || err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Failed to get user", gc)
return
}
resp.Username = user.Name
resp.Admin = user.Policy.IsAdministrator
resp.AccountsAdmin = false
if !app.config.Section("ui").Key("allow_all").MustBool(false) {
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
if emailStore, ok := app.storage.GetEmailsKey(resp.Id); ok {
resp.AccountsAdmin = emailStore.Admin
}
resp.AccountsAdmin = resp.AccountsAdmin || (adminOnly && resp.Admin)
}
resp.Disabled = user.Policy.IsDisabled
if exp, ok := app.storage.GetUserExpiryKey(user.ID); ok {
resp.Expiry = exp.Expiry.Unix()
}
if emailEnabled {
resp.Email = &MyDetailsContactMethodsDTO{}
if email, ok := app.storage.GetEmailsKey(user.ID); ok && email.Addr != "" {
resp.Email.Value = email.Addr
resp.Email.Enabled = email.Contact
}
}
if discordEnabled {
resp.Discord = &MyDetailsContactMethodsDTO{}
if discord, ok := app.storage.GetDiscordKey(user.ID); ok {
resp.Discord.Value = RenderDiscordUsername(discord)
resp.Discord.Enabled = discord.Contact
}
}
if telegramEnabled {
resp.Telegram = &MyDetailsContactMethodsDTO{}
if telegram, ok := app.storage.GetTelegramKey(user.ID); ok {
resp.Telegram.Value = telegram.Username
resp.Telegram.Enabled = telegram.Contact
}
}
if matrixEnabled {
resp.Matrix = &MyDetailsContactMethodsDTO{}
if matrix, ok := app.storage.GetMatrixKey(user.ID); ok {
resp.Matrix.Value = matrix.UserID
resp.Matrix.Enabled = matrix.Contact
}
}
if app.config.Section("user_page").Key("referrals").MustBool(false) {
// 1. Look for existing template bound to this Jellyfin ID
// If one exists, that means its just for us and so we
// can use it directly.
inv := Invite{}
err := app.storage.db.FindOne(&inv, badgerhold.Where("ReferrerJellyfinID").Eq(resp.Id))
if err == nil {
resp.HasReferrals = true
} else {
// 2. Look for a template matching the key found in the user storage
// Since this key is shared between users in a profile, we make a copy.
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
err = app.storage.db.Get(user.ReferralTemplateKey, &inv)
if ok && err == nil {
resp.HasReferrals = true
}
}
}
gc.JSON(200, resp)
}
// @Summary Sets whether to notify yourself through telegram/discord/matrix/email or not.
// @Produce json
// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
// @Success 200 {object} boolResponse
// @Success 400 {object} boolResponse
// @Success 500 {object} boolResponse
// @Router /my/contact [post]
// @Security Bearer
// @tags User Page
func (app *appContext) SetMyContactMethods(gc *gin.Context) {
var req SetContactMethodsDTO
gc.BindJSON(&req)
req.ID = gc.GetString("jfId")
if req.ID == "" {
respondBool(400, false, gc)
return
}
app.setContactMethods(req, gc)
}
// @Summary Logout by deleting refresh token from cookies.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /my/logout [post]
// @Security Bearer
// @tags User Page
func (app *appContext) LogoutUser(gc *gin.Context) {
cookie, err := gc.Cookie("user-refresh")
if err != nil {
msg := fmt.Sprintf(lm.FailedGetCookies, "user-refresh", err)
app.debug.Println(msg)
respond(500, msg, gc)
return
}
app.invalidTokens = append(app.invalidTokens, cookie)
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
respondBool(200, true, gc)
}
// @Summary confirm an action (e.g. changing an email address.)
// @Produce json
// @Param jwt path string true "jwt confirmation code"
// @Router /my/confirm/{jwt} [post]
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 404
// @Success 303
// @Failure 500 {object} stringResponse
// @tags User Page
func (app *appContext) ConfirmMyAction(gc *gin.Context) {
app.confirmMyAction(gc, "")
}
func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
var claims jwt.MapClaims
var target ConfirmationTarget
var id string
fail := func() {
gcHTML(gc, 404, "404.html", gin.H{
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
// Validate key
if key == "" {
key = gc.Param("jwt")
}
token, err := jwt.Parse(key, checkToken)
if err != nil {
app.err.Printf(lm.FailedParseJWT, err)
fail()
// respond(500, "unknownError", gc)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
app.err.Println(lm.FailedCastJWT)
fail()
// respond(500, "unknownError", gc)
return
}
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
if !(ok && token.Valid && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
app.err.Println(lm.InvalidJWT)
fail()
// respond(400, "invalidKey", gc)
return
}
target = ConfirmationTarget(int(claims["target"].(float64)))
id = claims["id"].(string)
// Perform an Action
if target == NoOp {
gc.Redirect(http.StatusSeeOther, "/my/account")
return
} else if target == UserEmailChange {
app.modifyEmail(id, claims["email"].(string))
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked,
UserID: gc.GetString("jfId"),
SourceType: ActivityUser,
Source: gc.GetString("jfId"),
Value: "email",
Time: time.Now(),
}, gc, true)
app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId"))
gc.Redirect(http.StatusSeeOther, "/my/account")
return
}
}
// @Summary Modify your email address.
// @Produce json
// @Param ModifyMyEmailDTO body ModifyMyEmailDTO true "New email address."
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 401 {object} stringResponse
// @Failure 500 {object} stringResponse
// @Router /my/email [post]
// @Security Bearer
// @tags User Page
func (app *appContext) ModifyMyEmail(gc *gin.Context) {
var req ModifyMyEmailDTO
gc.BindJSON(&req)
if !strings.ContainsRune(req.Email, '@') {
respond(400, "Invalid Email Address", gc)
return
}
id := gc.GetString("jfId")
// We'll use the ConfirmMyAction route to do the work, even if we don't need to confirm the address.
claims := jwt.MapClaims{
"valid": true,
"id": id,
"email": req.Email,
"type": "confirmation",
"target": UserEmailChange,
"exp": time.Now().Add(time.Hour).Unix(),
}
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 emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
user, status, err := app.jf.UserByID(id, false)
name := ""
if status == 200 && err == nil {
name = user.Name
}
app.debug.Printf(lm.EmailConfirmationRequired, id)
respond(401, "confirmEmail", gc)
msg, err := app.email.constructConfirmation("", name, key, app, false)
if err != nil {
app.err.Printf(lm.FailedConstructConfirmationEmail, id, err)
} else if err := app.email.send(msg, req.Email); err != nil {
app.err.Printf(lm.FailedSendConfirmationEmail, id, req.Email, err)
} else {
app.err.Printf(lm.SentConfirmationEmail, id, req.Email)
}
return
}
app.confirmMyAction(gc, key)
return
}
// @Summary Returns a 10-minute, one-use Discord server invite
// @Produce json
// @Success 200 {object} DiscordInviteDTO
// @Failure 400 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param invCode path string true "invite Code"
// @Router /my/discord/invite [get]
// @Security Bearer
// @tags User Page
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
if app.discord.InviteChannel.Name == "" {
respondBool(400, false, gc)
return
}
invURL, iconURL := app.discord.NewTempInvite(10*60, 1)
if invURL == "" {
respondBool(500, false, gc)
return
}
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
}
// @Summary Returns a linking PIN for discord/telegram
// @Produce json
// @Success 200 {object} GetMyPINDTO
// @Failure 400 {object} stringResponse
// Param service path string true "discord/telegram"
// @Router /my/pin/{service} [get]
// @Security Bearer
// @tags User Page
func (app *appContext) GetMyPIN(gc *gin.Context) {
service := gc.Param("service")
resp := GetMyPINDTO{}
switch service {
case "discord":
resp.PIN = app.discord.NewAssignedAuthToken(gc.GetString("jfId"))
break
case "telegram":
resp.PIN = app.telegram.NewAssignedAuthToken(gc.GetString("jfId"))
break
default:
respond(400, "invalid service", gc)
return
}
gc.JSON(200, resp)
}
// @Summary Returns true/false on whether or not your discord PIN was verified, and assigns the discord user to you.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Param pin path string true "PIN code to check"
// @Router /my/discord/verified/{pin} [get]
// @Security Bearer
// @tags User Page
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
pin := gc.Param("pin")
dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
app.discord.DeleteVerifiedToken(pin)
if !ok {
respondBool(200, false, gc)
return
}
if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(dcUser.ID) {
respondBool(400, false, gc)
return
}
existingUser, ok := app.storage.GetDiscordKey(gc.GetString("jfId"))
if ok {
dcUser.Lang = existingUser.Lang
dcUser.Contact = existingUser.Contact
}
app.storage.SetDiscordKey(gc.GetString("jfId"), dcUser)
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldDiscord: dcUser.ID,
jellyseerr.FieldDiscordEnabled: dcUser.Contact,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked,
UserID: gc.GetString("jfId"),
SourceType: ActivityUser,
Source: gc.GetString("jfId"),
Value: "discord",
Time: time.Now(),
}, gc, true)
respondBool(200, true, gc)
}
// @Summary Returns true/false on whether or not your telegram PIN was verified, and assigns the telegram user to you.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Param pin path string true "PIN code to check"
// @Router /my/telegram/verified/{pin} [get]
// @Security Bearer
// @tags User Page
func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
pin := gc.Param("pin")
token, ok := app.telegram.AssignedTokenVerified(pin, gc.GetString("jfId"))
app.telegram.DeleteVerifiedToken(pin)
if !ok {
respondBool(200, false, gc)
return
}
if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
respondBool(400, false, gc)
return
}
tgUser := TelegramUser{
ChatID: token.ChatID,
Username: token.Username,
Contact: true,
}
if lang, ok := app.telegram.languages[tgUser.ChatID]; ok {
tgUser.Lang = lang
}
existingUser, ok := app.storage.GetTelegramKey(gc.GetString("jfId"))
if ok {
tgUser.Lang = existingUser.Lang
tgUser.Contact = existingUser.Contact
}
app.storage.SetTelegramKey(gc.GetString("jfId"), tgUser)
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldTelegram: tgUser.ChatID,
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked,
UserID: gc.GetString("jfId"),
SourceType: ActivityUser,
Source: gc.GetString("jfId"),
Value: "telegram",
Time: time.Now(),
}, gc, true)
respondBool(200, true, gc)
}
// @Summary Generate and send a new PIN to your given matrix user.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
// @Router /my/matrix/user [post]
// @Security Bearer
// @tags User Page
func (app *appContext) MatrixSendMyPIN(gc *gin.Context) {
var req MatrixSendPINDTO
gc.BindJSON(&req)
if req.UserID == "" {
respond(400, "errorNoUserID", gc)
return
}
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
for _, u := range app.storage.GetMatrix() {
if req.UserID == u.UserID {
respondBool(400, false, gc)
return
}
}
}
ok := app.matrix.SendStart(req.UserID)
if !ok {
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Check whether your matrix PIN is valid, and link the account to yours if so.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Param pin path string true "PIN code to check"
// @Param invCode path string true "invite Code"
// @Param userID path string true "Matrix User ID"
// @Router /my/matrix/verified/{userID}/{pin} [get]
// @Security Bearer
// @tags User Page
func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
userID := gc.Param("userID")
pin := gc.Param("pin")
user, ok := app.matrix.tokens[pin]
if !ok {
app.debug.Printf(lm.InvalidPIN, pin)
respondBool(200, false, gc)
return
}
if user.User.UserID != userID {
app.debug.Printf(lm.UnauthorizedPIN, pin)
respondBool(200, false, gc)
return
}
mxUser := *user.User
mxUser.Contact = true
existingUser, ok := app.storage.GetMatrixKey(gc.GetString("jfId"))
if ok {
mxUser.Lang = existingUser.Lang
mxUser.Contact = existingUser.Contact
}
app.storage.SetMatrixKey(gc.GetString("jfId"), mxUser)
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked,
UserID: gc.GetString("jfId"),
SourceType: ActivityUser,
Source: gc.GetString("jfId"),
Value: "matrix",
Time: time.Now(),
}, gc, true)
delete(app.matrix.tokens, pin)
respondBool(200, true, gc)
}
// @Summary unlink the Discord account from your Jellyfin user. Always succeeds.
// @Produce json
// @Success 200 {object} boolResponse
// @Router /my/discord [delete]
// @Security Bearer
// @Tags User Page
func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
app.storage.DeleteDiscordKey(gc.GetString("jfId"))
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
jellyseerr.FieldDiscordEnabled: false,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked,
UserID: gc.GetString("jfId"),
SourceType: ActivityUser,
Source: gc.GetString("jfId"),
Value: "discord",
Time: time.Now(),
}, gc, true)
respondBool(200, true, gc)
}
// @Summary unlink the Telegram account from your Jellyfin user. Always succeeds.
// @Produce json
// @Success 200 {object} boolResponse
// @Router /my/telegram [delete]
// @Security Bearer
// @Tags User Page
func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
app.storage.DeleteTelegramKey(gc.GetString("jfId"))
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
jellyseerr.FieldTelegramEnabled: false,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked,
UserID: gc.GetString("jfId"),
SourceType: ActivityUser,
Source: gc.GetString("jfId"),
Value: "telegram",
Time: time.Now(),
}, gc, true)
respondBool(200, true, gc)
}
// @Summary unlink the Matrix account from your Jellyfin user. Always succeeds.
// @Produce json
// @Success 200 {object} boolResponse
// @Router /my/matrix [delete]
// @Security Bearer
// @Tags User Page
func (app *appContext) UnlinkMyMatrix(gc *gin.Context) {
app.storage.DeleteMatrixKey(gc.GetString("jfId"))
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked,
UserID: gc.GetString("jfId"),
SourceType: ActivityUser,
Source: gc.GetString("jfId"),
Value: "matrix",
Time: time.Now(),
}, gc, true)
respondBool(200, true, gc)
}
// @Summary Generate & send a password reset link if the given username/email/contact method exists. Doesn't give you any info about it's success.
// @Produce json
// @Param address path string true "address/contact method associated w/ your account."
// @Success 204 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /my/password/reset/{address} [post]
// @Tags User Page
func (app *appContext) ResetMyPassword(gc *gin.Context) {
// All requests should take 1 second, to make it harder to tell if a success occured or not.
timerWait := make(chan bool)
cancel := time.AfterFunc(1*time.Second, func() {
timerWait <- true
})
usernameAllowed := app.config.Section("user_page").Key("allow_pwr_username").MustBool(true)
emailAllowed := app.config.Section("user_page").Key("allow_pwr_email").MustBool(true)
contactMethodAllowed := app.config.Section("user_page").Key("allow_pwr_contact_method").MustBool(true)
address := gc.Param("address")
if address == "" {
cancel.Stop()
respondBool(400, false, gc)
return
}
var pwr InternalPWR
var err error
jfUser, ok := app.ReverseUserSearch(address, usernameAllowed, emailAllowed, contactMethodAllowed)
if !ok {
app.debug.Printf(lm.FailedGetUsers, lm.Jellyfin, "no results")
for range timerWait {
respondBool(204, true, gc)
return
}
return
}
pwr, err = app.GenInternalReset(jfUser.ID)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
for range timerWait {
respondBool(204, true, gc)
return
}
return
}
if app.internalPWRs == nil {
app.internalPWRs = map[string]InternalPWR{}
}
app.internalPWRs[pwr.PIN] = pwr
// FIXME: Send to all contact methods
msg, err := app.email.constructReset(
PasswordReset{
Pin: pwr.PIN,
Username: pwr.Username,
Expiry: pwr.Expiry,
Internal: true,
}, app, false,
)
if err != nil {
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
for range timerWait {
respondBool(204, true, gc)
return
}
return
} else if err := app.sendByID(msg, jfUser.ID); err != nil {
app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, "?", err)
} else {
app.info.Printf(lm.SentPWRMessage, pwr.Username, "?")
}
for range timerWait {
respondBool(204, true, gc)
return
}
}
// @Summary Change your password, given the old one and the new one.
// @Produce json
// @Param ChangeMyPasswordDTO body ChangeMyPasswordDTO true "User's old & new passwords."
// @Success 204 {object} boolResponse
// @Failure 400 {object} PasswordValidation
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /my/password [post]
// @Security Bearer
// @Tags User Page
func (app *appContext) ChangeMyPassword(gc *gin.Context) {
var req ChangeMyPasswordDTO
gc.BindJSON(&req)
if req.Old == "" || req.New == "" {
respondBool(400, false, gc)
}
validation := app.validator.validate(req.New)
for _, val := range validation {
if !val {
gc.JSON(400, validation)
return
}
}
user, status, err := app.jf.UserByID(gc.GetString("jfId"), false)
if status != 200 || err != nil {
app.err.Printf(lm.FailedGetUser, gc.GetString("jfId"), lm.Jellyfin, err)
respondBool(500, false, gc)
return
}
// Authenticate as user to confirm old password.
user, status, err = app.authJf.Authenticate(user.Name, req.Old)
if status != 200 || err != nil {
respondBool(401, false, gc)
return
}
status, err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New)
if (status != 200 && status != 204) || err != nil {
respondBool(500, false, gc)
return
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityChangePassword,
UserID: user.ID,
SourceType: ActivityUser,
Source: user.ID,
Time: time.Now(),
}, gc, true)
if app.config.Section("ombi").Key("enabled").MustBool(false) {
func() {
ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId"))
if status != 200 || err != nil {
app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, err)
return
}
ombiUser["password"] = req.New
status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
return
}
app.debug.Printf(lm.ChangePassword, lm.Ombi, ombiUser["userName"])
}()
}
cookie, err := gc.Cookie("user-refresh")
if err == nil {
app.invalidTokens = append(app.invalidTokens, cookie)
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
} else {
app.debug.Printf(lm.FailedGetCookies, "user-refresh", err)
}
respondBool(204, true, gc)
}
// @Summary Get or generate a new referral code.
// @Produce json
// @Success 200 {object} GetMyReferralRespDTO
// @Failure 400 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /my/referral [get]
// @Security Bearer
// @Tags User Page
func (app *appContext) GetMyReferral(gc *gin.Context) {
// 1. Look for existing template bound to this Jellyfin ID
// If one exists, that means its just for us and so we
// can use it directly.
inv := Invite{}
err := app.storage.db.FindOne(&inv, badgerhold.Where("ReferrerJellyfinID").Eq(gc.GetString("jfId")))
if err != nil {
// 2. Look for a template matching the key found in the user storage
// Since this key is shared between users in a profile, we make a copy.
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
err = app.storage.db.Get(user.ReferralTemplateKey, &inv)
if !ok || err != nil || user.ReferralTemplateKey == "" {
app.debug.Printf(lm.FailedGetReferralTemplate, user.ReferralTemplateKey, err)
respondBool(400, false, gc)
return
}
inv.Code = GenerateInviteCode()
expiryDelta := inv.ValidTill.Sub(inv.Created)
inv.Created = time.Now()
if inv.UseReferralExpiry {
inv.ValidTill = inv.Created.Add(expiryDelta)
} else {
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
}
inv.IsReferral = true
inv.ReferrerJellyfinID = gc.GetString("jfId")
app.storage.SetInvitesKey(inv.Code, inv)
} else if time.Now().After(inv.ValidTill) {
// 3. We found an invite for us, but it's expired.
// We delete it from storage, and put it back with a fresh code and expiry.
// If UseReferralExpiry is enabled, we delete it and return nothing.
app.storage.DeleteInvitesKey(inv.Code)
if inv.UseReferralExpiry {
app.debug.Printf(lm.DeleteOldReferral, inv.Code)
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
if ok {
user.ReferralTemplateKey = ""
app.storage.SetEmailsKey(gc.GetString("jfId"), user)
}
app.debug.Printf("Ignoring referral request, expired.")
respondBool(400, false, gc)
return
}
app.debug.Printf(lm.RenewOldReferral, inv.Code)
inv.Code = GenerateInviteCode()
inv.Created = time.Now()
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
app.storage.SetInvitesKey(inv.Code, inv)
}
gc.JSON(200, GetMyReferralRespDTO{
Code: inv.Code,
RemainingUses: inv.RemainingUses,
NoLimit: inv.NoLimit,
Expiry: inv.ValidTill.Unix(),
UseExpiry: inv.UseReferralExpiry,
})
}