mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
referrals: add "use expiry" option
adds an option when enabling referrals to use the duration of the source invited (i.e., months, days, hours) for the referral invite. If enabled, the user won't be able to make a new referral link after it expires. For referrals enabled for new users via a profile, the clock starts ticking as soon as the account is created.
This commit is contained in:
parent
d0de1142ae
commit
a66c522b73
@ -45,14 +45,24 @@ func (app *appContext) checkInvites() {
|
||||
app.storage.SetInvitesKey(data.Code, data)
|
||||
}
|
||||
|
||||
if data.IsReferral {
|
||||
if data.IsReferral && (!data.UseReferralExpiry || data.ReferrerJellyfinID == "") {
|
||||
continue
|
||||
}
|
||||
expiry := data.ValidTill
|
||||
if !currentTime.After(expiry) {
|
||||
continue
|
||||
}
|
||||
|
||||
app.debug.Printf("Housekeeping: Deleting old invite %s", data.Code)
|
||||
|
||||
// Disable referrals for the user if UseReferralExpiry is enabled, so no new ones are made.
|
||||
if data.IsReferral && data.UseReferralExpiry && data.ReferrerJellyfinID != "" {
|
||||
user, ok := app.storage.GetEmailsKey(data.ReferrerJellyfinID)
|
||||
if ok {
|
||||
user.ReferralTemplateKey = ""
|
||||
app.storage.SetEmailsKey(data.ReferrerJellyfinID, user)
|
||||
}
|
||||
}
|
||||
notify := data.Notify
|
||||
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", data.Code)
|
||||
@ -136,6 +146,13 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
}
|
||||
wait.Wait()
|
||||
}
|
||||
if inv.IsReferral && inv.ReferrerJellyfinID != "" && inv.UseReferralExpiry {
|
||||
user, ok := app.storage.GetEmailsKey(inv.ReferrerJellyfinID)
|
||||
if ok {
|
||||
user.ReferralTemplateKey = ""
|
||||
app.storage.SetEmailsKey(inv.ReferrerJellyfinID, user)
|
||||
}
|
||||
}
|
||||
match = false
|
||||
app.storage.DeleteInvitesKey(code)
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
|
@ -130,15 +130,17 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param profile path string true "name of profile to enable referrals for."
|
||||
// @Param invite path string true "invite code to create referral template from."
|
||||
// @Param useExpiry path string true "with-expiry or none."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /profiles/referral/{profile}/{invite} [post]
|
||||
// @Router /profiles/referral/{profile}/{invite}/{useExpiry} [post]
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) EnableReferralForProfile(gc *gin.Context) {
|
||||
profileName := gc.Param("profile")
|
||||
invCode := gc.Param("invite")
|
||||
useExpiry := gc.Param("useExpiry") == "with-expiry"
|
||||
inv, ok := app.storage.GetInvitesKey(invCode)
|
||||
if !ok {
|
||||
respond(400, "Invalid invite code", gc)
|
||||
@ -154,9 +156,15 @@ func (app *appContext) EnableReferralForProfile(gc *gin.Context) {
|
||||
|
||||
// Generate new code for referral template
|
||||
inv.Code = GenerateInviteCode()
|
||||
expiryDelta := inv.ValidTill.Sub(inv.Created)
|
||||
inv.Created = time.Now()
|
||||
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
|
||||
if useExpiry {
|
||||
inv.ValidTill = inv.Created.Add(expiryDelta)
|
||||
} else {
|
||||
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
|
||||
}
|
||||
inv.IsReferral = true
|
||||
inv.UseReferralExpiry = useExpiry
|
||||
// Since this is a template for multiple users, ReferrerJellyfinID is not set.
|
||||
// inv.ReferrerJellyfinID = ...
|
||||
|
||||
|
@ -746,21 +746,37 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
||||
// 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 {
|
||||
if !ok || err != nil || user.ReferralTemplateKey == "" {
|
||||
app.debug.Printf("Ignoring referral request, couldn't find template.")
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
inv.Code = GenerateInviteCode()
|
||||
expiryDelta := inv.ValidTill.Sub(inv.Created)
|
||||
inv.Created = time.Now()
|
||||
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
inv.Code = GenerateInviteCode()
|
||||
inv.Created = time.Now()
|
||||
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
|
||||
@ -771,5 +787,6 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
||||
RemainingUses: inv.RemainingUses,
|
||||
NoLimit: inv.NoLimit,
|
||||
Expiry: inv.ValidTill.Unix(),
|
||||
UseExpiry: inv.UseReferralExpiry,
|
||||
})
|
||||
}
|
||||
|
26
api-users.go
26
api-users.go
@ -367,6 +367,19 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
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 app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
@ -729,10 +742,11 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
|
||||
// @Param EnableDisableReferralDTO body EnableDisableReferralDTO true "List of users"
|
||||
// @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."
|
||||
// @Param useExpiry path string true "with-expiry or none."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Router /users/referral/{mode}/{source} [post]
|
||||
// @Router /users/referral/{mode}/{source}/{useExpiry} [post]
|
||||
// @Security Bearer
|
||||
// @tags Users
|
||||
func (app *appContext) EnableReferralForUsers(gc *gin.Context) {
|
||||
@ -740,7 +754,7 @@ func (app *appContext) EnableReferralForUsers(gc *gin.Context) {
|
||||
gc.BindJSON(&req)
|
||||
mode := gc.Param("mode")
|
||||
source := gc.Param("source")
|
||||
|
||||
useExpiry := gc.Param("useExpiry") == "with-expiry"
|
||||
baseInv := Invite{}
|
||||
if mode == "profile" {
|
||||
profile, ok := app.storage.GetProfileKey(source)
|
||||
@ -768,10 +782,16 @@ func (app *appContext) EnableReferralForUsers(gc *gin.Context) {
|
||||
// 2. Generate referral invite.
|
||||
inv := baseInv
|
||||
inv.Code = GenerateInviteCode()
|
||||
expiryDelta := inv.ValidTill.Sub(inv.Created)
|
||||
inv.Created = time.Now()
|
||||
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
|
||||
if useExpiry {
|
||||
inv.ValidTill = inv.Created.Add(expiryDelta)
|
||||
} else {
|
||||
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
|
||||
}
|
||||
inv.IsReferral = true
|
||||
inv.ReferrerJellyfinID = u
|
||||
inv.UseReferralExpiry = useExpiry
|
||||
app.storage.SetInvitesKey(inv.Code, inv)
|
||||
}
|
||||
}
|
||||
|
@ -130,6 +130,11 @@
|
||||
<div class="select ~neutral @low mb-4 unfocused">
|
||||
<select id="enable-referrals-user-invites"></select>
|
||||
</div>
|
||||
<label class="switch mb-4">
|
||||
<input type="checkbox" id="enable-referrals-user-expiry">
|
||||
<span>{{ .strings.useInviteExpiry }}</span>
|
||||
<span class="flex flex-row support mt-2">{{ .strings.useInviteExpiryNote }}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
|
||||
@ -144,6 +149,11 @@
|
||||
<div class="select ~neutral @low mb-4 mt-2">
|
||||
<select id="enable-referrals-profile-invites"></select>
|
||||
</div>
|
||||
<label class="switch mb-4">
|
||||
<input type="checkbox" id="enable-referrals-profile-expiry">
|
||||
<span>{{ .strings.useInviteExpiry }}</span>
|
||||
<span class="flex flex-row support mt-2">{{ .strings.useInviteExpiryNote }}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
|
||||
|
@ -155,7 +155,7 @@
|
||||
<div>
|
||||
<div class="card @low dark:~d_neutral unfocused" id="card-referrals">
|
||||
<span class="heading mb-2">{{ .strings.referrals }}</span>
|
||||
<aside class="aside ~neutral my-4 col">{{ .strings.referralsDescription }}</aside>
|
||||
<aside class="aside ~neutral my-4 col user-referrals-description"></aside>
|
||||
<div class="row flex-expand">
|
||||
<div class="user-referrals-info"></div>
|
||||
<div class="grid my-2">
|
||||
|
@ -76,6 +76,8 @@
|
||||
"disableReferrals": "Disable Referrals",
|
||||
"enableReferralsDescription": "Give users a personal referral link similiar to an invite, to send to friends/family. Can be sourced from a referral template in a profile, or from an existing invite.",
|
||||
"enableReferralsProfileDescription": "Give users created with this profile a personal referral link similiar to an invite, to send to friends/family. Create an invite with the desired settings, then select it here. Each referral will then be based on this invite. You can delete the invite once complete.",
|
||||
"useInviteExpiry": "Set expiry from profile/invite",
|
||||
"useInviteExpiryNote": "By default, invites expire after 90 days but can be renewed by the user. Enable for the referral to be disabled after the time set.",
|
||||
"applyHomescreenLayout": "Apply homescreen layout",
|
||||
"sendDeleteNotificationEmail": "Send notification message",
|
||||
"sendDeleteNotifiationExample": "Your account has been deleted.",
|
||||
|
@ -36,6 +36,7 @@
|
||||
"resetSentDescription": "If an account with the given username/contact method exists, a password reset link has been sent via all contact methods available. The code will expire in 30 minutes.",
|
||||
"changePassword": "Change Password",
|
||||
"referralsDescription": "Invite friends & family to Jellyfin with this link. Come back here for a new one if it expires.",
|
||||
"referralsWithExpiryDescription": "Invite friends & family to Jellyfin with this link. The link will be disabled once it expires.",
|
||||
"copyReferral": "Copy Link",
|
||||
"invitedBy": "You were invited by user {user}."
|
||||
},
|
||||
|
@ -424,7 +424,8 @@ type GetMyReferralRespDTO struct {
|
||||
Code string `json:"code"`
|
||||
RemainingUses int `json:"remaining_uses"`
|
||||
NoLimit bool `json:"no_limit"`
|
||||
Expiry int64 `json:"expiry"` // Come back after this time to get a new referral
|
||||
Expiry int64 `json:"expiry"` // Come back after this time to get a new referral (if UseExpiry, a new one can't be made).
|
||||
UseExpiry bool `json:"use_expiry"`
|
||||
}
|
||||
|
||||
type EnableDisableReferralDTO struct {
|
||||
|
@ -229,9 +229,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
}
|
||||
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)
|
||||
api.POST(p+"/users/referral/:mode/:source/:useExpiry", app.EnableReferralForUsers)
|
||||
api.DELETE(p+"/users/referral", app.DisableReferralForUsers)
|
||||
api.POST(p+"/profiles/referral/:profile/:invite", app.EnableReferralForProfile)
|
||||
api.POST(p+"/profiles/referral/:profile/:invite/:useExpiry", app.EnableReferralForProfile)
|
||||
api.DELETE(p+"/profiles/referral/:profile", app.DisableReferralForProfile)
|
||||
}
|
||||
|
||||
|
18
storage.go
18
storage.go
@ -659,15 +659,15 @@ type Invite struct {
|
||||
UserMinutes int `json:"user-minutes,omitempty"`
|
||||
SendTo string `json:"email"`
|
||||
// Used to be stored as formatted time, now as Unix.
|
||||
UsedBy [][]string `json:"used-by"`
|
||||
Notify map[string]map[string]bool `json:"notify"`
|
||||
Profile string `json:"profile"`
|
||||
Label string `json:"label,omitempty"`
|
||||
UserLabel string `json:"user_label,omitempty" example:"Friend"` // Label to apply to users created w/ this invite.
|
||||
Captchas map[string]Captcha // Map of Captcha IDs to images & answers
|
||||
IsReferral bool `json:"is_referral" badgerhold:"index"`
|
||||
ReferrerJellyfinID string `json:"referrer_id"`
|
||||
ReferrerTemplateForProfile string
|
||||
UsedBy [][]string `json:"used-by"`
|
||||
Notify map[string]map[string]bool `json:"notify"`
|
||||
Profile string `json:"profile"`
|
||||
Label string `json:"label,omitempty"`
|
||||
UserLabel string `json:"user_label,omitempty" example:"Friend"` // Label to apply to users created w/ this invite.
|
||||
Captchas map[string]Captcha // Map of Captcha IDs to images & answers
|
||||
IsReferral bool `json:"is_referral" badgerhold:"index"`
|
||||
ReferrerJellyfinID string `json:"referrer_id"`
|
||||
UseReferralExpiry bool `json:"use_referral_expiry"`
|
||||
}
|
||||
|
||||
type Captcha struct {
|
||||
|
@ -783,6 +783,7 @@ export class accountsList {
|
||||
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 _referralsExpiry = document.getElementById("enable-referrals-user-expiry") as HTMLInputElement;
|
||||
private _searchBox = document.getElementById("accounts-search") as HTMLInputElement;
|
||||
private _search: Search;
|
||||
|
||||
@ -1578,7 +1579,7 @@ export class accountsList {
|
||||
send["from"] = "invite";
|
||||
send["id"] = this._referralsInviteSelect.value;
|
||||
}
|
||||
_post("/users/referral/" + send["from"] + "/" + (send["id"] ? send["id"] : send["profile"]), send, (req: XMLHttpRequest) => {
|
||||
_post("/users/referral/" + send["from"] + "/" + (send["id"] ? send["id"] : send["profile"]) + "/" + (this._referralsExpiry.checked ? "with-expiry" : "none"), send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
if (req.status == 400) {
|
||||
@ -1593,6 +1594,7 @@ export class accountsList {
|
||||
};
|
||||
this._enableReferralsProfile.checked = true;
|
||||
this._enableReferralsInvite.checked = false;
|
||||
this._referralsExpiry.checked = false;
|
||||
window.modals.enableReferralsUser.show();
|
||||
}
|
||||
|
||||
|
@ -225,6 +225,7 @@ export class ProfileEditor {
|
||||
|
||||
enableReferrals = (name: string) => {
|
||||
const referralsInviteSelect = document.getElementById("enable-referrals-profile-invites") as HTMLSelectElement;
|
||||
const referralsExpiry = document.getElementById("enable-referrals-profile-expiry") as HTMLInputElement;
|
||||
_get("/invites", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState != 4 || req.status != 200) return;
|
||||
|
||||
@ -257,7 +258,7 @@ export class ProfileEditor {
|
||||
"invite": referralsInviteSelect.value
|
||||
};
|
||||
|
||||
_post("/profiles/referral/" + send["profile"] + "/" + send["invite"], send, (req: XMLHttpRequest) => {
|
||||
_post("/profiles/referral/" + send["profile"] + "/" + send["invite"] + "/" + (referralsExpiry.checked ? "with-expiry" : "none"), send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
if (req.status == 400) {
|
||||
@ -270,6 +271,7 @@ export class ProfileEditor {
|
||||
}
|
||||
});
|
||||
};
|
||||
referralsExpiry.checked = false;
|
||||
window.modals.profiles.close();
|
||||
window.modals.enableReferralsProfile.show();
|
||||
};
|
||||
|
15
ts/user.ts
15
ts/user.ts
@ -116,6 +116,7 @@ interface MyReferral {
|
||||
remaining_uses: number;
|
||||
no_limit: boolean;
|
||||
expiry: number;
|
||||
use_expiry: boolean;
|
||||
}
|
||||
|
||||
interface ContactDTO {
|
||||
@ -252,6 +253,7 @@ class ReferralCard {
|
||||
private _url: string;
|
||||
private _expiry: Date;
|
||||
private _expiryUnix: number;
|
||||
private _useExpiry: boolean;
|
||||
private _remainingUses: number;
|
||||
private _noLimit: boolean;
|
||||
|
||||
@ -259,6 +261,7 @@ class ReferralCard {
|
||||
private _infoArea: HTMLDivElement;
|
||||
private _remainingUsesEl: HTMLSpanElement;
|
||||
private _expiryEl: HTMLSpanElement;
|
||||
private _descriptionEl: HTMLSpanElement;
|
||||
|
||||
get code(): string { return this._code; }
|
||||
set code(c: string) {
|
||||
@ -294,11 +297,22 @@ class ReferralCard {
|
||||
this._expiry = new Date(expiryUnix * 1000);
|
||||
this._expiryEl.textContent = toDateString(this._expiry);
|
||||
}
|
||||
|
||||
get use_expiry(): boolean { return this._useExpiry; }
|
||||
set use_expiry(v: boolean) {
|
||||
this._useExpiry = v;
|
||||
if (v) {
|
||||
this._descriptionEl.textContent = window.lang.strings("referralsWithExpiryDescription");
|
||||
} else {
|
||||
this._descriptionEl.textContent = window.lang.strings("referralsDescription");
|
||||
}
|
||||
}
|
||||
|
||||
constructor(card: HTMLElement) {
|
||||
this._card = card;
|
||||
this._button = this._card.querySelector(".user-referrals-button") as HTMLButtonElement;
|
||||
this._infoArea = this._card.querySelector(".user-referrals-info") as HTMLDivElement;
|
||||
this._descriptionEl = this._card.querySelector(".user-referrals-description") as HTMLSpanElement;
|
||||
|
||||
this._infoArea.innerHTML = `
|
||||
<div class="row my-3">
|
||||
@ -344,6 +358,7 @@ class ReferralCard {
|
||||
this.no_limit = referral.no_limit;
|
||||
this.expiry = referral.expiry;
|
||||
this._card.classList.remove("unfocused");
|
||||
this.use_expiry = referral.use_expiry;
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user