1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-06-26 19:37:46 +02:00

referrals: 1/2 generation routes, display route, partial frontend

route for generation/enabling of referral for user(s) done? the frontend
is mostly done, but functionality is not there yet. Route for finding
and displaying referral to user is done. Also the config option for
referral is there, in user page settings.
This commit is contained in:
Harvey Tindall 2023-06-28 16:05:24 +01:00
parent 423fc4ac80
commit 9c2f27bcdb
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
13 changed files with 320 additions and 21 deletions

View File

@ -16,6 +16,9 @@ import (
func (app *appContext) checkInvites() { func (app *appContext) checkInvites() {
currentTime := time.Now() currentTime := time.Now()
for _, data := range app.storage.GetInvites() { for _, data := range app.storage.GetInvites() {
if data.IsReferral {
continue
}
expiry := data.ValidTill expiry := data.ValidTill
if !currentTime.After(expiry) { if !currentTime.After(expiry) {
continue continue
@ -222,6 +225,9 @@ func (app *appContext) GetInvites(gc *gin.Context) {
app.checkInvites() app.checkInvites()
var invites []inviteDTO var invites []inviteDTO
for _, inv := range app.storage.GetInvites() { for _, inv := range app.storage.GetInvites() {
if inv.IsReferral {
continue
}
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime) _, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
invite := inviteDTO{ invite := inviteDTO{
Code: inv.Code, Code: inv.Code,

View File

@ -3,11 +3,18 @@ package main
import ( import (
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
)
const (
REFERRAL_EXPIRY_DAYS = 365
) )
// @Summary Returns the logged-in user's Jellyfin ID & Username, and other details. // @Summary Returns the logged-in user's Jellyfin ID & Username, and other details.
@ -621,3 +628,62 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
} }
respondBool(204, true, gc) 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.Find(&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 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 {
app.debug.Printf("Ignoring referral request, couldn't find template.")
respondBool(400, false, gc)
return
}
inv.Code = shortuuid.New()
// make sure code doesn't begin with number
_, err := strconv.Atoi(string(inv.Code[0]))
for err == nil {
inv.Code = shortuuid.New()
_, err = strconv.Atoi(string(inv.Code[0]))
}
inv.Created = time.Now()
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
inv.IsReferral = true
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.
app.storage.DeleteInvitesKey(inv.Code)
inv.Code = shortuuid.New()
// make sure code doesn't begin with number
_, err := strconv.Atoi(string(inv.Code[0]))
for err == nil {
inv.Code = shortuuid.New()
_, err = strconv.Atoi(string(inv.Code[0]))
}
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,
})
}

View File

@ -3,12 +3,14 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
) )
// @Summary Creates a new Jellyfin user without an invite. // @Summary Creates a new Jellyfin user without an invite.
@ -629,6 +631,58 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
respondBool(204, true, gc) respondBool(204, true, gc)
} }
// @Summary Enable referrals for the given user(s) based on the rules set in the given invite code, or profile.
// @Produce json
// @Param mode path string true "mode of template sourcing from 'invite' or 'profile'."
// @Param source path string true "invite code or profile name, depending on what mode is."
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/referral/{mode}/{source} [post]
// @Security Bearer
// @tags Users
func (app *appContext) EnableReferralForUsers(gc *gin.Context) {
var req EnableDisableReferralDTO
gc.BindJSON(&req)
mode := gc.Param("mode")
source := gc.Param("source")
baseInv := Invite{}
if mode == "profile" {
profile, ok := app.storage.GetProfileKey(source)
err := app.storage.db.Get(profile.ReferralTemplateKey, &baseInv)
if !ok || profile.ReferralTemplateKey == "" || err != nil {
app.debug.Printf("Couldn't find template to source from")
respondBool(400, false, gc)
return
}
} else if mode == "invite" {
// Get the invite, and modify it to turn it into a referral
err := app.storage.db.Get(source, &baseInv)
if err != nil {
app.debug.Printf("Couldn't find invite to source from")
respondBool(400, false, gc)
return
}
}
for _, u := range req.Users {
inv := baseInv
inv.Code = shortuuid.New()
// make sure code doesn't begin with number
_, err := strconv.Atoi(string(inv.Code[0]))
for err == nil {
inv.Code = shortuuid.New()
_, err = strconv.Atoi(string(inv.Code[0]))
}
inv.Created = time.Now()
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
inv.IsReferral = true
inv.ReferrerJellyfinID = u
app.storage.SetInvitesKey(inv.Code, inv)
}
}
// @Summary Send an announcement via email to a given list of users. // @Summary Send an announcement via email to a given list of users.
// @Produce json // @Produce json
// @Param announcementDTO body announcementDTO true "Announcement request object" // @Param announcementDTO body announcementDTO true "Announcement request object"

View File

@ -405,6 +405,22 @@
"depends_true": "enabled", "depends_true": "enabled",
"required": "false", "required": "false",
"description": "Click the edit icon next to the \"User Page\" Setting to add custom Markdown messages that will be shown to the user. Note message cards are not private, little effort is required for anyone to view them." "description": "Click the edit icon next to the \"User Page\" Setting to add custom Markdown messages that will be shown to the user. Note message cards are not private, little effort is required for anyone to view them."
},
"referrals": {
"name": "User Referrals",
"required": false,
"requires_restart": false,
"type": "bool",
"value": true,
"description": "Users are given their own \"invite\" to send to others."
},
"referrals_note": {
"name": "Using Referrals:",
"type": "note",
"value": "",
"depends_true": "referrals",
"required": "false",
"description": "Create an invite with your desired settings, then either assign it to a user in the accounts tab, or to a profile in settings."
} }
} }
}, },

