diff --git a/api-profiles.go b/api-profiles.go
index b118838..5220782 100644
--- a/api-profiles.go
+++ b/api-profiles.go
@@ -1,9 +1,11 @@
package main
import (
+ "strconv"
"time"
"github.com/gin-gonic/gin"
+ "github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
)
@@ -19,13 +21,23 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
DefaultProfile: app.storage.GetDefaultProfile().Name,
Profiles: map[string]profileDTO{},
}
+ referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
+ baseInv := Invite{}
for _, p := range app.storage.GetProfiles() {
- out.Profiles[p.Name] = profileDTO{
- Admin: p.Admin,
- LibraryAccess: p.LibraryAccess,
- FromUser: p.FromUser,
- Ombi: p.Ombi != nil,
+ pdto := profileDTO{
+ Admin: p.Admin,
+ LibraryAccess: p.LibraryAccess,
+ FromUser: p.FromUser,
+ Ombi: p.Ombi != nil,
+ ReferralsEnabled: false,
}
+ if referralsEnabled {
+ err := app.storage.db.Get(p.ReferralTemplateKey, &baseInv)
+ if p.ReferralTemplateKey != "" && err == nil {
+ pdto.ReferralsEnabled = true
+ }
+ }
+ out.Profiles[p.Name] = pdto
}
gc.JSON(200, out)
}
@@ -111,3 +123,63 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
app.storage.DeleteProfileKey(name)
respondBool(200, true, gc)
}
+
+// @Summary Enable referrals for a profile, sourced from the given invite by its code.
+// @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."
+// @Success 200 {object} boolResponse
+// @Failure 400 {object} stringResponse
+// @Failure 500 {object} stringResponse
+// @Router /profiles/referral/{profile}/{invite} [post]
+// @Security Bearer
+// @tags Profiles & Settings
+func (app *appContext) EnableReferralForProfile(gc *gin.Context) {
+ profileName := gc.Param("profile")
+ invCode := gc.Param("invite")
+ inv, ok := app.storage.GetInvitesKey(invCode)
+ if !ok {
+ respond(400, "Invalid invite code", gc)
+ app.err.Printf("\"%s\": Failed to enable referrals: invite not found", profileName)
+ return
+ }
+ profile, ok := app.storage.GetProfileKey(profileName)
+ if !ok {
+ respond(400, "Invalid profile", gc)
+ app.err.Printf("\"%s\": Failed to enable referrals: profile not found", profileName)
+ return
+ }
+
+ // Generate new code for referral template
+ 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
+ // Since this is a template for multiple users, ReferrerJellyfinID is not set.
+ // inv.ReferrerJellyfinID = ...
+
+ app.storage.SetInvitesKey(inv.Code, inv)
+
+ profile.ReferralTemplateKey = inv.Code
+
+ app.storage.SetProfileKey(profile.Name, profile)
+
+ respondBool(200, true, gc)
+}
+
+// @Summary Disable referrals for a profile, and removes the referral template. no-op if not enabled.
+// @Produce json
+// @Param profile path string true "name of profile to enable referrals for."
+// @Success 200 {object} boolResponse
+// @Router /profiles/referral/{profile} [delete]
+// @Security Bearer
+// @tags Profiles & Settings
+func (app *appContext) DisableReferralForProfile(gc *gin.Context) {
+ respondBool(200, true, gc)
+}
diff --git a/api-userpage.go b/api-userpage.go
index 30f50a6..367448a 100644
--- a/api-userpage.go
+++ b/api-userpage.go
@@ -646,7 +646,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
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.
+ // 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 {
diff --git a/api-users.go b/api-users.go
index 43a9acb..ce5441a 100644
--- a/api-users.go
+++ b/api-users.go
@@ -657,6 +657,7 @@ func (app *appContext) EnableReferralForUsers(gc *gin.Context) {
return
}
+ app.debug.Printf("Found referral template in profile: %+v\n", profile.ReferralTemplateKey)
} else if mode == "invite" {
// Get the invite, and modify it to turn it into a referral
err := app.storage.db.Get(source, &baseInv)
diff --git a/config/config-base.json b/config/config-base.json
index f567927..dd96956 100644
--- a/config/config-base.json
+++ b/config/config-base.json
@@ -385,7 +385,7 @@
"enabled": {
"name": "Enabled",
"required": false,
- "requires_restart": false,
+ "requires_restart": true,
"type": "bool",
"value": true
},
@@ -409,7 +409,7 @@
"referrals": {
"name": "User Referrals",
"required": false,
- "requires_restart": false,
+ "requires_restart": true,
"type": "bool",
"value": true,
"description": "Users are given their own \"invite\" to send to others."
diff --git a/html/admin.html b/html/admin.html
index 538cbc6..8eac284 100644
--- a/html/admin.html
+++ b/html/admin.html
@@ -135,6 +135,20 @@
+
{{ end }}
{{ .strings.modifySettings }}
- {{ .strings.enableReferrals }}
+ {{ if .referralsEnabled }}
+ {{ .strings.enableReferrals }}
+ {{ end }}
{{ .strings.extendExpiry }}
{{ .strings.disable }}
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json
index 354f7ad..d799318 100644
--- a/lang/admin/en-us.json
+++ b/lang/admin/en-us.json
@@ -65,7 +65,8 @@
"modifySettings": "Modify Settings",
"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.",
+ "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.",
"applyHomescreenLayout": "Apply homescreen layout",
"sendDeleteNotificationEmail": "Send notification message",
"sendDeleteNotifiationExample": "Your account has been deleted.",
@@ -135,6 +136,7 @@
"updateAppliedRefresh": "Update applied, please refresh.",
"telegramVerified": "Telegram account verified.",
"accountConnected": "Account connected.",
+ "referralsEnabled": "Referrals enabled.",
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
"errorSettingsFailed": "Application failed.",
diff --git a/lang/common/en-us.json b/lang/common/en-us.json
index 43539db..1c72c34 100644
--- a/lang/common/en-us.json
+++ b/lang/common/en-us.json
@@ -39,7 +39,8 @@
"add": "Add",
"edit": "Edit",
"delete": "Delete",
- "myAccount": "My Account"
+ "myAccount": "My Account",
+ "referrals": "Referrals"
},
"notifications": {
"errorLoginBlank": "The username and/or password were left blank.",
diff --git a/models.go b/models.go
index c075193..357b0b7 100644
--- a/models.go
+++ b/models.go
@@ -71,10 +71,11 @@ type inviteProfileDTO struct {
}
type profileDTO struct {
- Admin bool `json:"admin" example:"false"` // Whether profile has admin rights or not
- LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
- FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
- Ombi bool `json:"ombi"` // Whether or not Ombi settings are stored in this profile.
+ Admin bool `json:"admin" example:"false"` // Whether profile has admin rights or not
+ LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
+ FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
+ Ombi bool `json:"ombi"` // Whether or not Ombi settings are stored in this profile.
+ ReferralsEnabled bool `json:"referrals_enabled" example:"true"` // Whether or not the profile has referrals enabled, and has a template invite stored.
}
type getProfilesDTO struct {
@@ -423,6 +424,5 @@ type GetMyReferralRespDTO struct {
}
type EnableDisableReferralDTO struct {
- Users []string `json:"users"`
- Enabled bool `json:"enabled"`
+ Users []string `json:"users"`
}
diff --git a/router.go b/router.go
index bc2ad70..4042c87 100644
--- a/router.go
+++ b/router.go
@@ -228,6 +228,8 @@ 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+"/profiles/referral/:profile/:invite", app.EnableReferralForProfile)
+ api.DELETE(p+"/profiles/referral/:profile", app.DisableReferralForProfile)
}
if userPageEnabled {
diff --git a/ts/admin.ts b/ts/admin.ts
index 7b68895..f29fe2b 100644
--- a/ts/admin.ts
+++ b/ts/admin.ts
@@ -81,6 +81,7 @@ window.availableProfiles = window.availableProfiles || [];
if (window.referralsEnabled) {
window.modals.enableReferralsUser = new Modal(document.getElementById("modal-enable-referrals-user"));
+ window.modals.enableReferralsProfile = new Modal(document.getElementById("modal-enable-referrals-profile"));
}
})();
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index a2762f9..563589c 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -1127,7 +1127,7 @@ export class accountsList {
}
}
}
- return result
+ return result;
};
@@ -1693,11 +1693,12 @@ export class accountsList {
}
innerHTML += ``;
}
+ this._enableReferralsInvite.checked = true;
} else {
this._enableReferralsInvite.checked = false;
- this._enableReferralsProfile.checked = true;
innerHTML += ``;
}
+ this._enableReferralsProfile.checked = !(this._enableReferralsInvite.checked);
this._referralsInviteSelect.innerHTML = innerHTML;
// 2. Profiles
@@ -1710,20 +1711,15 @@ export class accountsList {
});
})();
- // FIXME: Collect Profiles, Invite
- (() => {
- })();
-
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
};
+ // console.log("profile:", this._enableReferralsProfile.checked, this._enableReferralsInvite.checked);
if (this._enableReferralsProfile.checked && !this._enableReferralsInvite.checked) {
send["from"] = "profile";
send["profile"] = this._referralsProfileSelect.value;
@@ -1731,7 +1727,7 @@ export class accountsList {
send["from"] = "invite";
send["id"] = this._referralsInviteSelect.value;
}
- _post("/users/referrals/" + send["from"] + "/" + send["id"], send, (req: XMLHttpRequest) => {
+ _post("/users/referral/" + send["from"] + "/" + (send["id"] ? send["id"] : send["profile"]), send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
if (req.status == 400) {
@@ -1744,6 +1740,8 @@ export class accountsList {
}
});
};
+ this._enableReferralsProfile.checked = true;
+ this._enableReferralsInvite.checked = false;
window.modals.enableReferralsUser.show();
}
@@ -1879,10 +1877,10 @@ export class accountsList {
this._modifySettingsUser.onchange = checkSource;
if (window.referralsEnabled) {
- this._enableReferrals.onclick = this.enableReferrals;
const profileSpan = this._enableReferralsProfile.nextElementSibling as HTMLSpanElement;
const inviteSpan = this._enableReferralsInvite.nextElementSibling as HTMLSpanElement;
const checkReferralSource = () => {
+ console.log("States:", this._enableReferralsProfile.checked, this._enableReferralsInvite.checked);
if (this._enableReferralsProfile.checked) {
this._referralsInviteSelect.parentElement.classList.add("unfocused");
this._referralsProfileSelect.parentElement.classList.remove("unfocused")
@@ -1899,9 +1897,20 @@ export class accountsList {
profileSpan.classList.add("@low");
}
};
- this._enableReferralsProfile.onchange = checkReferralSource;
- this._enableReferralsInvite.onchange = checkReferralSource;
- checkReferralSource();
+ profileSpan.onclick = () => {
+ this._enableReferralsProfile.checked = true;
+ this._enableReferralsInvite.checked = false;
+ checkReferralSource();
+ };
+ inviteSpan.onclick = () => {;
+ this._enableReferralsInvite.checked = true;
+ this._enableReferralsProfile.checked = false;
+ checkReferralSource();
+ };
+ this._enableReferrals.onclick = () => {
+ this.enableReferrals();
+ profileSpan.onclick(null);
+ };
}
this._deleteUser.onclick = this.deleteUsers;
diff --git a/ts/modules/profiles.ts b/ts/modules/profiles.ts
index 39ad52c..7824ed3 100644
--- a/ts/modules/profiles.ts
+++ b/ts/modules/profiles.ts
@@ -5,6 +5,7 @@ interface Profile {
libraries: string;
fromUser: string;
ombi: boolean;
+ referrals_enabled: boolean;
}
class profile implements Profile {
@@ -16,6 +17,8 @@ class profile implements Profile {
private _fromUser: HTMLTableDataCellElement;
private _defaultRadio: HTMLInputElement;
private _ombi: boolean;
+ private _referralsButton: HTMLSpanElement;
+ private _referralsEnabled: boolean;
get name(): string { return this._name.textContent; }
set name(v: string) { this._name.textContent = v; }
@@ -51,7 +54,22 @@ class profile implements Profile {
get fromUser(): string { return this._fromUser.textContent; }
set fromUser(v: string) { this._fromUser.textContent = v; }
-
+
+ get referrals_enabled(): boolean { return this._referralsEnabled; }
+ set referrals_enabled(v: boolean) {
+ if (!window.referralsEnabled) return;
+ this._referralsEnabled = v;
+ if (v) {
+ this._referralsButton.textContent = window.lang.strings("delete");
+ this._referralsButton.classList.add("~critical");
+ this._referralsButton.classList.remove("~neutral");
+ } else {
+ this._referralsButton.textContent = window.lang.strings("add");
+ this._referralsButton.classList.add("~neutral");
+ this._referralsButton.classList.remove("~critical");
+ }
+ }
+
get default(): boolean { return this._defaultRadio.checked; }
set default(v: boolean) { this._defaultRadio.checked = v; }
@@ -64,6 +82,9 @@ class profile implements Profile {
if (window.ombiEnabled) innerHTML += `
|
`;
+ if (window.referralsEnabled) innerHTML += `
+ |
+ `;
innerHTML += `
|
|
@@ -75,6 +96,8 @@ class profile implements Profile {
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
if (window.ombiEnabled)
this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement;
+ if (window.referralsEnabled)
+ this._referralsButton = this._row.querySelector("span.profile-referrals") as HTMLSpanElement;
this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement;
this._defaultRadio.onclick = () => document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name }));
@@ -89,9 +112,11 @@ class profile implements Profile {
this.fromUser = p.fromUser;
this.libraries = p.libraries;
this.ombi = p.ombi;
+ this.referrals_enabled = p.referrals_enabled;
}
setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { this._ombiButton.onclick = () => ombiFunc(this._ombi); }
+ setReferralFunc = (referralFunc: (enabled: boolean) => void) => { this._referralsButton.onclick = () => referralFunc(this._referralsEnabled); }
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); }
@@ -143,6 +168,57 @@ export class ProfileEditor {
}
}
+ enableReferrals = (name: string) => {
+ const referralsInviteSelect = document.getElementById("enable-referrals-profile-invites") as HTMLSelectElement;
+ _get("/invites", null, (req: XMLHttpRequest) => {
+ if (req.readyState != 4 || req.status != 200) return;
+
+ let innerHTML = "";
+ let invites = req.response["invites"] as Array;
+ window.availableProfiles = req.response["profiles"];
+ if (invites) {
+ for (let inv of invites) {
+ let name = inv.code;
+ if (inv.label) {
+ name = `${inv.label} (${inv.code})`;
+ }
+ innerHTML += ``;
+ }
+ } else {
+ innerHTML += ``;
+ }
+
+ referralsInviteSelect.innerHTML = innerHTML;
+ });
+
+ const form = document.getElementById("form-enable-referrals-profile") as HTMLFormElement;
+ const button = form.querySelector("span.submit") as HTMLSpanElement;
+ form.onsubmit = (event: Event) => {
+ event.preventDefault();
+ toggleLoader(button);
+
+ let send = {
+ "profile": name,
+ "invite": referralsInviteSelect.value
+ };
+
+ _post("/profiles/referral/" + send["profile"] + "/" + send["invite"], 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.notif("referralsEnabled"));
+ }
+ window.modals.enableReferralsProfile.close();
+ this.load();
+ }
+ });
+ };
+ window.modals.profiles.close();
+ window.modals.enableReferralsProfile.show();
+ };
+
load = () => _get("/profiles", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200) {
@@ -173,6 +249,15 @@ export class ProfileEditor {
this._ombiProfiles.load(name);
}
});
+ if (window.referralsEnabled)
+ this._profiles[name].setReferralFunc((enabled: boolean) => {
+ if (enabled) {
+ // FIXME: Unlink template
+ console.log("FIXME");
+ } else {
+ this.enableReferrals(name);
+ }
+ });
this._table.appendChild(this._profiles[name].asElement());
}
}
diff --git a/ts/typings/d.ts b/ts/typings/d.ts
index 7b440c6..9befa20 100644
--- a/ts/typings/d.ts
+++ b/ts/typings/d.ts
@@ -115,6 +115,7 @@ declare interface Modals {
logs: Modal;
email?: Modal;
enableReferralsUser?: Modal;
+ enableReferralsProfile?: Modal;
}
interface Invite {