From 9c2f27bcdbf0c3673f68d3a08b2461b72539fd69 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 28 Jun 2023 16:05:24 +0100 Subject: [PATCH 01/14] 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. --- api-invites.go | 6 +++ api-userpage.go | 66 ++++++++++++++++++++++++++++++ api-users.go | 54 +++++++++++++++++++++++++ config/config-base.json | 16 ++++++++ html/admin.html | 30 ++++++++++++++ lang/admin/en-us.json | 7 ++++ models.go | 12 ++++++ router.go | 6 +++ storage.go | 47 +++++++++++---------- ts/admin.ts | 4 ++ ts/modules/accounts.ts | 90 +++++++++++++++++++++++++++++++++++++++++ ts/typings/d.ts | 2 + views.go | 1 + 13 files changed, 320 insertions(+), 21 deletions(-) diff --git a/api-invites.go b/api-invites.go index 4d94a0d..8042e2e 100644 --- a/api-invites.go +++ b/api-invites.go @@ -16,6 +16,9 @@ import ( func (app *appContext) checkInvites() { currentTime := time.Now() for _, data := range app.storage.GetInvites() { + if data.IsReferral { + continue + } expiry := data.ValidTill if !currentTime.After(expiry) { continue @@ -222,6 +225,9 @@ func (app *appContext) GetInvites(gc *gin.Context) { app.checkInvites() var invites []inviteDTO for _, inv := range app.storage.GetInvites() { + if inv.IsReferral { + continue + } _, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime) invite := inviteDTO{ Code: inv.Code, diff --git a/api-userpage.go b/api-userpage.go index a517ee8..30f50a6 100644 --- a/api-userpage.go +++ b/api-userpage.go @@ -3,11 +3,18 @@ package main import ( "net/http" "os" + "strconv" "strings" "time" "github.com/gin-gonic/gin" "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. @@ -621,3 +628,62 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) { } 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, + }) +} diff --git a/api-users.go b/api-users.go index 28a040e..43a9acb 100644 --- a/api-users.go +++ b/api-users.go @@ -3,12 +3,14 @@ package main import ( "fmt" "os" + "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" "github.com/hrfee/mediabrowser" + "github.com/lithammer/shortuuid/v3" ) // @Summary Creates a new Jellyfin user without an invite. @@ -629,6 +631,58 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) { 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. // @Produce json // @Param announcementDTO body announcementDTO true "Announcement request object" diff --git a/config/config-base.json b/config/config-base.json index 5d4fbe4..f567927 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -405,6 +405,22 @@ "depends_true": "enabled", "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." + }, + "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." } } }, diff --git a/html/admin.html b/html/admin.html index 81fd381..538cbc6 100644 --- a/html/admin.html +++ b/html/admin.html @@ -17,6 +17,7 @@ window.jellyfinLogin = {{ .jellyfinLogin }}; window.jfAdminOnly = {{ .jfAdminOnly }}; window.jfAllowAll = {{ .jfAllowAll }}; + window.referralsEnabled = {{ .referralsEnabled }}; Admin - jfa-go {{ template "header.html" . }} @@ -107,6 +108,34 @@ + {{ if .referralsEnabled }} + + {{ end }} {{ .strings.modifySettings }} + {{ .strings.enableReferrals }} {{ .strings.extendExpiry }} + {{ end }} {{ .strings.modifySettings }} - {{ .strings.enableReferrals }} + {{ if .referralsEnabled }} + {{ .strings.enableReferrals }} + {{ end }} {{ .strings.extendExpiry }} + {{ if .referralsEnabled }} +
+
+ {{ .strings.referrals }} + +
+
+
+ +
+
+
+
+ {{ end }} diff --git a/lang/admin/da-dk.json b/lang/admin/da-dk.json index da0e967..b147dc0 100644 --- a/lang/admin/da-dk.json +++ b/lang/admin/da-dk.json @@ -79,7 +79,6 @@ "inviteUsersCreated": "Oprettet brugere", "inviteNoProfile": "Ingen Profil", "inviteDateCreated": "Oprettet", - "inviteRemainingUses": "Resterende anvendelser", "inviteNoInvites": "Ingen", "inviteExpiresInTime": "Udløber om {n}", "notifyEvent": "Meddel den:", diff --git a/lang/admin/de-de.json b/lang/admin/de-de.json index bbcf259..79fb2aa 100644 --- a/lang/admin/de-de.json +++ b/lang/admin/de-de.json @@ -53,7 +53,6 @@ "inviteUsersCreated": "Erstellte Benutzer", "inviteNoProfile": "Kein Profil", "inviteDateCreated": "Erstellt", - "inviteRemainingUses": "Verbleibende Verwendungen", "inviteNoInvites": "Keine", "inviteExpiresInTime": "Läuft in {n} ab", "notifyEvent": "Benachrichtigen bei:", diff --git a/lang/admin/el-gr.json b/lang/admin/el-gr.json index e84f19b..1a2e369 100644 --- a/lang/admin/el-gr.json +++ b/lang/admin/el-gr.json @@ -56,7 +56,6 @@ "inviteUsersCreated": "Δημιουργηθέντες χρήστες", "inviteNoProfile": "Κανένα Προφίλ", "inviteDateCreated": "Δημιουργηθέντα", - "inviteRemainingUses": "Εναπομείναντες χρήσεις", "inviteNoInvites": "Καμία", "inviteExpiresInTime": "Λήγει σε {n}", "notifyEvent": "Ενημέρωση όταν:", diff --git a/lang/admin/en-gb.json b/lang/admin/en-gb.json index d716caa..cee85b9 100644 --- a/lang/admin/en-gb.json +++ b/lang/admin/en-gb.json @@ -124,7 +124,6 @@ "addProfileStoreHomescreenLayout": "Store homescreen layout", "inviteNoUsersCreated": "None yet!", "inviteUsersCreated": "Created users", - "inviteRemainingUses": "Remaining uses", "inviteNoInvites": "None", "inviteExpiresInTime": "Expires in {n}", "notifyEvent": "Notify on:", diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index 57efcf2..2e3169c 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -95,7 +95,6 @@ "inviteUsersCreated": "Created users", "inviteNoProfile": "No Profile", "inviteDateCreated": "Created", - "inviteRemainingUses": "Remaining uses", "inviteNoInvites": "None", "inviteExpiresInTime": "Expires in {n}", "notifyEvent": "Notify on:", @@ -224,4 +223,4 @@ "plural": "Extended expiry for {n} users." } } -} +} \ No newline at end of file diff --git a/lang/admin/es-es.json b/lang/admin/es-es.json index 1506e76..536022d 100644 --- a/lang/admin/es-es.json +++ b/lang/admin/es-es.json @@ -75,7 +75,6 @@ "inviteUsersCreated": "Usuarios creados", "inviteNoProfile": "Sin perfil", "inviteDateCreated": "Creado", - "inviteRemainingUses": "Usos restantes", "inviteNoInvites": "Ninguno", "inviteExpiresInTime": "Caduca en {n}", "notifyEvent": "Notificar en:", diff --git a/lang/admin/fr-fr.json b/lang/admin/fr-fr.json index cb8eb5a..01a1b54 100644 --- a/lang/admin/fr-fr.json +++ b/lang/admin/fr-fr.json @@ -55,7 +55,6 @@ "inviteUsersCreated": "Utilisateurs créés", "inviteNoProfile": "Aucun profil", "inviteDateCreated": "Créer", - "inviteRemainingUses": "Utilisations restantes", "inviteNoInvites": "Aucune", "inviteExpiresInTime": "Expires dans {n}", "notifyEvent": "Notifier sur :", diff --git a/lang/admin/hu-hu.json b/lang/admin/hu-hu.json index 4161ef0..dca6069 100644 --- a/lang/admin/hu-hu.json +++ b/lang/admin/hu-hu.json @@ -87,7 +87,6 @@ "inviteUsersCreated": "", "inviteNoProfile": "", "inviteDateCreated": "", - "inviteRemainingUses": "", "inviteNoInvites": "", "inviteExpiresInTime": "", "notifyEvent": "", diff --git a/lang/admin/id-id.json b/lang/admin/id-id.json index 5957396..365ed65 100644 --- a/lang/admin/id-id.json +++ b/lang/admin/id-id.json @@ -56,7 +56,6 @@ "inviteUsersCreated": "Pengguna yang telah dibuat", "inviteNoProfile": "Tidak ada profil", "inviteDateCreated": "Dibuat", - "inviteRemainingUses": "Penggunaan yang tersisa", "inviteNoInvites": "Tidak ada", "inviteExpiresInTime": "Kadaluarsa dalam {n}", "notifyEvent": "Beritahu pada:", diff --git a/lang/admin/nl-nl.json b/lang/admin/nl-nl.json index 46b9452..a8e9172 100644 --- a/lang/admin/nl-nl.json +++ b/lang/admin/nl-nl.json @@ -53,7 +53,6 @@ "inviteUsersCreated": "Aangemaakte gebruikers", "inviteNoProfile": "Geen profiel", "inviteDateCreated": "Aangemaakt", - "inviteRemainingUses": "Resterend aantal keer te gebruiken", "inviteNoInvites": "Geen", "inviteExpiresInTime": "Verloopt over {n}", "notifyEvent": "Meldingen:", diff --git a/lang/admin/pl-pl.json b/lang/admin/pl-pl.json index 6b1bdc2..fddce21 100644 --- a/lang/admin/pl-pl.json +++ b/lang/admin/pl-pl.json @@ -87,7 +87,6 @@ "inviteUsersCreated": "", "inviteNoProfile": "", "inviteDateCreated": "Utworzone", - "inviteRemainingUses": "", "inviteNoInvites": "", "inviteExpiresInTime": "", "notifyEvent": "", diff --git a/lang/admin/pt-br.json b/lang/admin/pt-br.json index 0310937..37b63eb 100644 --- a/lang/admin/pt-br.json +++ b/lang/admin/pt-br.json @@ -54,7 +54,6 @@ "inviteUsersCreated": "Usuários criado", "inviteNoProfile": "Sem Perfil", "inviteDateCreated": "Criado", - "inviteRemainingUses": "Uso restantes", "inviteNoInvites": "Nenhum", "inviteExpiresInTime": "Expira em {n}", "notifyEvent": "Notificar em:", diff --git a/lang/admin/sv-se.json b/lang/admin/sv-se.json index 1f7e1f1..823167c 100644 --- a/lang/admin/sv-se.json +++ b/lang/admin/sv-se.json @@ -65,7 +65,6 @@ "inviteUsersCreated": "Skapade användare", "inviteNoProfile": "Ingen profil", "inviteDateCreated": "Skapad", - "inviteRemainingUses": "Återstående användningar", "inviteNoInvites": "Ingen", "inviteExpiresInTime": "Går ut om {n}", "notifyEvent": "Meddela den:", diff --git a/lang/admin/vi-vn.json b/lang/admin/vi-vn.json index 7dd7e56..3ad6ba2 100644 --- a/lang/admin/vi-vn.json +++ b/lang/admin/vi-vn.json @@ -86,7 +86,6 @@ "inviteUsersCreated": "Người dùng đã tạo", "inviteNoProfile": "Không có Tài khoản mẫu", "inviteDateCreated": "Tạo", - "inviteRemainingUses": "Số lần sử dụng còn lại", "inviteNoInvites": "Không có", "inviteExpiresInTime": "Hết hạn trong {n}", "notifyEvent": "Thông báo khi:", diff --git a/lang/admin/zh-hans.json b/lang/admin/zh-hans.json index 6190c8f..0309721 100644 --- a/lang/admin/zh-hans.json +++ b/lang/admin/zh-hans.json @@ -80,7 +80,6 @@ "inviteUsersCreated": "已创建的用户", "inviteNoProfile": "没有个人资料", "inviteDateCreated": "已创建", - "inviteRemainingUses": "剩余使用次数", "inviteNoInvites": "无", "inviteExpiresInTime": "在 {n} 到期", "notifyEvent": "通知:", diff --git a/lang/admin/zh-hant.json b/lang/admin/zh-hant.json index 5546e09..2cbf43b 100644 --- a/lang/admin/zh-hant.json +++ b/lang/admin/zh-hant.json @@ -87,7 +87,6 @@ "inviteUsersCreated": "創建的帳戶", "inviteNoProfile": "無資料", "inviteDateCreated": "已創建", - "inviteRemainingUses": "剩餘使用次數", "inviteNoInvites": "無", "inviteExpiresInTime": "在 {n} 到期", "notifyEvent": "通知:", diff --git a/lang/common/da-dk.json b/lang/common/da-dk.json index ce3fb1d..3a196a7 100644 --- a/lang/common/da-dk.json +++ b/lang/common/da-dk.json @@ -35,7 +35,8 @@ "expiry": "Udløb", "add": "Tilføj", "edit": "Rediger", - "delete": "Slet" + "delete": "Slet", + "inviteRemainingUses": "Resterende anvendelser" }, "notifications": { "errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.", diff --git a/lang/common/de-de.json b/lang/common/de-de.json index 6459eef..024a4de 100644 --- a/lang/common/de-de.json +++ b/lang/common/de-de.json @@ -35,7 +35,8 @@ "expiry": "Ablaufdatum", "add": "Hinzufügen", "edit": "Bearbeiten", - "delete": "Löschen" + "delete": "Löschen", + "inviteRemainingUses": "Verbleibende Verwendungen" }, "notifications": { "errorLoginBlank": "Der Benutzername und/oder das Passwort wurden nicht ausgefüllt.", diff --git a/lang/common/el-gr.json b/lang/common/el-gr.json index 3c0c5c9..8a06da8 100644 --- a/lang/common/el-gr.json +++ b/lang/common/el-gr.json @@ -25,7 +25,8 @@ "disable": "Απενεργοποίηση", "expiry": "Λήξη", "edit": "Επεξεργασία", - "delete": "Διαγραφή" + "delete": "Διαγραφή", + "inviteRemainingUses": "Εναπομείναντες χρήσεις" }, "notifications": { "errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.", diff --git a/lang/common/en-gb.json b/lang/common/en-gb.json index ff6147b..6283091 100644 --- a/lang/common/en-gb.json +++ b/lang/common/en-gb.json @@ -35,7 +35,8 @@ "expiry": "Expiry", "add": "Add", "edit": "Edit", - "delete": "Delete" + "delete": "Delete", + "inviteRemainingUses": "Remaining uses" }, "notifications": { "errorLoginBlank": "The username and/or password was left blank.", diff --git a/lang/common/en-us.json b/lang/common/en-us.json index 1c72c34..57e4046 100644 --- a/lang/common/en-us.json +++ b/lang/common/en-us.json @@ -40,7 +40,8 @@ "edit": "Edit", "delete": "Delete", "myAccount": "My Account", - "referrals": "Referrals" + "referrals": "Referrals", + "inviteRemainingUses": "Remaining uses" }, "notifications": { "errorLoginBlank": "The username and/or password were left blank.", @@ -63,4 +64,4 @@ "plural": "{n} Days" } } -} +} \ No newline at end of file diff --git a/lang/common/es-es.json b/lang/common/es-es.json index f0bec53..f28e38d 100644 --- a/lang/common/es-es.json +++ b/lang/common/es-es.json @@ -35,7 +35,8 @@ "expiry": "Expiración", "add": "Agregar", "edit": "Editar", - "delete": "Eliminar" + "delete": "Eliminar", + "inviteRemainingUses": "Usos restantes" }, "notifications": { "errorLoginBlank": "El nombre de usuario y/o la contraseña se dejaron en blanco.", diff --git a/lang/common/fr-fr.json b/lang/common/fr-fr.json index d7dfd5c..78abba9 100644 --- a/lang/common/fr-fr.json +++ b/lang/common/fr-fr.json @@ -35,7 +35,8 @@ "expiry": "Expiration", "add": "Ajouter", "edit": "Éditer", - "delete": "Effacer" + "delete": "Effacer", + "inviteRemainingUses": "Utilisations restantes" }, "notifications": { "errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.", diff --git a/lang/common/id-id.json b/lang/common/id-id.json index e8f4e9b..c6b10b5 100644 --- a/lang/common/id-id.json +++ b/lang/common/id-id.json @@ -19,7 +19,8 @@ "login": "Masuk", "logout": "Keluar", "edit": "Edit", - "delete": "Hapus" + "delete": "Hapus", + "inviteRemainingUses": "Penggunaan yang tersisa" }, "notifications": { "errorLoginBlank": "Nama pengguna dan / atau sandi kosong.", diff --git a/lang/common/it-it.json b/lang/common/it-it.json index 58d5021..9a5db2d 100644 --- a/lang/common/it-it.json +++ b/lang/common/it-it.json @@ -28,4 +28,4 @@ }, "notifications": {}, "quantityStrings": {} -} +} \ No newline at end of file diff --git a/lang/common/nds.json b/lang/common/nds.json new file mode 100644 index 0000000..a7f6836 --- /dev/null +++ b/lang/common/nds.json @@ -0,0 +1,8 @@ +{ + "meta": { + "name": "Nedderdütsch (NDS)" + }, + "strings": {}, + "notifications": {}, + "quantityStrings": {} +} \ No newline at end of file diff --git a/lang/common/nl-nl.json b/lang/common/nl-nl.json index 0591de1..cb5213d 100644 --- a/lang/common/nl-nl.json +++ b/lang/common/nl-nl.json @@ -35,7 +35,8 @@ "expiry": "Verloop", "add": "Voeg toe", "edit": "Bewerken", - "delete": "Verwijderen" + "delete": "Verwijderen", + "inviteRemainingUses": "Resterend aantal keer te gebruiken" }, "notifications": { "errorLoginBlank": "De gebruikersnaam en/of wachtwoord is leeg.", diff --git a/lang/common/pt-br.json b/lang/common/pt-br.json index b7953fa..006bbb6 100644 --- a/lang/common/pt-br.json +++ b/lang/common/pt-br.json @@ -35,7 +35,8 @@ "expiry": "Expira", "add": "Adicionar", "edit": "Editar", - "delete": "Deletar" + "delete": "Deletar", + "inviteRemainingUses": "Uso restantes" }, "notifications": { "errorLoginBlank": "O nome de usuário e/ou senha foram deixados em branco.", diff --git a/lang/common/sl-si.json b/lang/common/sl-si.json index 95187bd..9ecaecd 100644 --- a/lang/common/sl-si.json +++ b/lang/common/sl-si.json @@ -28,4 +28,4 @@ }, "notifications": {}, "quantityStrings": {} -} +} \ No newline at end of file diff --git a/lang/common/sv-se.json b/lang/common/sv-se.json index 0eb8b5f..ff602c8 100644 --- a/lang/common/sv-se.json +++ b/lang/common/sv-se.json @@ -22,7 +22,8 @@ "disabled": "Inaktiverad", "expiry": "Löper ut", "edit": "Redigera", - "delete": "Radera" + "delete": "Radera", + "inviteRemainingUses": "Återstående användningar" }, "notifications": { "errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.", diff --git a/lang/common/vi-vn.json b/lang/common/vi-vn.json index 3a8abbd..f4584ed 100644 --- a/lang/common/vi-vn.json +++ b/lang/common/vi-vn.json @@ -13,7 +13,8 @@ "expiry": "Hết hạn", "add": "Thêm", "edit": "Chỉnh sửa", - "delete": "Xóa" + "delete": "Xóa", + "inviteRemainingUses": "Số lần sử dụng còn lại" }, "notifications": { "errorConnection": "Không thể kết nối với jfa-go.", diff --git a/lang/common/zh-hans.json b/lang/common/zh-hans.json index 2d12e74..faab5b4 100644 --- a/lang/common/zh-hans.json +++ b/lang/common/zh-hans.json @@ -35,7 +35,8 @@ "expiry": "到期", "add": "添加", "edit": "编辑", - "delete": "删除" + "delete": "删除", + "inviteRemainingUses": "剩余使用次数" }, "notifications": { "errorLoginBlank": "用户名/密码留空。", diff --git a/lang/common/zh-hant.json b/lang/common/zh-hant.json index 8b0b42f..87c4409 100644 --- a/lang/common/zh-hant.json +++ b/lang/common/zh-hant.json @@ -35,7 +35,8 @@ "expiry": "到期", "add": "添加", "edit": "編輯", - "delete": "刪除" + "delete": "刪除", + "inviteRemainingUses": "剩餘使用次數" }, "notifications": { "errorLoginBlank": "帳戶名稱和/或密碼留空。", diff --git a/lang/email/it-it.json b/lang/email/it-it.json index fe844bc..89b12b8 100644 --- a/lang/email/it-it.json +++ b/lang/email/it-it.json @@ -49,4 +49,4 @@ "clickBelow": "", "confirmEmail": "" } -} +} \ No newline at end of file diff --git a/lang/form/en-us.json b/lang/form/en-us.json index 5fd7b4c..e89aaf5 100644 --- a/lang/form/en-us.json +++ b/lang/form/en-us.json @@ -34,7 +34,9 @@ "resetPasswordThroughLink": "To reset your password, enter your username, email address or a linked contact method username, and submit. A link will be sent to reset your password.", "resetSent": "Reset Sent.", "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" + "changePassword": "Change Password", + "referralsDescription": "Invite friends & family to Jellyfin with this link. Come back here for a new one if it expires.", + "copyReferral": "Copy Link" }, "notifications": { "errorUserExists": "User already exists.", @@ -76,4 +78,4 @@ "plural": "Must have at least {n} special characters" } } -} +} \ No newline at end of file diff --git a/lang/form/it-it.json b/lang/form/it-it.json index aace6a0..1a7c9ab 100644 --- a/lang/form/it-it.json +++ b/lang/form/it-it.json @@ -48,4 +48,4 @@ "plural": "Deve avere almeno {n} caratteri speciali" } } -} +} \ No newline at end of file diff --git a/lang/form/sl-si.json b/lang/form/sl-si.json index fff664a..46dadbb 100644 --- a/lang/form/sl-si.json +++ b/lang/form/sl-si.json @@ -57,4 +57,4 @@ "plural": "Potrebnih je vsaj {n} posebnih znakov" } } -} +} \ No newline at end of file diff --git a/lang/pwreset/sl-si.json b/lang/pwreset/sl-si.json index f867af0..2168146 100644 --- a/lang/pwreset/sl-si.json +++ b/lang/pwreset/sl-si.json @@ -13,4 +13,4 @@ "changeYourPassword": "Spremenite svoje geslo po prijavi.", "enterYourPassword": "Vnesite svoje novo geslo spodaj." } -} +} \ No newline at end of file diff --git a/lang/setup/en-us.json b/lang/setup/en-us.json index eac01bf..1159d34 100644 --- a/lang/setup/en-us.json +++ b/lang/setup/en-us.json @@ -157,4 +157,4 @@ "emailMessage": "Email Message", "emailMessageNotice": "Displays at the bottom of emails." } -} +} \ No newline at end of file diff --git a/lang/setup/nds.json b/lang/setup/nds.json index d45cc97..3cd0f7b 100644 --- a/lang/setup/nds.json +++ b/lang/setup/nds.json @@ -149,4 +149,4 @@ "emailMessage": "", "emailMessageNotice": "" } -} +} \ No newline at end of file diff --git a/lang/setup/sl-si.json b/lang/setup/sl-si.json index 9d7a172..0b770ea 100644 --- a/lang/setup/sl-si.json +++ b/lang/setup/sl-si.json @@ -146,4 +146,4 @@ "emailMessage": "", "emailMessageNotice": "" } -} +} \ No newline at end of file diff --git a/lang/telegram/it-it.json b/lang/telegram/it-it.json index 3273db0..c03f59c 100644 --- a/lang/telegram/it-it.json +++ b/lang/telegram/it-it.json @@ -13,4 +13,4 @@ "languageSet": "", "discordDMs": "" } -} +} \ No newline at end of file diff --git a/lang/telegram/nds.json b/lang/telegram/nds.json index 2b8ad0a..26d8053 100644 --- a/lang/telegram/nds.json +++ b/lang/telegram/nds.json @@ -13,4 +13,4 @@ "languageSet": "", "discordDMs": "" } -} +} \ No newline at end of file diff --git a/lang/telegram/sl-si.json b/lang/telegram/sl-si.json index 96a211d..616961d 100644 --- a/lang/telegram/sl-si.json +++ b/lang/telegram/sl-si.json @@ -13,4 +13,4 @@ "languageSet": "Jezik nastavljen na {language}.", "discordDMs": "Prosimo preverite svoja zasebna sporočila za odziv." } -} +} \ No newline at end of file diff --git a/models.go b/models.go index ee6e089..d9ac8aa 100644 --- a/models.go +++ b/models.go @@ -390,6 +390,7 @@ type MyDetailsDTO struct { Discord *MyDetailsContactMethodsDTO `json:"discord,omitempty"` Telegram *MyDetailsContactMethodsDTO `json:"telegram,omitempty"` Matrix *MyDetailsContactMethodsDTO `json:"matrix,omitempty"` + HasReferrals bool `json:"has_referrals,omitempty"` } type MyDetailsContactMethodsDTO struct { @@ -418,10 +419,10 @@ type ChangeMyPasswordDTO struct { } 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 + 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 } type EnableDisableReferralDTO struct { diff --git a/scripts/langmover/common.json b/scripts/langmover/common.json index 3d078d3..a4421d5 100644 --- a/scripts/langmover/common.json +++ b/scripts/langmover/common.json @@ -38,7 +38,10 @@ "expiry": "common", "add": "common", "edit": "common", - "delete": "admin" + "delete": "common", + "myAccount": "common", + "referrals": "common", + "inviteRemainingUses": "admin" }, "notifications": { "errorLoginBlank": "common", diff --git a/ts/user.ts b/ts/user.ts index 56401f6..b7bfaba 100644 --- a/ts/user.ts +++ b/ts/user.ts @@ -1,7 +1,7 @@ import { ThemeManager } from "./modules/theme.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { Modal } from "./modules/modal.js"; -import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader, addLoader, removeLoader } from "./modules/common.js"; +import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader, addLoader, removeLoader, toClipboard } from "./modules/common.js"; import { Login } from "./modules/login.js"; import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js"; import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js"; @@ -18,6 +18,7 @@ interface userWindow extends Window { matrixUserID: string; discordSendPINMessage: string; pwrEnabled: string; + referralsEnabled: boolean; } declare var window: userWindow; @@ -107,6 +108,14 @@ interface MyDetails { discord?: MyDetailsContactMethod; telegram?: MyDetailsContactMethod; matrix?: MyDetailsContactMethod; + has_referrals: boolean; +} + +interface MyReferral { + code: string; + remaining_uses: string; + no_limit: boolean; + expiry: number; } interface ContactDTO { @@ -513,6 +522,62 @@ document.addEventListener("details-reload", () => { } else if (!statusCard.classList.contains("unfocused")) { setBestRowSpan(passwordCard, true); } + + let referralCard = document.getElementById("card-referrals"); + if (window.referralsEnabled && typeof(referralCard) != "undefined" && referralCard != null) { + if (details.has_referrals) { + _get("/my/referral", null, (req: XMLHttpRequest) => { + if (req.readyState != 4 || req.status != 200) return; + const referral: MyReferral = req.response as MyReferral; + const infoArea = referralCard.querySelector(".user-referrals-info") as HTMLDivElement; + + infoArea.innerHTML = ` +
+
+ ${referral.no_limit ? "∞" : referral.remaining_uses} ${window.lang.strings("inviteRemainingUses")} +
+
+
+
+ ${window.lang.strings("expiry")} ${toDateString(new Date(referral.expiry * 1000))} +
+
+ `; + + const linkButton = referralCard.querySelector(".user-referrals-button") as HTMLButtonElement; + + let codeLink = window.location.href; + for (let split of ["#", "?", "account", "my"]) { + codeLink = codeLink.split(split)[0]; + } + if (codeLink.slice(-1) != "/") { codeLink += "/"; } + codeLink = codeLink + "invite/" + referral.code; + + linkButton.addEventListener("click", () => { + toClipboard(codeLink); + const content = linkButton.innerHTML; + linkButton.innerHTML = ` + ${window.lang.strings("copied")} + `; + linkButton.classList.add("~positive"); + linkButton.classList.remove("~info"); + setTimeout(() => { + linkButton.classList.add("~info"); + linkButton.classList.remove("~positive"); + linkButton.innerHTML = content; + }, 2000); + }); + + + + + referralCard.classList.remove("unfocused"); + + }); + } else { + referralCard.classList.add("unfocused"); + } + } } }); }); diff --git a/views.go b/views.go index 8b2bcf3..2d311ec 100644 --- a/views.go +++ b/views.go @@ -204,6 +204,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) { "langName": lang, "jfLink": app.config.Section("ui").Key("redirect_url").String(), "requirements": app.validator.getCriteria(), + "referralsEnabled": app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false), } if telegramEnabled { data["telegramUsername"] = app.telegram.username From 41c092f578c6fdd5e609c0d117c67ee8714f43f2 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 7 Sep 2023 20:19:25 +0100 Subject: [PATCH 13/14] referrals: show referrer username on form --- html/form.html | 9 ++++++--- lang/form/en-us.json | 5 +++-- views.go | 9 +++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/html/form.html b/html/form.html index 6bd2638..787cdb0 100644 --- a/html/form.html +++ b/html/form.html @@ -43,7 +43,7 @@
-
+
{{ if .passwordReset }} {{ .strings.passwordReset }} @@ -53,11 +53,14 @@ {{ if .passwordReset }} - {{ .strings.enterYourPassword }} + {{ .strings.enterYourPassword }} {{ else }} - {{ .helpMessage }} + {{ .helpMessage }} {{ end }} + {{ if .fromUser }} + {{ .strings.invitedBy }} {{ .fromUser }} + {{ end }}
diff --git a/lang/form/en-us.json b/lang/form/en-us.json index e89aaf5..14f7acf 100644 --- a/lang/form/en-us.json +++ b/lang/form/en-us.json @@ -36,7 +36,8 @@ "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.", - "copyReferral": "Copy Link" + "copyReferral": "Copy Link", + "invitedBy": "Invited By" }, "notifications": { "errorUserExists": "User already exists.", @@ -78,4 +79,4 @@ "plural": "Must have at least {n} special characters" } } -} \ No newline at end of file +} diff --git a/views.go b/views.go index 2d311ec..704a35d 100644 --- a/views.go +++ b/views.go @@ -619,6 +619,14 @@ func (app *appContext) InviteProxy(gc *gin.Context) { } userPageAddress += "/my/account" + fromUser := "" + if inv.ReferrerJellyfinID != "" { + sender, status, err := app.jf.UserByID(inv.ReferrerJellyfinID, false) + if status == 200 && err == nil { + fromUser = sender.Name + } + } + data := gin.H{ "urlBase": app.getURLBase(gc), "cssClass": app.cssClass, @@ -654,6 +662,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { "reCAPTCHASiteKey": app.config.Section("captcha").Key("recaptcha_site_key").MustString(""), "userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false), "userPageAddress": userPageAddress, + "fromUser": fromUser, } if telegram { data["telegramPIN"] = app.telegram.NewAuthToken() From b08527bce2af6fd1545591cfb0c015d46519018f Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 7 Sep 2023 20:42:40 +0100 Subject: [PATCH 14/14] userpage: cleanup referral code moved to its own class, like the expiry card. --- ts/user.ts | 156 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 108 insertions(+), 48 deletions(-) diff --git a/ts/user.ts b/ts/user.ts index b7bfaba..05afdb9 100644 --- a/ts/user.ts +++ b/ts/user.ts @@ -113,7 +113,7 @@ interface MyDetails { interface MyReferral { code: string; - remaining_uses: string; + remaining_uses: number; no_limit: boolean; expiry: number; } @@ -246,6 +246,107 @@ class ContactMethods { }; } +class ReferralCard { + private _card: HTMLElement; + private _code: string; + private _url: string; + private _expiry: Date; + private _expiryUnix: number; + private _remainingUses: number; + private _noLimit: boolean; + + private _button: HTMLButtonElement; + private _infoArea: HTMLDivElement; + private _remainingUsesEl: HTMLSpanElement; + private _expiryEl: HTMLSpanElement; + + get code(): string { return this._code; } + set code(c: string) { + this._code = c; + let url = window.location.href; + for (let split of ["#", "?", "account", "my"]) { + url = url.split(split)[0]; + } + if (url.slice(-1) != "/") { url += "/"; } + url = url + "invite/" + this._code; + this._url = url; + } + + get remaining_uses(): number { return this._remainingUses; } + set remaining_uses(v: number) { + this._remainingUses = v; + if (v > 0 && !(this._noLimit)) + this._remainingUsesEl.textContent = `${v}`; + } + + get no_limit(): boolean { return this._noLimit; } + set no_limit(v: boolean) { + this._noLimit = v; + if (v) + this._remainingUsesEl.textContent = `∞`; + else + this._remainingUsesEl.textContent = `${this._remainingUses}`; + } + + get expiry(): Date { return this._expiry; }; + set expiry(expiryUnix: number) { + this._expiryUnix = expiryUnix; + this._expiry = new Date(expiryUnix * 1000); + this._expiryEl.textContent = toDateString(this._expiry); + } + + 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._infoArea.innerHTML = ` +
+
+ ${window.lang.strings("inviteRemainingUses")} +
+
+
+
+ ${window.lang.strings("expiry")} +
+
+ `; + + this._remainingUsesEl = this._infoArea.querySelector(".referral-remaining-uses") as HTMLSpanElement; + this._expiryEl = this._infoArea.querySelector(".referral-expiry") as HTMLSpanElement; + + document.addEventListener("timefmt-change", () => { + this.expiry = this._expiryUnix; + }); + + this._button.addEventListener("click", () => { + toClipboard(this._url); + const content = this._button.innerHTML; + this._button.innerHTML = ` + ${window.lang.strings("copied")} + `; + this._button.classList.add("~positive"); + this._button.classList.remove("~info"); + setTimeout(() => { + this._button.classList.add("~info"); + this._button.classList.remove("~positive"); + this._button.innerHTML = content; + }, 2000); + }); + } + + hide = () => this._card.classList.add("unfocused"); + + update = (referral: MyReferral) => { + this.code = referral.code; + this.remaining_uses = referral.remaining_uses; + this.no_limit = referral.no_limit; + this.expiry = referral.expiry; + this._card.classList.remove("unfocused"); + }; +} + class ExpiryCard { private _card: HTMLElement; private _expiry: Date; @@ -327,6 +428,9 @@ class ExpiryCard { var expiryCard = new ExpiryCard(statusCard); +var referralCard: ReferralCard; +if (window.referralsEnabled) referralCard = new ReferralCard(document.getElementById("card-referrals")); + var contactMethodList = new ContactMethods(contactCard); const addEditEmail = (add: boolean): void => { @@ -523,59 +627,15 @@ document.addEventListener("details-reload", () => { setBestRowSpan(passwordCard, true); } - let referralCard = document.getElementById("card-referrals"); - if (window.referralsEnabled && typeof(referralCard) != "undefined" && referralCard != null) { + if (window.referralsEnabled) { if (details.has_referrals) { _get("/my/referral", null, (req: XMLHttpRequest) => { if (req.readyState != 4 || req.status != 200) return; const referral: MyReferral = req.response as MyReferral; - const infoArea = referralCard.querySelector(".user-referrals-info") as HTMLDivElement; - - infoArea.innerHTML = ` -
-
- ${referral.no_limit ? "∞" : referral.remaining_uses} ${window.lang.strings("inviteRemainingUses")} -
-
-
-
- ${window.lang.strings("expiry")} ${toDateString(new Date(referral.expiry * 1000))} -
-
- `; - - const linkButton = referralCard.querySelector(".user-referrals-button") as HTMLButtonElement; - - let codeLink = window.location.href; - for (let split of ["#", "?", "account", "my"]) { - codeLink = codeLink.split(split)[0]; - } - if (codeLink.slice(-1) != "/") { codeLink += "/"; } - codeLink = codeLink + "invite/" + referral.code; - - linkButton.addEventListener("click", () => { - toClipboard(codeLink); - const content = linkButton.innerHTML; - linkButton.innerHTML = ` - ${window.lang.strings("copied")} - `; - linkButton.classList.add("~positive"); - linkButton.classList.remove("~info"); - setTimeout(() => { - linkButton.classList.add("~info"); - linkButton.classList.remove("~positive"); - linkButton.innerHTML = content; - }, 2000); - }); - - - - - referralCard.classList.remove("unfocused"); - + referralCard.update(referral); }); } else { - referralCard.classList.add("unfocused"); + referralCard.hide(); } } }