View File

@ -17,6 +17,7 @@
window.jellyfinLogin = {{ .jellyfinLogin }}; window.jellyfinLogin = {{ .jellyfinLogin }};
window.jfAdminOnly = {{ .jfAdminOnly }}; window.jfAdminOnly = {{ .jfAdminOnly }};
window.jfAllowAll = {{ .jfAllowAll }}; window.jfAllowAll = {{ .jfAllowAll }};
window.referralsEnabled = {{ .referralsEnabled }};
</script> </script>
<title>Admin - jfa-go</title> <title>Admin - jfa-go</title>
{{ template "header.html" . }} {{ template "header.html" . }}
@ -107,6 +108,34 @@
</label> </label>
</form> </form>
</div> </div>
{{ if .referralsEnabled }}
<div id="modal-enable-referrals-user" class="modal">
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-enable-referrals-user" href="">
<span class="heading"><span id="header-enable-referrals-user"></span> <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.enableReferralsDescription }}</p>
<div class="flex flex-row mb-4">
<label class="flex-row-group mr-2">
<input type="radio" name="enable-referrals-user-source" class="unfocused" id="radio-referrals-use-profile" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
</label>
<label class="flex-row-group ml-2">
<input type="radio" name="enable-referrals-user-source" class="unfocused" id="radio-referrals-use-invite">
<span class="button ~neutral @low supra full-width center">{{ .strings.invite }}</span>
</label>
</div>
<div class="select ~neutral @low mb-4">
<select id="enable-referrals-user-profiles"></select>
</div>
<div class="select ~neutral @low mb-4 unfocused">
<select id="enable-referrals-user-invites"></select>
</div>
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
</label>
</form>
</div>
{{ end }}
<div id="modal-delete-user" class="modal"> <div id="modal-delete-user" class="modal">
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-delete-user" href=""> <form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-delete-user" href="">
<span class="heading"><span id="header-delete-user"></span> <span class="modal-close">&times;</span></span> <span class="heading"><span id="header-delete-user"></span> <span class="modal-close">&times;</span></span>
@ -613,6 +642,7 @@
</div> </div>
</div> </div>
<span class="col button ~urge @low center max-w-[20%]" id="accounts-modify-user">{{ .strings.modifySettings }}</span> <span class="col button ~urge @low center max-w-[20%]" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
<span class="col button ~urge @low center max-w-[20%]" id="accounts-enable-referrals">{{ .strings.enableReferrals }}</span>
<span class="col button ~warning @low center max-w-[20%]" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span> <span class="col button ~warning @low center max-w-[20%]" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
<div id="accounts-disable-enable-dropdown" class="col dropdown manual pb-0i max-w-[20%]" tabindex="0"> <div id="accounts-disable-enable-dropdown" class="col dropdown manual pb-0i max-w-[20%]" tabindex="0">
<span class="w-100 button ~positive @low center" id="accounts-disable-enable">{{ .strings.disable }}</span> <span class="w-100 button ~positive @low center" id="accounts-disable-enable">{{ .strings.disable }}</span>

View File

@ -4,6 +4,7 @@
}, },
"strings": { "strings": {
"invites": "Invites", "invites": "Invites",
"invite": "Invite",
"accounts": "Accounts", "accounts": "Accounts",
"settings": "Settings", "settings": "Settings",
"inviteMonths": "Months", "inviteMonths": "Months",
@ -63,6 +64,8 @@
"markdownSupported": "Markdown is supported.", "markdownSupported": "Markdown is supported.",
"modifySettings": "Modify Settings", "modifySettings": "Modify Settings",
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.", "modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
"enableReferrals": "Enable Referrals",
"enableReferralsDescription": "Give users their a referral link similiar to an invite, to send to friends/family. Can be sourced from a referral template in a profile, or from an exsiting invite.",
"applyHomescreenLayout": "Apply homescreen layout", "applyHomescreenLayout": "Apply homescreen layout",
"sendDeleteNotificationEmail": "Send notification message", "sendDeleteNotificationEmail": "Send notification message",
"sendDeleteNotifiationExample": "Your account has been deleted.", "sendDeleteNotifiationExample": "Your account has been deleted.",
@ -160,6 +163,10 @@
"singular": "Modify Settings for {n} user", "singular": "Modify Settings for {n} user",
"plural": "Modify Settings for {n} users" "plural": "Modify Settings for {n} users"
}, },
"enableReferralsFor": {
"singular": "Enable Referrals for {n} user",
"plural": "Enable Referrals for {n} users"
},
"deleteNUsers": { "deleteNUsers": {
"singular": "Delete {n} user", "singular": "Delete {n} user",
"plural": "Delete {n} users" "plural": "Delete {n} users"

View File

@ -414,3 +414,15 @@ type ChangeMyPasswordDTO struct {
Old string `json:"old"` Old string `json:"old"`
New string `json:"new"` New string `json:"new"`
} }
type GetMyReferralRespDTO struct {
Code string `json:"code"`
RemainingUses int `json:"remaining-uses"`
NoLimit bool `json:"no-limit"`
Expiry time.Time `json:"expiry"` // Come back after this time to get a new referral
}
type EnableDisableReferralDTO struct {
Users []string `json:"users"`
Enabled bool `json:"enabled"`
}

View File

@ -226,6 +226,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.DELETE(p+"/profiles/ombi/:profile", app.DeleteOmbiProfile) api.DELETE(p+"/profiles/ombi/:profile", app.DeleteOmbiProfile)
} }
api.POST(p+"/matrix/login", app.MatrixLogin) api.POST(p+"/matrix/login", app.MatrixLogin)
if app.config.Section("user_page").Key("referrals").MustBool(false) {
api.POST(p+"/users/referral/:mode/:source", app.EnableReferralForUsers)
}
if userPageEnabled { if userPageEnabled {
user.GET(p+"/details", app.MyDetails) user.GET(p+"/details", app.MyDetails)
@ -242,6 +245,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
user.DELETE(p+"/telegram", app.UnlinkMyTelegram) user.DELETE(p+"/telegram", app.UnlinkMyTelegram)
user.DELETE(p+"/matrix", app.UnlinkMyMatrix) user.DELETE(p+"/matrix", app.UnlinkMyMatrix)
user.POST(p+"/password", app.ChangeMyPassword) user.POST(p+"/password", app.ChangeMyPassword)
if app.config.Section("user_page").Key("referrals").MustBool(false) {
user.GET(p+"/referral", app.GetMyReferral)
}
} }
} }
} }

View File

@ -429,15 +429,16 @@ type DiscordUser struct {
Discriminator string Discriminator string
Lang string Lang string
Contact bool Contact bool
JellyfinID string `json:"-" badgerhold:"key"` // Used internally in discord.go JellyfinID string `json:"-" badgerhold:"key"`
} }
type EmailAddress struct { type EmailAddress struct {
Addr string `badgerhold:"index"` Addr string `badgerhold:"index"`
Label string // User Label. Label string // User Label.
Contact bool Contact bool
Admin bool // Whether or not user is jfa-go admin. Admin bool // Whether or not user is jfa-go admin.
JellyfinID string `badgerhold:"key"` JellyfinID string `badgerhold:"key"`
ReferralTemplateKey string
} }
type customEmails struct { type customEmails struct {
@ -470,16 +471,17 @@ type userPageContent struct {
// timePattern: %Y-%m-%dT%H:%M:%S.%f // timePattern: %Y-%m-%dT%H:%M:%S.%f
type Profile struct { type Profile struct {
Name string `badgerhold:"key"` Name string `badgerhold:"key"`
Admin bool `json:"admin,omitempty" badgerhold:"index"` Admin bool `json:"admin,omitempty" badgerhold:"index"`
LibraryAccess string `json:"libraries,omitempty"` LibraryAccess string `json:"libraries,omitempty"`
FromUser string `json:"fromUser,omitempty"` FromUser string `json:"fromUser,omitempty"`
Homescreen bool `json:"homescreen"` Homescreen bool `json:"homescreen"`
Policy mediabrowser.Policy `json:"policy,omitempty"` Policy mediabrowser.Policy `json:"policy,omitempty"`
Configuration mediabrowser.Configuration `json:"configuration,omitempty"` Configuration mediabrowser.Configuration `json:"configuration,omitempty"`
Displayprefs map[string]interface{} `json:"displayprefs,omitempty"` Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
Default bool `json:"default,omitempty"` Default bool `json:"default,omitempty"`
Ombi map[string]interface{} `json:"ombi,omitempty"` Ombi map[string]interface{} `json:"ombi,omitempty"`
ReferralTemplateKey string
} }
type Invite struct { type Invite struct {
@ -495,11 +497,14 @@ type Invite struct {
UserMinutes int `json:"user-minutes,omitempty"` UserMinutes int `json:"user-minutes,omitempty"`
SendTo string `json:"email"` SendTo string `json:"email"`
// Used to be stored as formatted time, now as Unix. // Used to be stored as formatted time, now as Unix.
UsedBy [][]string `json:"used-by"` UsedBy [][]string `json:"used-by"`
Notify map[string]map[string]bool `json:"notify"` Notify map[string]map[string]bool `json:"notify"`
Profile string `json:"profile"` Profile string `json:"profile"`
Label string `json:"label,omitempty"` Label string `json:"label,omitempty"`
Captchas map[string]*captcha.Data // Map of Captcha IDs to answers Captchas map[string]*captcha.Data // Map of Captcha IDs to answers
IsReferral bool `json:"is_referral" badgerhold:"index"`
ReferrerJellyfinID string `json:"referrer_id"`
ReferrerTemplateForProfile string
} }
type Lang struct { type Lang struct {

View File

@ -78,6 +78,10 @@ window.availableProfiles = window.availableProfiles || [];
if (window.linkResetEnabled) { if (window.linkResetEnabled) {
window.modals.sendPWR = new Modal(document.getElementById("modal-send-pwr")); window.modals.sendPWR = new Modal(document.getElementById("modal-send-pwr"));
} }
if (window.referralsEnabled) {
window.modals.enableReferralsUser = new Modal(document.getElementById("modal-enable-referrals-user"));
}
})(); })();
var inviteCreator = new createInvite(); var inviteCreator = new createInvite();

View File

@ -748,9 +748,14 @@ export class accountsList {
private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement; private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement;
private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement; private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement;
private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement; private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement;
private _enableReferrals = document.getElementById("accounts-enable-referrals") as HTMLSpanElement;
private _enableReferralsProfile = document.getElementById("radio-referrals-use-profile") as HTMLInputElement;
private _enableReferralsInvite = document.getElementById("radio-referrals-use-invite") as HTMLInputElement;
private _sendPWR = document.getElementById("accounts-send-pwr") as HTMLSpanElement; private _sendPWR = document.getElementById("accounts-send-pwr") as HTMLSpanElement;
private _profileSelect = document.getElementById("modify-user-profiles") as HTMLSelectElement; private _profileSelect = document.getElementById("modify-user-profiles") as HTMLSelectElement;
private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement; private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement;
private _referralsProfileSelect = document.getElementById("enable-referrals-user-profiles") as HTMLSelectElement;
private _referralsInviteSelect = document.getElementById("enable-referrals-user-invites") as HTMLSelectElement;
private _search = document.getElementById("accounts-search") as HTMLInputElement; private _search = document.getElementById("accounts-search") as HTMLInputElement;
private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement; private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
@ -1154,6 +1159,9 @@ export class accountsList {
this._selectAll.indeterminate = false; this._selectAll.indeterminate = false;
this._selectAll.checked = false; this._selectAll.checked = false;
this._modifySettings.classList.add("unfocused"); this._modifySettings.classList.add("unfocused");
if (window.referralsEnabled) {
this._enableReferrals.classList.add("unfocused");
}
this._deleteUser.classList.add("unfocused"); this._deleteUser.classList.add("unfocused");
if (window.emailEnabled || window.telegramEnabled) { if (window.emailEnabled || window.telegramEnabled) {
this._announceButton.parentElement.classList.add("unfocused"); this._announceButton.parentElement.classList.add("unfocused");
@ -1176,6 +1184,9 @@ export class accountsList {
this._selectAll.indeterminate = true; this._selectAll.indeterminate = true;
} }
this._modifySettings.classList.remove("unfocused"); this._modifySettings.classList.remove("unfocused");
if (window.referralsEnabled) {
this._enableReferrals.classList.remove("unfocused");
}
this._deleteUser.classList.remove("unfocused"); this._deleteUser.classList.remove("unfocused");
this._deleteUser.textContent = window.lang.quantity("deleteUser", list.length); this._deleteUser.textContent = window.lang.quantity("deleteUser", list.length);
if (window.emailEnabled || window.telegramEnabled) { if (window.emailEnabled || window.telegramEnabled) {
@ -1662,6 +1673,60 @@ export class accountsList {
}; };
window.modals.modifyUser.show(); window.modals.modifyUser.show();
} }
enableReferrals = () => {
const modalHeader = document.getElementById("header-enable-referrals-user");
modalHeader.textContent = window.lang.quantity("enableReferralsFor", this._collectUsers().length)
let list = this._collectUsers();
// FIXME: Collect Profiles, Invite
(() => {
let innerHTML = "";
for (const profile of window.availableProfiles) {
innerHTML += `<option value="${profile}">${profile}</option>`;
}
this._referralsProfileSelect.innerHTML = innerHTML;
})();
(() => {
let innerHTML = "";
// for (let id in this._users) {
// innerHTML += `<option value="${id}">${this._users[id].name}</option>`;
// }
this._referralsInviteSelect.innerHTML = innerHTML;
})();
const form = document.getElementById("form-enable-referrals-user") as HTMLFormElement;
const button = form.querySelector("span.submit") as HTMLSpanElement;
this._enableReferralsProfile.checked = true;
this._enableReferralsInvite.checked = false;
form.onsubmit = (event: Event) => {
event.preventDefault();
toggleLoader(button);
let send = {
"users": list
};
if (this._enableReferralsProfile.checked && !this._enableReferralsInvite.checked) {
send["from"] = "profile";
send["profile"] = this._referralsProfileSelect.value;
} else if (this._enableReferralsInvite.checked && !this._enableReferralsProfile.checked) {
send["from"] = "invite";
send["id"] = this._referralsInviteSelect.value;
}
_post("/users/referrals/" + send["from"] + "/" + send["id"], send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
if (req.status == 400) {
window.notifications.customError("unknownError", window.lang.notif("errorUnknown"));
} else if (req.status == 200 || req.status == 204) {
window.notifications.customSuccess("enableReferralsSuccess", window.lang.quantity("appliedSettings", this._collectUsers().length));
}
this.reload();
window.modals.enableReferralsUser.close();
}
});
};
window.modals.enableReferralsUser.show();
}
extendExpiry = (enableUser?: boolean) => { extendExpiry = (enableUser?: boolean) => {
const list = this._collectUsers(); const list = this._collectUsers();
@ -1794,6 +1859,31 @@ export class accountsList {
this._modifySettingsProfile.onchange = checkSource; this._modifySettingsProfile.onchange = checkSource;
this._modifySettingsUser.onchange = checkSource; this._modifySettingsUser.onchange = checkSource;
if (window.referralsEnabled) {
this._enableReferrals.onclick = this.enableReferrals;
const checkReferralSource = () => {
const profileSpan = this._enableReferralsProfile.nextElementSibling as HTMLSpanElement;
const inviteSpan = this._enableReferralsInvite.nextElementSibling as HTMLSpanElement;
if (this._enableReferralsProfile.checked) {
this._referralsInviteSelect.parentElement.classList.add("unfocused");
this._referralsProfileSelect.parentElement.classList.remove("unfocused")
profileSpan.classList.add("@high");
profileSpan.classList.remove("@low");
inviteSpan.classList.remove("@high");
inviteSpan.classList.add("@low");
} else {
this._referralsInviteSelect.parentElement.classList.remove("unfocused");
this._referralsProfileSelect.parentElement.classList.add("unfocused");
inviteSpan.classList.add("@high");
inviteSpan.classList.remove("@low");
profileSpan.classList.remove("@high");
profileSpan.classList.add("@low");
}
};
this._enableReferralsProfile.onchange = checkReferralSource;
this._enableReferralsInvite.onchange = checkReferralSource;
}
this._deleteUser.onclick = this.deleteUsers; this._deleteUser.onclick = this.deleteUsers;
this._deleteUser.classList.add("unfocused"); this._deleteUser.classList.add("unfocused");

View File

@ -40,6 +40,7 @@ declare interface Window {
jellyfinLogin: boolean; jellyfinLogin: boolean;
jfAdminOnly: boolean; jfAdminOnly: boolean;
jfAllowAll: boolean; jfAllowAll: boolean;
referralsEnabled: boolean;
} }
declare interface Update { declare interface Update {
@ -113,6 +114,7 @@ declare interface Modals {
pwr?: Modal; pwr?: Modal;
logs: Modal; logs: Modal;
email?: Modal; email?: Modal;
enableReferralsUser?: Modal;
} }
interface Invite { interface Invite {

View File

@ -173,6 +173,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"jfAdminOnly": jfAdminOnly, "jfAdminOnly": jfAdminOnly,
"jfAllowAll": jfAllowAll, "jfAllowAll": jfAllowAll,
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false), "userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
"referralsEnabled": app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false),
}) })
} }