Compare commits
40 Commits
1c9fdae929
...
992572494a
Author | SHA1 | Date | |
---|---|---|---|
|
992572494a | ||
084f8aa658 | |||
084a62e60f | |||
655dc88c62 | |||
46109d1ea3 | |||
f7d931be0c | |||
8d6af53e54 | |||
8d3bd52fc5 | |||
8da95ed824 | |||
8207a75820 | |||
2c48ce0152 | |||
dae0ad1de5 | |||
7c76b58ab8 | |||
4ea2dfdfb7 | |||
d8d478a95e | |||
4c20250888 | |||
f5a15905e4 | |||
53742e5ec2 | |||
504c75566a | |||
ed4dcbac3b | |||
a0f1cd5814 | |||
4607a30e6a | |||
fca370b9d9 | |||
dc3f1661e8 | |||
463fe97b29 | |||
b08527bce2 | |||
41c092f578 | |||
311ecb7030 | |||
0a82f889f3 | |||
00e6da520d | |||
0b830e9b5e | |||
468b2f3284 | |||
db21131185 | |||
7d9555fdf7 | |||
729552a827 | |||
cdc8f9af4b | |||
9e5034ebab | |||
|
6ede094034 | ||
c2f835c897 | |||
9c2f27bcdb |
15
README.md
@ -38,9 +38,12 @@ a rewrite of [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) (or
|
|||||||
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
|
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
|
||||||
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
|
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
|
||||||
* Telegram/Discord/Matrix Integration: Verify users via a chat bot, and send Password Resets, Announcements, etc. through it.
|
* Telegram/Discord/Matrix Integration: Verify users via a chat bot, and send Password Resets, Announcements, etc. through it.
|
||||||
|
* "My Account" Page: Allows users to reset their password, manage contact details, view their account expiry date, and send referrals. Custom messages can be added, with markdown.
|
||||||
|
* Referrals: Users can be given special invites to send to their friends and families.
|
||||||
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
||||||
* Email addresses can optionally be used instead of usernames
|
* Email addresses can optionally be used instead of usernames
|
||||||
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to them via email/telegram.
|
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to them via email/telegram.
|
||||||
|
* Can also be done through the "My Account" page if enabled.
|
||||||
* Admin Notifications: Get notified when someone creates an account, or an invite expires.
|
* Admin Notifications: Get notified when someone creates an account, or an invite expires.
|
||||||
* 📣 Announcements: Bulk message your users with announcements about your server.
|
* 📣 Announcements: Bulk message your users with announcements about your server.
|
||||||
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
|
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
|
||||||
@ -52,13 +55,10 @@ a rewrite of [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) (or
|
|||||||
|
|
||||||
#### Interface
|
#### Interface
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="images/demo.gif" width="100%"></img>
|
<img src="images/invites.png" width="47%" style="margin-left: 1.5%;" align="top" alt="Invites tab"></img>
|
||||||
</p>
|
<img src="images/create.png" width="47%" style="margin-right: 1.5%;" align="top" alt="Accounts creation"></img>
|
||||||
|
<img src="images/myaccount.png" width="47%" style="margin-left: 1.5%; margin-top: 1rem;" align="top" alt="My Account Page"></img>
|
||||||
<p align="center">
|
<img src="images/accounts.png" width="47%" style="margin-right: 1.5%; margin-top: 1rem;" align="top" alt="Accounts tab"></img>
|
||||||
<img src="images/invites.png" width="31%" style="margin-left: 1.5%;" alt="Invites tab"></img>
|
|
||||||
<img src="images/accounts.png" width="31%" style="margin-right: 1.5%;" alt="Accounts tab"></img>
|
|
||||||
<img src="images/create.png" width="31%" style="margin-right: 1.5%;" alt="Accounts creation"></img>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
#### Install
|
#### Install
|
||||||
@ -175,3 +175,4 @@ For translations, use the weblate instance [here](https://weblate.jfa-go.com/eng
|
|||||||
Big thanks to those who sponsor me. You can see them below:
|
Big thanks to those who sponsor me. You can see them below:
|
||||||
|
|
||||||
[<img src="https://sponsors-endpoint.hrfee.pw/sponsor/avatar/0" width="35">](https://sponsors-endpoint.hrfee.pw/sponsor/profile/0)
|
[<img src="https://sponsors-endpoint.hrfee.pw/sponsor/avatar/0" width="35">](https://sponsors-endpoint.hrfee.pw/sponsor/profile/0)
|
||||||
|
[<img src="https://sponsors-endpoint.hrfee.pw/sponsor/avatar/1" width="35">](https://sponsors-endpoint.hrfee.pw/sponsor/profile/0)
|
||||||
|
@ -13,9 +13,29 @@ import (
|
|||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CAPTCHA_VALIDITY = 20 * 60 // Seconds
|
||||||
|
)
|
||||||
|
|
||||||
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() {
|
||||||
|
captchas := data.Captchas
|
||||||
|
captchasExpired := false
|
||||||
|
for key, capt := range data.Captchas {
|
||||||
|
if time.Now().After(capt.Generated.Add(CAPTCHA_VALIDITY * time.Second)) {
|
||||||
|
delete(captchas, key)
|
||||||
|
captchasExpired = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if captchasExpired {
|
||||||
|
data.Captchas = captchas
|
||||||
|
app.storage.SetInvitesKey(data.Code, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.IsReferral {
|
||||||
|
continue
|
||||||
|
}
|
||||||
expiry := data.ValidTill
|
expiry := data.ValidTill
|
||||||
if !currentTime.After(expiry) {
|
if !currentTime.After(expiry) {
|
||||||
continue
|
continue
|
||||||
@ -141,6 +161,9 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
if req.Label != "" {
|
if req.Label != "" {
|
||||||
invite.Label = req.Label
|
invite.Label = req.Label
|
||||||
}
|
}
|
||||||
|
if req.UserLabel != "" {
|
||||||
|
invite.UserLabel = req.UserLabel
|
||||||
|
}
|
||||||
invite.Created = currentTime
|
invite.Created = currentTime
|
||||||
if req.MultipleUses {
|
if req.MultipleUses {
|
||||||
if req.NoLimit {
|
if req.NoLimit {
|
||||||
@ -222,6 +245,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,
|
||||||
@ -238,6 +264,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
Profile: inv.Profile,
|
Profile: inv.Profile,
|
||||||
NoLimit: inv.NoLimit,
|
NoLimit: inv.NoLimit,
|
||||||
Label: inv.Label,
|
Label: inv.Label,
|
||||||
|
UserLabel: inv.UserLabel,
|
||||||
}
|
}
|
||||||
if len(inv.UsedBy) != 0 {
|
if len(inv.UsedBy) != 0 {
|
||||||
invite.UsedBy = map[string]int64{}
|
invite.UsedBy = map[string]int64{}
|
||||||
|
@ -162,7 +162,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
|||||||
username := app.storage.lang.Email[lang].Strings.get("username")
|
username := app.storage.lang.Email[lang].Strings.get("username")
|
||||||
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
||||||
customMessage, ok := app.storage.GetCustomContentKey(id)
|
customMessage, ok := app.storage.GetCustomContentKey(id)
|
||||||
if !ok {
|
if !ok && id != "Announcement" {
|
||||||
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
|
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/lithammer/shortuuid/v3"
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,13 +21,23 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
|
|||||||
DefaultProfile: app.storage.GetDefaultProfile().Name,
|
DefaultProfile: app.storage.GetDefaultProfile().Name,
|
||||||
Profiles: map[string]profileDTO{},
|
Profiles: map[string]profileDTO{},
|
||||||
}
|
}
|
||||||
|
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
|
||||||
|
baseInv := Invite{}
|
||||||
for _, p := range app.storage.GetProfiles() {
|
for _, p := range app.storage.GetProfiles() {
|
||||||
out.Profiles[p.Name] = profileDTO{
|
pdto := profileDTO{
|
||||||
Admin: p.Admin,
|
Admin: p.Admin,
|
||||||
LibraryAccess: p.LibraryAccess,
|
LibraryAccess: p.LibraryAccess,
|
||||||
FromUser: p.FromUser,
|
FromUser: p.FromUser,
|
||||||
Ombi: p.Ombi != nil,
|
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)
|
gc.JSON(200, out)
|
||||||
}
|
}
|
||||||
@ -111,3 +123,76 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
|
|||||||
app.storage.DeleteProfileKey(name)
|
app.storage.DeleteProfileKey(name)
|
||||||
respondBool(200, true, gc)
|
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) {
|
||||||
|
profileName := gc.Param("profile")
|
||||||
|
profile, ok := app.storage.GetProfileKey(profileName)
|
||||||
|
if !ok {
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.storage.DeleteInvitesKey(profile.ReferralTemplateKey)
|
||||||
|
|
||||||
|
profile.ReferralTemplateKey = ""
|
||||||
|
|
||||||
|
app.storage.SetProfileKey(profileName, profile)
|
||||||
|
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
@ -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 = 90
|
||||||
)
|
)
|
||||||
|
|
||||||
// @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.
|
||||||
@ -74,6 +81,25 @@ func (app *appContext) MyDetails(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if app.config.Section("user_page").Key("referrals").MustBool(false) {
|
||||||
|
// 1. Look for existing template bound to this Jellyfin ID
|
||||||
|
// If one exists, that means its just for us and so we
|
||||||
|
// can use it directly.
|
||||||
|
inv := Invite{}
|
||||||
|
err := app.storage.db.FindOne(&inv, badgerhold.Where("ReferrerJellyfinID").Eq(resp.Id))
|
||||||
|
if err == nil {
|
||||||
|
resp.HasReferrals = true
|
||||||
|
} else {
|
||||||
|
// 2. Look for a template matching the key found in the user storage
|
||||||
|
// Since this key is shared between users in a profile, we make a copy.
|
||||||
|
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
|
||||||
|
err = app.storage.db.Get(user.ReferralTemplateKey, &inv)
|
||||||
|
if ok && err == nil {
|
||||||
|
resp.HasReferrals = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
gc.JSON(200, resp)
|
gc.JSON(200, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -621,3 +647,63 @@ 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.FindOne(&inv, badgerhold.Where("ReferrerJellyfinID").Eq(gc.GetString("jfId")))
|
||||||
|
if err != nil {
|
||||||
|
// 2. Look for a template matching the key found in the user storage
|
||||||
|
// Since this key is shared between users in a profile, we make a copy.
|
||||||
|
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
|
||||||
|
err = app.storage.db.Get(user.ReferralTemplateKey, &inv)
|
||||||
|
if !ok || err != nil {
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
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.Unix(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
126
api-users.go
@ -3,12 +3,15 @@ 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"
|
||||||
|
"github.com/timshannon/badgerhold/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Summary Creates a new Jellyfin user without an invite.
|
// @Summary Creates a new Jellyfin user without an invite.
|
||||||
@ -301,6 +304,16 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
id := user.ID
|
id := user.ID
|
||||||
|
|
||||||
|
emailStore := EmailAddress{
|
||||||
|
Addr: req.Email,
|
||||||
|
Contact: (req.Email != ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if invite.UserLabel != "" {
|
||||||
|
emailStore.Label = invite.UserLabel
|
||||||
|
}
|
||||||
|
|
||||||
var profile Profile
|
var profile Profile
|
||||||
if invite.Profile != "" {
|
if invite.Profile != "" {
|
||||||
app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile)
|
app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile)
|
||||||
@ -322,10 +335,15 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
if !((status == 200 || status == 204) && err == nil) {
|
if !((status == 200 || status == 204) && err == nil) {
|
||||||
app.err.Printf("%s: Failed to set configuration template (%d): %v", req.Code, status, err)
|
app.err.Printf("%s: Failed to set configuration template (%d): %v", req.Code, status, err)
|
||||||
}
|
}
|
||||||
|
if app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false) && profile.ReferralTemplateKey != "" {
|
||||||
|
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 app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||||
if req.Email != "" {
|
if req.Email != "" || invite.UserLabel != "" {
|
||||||
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
|
app.storage.SetEmailsKey(id, emailStore)
|
||||||
}
|
}
|
||||||
expiry := time.Time{}
|
expiry := time.Time{}
|
||||||
if invite.UserExpiry {
|
if invite.UserExpiry {
|
||||||
@ -629,6 +647,88 @@ 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 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."
|
||||||
|
// @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
|
||||||
|
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
app.debug.Printf("Couldn't find invite to source from")
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, u := range req.Users {
|
||||||
|
// 1. Wipe out any existing referral codes.
|
||||||
|
app.storage.db.DeleteMatching(Invite{}, badgerhold.Where("ReferrerJellyfinID").Eq(u))
|
||||||
|
|
||||||
|
// 2. Generate referral invite.
|
||||||
|
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 Disable referrals for the given user(s).
|
||||||
|
// @Produce json
|
||||||
|
// @Param EnableDisableReferralDTO body EnableDisableReferralDTO true "List of users"
|
||||||
|
// @Success 200 {object} boolResponse
|
||||||
|
// @Router /users/referral [delete]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Users
|
||||||
|
func (app *appContext) DisableReferralForUsers(gc *gin.Context) {
|
||||||
|
var req EnableDisableReferralDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
for _, u := range req.Users {
|
||||||
|
// 1. Delete directly bound template
|
||||||
|
app.storage.db.DeleteMatching(Invite{}, badgerhold.Where("ReferrerJellyfinID").Eq(u))
|
||||||
|
// 2. Check for and delete profile-attached template
|
||||||
|
user, ok := app.storage.GetEmailsKey(u)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
user.ReferralTemplateKey = ""
|
||||||
|
app.storage.SetEmailsKey(u, user)
|
||||||
|
}
|
||||||
|
respondBool(200, true, gc)
|
||||||
|
}
|
||||||
|
|
||||||
// @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"
|
||||||
@ -833,13 +933,15 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
||||||
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
|
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
|
||||||
|
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
|
||||||
i := 0
|
i := 0
|
||||||
for _, jfUser := range users {
|
for _, jfUser := range users {
|
||||||
user := respUser{
|
user := respUser{
|
||||||
ID: jfUser.ID,
|
ID: jfUser.ID,
|
||||||
Name: jfUser.Name,
|
Name: jfUser.Name,
|
||||||
Admin: jfUser.Policy.IsAdministrator,
|
Admin: jfUser.Policy.IsAdministrator,
|
||||||
Disabled: jfUser.Policy.IsDisabled,
|
Disabled: jfUser.Policy.IsDisabled,
|
||||||
|
ReferralsEnabled: false,
|
||||||
}
|
}
|
||||||
if !jfUser.LastActivityDate.IsZero() {
|
if !jfUser.LastActivityDate.IsZero() {
|
||||||
user.LastActive = jfUser.LastActivityDate.Unix()
|
user.LastActive = jfUser.LastActivityDate.Unix()
|
||||||
@ -868,6 +970,18 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
|||||||
user.DiscordID = dcUser.ID
|
user.DiscordID = dcUser.ID
|
||||||
user.NotifyThroughDiscord = dcUser.Contact
|
user.NotifyThroughDiscord = dcUser.Contact
|
||||||
}
|
}
|
||||||
|
// FIXME: Send referral data
|
||||||
|
referrerInv := Invite{}
|
||||||
|
if referralsEnabled {
|
||||||
|
// 1. Directly attached invite.
|
||||||
|
err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("ReferrerJellyfinID").Eq(jfUser.ID))
|
||||||
|
if err == nil {
|
||||||
|
user.ReferralsEnabled = true
|
||||||
|
// 2. Referrals via profile template. Shallow check, doesn't look for the thing in the database.
|
||||||
|
} else if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok && email.ReferralTemplateKey != "" {
|
||||||
|
user.ReferralsEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
resp.UserList[i] = user
|
resp.UserList[i] = user
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
@ -377,15 +377,15 @@
|
|||||||
"user_page": {
|
"user_page": {
|
||||||
"order": [],
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "User Page",
|
"name": "User Page/\"My Account\"",
|
||||||
"description": "The User Page (My Account) allows users to access and modify info directly, such as changing/adding contact methods, seeing their expiry date, or changing their password. Password resets can also be initiated from here, given a contact method or username. ",
|
"description": "The User Page (My Account) allows users to access and modify info directly, such as changing/adding contact methods, seeing their expiry date, sending referrals or changing their password. Password resets can also be initiated from here, given a contact method or username. ",
|
||||||
"depends_true": "ui|jellyfin_login"
|
"depends_true": "ui|jellyfin_login"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "Enabled",
|
"name": "Enabled",
|
||||||
"required": false,
|
"required": false,
|
||||||
"requires_restart": false,
|
"requires_restart": true,
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"value": true
|
"value": true
|
||||||
},
|
},
|
||||||
@ -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": true,
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -996,6 +1012,14 @@
|
|||||||
"value": true,
|
"value": true,
|
||||||
"description": "Enable to store provided email addresses, monitor Jellyfin directory for pw-resets, and send reset pins"
|
"description": "Enable to store provided email addresses, monitor Jellyfin directory for pw-resets, and send reset pins"
|
||||||
},
|
},
|
||||||
|
"pwr_note": {
|
||||||
|
"name": "Setup:",
|
||||||
|
"type": "note",
|
||||||
|
"value": "",
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"required": "false",
|
||||||
|
"description": "There are multiple ways password resets can be set up. See the <a href=\"https://wiki.jfa-go.com/docs/pwr/\" target=\"_blank\">wiki page</a> for more information."
|
||||||
|
},
|
||||||
"watch_directory": {
|
"watch_directory": {
|
||||||
"name": "Jellyfin directory",
|
"name": "Jellyfin directory",
|
||||||
"required": false,
|
"required": false,
|
||||||
|
@ -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,48 @@
|
|||||||
</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">×</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>
|
||||||
|
<div id="modal-enable-referrals-profile" class="modal">
|
||||||
|
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-enable-referrals-profile" href="">
|
||||||
|
<span class="heading"><span id="header-enable-referrals-profile">{{ .strings.enableReferrals }}</span> <span class="modal-close">×</span></span>
|
||||||
|
<p class="content my-4">{{ .strings.enableReferralsProfileDescription }}</p>
|
||||||
|
<label class="supra" for="enable-referrals-profile-invites">{{ .strings.invite }}</label>
|
||||||
|
<div class="select ~neutral @low mb-4 mt-2">
|
||||||
|
<select id="enable-referrals-profile-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">×</span></span>
|
<span class="heading"><span id="header-delete-user"></span> <span class="modal-close">×</span></span>
|
||||||
@ -303,6 +346,9 @@
|
|||||||
{{ if .ombiEnabled }}
|
{{ if .ombiEnabled }}
|
||||||
<th>Ombi</th>
|
<th>Ombi</th>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .referralsEnabled }}
|
||||||
|
<th>{{ .strings.referrals }}</th>
|
||||||
|
{{ end }}
|
||||||
<th>{{ .strings.from }}</th>
|
<th>{{ .strings.from }}</th>
|
||||||
<th>{{ .strings.userProfilesLibraries }}</th>
|
<th>{{ .strings.userProfilesLibraries }}</th>
|
||||||
<th><span class="button ~neutral @high" id="button-profile-create">{{ .strings.create }}</span></th>
|
<th><span class="button ~neutral @high" id="button-profile-create">{{ .strings.create }}</span></th>
|
||||||
@ -396,7 +442,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="notification-box"></div>
|
<div id="notification-box"></div>
|
||||||
<div class="top-4 left-4 absolute">
|
<div class="top-4 left-4 absolute">
|
||||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
|
||||||
<span class="button ~urge dropdown-button">
|
<span class="button ~urge dropdown-button">
|
||||||
<i class="ri-global-line"></i>
|
<i class="ri-global-line"></i>
|
||||||
<span class="ml-2 chev"></span>
|
<span class="ml-2 chev"></span>
|
||||||
@ -543,6 +589,11 @@
|
|||||||
<label class="label supra" for="create-label"> {{ .strings.label }}</label>
|
<label class="label supra" for="create-label"> {{ .strings.label }}</label>
|
||||||
<input type="text" id="create-label" class="input ~neutral @low mb-2 mt-4">
|
<input type="text" id="create-label" class="input ~neutral @low mb-2 mt-4">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="label supra" for="create-user-label"> {{ .strings.userLabel }}</label>
|
||||||
|
<p class="support">{{ .strings.userLabelDescription }}</p>
|
||||||
|
<input type="text" id="create-user-label" class="input ~neutral @low mb-2 mt-4">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card ~neutral @low col">
|
<div class="card ~neutral @low col">
|
||||||
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
|
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
|
||||||
@ -596,8 +647,8 @@
|
|||||||
<span class="button ~neutral @low center -ml-8" id="accounts-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
<span class="button ~neutral @low center -ml-8" id="accounts-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="supra py-1 sm hidden" id="accounts-search-options-header">{{ .strings.searchOptions }}</div>
|
<div class="supra py-1 sm hidden" id="accounts-search-options-header">{{ .strings.searchOptions }}</div>
|
||||||
<div class="row -mx-2">
|
<div class="row -mx-2 mb-2">
|
||||||
<button type="button" class="button ~neutral @low center m-2 hidden"><span id="accounts-sort-by-field"></span> <i class="ri-close-line ml-2 text-2xl"></i></button>
|
<button type="button" class="button ~neutral @low center mx-2 hidden"><span id="accounts-sort-by-field"></span> <i class="ri-close-line ml-2 text-2xl"></i></button>
|
||||||
<span id="accounts-filter-area"></span>
|
<span id="accounts-filter-area"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="supra py-1 sm">{{ .strings.actions }}</div>
|
<div class="supra py-1 sm">{{ .strings.actions }}</div>
|
||||||
@ -613,6 +664,9 @@
|
|||||||
</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>
|
||||||
|
{{ if .referralsEnabled }}
|
||||||
|
<span class="col button ~urge @low center max-w-[20%]" id="accounts-enable-referrals">{{ .strings.enableReferrals }}</span>
|
||||||
|
{{ end }}
|
||||||
<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>
|
||||||
@ -644,6 +698,9 @@
|
|||||||
{{ if .discordEnabled }}
|
{{ if .discordEnabled }}
|
||||||
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-discord">Discord</th>
|
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-discord">Discord</th>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .referralsEnabled }}
|
||||||
|
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-referrals">{{ .strings.referrals }}</th>
|
||||||
|
{{ end }}
|
||||||
<th class="grid gap-4 place-items-stretch accounts-header-expiry">{{ .strings.expiry }}</th>
|
<th class="grid gap-4 place-items-stretch accounts-header-expiry">{{ .strings.expiry }}</th>
|
||||||
<th class="grid gap-4 place-items-stretch accounts-header-last-active">{{ .strings.lastActiveTime }}</th>
|
<th class="grid gap-4 place-items-stretch accounts-header-last-active">{{ .strings.lastActiveTime }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
<div id="notification-box"></div>
|
<div id="notification-box"></div>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<div class="card dark:~d_neutral @low">
|
<div class="card dark:~d_neutral @low">
|
||||||
<div class="flex flex-col md:flex-row gap-3 inline align-baseline">
|
<div class="flex flex-col md:flex-row gap-3 items-baseline mb-2">
|
||||||
<span class="heading mr-5">
|
<span class="heading mr-5">
|
||||||
{{ if .passwordReset }}
|
{{ if .passwordReset }}
|
||||||
{{ .strings.passwordReset }}
|
{{ .strings.passwordReset }}
|
||||||
@ -53,9 +53,9 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="subheading">
|
<span class="subheading">
|
||||||
{{ if .passwordReset }}
|
{{ if .passwordReset }}
|
||||||
{{ .strings.enterYourPassword }}
|
{{ .strings.enterYourPassword }}
|
||||||
{{ else }}
|
{{ else }}
|
||||||
{{ .helpMessage }}
|
{{ .helpMessage }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -123,6 +123,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-initial">
|
<div class="flex-initial">
|
||||||
|
{{ if .fromUser }}
|
||||||
|
<aside class="col aside sm ~positive mb-4" id="invite-from-user" data-from="{{ .fromUser }}">{{ .strings.invitedBy }}</aside>
|
||||||
|
{{ end }}
|
||||||
<div class="card ~neutral @low mb-4">
|
<div class="card ~neutral @low mb-4">
|
||||||
<span class="label supra">{{ .strings.passwordRequirementsHeader }}</span>
|
<span class="label supra">{{ .strings.passwordRequirementsHeader }}</span>
|
||||||
<ul>
|
<ul>
|
||||||
@ -143,7 +146,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .contactMessage }}
|
{{ if .contactMessage }}
|
||||||
<aside class="col aside sm ~info mt-4">{{ .contactMessage }}</aside>
|
<aside class="col aside sm ~info mt-4">{{ .contactMessage }}</aside>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
window.matrixRequired = {{ .matrixRequired }};
|
window.matrixRequired = {{ .matrixRequired }};
|
||||||
window.matrixUserID = "{{ .matrixUser }}";
|
window.matrixUserID = "{{ .matrixUser }}";
|
||||||
window.validationStrings = JSON.parse({{ .validationStrings }});
|
window.validationStrings = JSON.parse({{ .validationStrings }});
|
||||||
|
window.referralsEnabled = {{ .referralsEnabled }};
|
||||||
</script>
|
</script>
|
||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
<title>{{ .strings.myAccount }}</title>
|
<title>{{ .strings.myAccount }}</title>
|
||||||
@ -150,6 +151,20 @@
|
|||||||
<div class="user-expiry-countdown"></div>
|
<div class="user-expiry-countdown"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{ if .referralsEnabled }}
|
||||||
|
<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>
|
||||||
|
<div class="row flex-expand">
|
||||||
|
<div class="user-referrals-info"></div>
|
||||||
|
<div class="grid my-2">
|
||||||
|
<button type="button" class="user-referrals-button button ~info dark:~d_info @low" title="Copy">{{ .strings.copyReferral }}<i class="ri-file-copy-line ml-2"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="{{ .urlBase }}/js/user.js" type="module"></script>
|
<script src="{{ .urlBase }}/js/user.js" type="module"></script>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
# Images
|
# Images
|
||||||
|
|
||||||
This holds any images on the main README, and the base files for the icons and banner. The font used, like Jellyfin, is [Quicksand](https://fonts.google.com/specimen/Quicksand) by Andrew Paglinawan.
|
This holds any images on the main README, and the base files for the icons and banner. The font used pre-0.5.0, like Jellyfin, is [Quicksand](https://fonts.google.com/specimen/Quicksand) by Andrew Paglinawan. These old versions are prefixed with `-quicksand` in `src/`.
|
||||||
|
|
||||||
|
Post-0.5.0, the font used is Hanken Grotesk, available under SIL OFL 1.1 License.
|
||||||
|
https://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web
|
||||||
|
|
||||||
"Go" text logo and Gopher image: Copyright 2018 The Go Authors. All rights reserved.
|
"Go" text logo and Gopher image: Copyright 2018 The Go Authors. All rights reserved.
|
||||||
https://creativecommons.org/licenses/by/3.0/legalcode
|
https://creativecommons.org/licenses/by/3.0/legalcode
|
||||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 523 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 384 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 411 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 38 KiB |
BIN
images/myaccount.png
Normal file
After Width: | Height: | Size: 535 KiB |
412
images/src/banner-hanken.svg
Normal file
After Width: | Height: | Size: 64 KiB |
283
images/src/banner-quicksand.svg
Normal file
After Width: | Height: | Size: 58 KiB |
736
images/src/jfa-go-social-hanken.svg
Normal file
After Width: | Height: | Size: 91 KiB |
337
images/src/jfa-go-social-quicksand.svg
Normal file
After Width: | Height: | Size: 59 KiB |
@ -79,7 +79,6 @@
|
|||||||
"inviteUsersCreated": "Oprettet brugere",
|
"inviteUsersCreated": "Oprettet brugere",
|
||||||
"inviteNoProfile": "Ingen Profil",
|
"inviteNoProfile": "Ingen Profil",
|
||||||
"inviteDateCreated": "Oprettet",
|
"inviteDateCreated": "Oprettet",
|
||||||
"inviteRemainingUses": "Resterende anvendelser",
|
|
||||||
"inviteNoInvites": "Ingen",
|
"inviteNoInvites": "Ingen",
|
||||||
"inviteExpiresInTime": "Udløber om {n}",
|
"inviteExpiresInTime": "Udløber om {n}",
|
||||||
"notifyEvent": "Meddel den:",
|
"notifyEvent": "Meddel den:",
|
||||||
|
@ -53,7 +53,6 @@
|
|||||||
"inviteUsersCreated": "Erstellte Benutzer",
|
"inviteUsersCreated": "Erstellte Benutzer",
|
||||||
"inviteNoProfile": "Kein Profil",
|
"inviteNoProfile": "Kein Profil",
|
||||||
"inviteDateCreated": "Erstellt",
|
"inviteDateCreated": "Erstellt",
|
||||||
"inviteRemainingUses": "Verbleibende Verwendungen",
|
|
||||||
"inviteNoInvites": "Keine",
|
"inviteNoInvites": "Keine",
|
||||||
"inviteExpiresInTime": "Läuft in {n} ab",
|
"inviteExpiresInTime": "Läuft in {n} ab",
|
||||||
"notifyEvent": "Benachrichtigen bei:",
|
"notifyEvent": "Benachrichtigen bei:",
|
||||||
|
@ -56,7 +56,6 @@
|
|||||||
"inviteUsersCreated": "Δημιουργηθέντες χρήστες",
|
"inviteUsersCreated": "Δημιουργηθέντες χρήστες",
|
||||||
"inviteNoProfile": "Κανένα Προφίλ",
|
"inviteNoProfile": "Κανένα Προφίλ",
|
||||||
"inviteDateCreated": "Δημιουργηθέντα",
|
"inviteDateCreated": "Δημιουργηθέντα",
|
||||||
"inviteRemainingUses": "Εναπομείναντες χρήσεις",
|
|
||||||
"inviteNoInvites": "Καμία",
|
"inviteNoInvites": "Καμία",
|
||||||
"inviteExpiresInTime": "Λήγει σε {n}",
|
"inviteExpiresInTime": "Λήγει σε {n}",
|
||||||
"notifyEvent": "Ενημέρωση όταν:",
|
"notifyEvent": "Ενημέρωση όταν:",
|
||||||
|
@ -124,7 +124,6 @@
|
|||||||
"addProfileStoreHomescreenLayout": "Store homescreen layout",
|
"addProfileStoreHomescreenLayout": "Store homescreen layout",
|
||||||
"inviteNoUsersCreated": "None yet!",
|
"inviteNoUsersCreated": "None yet!",
|
||||||
"inviteUsersCreated": "Created users",
|
"inviteUsersCreated": "Created users",
|
||||||
"inviteRemainingUses": "Remaining uses",
|
|
||||||
"inviteNoInvites": "None",
|
"inviteNoInvites": "None",
|
||||||
"inviteExpiresInTime": "Expires in {n}",
|
"inviteExpiresInTime": "Expires in {n}",
|
||||||
"notifyEvent": "Notify on:",
|
"notifyEvent": "Notify on:",
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"strings": {
|
"strings": {
|
||||||
"invites": "Invites",
|
"invites": "Invites",
|
||||||
|
"invite": "Invite",
|
||||||
"accounts": "Accounts",
|
"accounts": "Accounts",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"inviteMonths": "Months",
|
"inviteMonths": "Months",
|
||||||
@ -40,6 +41,8 @@
|
|||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
|
"userLabel": "User Label",
|
||||||
|
"userLabelDescription": "Label to apply to users created with this invite.",
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
"announce": "Announce",
|
"announce": "Announce",
|
||||||
"templates": "Templates",
|
"templates": "Templates",
|
||||||
@ -63,6 +66,10 @@
|
|||||||
"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",
|
||||||
|
"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.",
|
||||||
"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.",
|
||||||
@ -90,7 +97,6 @@
|
|||||||
"inviteUsersCreated": "Created users",
|
"inviteUsersCreated": "Created users",
|
||||||
"inviteNoProfile": "No Profile",
|
"inviteNoProfile": "No Profile",
|
||||||
"inviteDateCreated": "Created",
|
"inviteDateCreated": "Created",
|
||||||
"inviteRemainingUses": "Remaining uses",
|
|
||||||
"inviteNoInvites": "None",
|
"inviteNoInvites": "None",
|
||||||
"inviteExpiresInTime": "Expires in {n}",
|
"inviteExpiresInTime": "Expires in {n}",
|
||||||
"notifyEvent": "Notify on:",
|
"notifyEvent": "Notify on:",
|
||||||
@ -132,6 +138,7 @@
|
|||||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||||
"telegramVerified": "Telegram account verified.",
|
"telegramVerified": "Telegram account verified.",
|
||||||
"accountConnected": "Account connected.",
|
"accountConnected": "Account connected.",
|
||||||
|
"referralsEnabled": "Referrals enabled.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||||
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
||||||
"errorSettingsFailed": "Application failed.",
|
"errorSettingsFailed": "Application failed.",
|
||||||
@ -152,6 +159,7 @@
|
|||||||
"errorSendWelcomeEmail": "Failed to send welcome message (check console/logs)",
|
"errorSendWelcomeEmail": "Failed to send welcome message (check console/logs)",
|
||||||
"errorApplyUpdate": "Failed to apply update, try manually.",
|
"errorApplyUpdate": "Failed to apply update, try manually.",
|
||||||
"errorCheckUpdate": "Failed to check for update.",
|
"errorCheckUpdate": "Failed to check for update.",
|
||||||
|
"errorNoReferralTemplate": "Profile doesn't contain referral template, add one in settings.",
|
||||||
"updateAvailable": "A new update is available, check settings.",
|
"updateAvailable": "A new update is available, check settings.",
|
||||||
"noUpdatesAvailable": "No new updates available."
|
"noUpdatesAvailable": "No new updates available."
|
||||||
},
|
},
|
||||||
@ -160,6 +168,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"
|
||||||
|
@ -75,7 +75,6 @@
|
|||||||
"inviteUsersCreated": "Usuarios creados",
|
"inviteUsersCreated": "Usuarios creados",
|
||||||
"inviteNoProfile": "Sin perfil",
|
"inviteNoProfile": "Sin perfil",
|
||||||
"inviteDateCreated": "Creado",
|
"inviteDateCreated": "Creado",
|
||||||
"inviteRemainingUses": "Usos restantes",
|
|
||||||
"inviteNoInvites": "Ninguno",
|
"inviteNoInvites": "Ninguno",
|
||||||
"inviteExpiresInTime": "Caduca en {n}",
|
"inviteExpiresInTime": "Caduca en {n}",
|
||||||
"notifyEvent": "Notificar en:",
|
"notifyEvent": "Notificar en:",
|
||||||
|
@ -55,7 +55,6 @@
|
|||||||
"inviteUsersCreated": "Utilisateurs créés",
|
"inviteUsersCreated": "Utilisateurs créés",
|
||||||
"inviteNoProfile": "Aucun profil",
|
"inviteNoProfile": "Aucun profil",
|
||||||
"inviteDateCreated": "Créer",
|
"inviteDateCreated": "Créer",
|
||||||
"inviteRemainingUses": "Utilisations restantes",
|
|
||||||
"inviteNoInvites": "Aucune",
|
"inviteNoInvites": "Aucune",
|
||||||
"inviteExpiresInTime": "Expires dans {n}",
|
"inviteExpiresInTime": "Expires dans {n}",
|
||||||
"notifyEvent": "Notifier sur :",
|
"notifyEvent": "Notifier sur :",
|
||||||
|
@ -87,7 +87,6 @@
|
|||||||
"inviteUsersCreated": "",
|
"inviteUsersCreated": "",
|
||||||
"inviteNoProfile": "",
|
"inviteNoProfile": "",
|
||||||
"inviteDateCreated": "",
|
"inviteDateCreated": "",
|
||||||
"inviteRemainingUses": "",
|
|
||||||
"inviteNoInvites": "",
|
"inviteNoInvites": "",
|
||||||
"inviteExpiresInTime": "",
|
"inviteExpiresInTime": "",
|
||||||
"notifyEvent": "",
|
"notifyEvent": "",
|
||||||
|
@ -56,7 +56,6 @@
|
|||||||
"inviteUsersCreated": "Pengguna yang telah dibuat",
|
"inviteUsersCreated": "Pengguna yang telah dibuat",
|
||||||
"inviteNoProfile": "Tidak ada profil",
|
"inviteNoProfile": "Tidak ada profil",
|
||||||
"inviteDateCreated": "Dibuat",
|
"inviteDateCreated": "Dibuat",
|
||||||
"inviteRemainingUses": "Penggunaan yang tersisa",
|
|
||||||
"inviteNoInvites": "Tidak ada",
|
"inviteNoInvites": "Tidak ada",
|
||||||
"inviteExpiresInTime": "Kadaluarsa dalam {n}",
|
"inviteExpiresInTime": "Kadaluarsa dalam {n}",
|
||||||
"notifyEvent": "Beritahu pada:",
|
"notifyEvent": "Beritahu pada:",
|
||||||
|
@ -53,7 +53,6 @@
|
|||||||
"inviteUsersCreated": "Aangemaakte gebruikers",
|
"inviteUsersCreated": "Aangemaakte gebruikers",
|
||||||
"inviteNoProfile": "Geen profiel",
|
"inviteNoProfile": "Geen profiel",
|
||||||
"inviteDateCreated": "Aangemaakt",
|
"inviteDateCreated": "Aangemaakt",
|
||||||
"inviteRemainingUses": "Resterend aantal keer te gebruiken",
|
|
||||||
"inviteNoInvites": "Geen",
|
"inviteNoInvites": "Geen",
|
||||||
"inviteExpiresInTime": "Verloopt over {n}",
|
"inviteExpiresInTime": "Verloopt over {n}",
|
||||||
"notifyEvent": "Meldingen:",
|
"notifyEvent": "Meldingen:",
|
||||||
|
@ -87,7 +87,6 @@
|
|||||||
"inviteUsersCreated": "",
|
"inviteUsersCreated": "",
|
||||||
"inviteNoProfile": "",
|
"inviteNoProfile": "",
|
||||||
"inviteDateCreated": "Utworzone",
|
"inviteDateCreated": "Utworzone",
|
||||||
"inviteRemainingUses": "",
|
|
||||||
"inviteNoInvites": "",
|
"inviteNoInvites": "",
|
||||||
"inviteExpiresInTime": "",
|
"inviteExpiresInTime": "",
|
||||||
"notifyEvent": "",
|
"notifyEvent": "",
|
||||||
|
@ -54,7 +54,6 @@
|
|||||||
"inviteUsersCreated": "Usuários criado",
|
"inviteUsersCreated": "Usuários criado",
|
||||||
"inviteNoProfile": "Sem Perfil",
|
"inviteNoProfile": "Sem Perfil",
|
||||||
"inviteDateCreated": "Criado",
|
"inviteDateCreated": "Criado",
|
||||||
"inviteRemainingUses": "Uso restantes",
|
|
||||||
"inviteNoInvites": "Nenhum",
|
"inviteNoInvites": "Nenhum",
|
||||||
"inviteExpiresInTime": "Expira em {n}",
|
"inviteExpiresInTime": "Expira em {n}",
|
||||||
"notifyEvent": "Notificar em:",
|
"notifyEvent": "Notificar em:",
|
||||||
|
@ -65,7 +65,6 @@
|
|||||||
"inviteUsersCreated": "Skapade användare",
|
"inviteUsersCreated": "Skapade användare",
|
||||||
"inviteNoProfile": "Ingen profil",
|
"inviteNoProfile": "Ingen profil",
|
||||||
"inviteDateCreated": "Skapad",
|
"inviteDateCreated": "Skapad",
|
||||||
"inviteRemainingUses": "Återstående användningar",
|
|
||||||
"inviteNoInvites": "Ingen",
|
"inviteNoInvites": "Ingen",
|
||||||
"inviteExpiresInTime": "Går ut om {n}",
|
"inviteExpiresInTime": "Går ut om {n}",
|
||||||
"notifyEvent": "Meddela den:",
|
"notifyEvent": "Meddela den:",
|
||||||
|
@ -86,7 +86,6 @@
|
|||||||
"inviteUsersCreated": "Người dùng đã tạo",
|
"inviteUsersCreated": "Người dùng đã tạo",
|
||||||
"inviteNoProfile": "Không có Tài khoản mẫu",
|
"inviteNoProfile": "Không có Tài khoản mẫu",
|
||||||
"inviteDateCreated": "Tạo",
|
"inviteDateCreated": "Tạo",
|
||||||
"inviteRemainingUses": "Số lần sử dụng còn lại",
|
|
||||||
"inviteNoInvites": "Không có",
|
"inviteNoInvites": "Không có",
|
||||||
"inviteExpiresInTime": "Hết hạn trong {n}",
|
"inviteExpiresInTime": "Hết hạn trong {n}",
|
||||||
"notifyEvent": "Thông báo khi:",
|
"notifyEvent": "Thông báo khi:",
|
||||||
|
@ -80,7 +80,6 @@
|
|||||||
"inviteUsersCreated": "已创建的用户",
|
"inviteUsersCreated": "已创建的用户",
|
||||||
"inviteNoProfile": "没有个人资料",
|
"inviteNoProfile": "没有个人资料",
|
||||||
"inviteDateCreated": "已创建",
|
"inviteDateCreated": "已创建",
|
||||||
"inviteRemainingUses": "剩余使用次数",
|
|
||||||
"inviteNoInvites": "无",
|
"inviteNoInvites": "无",
|
||||||
"inviteExpiresInTime": "在 {n} 到期",
|
"inviteExpiresInTime": "在 {n} 到期",
|
||||||
"notifyEvent": "通知:",
|
"notifyEvent": "通知:",
|
||||||
|
@ -87,7 +87,6 @@
|
|||||||
"inviteUsersCreated": "創建的帳戶",
|
"inviteUsersCreated": "創建的帳戶",
|
||||||
"inviteNoProfile": "無資料",
|
"inviteNoProfile": "無資料",
|
||||||
"inviteDateCreated": "已創建",
|
"inviteDateCreated": "已創建",
|
||||||
"inviteRemainingUses": "剩餘使用次數",
|
|
||||||
"inviteNoInvites": "無",
|
"inviteNoInvites": "無",
|
||||||
"inviteExpiresInTime": "在 {n} 到期",
|
"inviteExpiresInTime": "在 {n} 到期",
|
||||||
"notifyEvent": "通知:",
|
"notifyEvent": "通知:",
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
"expiry": "Udløb",
|
"expiry": "Udløb",
|
||||||
"add": "Tilføj",
|
"add": "Tilføj",
|
||||||
"edit": "Rediger",
|
"edit": "Rediger",
|
||||||
"delete": "Slet"
|
"delete": "Slet",
|
||||||
|
"inviteRemainingUses": "Resterende anvendelser"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.",
|
"errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.",
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
"expiry": "Ablaufdatum",
|
"expiry": "Ablaufdatum",
|
||||||
"add": "Hinzufügen",
|
"add": "Hinzufügen",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"delete": "Löschen"
|
"delete": "Löschen",
|
||||||
|
"inviteRemainingUses": "Verbleibende Verwendungen"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "Der Benutzername und/oder das Passwort wurden nicht ausgefüllt.",
|
"errorLoginBlank": "Der Benutzername und/oder das Passwort wurden nicht ausgefüllt.",
|
||||||
|
@ -25,7 +25,8 @@
|
|||||||
"disable": "Απενεργοποίηση",
|
"disable": "Απενεργοποίηση",
|
||||||
"expiry": "Λήξη",
|
"expiry": "Λήξη",
|
||||||
"edit": "Επεξεργασία",
|
"edit": "Επεξεργασία",
|
||||||
"delete": "Διαγραφή"
|
"delete": "Διαγραφή",
|
||||||
|
"inviteRemainingUses": "Εναπομείναντες χρήσεις"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",
|
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
"expiry": "Expiry",
|
"expiry": "Expiry",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete"
|
"delete": "Delete",
|
||||||
|
"inviteRemainingUses": "Remaining uses"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "The username and/or password was left blank.",
|
"errorLoginBlank": "The username and/or password was left blank.",
|
||||||
|
@ -39,7 +39,9 @@
|
|||||||
"add": "Add",
|
"add": "Add",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"myAccount": "My Account"
|
"myAccount": "My Account",
|
||||||
|
"referrals": "Referrals",
|
||||||
|
"inviteRemainingUses": "Remaining uses"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "The username and/or password were left blank.",
|
"errorLoginBlank": "The username and/or password were left blank.",
|
||||||
@ -62,4 +64,4 @@
|
|||||||
"plural": "{n} Days"
|
"plural": "{n} Days"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -35,7 +35,8 @@
|
|||||||
"expiry": "Expiración",
|
"expiry": "Expiración",
|
||||||
"add": "Agregar",
|
"add": "Agregar",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"delete": "Eliminar"
|
"delete": "Eliminar",
|
||||||
|
"inviteRemainingUses": "Usos restantes"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "El nombre de usuario y/o la contraseña se dejaron en blanco.",
|
"errorLoginBlank": "El nombre de usuario y/o la contraseña se dejaron en blanco.",
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
"expiry": "Expiration",
|
"expiry": "Expiration",
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
"edit": "Éditer",
|
"edit": "Éditer",
|
||||||
"delete": "Effacer"
|
"delete": "Effacer",
|
||||||
|
"inviteRemainingUses": "Utilisations restantes"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.",
|
"errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.",
|
||||||
|
@ -19,7 +19,8 @@
|
|||||||
"login": "Masuk",
|
"login": "Masuk",
|
||||||
"logout": "Keluar",
|
"logout": "Keluar",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Hapus"
|
"delete": "Hapus",
|
||||||
|
"inviteRemainingUses": "Penggunaan yang tersisa"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "Nama pengguna dan / atau sandi kosong.",
|
"errorLoginBlank": "Nama pengguna dan / atau sandi kosong.",
|
||||||
|
@ -28,4 +28,4 @@
|
|||||||
},
|
},
|
||||||
"notifications": {},
|
"notifications": {},
|
||||||
"quantityStrings": {}
|
"quantityStrings": {}
|
||||||
}
|
}
|
8
lang/common/nds.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"name": "Nedderdütsch (NDS)"
|
||||||
|
},
|
||||||
|
"strings": {},
|
||||||
|
"notifications": {},
|
||||||
|
"quantityStrings": {}
|
||||||
|
}
|
@ -35,7 +35,8 @@
|
|||||||
"expiry": "Verloop",
|
"expiry": "Verloop",
|
||||||
"add": "Voeg toe",
|
"add": "Voeg toe",
|
||||||
"edit": "Bewerken",
|
"edit": "Bewerken",
|
||||||
"delete": "Verwijderen"
|
"delete": "Verwijderen",
|
||||||
|
"inviteRemainingUses": "Resterend aantal keer te gebruiken"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "De gebruikersnaam en/of wachtwoord is leeg.",
|
"errorLoginBlank": "De gebruikersnaam en/of wachtwoord is leeg.",
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
"expiry": "Expira",
|
"expiry": "Expira",
|
||||||
"add": "Adicionar",
|
"add": "Adicionar",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"delete": "Deletar"
|
"delete": "Deletar",
|
||||||
|
"inviteRemainingUses": "Uso restantes"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "O nome de usuário e/ou senha foram deixados em branco.",
|
"errorLoginBlank": "O nome de usuário e/ou senha foram deixados em branco.",
|
||||||
|
@ -28,4 +28,4 @@
|
|||||||
},
|
},
|
||||||
"notifications": {},
|
"notifications": {},
|
||||||
"quantityStrings": {}
|
"quantityStrings": {}
|
||||||
}
|
}
|
@ -22,7 +22,8 @@
|
|||||||
"disabled": "Inaktiverad",
|
"disabled": "Inaktiverad",
|
||||||
"expiry": "Löper ut",
|
"expiry": "Löper ut",
|
||||||
"edit": "Redigera",
|
"edit": "Redigera",
|
||||||
"delete": "Radera"
|
"delete": "Radera",
|
||||||
|
"inviteRemainingUses": "Återstående användningar"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.",
|
"errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.",
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
"expiry": "Hết hạn",
|
"expiry": "Hết hạn",
|
||||||
"add": "Thêm",
|
"add": "Thêm",
|
||||||
"edit": "Chỉnh sửa",
|
"edit": "Chỉnh sửa",
|
||||||
"delete": "Xóa"
|
"delete": "Xóa",
|
||||||
|
"inviteRemainingUses": "Số lần sử dụng còn lại"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorConnection": "Không thể kết nối với jfa-go.",
|
"errorConnection": "Không thể kết nối với jfa-go.",
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
"expiry": "到期",
|
"expiry": "到期",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
"delete": "删除"
|
"delete": "删除",
|
||||||
|
"inviteRemainingUses": "剩余使用次数"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "用户名/密码留空。",
|
"errorLoginBlank": "用户名/密码留空。",
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
"expiry": "到期",
|
"expiry": "到期",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
"delete": "刪除"
|
"delete": "刪除",
|
||||||
|
"inviteRemainingUses": "剩餘使用次數"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "帳戶名稱和/或密碼留空。",
|
"errorLoginBlank": "帳戶名稱和/或密碼留空。",
|
||||||
|
@ -49,4 +49,4 @@
|
|||||||
"clickBelow": "",
|
"clickBelow": "",
|
||||||
"confirmEmail": ""
|
"confirmEmail": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -34,7 +34,10 @@
|
|||||||
"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.",
|
"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.",
|
"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.",
|
"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",
|
||||||
|
"invitedBy": "You were invited by user {user}."
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorUserExists": "User already exists.",
|
"errorUserExists": "User already exists.",
|
||||||
|
@ -48,4 +48,4 @@
|
|||||||
"plural": "Deve avere almeno {n} caratteri speciali"
|
"plural": "Deve avere almeno {n} caratteri speciali"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -57,4 +57,4 @@
|
|||||||
"plural": "Potrebnih je vsaj {n} posebnih znakov"
|
"plural": "Potrebnih je vsaj {n} posebnih znakov"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,4 +13,4 @@
|
|||||||
"changeYourPassword": "Spremenite svoje geslo po prijavi.",
|
"changeYourPassword": "Spremenite svoje geslo po prijavi.",
|
||||||
"enterYourPassword": "Vnesite svoje novo geslo spodaj."
|
"enterYourPassword": "Vnesite svoje novo geslo spodaj."
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
"endPage": {
|
"endPage": {
|
||||||
"finished": "Finished!",
|
"finished": "Finished!",
|
||||||
"restartMessage": "You can configure Discord/Telegram/Matrix bots, customize your messages and more in Settings. Click below to restart, then refresh the page.",
|
"restartMessage": "Features like Discord/Telegram/Matrix bots, custom Markdown messages, and a user-accessible \"My Account\" page can be found in Settings, so make sure to give it a browse. Click below to restart, then refresh the page.",
|
||||||
"refreshPage": "Refresh"
|
"refreshPage": "Refresh"
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
|
@ -149,4 +149,4 @@
|
|||||||
"emailMessage": "",
|
"emailMessage": "",
|
||||||
"emailMessageNotice": ""
|
"emailMessageNotice": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -146,4 +146,4 @@
|
|||||||
"emailMessage": "",
|
"emailMessage": "",
|
||||||
"emailMessageNotice": ""
|
"emailMessageNotice": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,4 +13,4 @@
|
|||||||
"languageSet": "",
|
"languageSet": "",
|
||||||
"discordDMs": ""
|
"discordDMs": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,4 +13,4 @@
|
|||||||
"languageSet": "",
|
"languageSet": "",
|
||||||
"discordDMs": ""
|
"discordDMs": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,4 +13,4 @@
|
|||||||
"languageSet": "Jezik nastavljen na {language}.",
|
"languageSet": "Jezik nastavljen na {language}.",
|
||||||
"discordDMs": "Prosimo preverite svoja zasebna sporočila za odziv."
|
"discordDMs": "Prosimo preverite svoja zasebna sporočila za odziv."
|
||||||
}
|
}
|
||||||
}
|
}
|
54
models.go
@ -48,21 +48,22 @@ type enableDisableUserDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type generateInviteDTO struct {
|
type generateInviteDTO struct {
|
||||||
Months int `json:"months" example:"0"` // Number of months
|
Months int `json:"months" example:"0"` // Number of months
|
||||||
Days int `json:"days" example:"1"` // Number of days
|
Days int `json:"days" example:"1"` // Number of days
|
||||||
Hours int `json:"hours" example:"2"` // Number of hours
|
Hours int `json:"hours" example:"2"` // Number of hours
|
||||||
Minutes int `json:"minutes" example:"3"` // Number of minutes
|
Minutes int `json:"minutes" example:"3"` // Number of minutes
|
||||||
UserExpiry bool `json:"user-expiry"` // Whether or not user expiry is enabled
|
UserExpiry bool `json:"user-expiry"` // Whether or not user expiry is enabled
|
||||||
UserMonths int `json:"user-months,omitempty" example:"1"` // Number of months till user expiry
|
UserMonths int `json:"user-months,omitempty" example:"1"` // Number of months till user expiry
|
||||||
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
||||||
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
||||||
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
||||||
SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name
|
SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name
|
||||||
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
||||||
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
||||||
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
||||||
Profile string `json:"profile" example:"DefaultProfile"` // Name of profile to apply on this invite
|
Profile string `json:"profile" example:"DefaultProfile"` // Name of profile to apply on this invite
|
||||||
Label string `json:"label" example:"For Friends"` // Optional label for the invite
|
Label string `json:"label" example:"For Friends"` // Optional label for the invite
|
||||||
|
UserLabel string `json:"user_label,omitempty" example:"Friend"` // Label to apply to users created w/ this invite.
|
||||||
}
|
}
|
||||||
|
|
||||||
type inviteProfileDTO struct {
|
type inviteProfileDTO struct {
|
||||||
@ -71,10 +72,11 @@ type inviteProfileDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type profileDTO struct {
|
type profileDTO struct {
|
||||||
Admin bool `json:"admin" example:"false"` // Whether profile has admin rights or not
|
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
|
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
|
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.
|
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 {
|
type getProfilesDTO struct {
|
||||||
@ -113,6 +115,7 @@ type inviteDTO struct {
|
|||||||
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
||||||
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
||||||
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
|
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
|
||||||
|
UserLabel string `json:"user_label,omitempty" example:"Friend"` // Label to apply to users created w/ this invite.
|
||||||
}
|
}
|
||||||
|
|
||||||
type getInvitesDTO struct {
|
type getInvitesDTO struct {
|
||||||
@ -150,6 +153,7 @@ type respUser struct {
|
|||||||
NotifyThroughMatrix bool `json:"notify_matrix"`
|
NotifyThroughMatrix bool `json:"notify_matrix"`
|
||||||
Label string `json:"label"` // Label of user, shown next to their name.
|
Label string `json:"label"` // Label of user, shown next to their name.
|
||||||
AccountsAdmin bool `json:"accounts_admin"` // Whether or not the user is a jfa-go admin.
|
AccountsAdmin bool `json:"accounts_admin"` // Whether or not the user is a jfa-go admin.
|
||||||
|
ReferralsEnabled bool `json:"referrals_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type getUsersDTO struct {
|
type getUsersDTO struct {
|
||||||
@ -388,6 +392,7 @@ type MyDetailsDTO struct {
|
|||||||
Discord *MyDetailsContactMethodsDTO `json:"discord,omitempty"`
|
Discord *MyDetailsContactMethodsDTO `json:"discord,omitempty"`
|
||||||
Telegram *MyDetailsContactMethodsDTO `json:"telegram,omitempty"`
|
Telegram *MyDetailsContactMethodsDTO `json:"telegram,omitempty"`
|
||||||
Matrix *MyDetailsContactMethodsDTO `json:"matrix,omitempty"`
|
Matrix *MyDetailsContactMethodsDTO `json:"matrix,omitempty"`
|
||||||
|
HasReferrals bool `json:"has_referrals,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MyDetailsContactMethodsDTO struct {
|
type MyDetailsContactMethodsDTO struct {
|
||||||
@ -414,3 +419,14 @@ 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 int64 `json:"expiry"` // Come back after this time to get a new referral
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnableDisableReferralDTO struct {
|
||||||
|
Users []string `json:"users"`
|
||||||
|
}
|
||||||
|
378
package-lock.json
generated
@ -33,7 +33,7 @@
|
|||||||
"live-server": "^1.2.2"
|
"live-server": "^1.2.2"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"esbuild": "^0.18.10"
|
"esbuild": "^0.18.20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@ -59,9 +59,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||||
"integrity": "sha512-3KClmVNd+Fku82uZJz5C4Rx8m1PPmWUFz5Zkw8jkpZPOmsq+EG1TTOtw1OXkHuX3WczOFQigrtf60B1ijKwNsg==",
|
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -74,9 +74,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||||
"integrity": "sha512-ynm4naLbNbK0ajf9LUWtQB+6Vfg1Z/AplArqr4tGebC00Z6m9Y91OVIcjDa461wGcZwcaHYaZAab4yJxfhisTQ==",
|
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -89,9 +89,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-vFfXj8P9Yfjh54yqUDEHKzqzYuEfPyAOl3z7R9hjkwt+NCvbn9VMxX+IILnAfdImRBfYVItgSUsqGKhJFnBwZw==",
|
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -104,9 +104,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||||
"integrity": "sha512-k2OJQ7ZxE6sVc91+MQeZH9gFeDAH2uIYALPAwTjTCvcPy9Dzrf7V7gFUQPYkn09zloWhQ+nvxWHia2x2ZLR0sQ==",
|
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -119,9 +119,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-tnz/mdZk1L1Z3WpGjin/L2bKTe8/AKZpI8fcCLtH+gq8WXWsCNJSxlesAObV4qbtTl6pG5vmqFXfWUQ5hV8PAQ==",
|
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -134,9 +134,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||||
"integrity": "sha512-QJluV0LwBrbHnYYwSKC+K8RGz0g/EyhpQH1IxdoFT0nM7PfgjE+aS8wxq/KFEsU0JkL7U/EEKd3O8xVBxXb2aA==",
|
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -149,9 +149,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-Hi/ycUkS6KTw+U9G5PK5NoK7CZboicaKUSVs0FSiPNtuCTzK6HNM4DIgniH7hFaeuszDS9T4dhAHWiLSt/Y5Ng==",
|
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -164,9 +164,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||||
"integrity": "sha512-HfFoxY172tVHPIvJy+FHxzB4l8xU7e5cxmNS11cQ2jt4JWAukn/7LXaPdZid41UyTweqa4P/1zs201gRGCTwHw==",
|
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -179,9 +179,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||||
"integrity": "sha512-Nz6XcfRBOO7jSrVpKAyEyFOPGhySPNlgumSDhWAspdQQ11ub/7/NZDMhWDFReE9QH/SsCOCLQbdj0atAk/HMOQ==",
|
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -194,9 +194,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||||
"integrity": "sha512-otMdmSmkMe+pmiP/bZBjfphyAsTsngyT9RCYwoFzqrveAbux9nYitDTpdgToG0Z0U55+PnH654gCH2GQ1aB6Yw==",
|
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@ -209,9 +209,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||||
"integrity": "sha512-t8tjFuON1koxskzQ4VFoh0T5UDUMiLYjwf9Wktd0tx8AoK6xgU+5ubKOpWpcnhEQ2tESS5u0v6QuN8PX/ftwcQ==",
|
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@ -224,9 +224,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||||
"integrity": "sha512-+dUkcVzcfEJHz3HEnVpIJu8z8Wdn2n/nWMWdl6FVPFGJAVySO4g3+XPzNKFytVFwf8hPVDwYXzVcu8GMFqsqZw==",
|
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@ -239,9 +239,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||||
"integrity": "sha512-sO3PjjxEGy+PY2qkGe2gwJbXdZN9wAYpVBZWFD0AwAoKuXRkWK0/zaMQ5ekUFJDRDCRm8x5U0Axaub7ynH/wVg==",
|
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@ -254,9 +254,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||||
"integrity": "sha512-JDtdbJg3yjDeXLv4lZYE1kiTnxv73/8cbPHY9T/dUKi8rYOM/k5b3W4UJLMUksuQ6nTm5c89W1nADsql6FW75A==",
|
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@ -269,9 +269,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||||
"integrity": "sha512-NLuSKcp8WckjD2a7z5kzLiCywFwBTMlIxDNuud1AUGVuwBBJSkuubp6cNjJ0p5c6CZaA3QqUGwjHJBiG1SoOFw==",
|
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@ -284,9 +284,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-wj2KRsCsFusli+6yFgNO/zmmLslislAWryJnodteRmGej7ZzinIbMdsyp13rVGde88zxJd5vercNYK9kuvlZaQ==",
|
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -299,9 +299,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-pQ9QqxEPI3cVRZyUtCoZxhZK3If+7RzR8L2yz2+TDzdygofIPOJFaAPkEJ5rYIbUO101RaiYxfdOBahYexLk5A==",
|
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -314,9 +314,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-k8GTIIW9I8pEEfoOUm32TpPMgSg06JhL5DO+ql66aLTkOQUs0TxCA67Wi7pv6z8iF8STCGcNbm3UWFHLuci+ag==",
|
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -329,9 +329,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-vIGYJIdEI6d4JBucAx8py792G8J0GP40qSH+EvSt80A4zvGd6jph+5t1g+eEXcS2aRpgZw6CrssNCFZxTdEsxw==",
|
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -344,9 +344,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||||
"integrity": "sha512-kRhNcMZFGMW+ZHCarAM1ypr8OZs0k688ViUCetVCef9p3enFxzWeBg9h/575Y0nsFu0ZItluCVF5gMR2pwOEpA==",
|
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -359,9 +359,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||||
"integrity": "sha512-AR9PX1whYaYh9p0EOaKna0h48F/A101Mt/ag72+kMkkBZXPQ7cjbz2syXI/HI3OlBdUytSdHneljfjvUoqwqiQ==",
|
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@ -374,9 +374,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-5sTkYhAGHNRr6bVf4RM0PsscqVr6/DBYdrlMh168oph3usid3lKHcHEEHmr34iZ9GHeeg2juFOxtpl6XyC3tpw==",
|
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1649,9 +1649,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||||
"integrity": "sha512-33WKo67auOXzZHBY/9DTJRo7kIvfU12S+D4sp2wIz39N88MDIaCGyCwbW01RR70pK6Iya0I74lHEpyLfFqOHPA==",
|
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -1661,28 +1661,28 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/android-arm": "0.18.10",
|
"@esbuild/android-arm": "0.18.20",
|
||||||
"@esbuild/android-arm64": "0.18.10",
|
"@esbuild/android-arm64": "0.18.20",
|
||||||
"@esbuild/android-x64": "0.18.10",
|
"@esbuild/android-x64": "0.18.20",
|
||||||
"@esbuild/darwin-arm64": "0.18.10",
|
"@esbuild/darwin-arm64": "0.18.20",
|
||||||
"@esbuild/darwin-x64": "0.18.10",
|
"@esbuild/darwin-x64": "0.18.20",
|
||||||
"@esbuild/freebsd-arm64": "0.18.10",
|
"@esbuild/freebsd-arm64": "0.18.20",
|
||||||
"@esbuild/freebsd-x64": "0.18.10",
|
"@esbuild/freebsd-x64": "0.18.20",
|
||||||
"@esbuild/linux-arm": "0.18.10",
|
"@esbuild/linux-arm": "0.18.20",
|
||||||
"@esbuild/linux-arm64": "0.18.10",
|
"@esbuild/linux-arm64": "0.18.20",
|
||||||
"@esbuild/linux-ia32": "0.18.10",
|
"@esbuild/linux-ia32": "0.18.20",
|
||||||
"@esbuild/linux-loong64": "0.18.10",
|
"@esbuild/linux-loong64": "0.18.20",
|
||||||
"@esbuild/linux-mips64el": "0.18.10",
|
"@esbuild/linux-mips64el": "0.18.20",
|
||||||
"@esbuild/linux-ppc64": "0.18.10",
|
"@esbuild/linux-ppc64": "0.18.20",
|
||||||
"@esbuild/linux-riscv64": "0.18.10",
|
"@esbuild/linux-riscv64": "0.18.20",
|
||||||
"@esbuild/linux-s390x": "0.18.10",
|
"@esbuild/linux-s390x": "0.18.20",
|
||||||
"@esbuild/linux-x64": "0.18.10",
|
"@esbuild/linux-x64": "0.18.20",
|
||||||
"@esbuild/netbsd-x64": "0.18.10",
|
"@esbuild/netbsd-x64": "0.18.20",
|
||||||
"@esbuild/openbsd-x64": "0.18.10",
|
"@esbuild/openbsd-x64": "0.18.20",
|
||||||
"@esbuild/sunos-x64": "0.18.10",
|
"@esbuild/sunos-x64": "0.18.20",
|
||||||
"@esbuild/win32-arm64": "0.18.10",
|
"@esbuild/win32-arm64": "0.18.20",
|
||||||
"@esbuild/win32-ia32": "0.18.10",
|
"@esbuild/win32-ia32": "0.18.20",
|
||||||
"@esbuild/win32-x64": "0.18.10"
|
"@esbuild/win32-x64": "0.18.20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
@ -4816,9 +4816,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver"
|
"semver": "bin/semver"
|
||||||
}
|
}
|
||||||
@ -6800,135 +6800,135 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm": {
|
"@esbuild/android-arm": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||||
"integrity": "sha512-3KClmVNd+Fku82uZJz5C4Rx8m1PPmWUFz5Zkw8jkpZPOmsq+EG1TTOtw1OXkHuX3WczOFQigrtf60B1ijKwNsg==",
|
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm64": {
|
"@esbuild/android-arm64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||||
"integrity": "sha512-ynm4naLbNbK0ajf9LUWtQB+6Vfg1Z/AplArqr4tGebC00Z6m9Y91OVIcjDa461wGcZwcaHYaZAab4yJxfhisTQ==",
|
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/android-x64": {
|
"@esbuild/android-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-vFfXj8P9Yfjh54yqUDEHKzqzYuEfPyAOl3z7R9hjkwt+NCvbn9VMxX+IILnAfdImRBfYVItgSUsqGKhJFnBwZw==",
|
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/darwin-arm64": {
|
"@esbuild/darwin-arm64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||||
"integrity": "sha512-k2OJQ7ZxE6sVc91+MQeZH9gFeDAH2uIYALPAwTjTCvcPy9Dzrf7V7gFUQPYkn09zloWhQ+nvxWHia2x2ZLR0sQ==",
|
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/darwin-x64": {
|
"@esbuild/darwin-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-tnz/mdZk1L1Z3WpGjin/L2bKTe8/AKZpI8fcCLtH+gq8WXWsCNJSxlesAObV4qbtTl6pG5vmqFXfWUQ5hV8PAQ==",
|
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/freebsd-arm64": {
|
"@esbuild/freebsd-arm64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||||
"integrity": "sha512-QJluV0LwBrbHnYYwSKC+K8RGz0g/EyhpQH1IxdoFT0nM7PfgjE+aS8wxq/KFEsU0JkL7U/EEKd3O8xVBxXb2aA==",
|
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/freebsd-x64": {
|
"@esbuild/freebsd-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-Hi/ycUkS6KTw+U9G5PK5NoK7CZboicaKUSVs0FSiPNtuCTzK6HNM4DIgniH7hFaeuszDS9T4dhAHWiLSt/Y5Ng==",
|
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-arm": {
|
"@esbuild/linux-arm": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||||
"integrity": "sha512-HfFoxY172tVHPIvJy+FHxzB4l8xU7e5cxmNS11cQ2jt4JWAukn/7LXaPdZid41UyTweqa4P/1zs201gRGCTwHw==",
|
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-arm64": {
|
"@esbuild/linux-arm64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||||
"integrity": "sha512-Nz6XcfRBOO7jSrVpKAyEyFOPGhySPNlgumSDhWAspdQQ11ub/7/NZDMhWDFReE9QH/SsCOCLQbdj0atAk/HMOQ==",
|
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-ia32": {
|
"@esbuild/linux-ia32": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||||
"integrity": "sha512-otMdmSmkMe+pmiP/bZBjfphyAsTsngyT9RCYwoFzqrveAbux9nYitDTpdgToG0Z0U55+PnH654gCH2GQ1aB6Yw==",
|
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-loong64": {
|
"@esbuild/linux-loong64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||||
"integrity": "sha512-t8tjFuON1koxskzQ4VFoh0T5UDUMiLYjwf9Wktd0tx8AoK6xgU+5ubKOpWpcnhEQ2tESS5u0v6QuN8PX/ftwcQ==",
|
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-mips64el": {
|
"@esbuild/linux-mips64el": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||||
"integrity": "sha512-+dUkcVzcfEJHz3HEnVpIJu8z8Wdn2n/nWMWdl6FVPFGJAVySO4g3+XPzNKFytVFwf8hPVDwYXzVcu8GMFqsqZw==",
|
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-ppc64": {
|
"@esbuild/linux-ppc64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||||
"integrity": "sha512-sO3PjjxEGy+PY2qkGe2gwJbXdZN9wAYpVBZWFD0AwAoKuXRkWK0/zaMQ5ekUFJDRDCRm8x5U0Axaub7ynH/wVg==",
|
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-riscv64": {
|
"@esbuild/linux-riscv64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||||
"integrity": "sha512-JDtdbJg3yjDeXLv4lZYE1kiTnxv73/8cbPHY9T/dUKi8rYOM/k5b3W4UJLMUksuQ6nTm5c89W1nADsql6FW75A==",
|
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-s390x": {
|
"@esbuild/linux-s390x": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||||
"integrity": "sha512-NLuSKcp8WckjD2a7z5kzLiCywFwBTMlIxDNuud1AUGVuwBBJSkuubp6cNjJ0p5c6CZaA3QqUGwjHJBiG1SoOFw==",
|
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-x64": {
|
"@esbuild/linux-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-wj2KRsCsFusli+6yFgNO/zmmLslislAWryJnodteRmGej7ZzinIbMdsyp13rVGde88zxJd5vercNYK9kuvlZaQ==",
|
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/netbsd-x64": {
|
"@esbuild/netbsd-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-pQ9QqxEPI3cVRZyUtCoZxhZK3If+7RzR8L2yz2+TDzdygofIPOJFaAPkEJ5rYIbUO101RaiYxfdOBahYexLk5A==",
|
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/openbsd-x64": {
|
"@esbuild/openbsd-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-k8GTIIW9I8pEEfoOUm32TpPMgSg06JhL5DO+ql66aLTkOQUs0TxCA67Wi7pv6z8iF8STCGcNbm3UWFHLuci+ag==",
|
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/sunos-x64": {
|
"@esbuild/sunos-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-vIGYJIdEI6d4JBucAx8py792G8J0GP40qSH+EvSt80A4zvGd6jph+5t1g+eEXcS2aRpgZw6CrssNCFZxTdEsxw==",
|
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-arm64": {
|
"@esbuild/win32-arm64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||||
"integrity": "sha512-kRhNcMZFGMW+ZHCarAM1ypr8OZs0k688ViUCetVCef9p3enFxzWeBg9h/575Y0nsFu0ZItluCVF5gMR2pwOEpA==",
|
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-ia32": {
|
"@esbuild/win32-ia32": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||||
"integrity": "sha512-AR9PX1whYaYh9p0EOaKna0h48F/A101Mt/ag72+kMkkBZXPQ7cjbz2syXI/HI3OlBdUytSdHneljfjvUoqwqiQ==",
|
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-x64": {
|
"@esbuild/win32-x64": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-5sTkYhAGHNRr6bVf4RM0PsscqVr6/DBYdrlMh168oph3usid3lKHcHEEHmr34iZ9GHeeg2juFOxtpl6XyC3tpw==",
|
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@jridgewell/gen-mapping": {
|
"@jridgewell/gen-mapping": {
|
||||||
@ -7899,33 +7899,33 @@
|
|||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||||
},
|
},
|
||||||
"esbuild": {
|
"esbuild": {
|
||||||
"version": "0.18.10",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||||
"integrity": "sha512-33WKo67auOXzZHBY/9DTJRo7kIvfU12S+D4sp2wIz39N88MDIaCGyCwbW01RR70pK6Iya0I74lHEpyLfFqOHPA==",
|
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@esbuild/android-arm": "0.18.10",
|
"@esbuild/android-arm": "0.18.20",
|
||||||
"@esbuild/android-arm64": "0.18.10",
|
"@esbuild/android-arm64": "0.18.20",
|
||||||
"@esbuild/android-x64": "0.18.10",
|
"@esbuild/android-x64": "0.18.20",
|
||||||
"@esbuild/darwin-arm64": "0.18.10",
|
"@esbuild/darwin-arm64": "0.18.20",
|
||||||
"@esbuild/darwin-x64": "0.18.10",
|
"@esbuild/darwin-x64": "0.18.20",
|
||||||
"@esbuild/freebsd-arm64": "0.18.10",
|
"@esbuild/freebsd-arm64": "0.18.20",
|
||||||
"@esbuild/freebsd-x64": "0.18.10",
|
"@esbuild/freebsd-x64": "0.18.20",
|
||||||
"@esbuild/linux-arm": "0.18.10",
|
"@esbuild/linux-arm": "0.18.20",
|
||||||
"@esbuild/linux-arm64": "0.18.10",
|
"@esbuild/linux-arm64": "0.18.20",
|
||||||
"@esbuild/linux-ia32": "0.18.10",
|
"@esbuild/linux-ia32": "0.18.20",
|
||||||
"@esbuild/linux-loong64": "0.18.10",
|
"@esbuild/linux-loong64": "0.18.20",
|
||||||
"@esbuild/linux-mips64el": "0.18.10",
|
"@esbuild/linux-mips64el": "0.18.20",
|
||||||
"@esbuild/linux-ppc64": "0.18.10",
|
"@esbuild/linux-ppc64": "0.18.20",
|
||||||
"@esbuild/linux-riscv64": "0.18.10",
|
"@esbuild/linux-riscv64": "0.18.20",
|
||||||
"@esbuild/linux-s390x": "0.18.10",
|
"@esbuild/linux-s390x": "0.18.20",
|
||||||
"@esbuild/linux-x64": "0.18.10",
|
"@esbuild/linux-x64": "0.18.20",
|
||||||
"@esbuild/netbsd-x64": "0.18.10",
|
"@esbuild/netbsd-x64": "0.18.20",
|
||||||
"@esbuild/openbsd-x64": "0.18.10",
|
"@esbuild/openbsd-x64": "0.18.20",
|
||||||
"@esbuild/sunos-x64": "0.18.10",
|
"@esbuild/sunos-x64": "0.18.20",
|
||||||
"@esbuild/win32-arm64": "0.18.10",
|
"@esbuild/win32-arm64": "0.18.20",
|
||||||
"@esbuild/win32-ia32": "0.18.10",
|
"@esbuild/win32-ia32": "0.18.20",
|
||||||
"@esbuild/win32-x64": "0.18.10"
|
"@esbuild/win32-x64": "0.18.20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"escalade": {
|
"escalade": {
|
||||||
@ -10364,9 +10364,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
|
||||||
},
|
},
|
||||||
"send": {
|
"send": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
|
@ -41,6 +41,6 @@
|
|||||||
"live-server": "^1.2.2"
|
"live-server": "^1.2.2"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"esbuild": "^0.18.10"
|
"esbuild": "^0.18.20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,6 +226,12 @@ 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)
|
||||||
|
api.DELETE(p+"/users/referral", app.DisableReferralForUsers)
|
||||||
|
api.POST(p+"/profiles/referral/:profile/:invite", app.EnableReferralForProfile)
|
||||||
|
api.DELETE(p+"/profiles/referral/:profile", app.DisableReferralForProfile)
|
||||||
|
}
|
||||||
|
|
||||||
if userPageEnabled {
|
if userPageEnabled {
|
||||||
user.GET(p+"/details", app.MyDetails)
|
user.GET(p+"/details", app.MyDetails)
|
||||||
@ -242,6 +248,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,10 @@
|
|||||||
"expiry": "common",
|
"expiry": "common",
|
||||||
"add": "common",
|
"add": "common",
|
||||||
"edit": "common",
|
"edit": "common",
|
||||||
"delete": "admin"
|
"delete": "common",
|
||||||
|
"myAccount": "common",
|
||||||
|
"referrals": "common",
|
||||||
|
"inviteRemainingUses": "admin"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorLoginBlank": "common",
|
"errorLoginBlank": "common",
|
||||||
|
55
storage.go
@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/steambap/captcha"
|
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -429,15 +428,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 +470,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 +496,21 @@ 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
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type Captcha struct {
|
||||||
|
Answer string
|
||||||
|
Image []byte // image/png
|
||||||
|
Generated time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type Lang struct {
|
type Lang struct {
|
||||||
|
@ -78,6 +78,11 @@ 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"));
|
||||||
|
window.modals.enableReferralsProfile = new Modal(document.getElementById("modal-enable-referrals-profile"));
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
var inviteCreator = new createInvite();
|
var inviteCreator = new createInvite();
|
||||||
|
@ -392,3 +392,8 @@ const create = (event: SubmitEvent) => {
|
|||||||
validator.validate();
|
validator.validate();
|
||||||
|
|
||||||
form.onsubmit = create;
|
form.onsubmit = create;
|
||||||
|
|
||||||
|
const invitedByAside = document.getElementById("invite-from-user");
|
||||||
|
if (typeof(invitedByAside) != "undefined" && invitedByAside != null) {
|
||||||
|
invitedByAside.textContent = invitedByAside.textContent.replace("{user}", invitedByAside.getAttribute("data-from"));
|
||||||
|
}
|
||||||
|
@ -162,6 +162,8 @@ export class Discord extends ServiceLinker {
|
|||||||
onclick() {
|
onclick() {
|
||||||
if (this._conf.inviteURL != "") {
|
if (this._conf.inviteURL != "") {
|
||||||
this._getInviteURL();
|
this._getInviteURL();
|
||||||
|
} else {
|
||||||
|
(document.getElementById("discord-invite") as HTMLSpanElement).parentElement.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onclick();
|
super.onclick();
|
||||||
|
@ -23,6 +23,7 @@ interface User {
|
|||||||
notify_matrix: boolean;
|
notify_matrix: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
accounts_admin: boolean;
|
accounts_admin: boolean;
|
||||||
|
referrals_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface getPinResponse {
|
interface getPinResponse {
|
||||||
@ -69,6 +70,8 @@ class user implements User {
|
|||||||
private _labelEditButton: HTMLElement;
|
private _labelEditButton: HTMLElement;
|
||||||
private _accounts_admin: HTMLInputElement
|
private _accounts_admin: HTMLInputElement
|
||||||
private _selected: boolean;
|
private _selected: boolean;
|
||||||
|
private _referralsEnabled: boolean;
|
||||||
|
private _referralsEnabledCheck: HTMLElement;
|
||||||
|
|
||||||
lastNotifyMethod = (): string => {
|
lastNotifyMethod = (): string => {
|
||||||
// Telegram, Matrix, Discord
|
// Telegram, Matrix, Discord
|
||||||
@ -162,6 +165,17 @@ class user implements User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get referrals_enabled(): boolean { return this._referralsEnabled; }
|
||||||
|
set referrals_enabled(v: boolean) {
|
||||||
|
this._referralsEnabled = v;
|
||||||
|
if (!window.referralsEnabled) return;
|
||||||
|
if (!v) {
|
||||||
|
this._referralsEnabledCheck.textContent = ``;
|
||||||
|
} else {
|
||||||
|
this._referralsEnabledCheck.innerHTML = `<i class="ri-check-line" aria-label="${window.lang.strings("enabled")}"></i>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _constructDropdown = (): HTMLDivElement => {
|
private _constructDropdown = (): HTMLDivElement => {
|
||||||
const el = document.createElement("div") as HTMLDivElement;
|
const el = document.createElement("div") as HTMLDivElement;
|
||||||
const telegram = this._telegramUsername != "";
|
const telegram = this._telegramUsername != "";
|
||||||
@ -506,6 +520,11 @@ class user implements User {
|
|||||||
<td class="accounts-discord"></td>
|
<td class="accounts-discord"></td>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
if (window.referralsEnabled) {
|
||||||
|
innerHTML += `
|
||||||
|
<td class="accounts-referrals text-center-i grid gap-4 place-items-stretch"></td>
|
||||||
|
`;
|
||||||
|
}
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
<td class="accounts-expiry"></td>
|
<td class="accounts-expiry"></td>
|
||||||
<td class="accounts-last-active whitespace-nowrap"></td>
|
<td class="accounts-last-active whitespace-nowrap"></td>
|
||||||
@ -544,6 +563,10 @@ class user implements User {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.referralsEnabled) {
|
||||||
|
this._referralsEnabledCheck = this._row.querySelector(".accounts-referrals");
|
||||||
|
}
|
||||||
|
|
||||||
this._notifyDropdown = this._constructDropdown();
|
this._notifyDropdown = this._constructDropdown();
|
||||||
|
|
||||||
@ -716,6 +739,7 @@ class user implements User {
|
|||||||
this.discord_id = user.discord_id;
|
this.discord_id = user.discord_id;
|
||||||
this.label = user.label;
|
this.label = user.label;
|
||||||
this.accounts_admin = user.accounts_admin;
|
this.accounts_admin = user.accounts_admin;
|
||||||
|
this.referrals_enabled = user.referrals_enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
asElement = (): HTMLTableRowElement => { return this._row; }
|
asElement = (): HTMLTableRowElement => { return this._row; }
|
||||||
@ -748,9 +772,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;
|
||||||
@ -906,6 +935,14 @@ export class accountsList {
|
|||||||
bool: true,
|
bool: true,
|
||||||
string: false,
|
string: false,
|
||||||
date: true
|
date: true
|
||||||
|
},
|
||||||
|
"referrals-enabled": {
|
||||||
|
name: window.lang.strings("referrals"),
|
||||||
|
getter: "referrals_enabled",
|
||||||
|
bool: true,
|
||||||
|
string: false,
|
||||||
|
date: false,
|
||||||
|
dependsOnTableHeader: "accounts-header-referrals"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -919,7 +956,6 @@ export class accountsList {
|
|||||||
|
|
||||||
// const words = query.split(" ");
|
// const words = query.split(" ");
|
||||||
let words: string[] = [];
|
let words: string[] = [];
|
||||||
// FIXME: SPLIT BY SPACE, UNLESS IN QUOTES
|
|
||||||
|
|
||||||
let quoteSymbol = ``;
|
let quoteSymbol = ``;
|
||||||
let queryStart = -1;
|
let queryStart = -1;
|
||||||
@ -985,7 +1021,6 @@ export class accountsList {
|
|||||||
boolState = false;
|
boolState = false;
|
||||||
}
|
}
|
||||||
if (isBool) {
|
if (isBool) {
|
||||||
// FIXME: Generate filter card for each filter class
|
|
||||||
const filterCard = document.createElement("span");
|
const filterCard = document.createElement("span");
|
||||||
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
|
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
|
||||||
filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "mx-2", "h-full");
|
filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "mx-2", "h-full");
|
||||||
@ -1058,7 +1093,7 @@ export class accountsList {
|
|||||||
|
|
||||||
let attempt: { year?: number, month?: number, day?: number, hour?: number, minute?: number } = dateParser.attempt(split[1]);
|
let attempt: { year?: number, month?: number, day?: number, hour?: number, minute?: number } = dateParser.attempt(split[1]);
|
||||||
// Month in Date objects is 0-based, so make our parsed date that way too
|
// Month in Date objects is 0-based, so make our parsed date that way too
|
||||||
if ("month" in attempt) attempt["month"] -= 1;
|
if ("month" in attempt) attempt.month -= 1;
|
||||||
|
|
||||||
let date: Date = (Date as any).fromString(split[1]) as Date;
|
let date: Date = (Date as any).fromString(split[1]) as Date;
|
||||||
console.log("Read", attempt, "and", date);
|
console.log("Read", attempt, "and", date);
|
||||||
@ -1124,7 +1159,7 @@ export class accountsList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -1154,6 +1189,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 +1214,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) {
|
||||||
@ -1184,6 +1225,7 @@ export class accountsList {
|
|||||||
let anyNonExpiries = list.length == 0 ? true : false;
|
let anyNonExpiries = list.length == 0 ? true : false;
|
||||||
let allNonExpiries = true;
|
let allNonExpiries = true;
|
||||||
let noContactCount = 0;
|
let noContactCount = 0;
|
||||||
|
let referralState = Number(this._users[list[0]].referrals_enabled); // -1 = hide, 0 = show "enable", 1 = show "disable"
|
||||||
// Only show enable/disable button if all selected have the same state.
|
// Only show enable/disable button if all selected have the same state.
|
||||||
this._shouldEnable = this._users[list[0]].disabled
|
this._shouldEnable = this._users[list[0]].disabled
|
||||||
let showDisableEnable = true;
|
let showDisableEnable = true;
|
||||||
@ -1203,6 +1245,9 @@ export class accountsList {
|
|||||||
if (!this._users[id].lastNotifyMethod()) {
|
if (!this._users[id].lastNotifyMethod()) {
|
||||||
noContactCount++;
|
noContactCount++;
|
||||||
}
|
}
|
||||||
|
if (window.referralsEnabled && referralState != -1 && Number(this._users[id].referrals_enabled) != referralState) {
|
||||||
|
referralState = -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._settingExpiry = false;
|
this._settingExpiry = false;
|
||||||
if (!anyNonExpiries && !allNonExpiries) {
|
if (!anyNonExpiries && !allNonExpiries) {
|
||||||
@ -1236,6 +1281,22 @@ export class accountsList {
|
|||||||
this._disableEnable.parentElement.classList.remove("unfocused");
|
this._disableEnable.parentElement.classList.remove("unfocused");
|
||||||
this._disableEnable.textContent = message;
|
this._disableEnable.textContent = message;
|
||||||
}
|
}
|
||||||
|
if (window.referralsEnabled) {
|
||||||
|
if (referralState == -1) {
|
||||||
|
this._enableReferrals.classList.add("unfocused");
|
||||||
|
} else {
|
||||||
|
this._enableReferrals.classList.remove("unfocused");
|
||||||
|
}
|
||||||
|
if (referralState == 0) {
|
||||||
|
this._enableReferrals.classList.add("~urge");
|
||||||
|
this._enableReferrals.classList.remove("~warning");
|
||||||
|
this._enableReferrals.textContent = window.lang.strings("enableReferrals");
|
||||||
|
} else if (referralState == 1) {
|
||||||
|
this._enableReferrals.classList.add("~warning");
|
||||||
|
this._enableReferrals.classList.remove("~urge");
|
||||||
|
this._enableReferrals.textContent = window.lang.strings("disableReferrals");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1662,6 +1723,90 @@ 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();
|
||||||
|
|
||||||
|
// Check if we're disabling or enabling
|
||||||
|
if (this._users[list[0]].referrals_enabled) {
|
||||||
|
_delete("/users/referral", {"users": list}, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState != 4 || req.status != 200) return;
|
||||||
|
window.notifications.customSuccess("disabledReferralsSuccess", window.lang.quantity("appliedSettings", list.length));
|
||||||
|
this.reload();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
_get("/invites", null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState != 4 || req.status != 200) return;
|
||||||
|
|
||||||
|
// 1. Invites
|
||||||
|
|
||||||
|
let innerHTML = "";
|
||||||
|
let invites = req.response["invites"] as Array<Invite>;
|
||||||
|
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 += `<option value="${inv.code}">${name}</option>`;
|
||||||
|
}
|
||||||
|
this._enableReferralsInvite.checked = true;
|
||||||
|
} else {
|
||||||
|
this._enableReferralsInvite.checked = false;
|
||||||
|
innerHTML += `<option>${window.lang.strings("inviteNoInvites")}</option>`;
|
||||||
|
}
|
||||||
|
this._enableReferralsProfile.checked = !(this._enableReferralsInvite.checked);
|
||||||
|
this._referralsInviteSelect.innerHTML = innerHTML;
|
||||||
|
|
||||||
|
// 2. Profiles
|
||||||
|
|
||||||
|
innerHTML = "";
|
||||||
|
for (const profile of window.availableProfiles) {
|
||||||
|
innerHTML += `<option value="${profile}">${profile}</option>`;
|
||||||
|
}
|
||||||
|
this._referralsProfileSelect.innerHTML = innerHTML;
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
const form = document.getElementById("form-enable-referrals-user") as HTMLFormElement;
|
||||||
|
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||||
|
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;
|
||||||
|
} else if (this._enableReferralsInvite.checked && !this._enableReferralsProfile.checked) {
|
||||||
|
send["from"] = "invite";
|
||||||
|
send["id"] = this._referralsInviteSelect.value;
|
||||||
|
}
|
||||||
|
_post("/users/referral/" + send["from"] + "/" + (send["id"] ? send["id"] : send["profile"]), send, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
toggleLoader(button);
|
||||||
|
if (req.status == 400) {
|
||||||
|
window.notifications.customError("noReferralTemplateError", window.lang.notif("errorNoReferralTemplate"));
|
||||||
|
} else if (req.status == 200 || req.status == 204) {
|
||||||
|
window.notifications.customSuccess("enableReferralsSuccess", window.lang.quantity("appliedSettings", list.length));
|
||||||
|
}
|
||||||
|
this.reload();
|
||||||
|
window.modals.enableReferralsUser.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this._enableReferralsProfile.checked = true;
|
||||||
|
this._enableReferralsInvite.checked = false;
|
||||||
|
window.modals.enableReferralsUser.show();
|
||||||
|
}
|
||||||
|
|
||||||
extendExpiry = (enableUser?: boolean) => {
|
extendExpiry = (enableUser?: boolean) => {
|
||||||
const list = this._collectUsers();
|
const list = this._collectUsers();
|
||||||
@ -1794,6 +1939,43 @@ export class accountsList {
|
|||||||
this._modifySettingsProfile.onchange = checkSource;
|
this._modifySettingsProfile.onchange = checkSource;
|
||||||
this._modifySettingsUser.onchange = checkSource;
|
this._modifySettingsUser.onchange = checkSource;
|
||||||
|
|
||||||
|
if (window.referralsEnabled) {
|
||||||
|
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")
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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;
|
this._deleteUser.onclick = this.deleteUsers;
|
||||||
this._deleteUser.classList.add("unfocused");
|
this._deleteUser.classList.add("unfocused");
|
||||||
|
|
||||||
@ -1873,8 +2055,8 @@ export class accountsList {
|
|||||||
this.loadPreview();
|
this.loadPreview();
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerNames: string[] = ["username", "access-jfa", "email", "telegram", "matrix", "discord", "expiry", "last-active"];
|
const headerNames: string[] = ["username", "access-jfa", "email", "telegram", "matrix", "discord", "expiry", "last-active", "referrals"];
|
||||||
const headerGetters: string[] = ["name", "accounts_admin", "email", "telegram", "matrix", "discord", "expiry", "last_active"];
|
const headerGetters: string[] = ["name", "accounts_admin", "email", "telegram", "matrix", "discord", "expiry", "last_active", "referrals_enabled"];
|
||||||
for (let i = 0; i < headerNames.length; i++) {
|
for (let i = 0; i < headerNames.length; i++) {
|
||||||
const header: HTMLTableHeaderCellElement = document.querySelector(".accounts-header-" + headerNames[i]) as HTMLTableHeaderCellElement;
|
const header: HTMLTableHeaderCellElement = document.querySelector(".accounts-header-" + headerNames[i]) as HTMLTableHeaderCellElement;
|
||||||
if (header !== null) {
|
if (header !== null) {
|
||||||
|
@ -39,6 +39,23 @@ class DOMInvite implements Invite {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _userLabel: string = "";
|
||||||
|
get user_label(): string { return this._userLabel; }
|
||||||
|
set user_label(label: string) {
|
||||||
|
this._userLabel = label;
|
||||||
|
const labelLabel = this._middle.querySelector(".user-label-label");
|
||||||
|
const value = this._middle.querySelector(".user-label");
|
||||||
|
if (label) {
|
||||||
|
labelLabel.textContent = window.lang.strings("userLabel");
|
||||||
|
value.textContent = label;
|
||||||
|
value.classList.remove("unfocused");
|
||||||
|
} else {
|
||||||
|
labelLabel.textContent = "";
|
||||||
|
value.textContent = "";
|
||||||
|
value.classList.add("unfocused");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _code: string = "None";
|
private _code: string = "None";
|
||||||
get code(): string { return this._code; }
|
get code(): string { return this._code; }
|
||||||
set code(code: string) {
|
set code(code: string) {
|
||||||
@ -351,6 +368,7 @@ class DOMInvite implements Invite {
|
|||||||
<p class="supra mb-4 top">${window.lang.strings("inviteDateCreated")} <strong class="inv-created"></strong></p>
|
<p class="supra mb-4 top">${window.lang.strings("inviteDateCreated")} <strong class="inv-created"></strong></p>
|
||||||
<p class="supra mb-4">${window.lang.strings("inviteRemainingUses")} <strong class="inv-remaining"></strong></p>
|
<p class="supra mb-4">${window.lang.strings("inviteRemainingUses")} <strong class="inv-remaining"></strong></p>
|
||||||
<p class="supra mb-4"><span class="user-expiry"></span> <strong class="user-expiry-time"></strong></p>
|
<p class="supra mb-4"><span class="user-expiry"></span> <strong class="user-expiry-time"></strong></p>
|
||||||
|
<p class="mb-4 flex items-center"><span class="user-label-label supra mr-2"></span> <span class="user-label chip ~blue unfocused"></span></p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this._right = document.createElement('div') as HTMLDivElement;
|
this._right = document.createElement('div') as HTMLDivElement;
|
||||||
@ -386,6 +404,9 @@ class DOMInvite implements Invite {
|
|||||||
if (invite.label) {
|
if (invite.label) {
|
||||||
this.label = invite.label;
|
this.label = invite.label;
|
||||||
}
|
}
|
||||||
|
if (invite.user_label) {
|
||||||
|
this.user_label = invite.user_label;
|
||||||
|
}
|
||||||
this.userExpiryTime = invite.userExpiryTime || "";
|
this.userExpiryTime = invite.userExpiryTime || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,6 +507,7 @@ function parseInvite(invite: { [f: string]: string | number | { [name: string]:
|
|||||||
parsed.code = invite["code"] as string;
|
parsed.code = invite["code"] as string;
|
||||||
parsed.send_to = invite["send_to"] as string || "";
|
parsed.send_to = invite["send_to"] as string || "";
|
||||||
parsed.label = invite["label"] as string || "";
|
parsed.label = invite["label"] as string || "";
|
||||||
|
parsed.user_label = invite["user_label"] as string || "";
|
||||||
let time = "";
|
let time = "";
|
||||||
let userExpiryTime = "";
|
let userExpiryTime = "";
|
||||||
const fields = ["months", "days", "hours", "minutes"];
|
const fields = ["months", "days", "hours", "minutes"];
|
||||||
@ -530,6 +552,7 @@ export class createInvite {
|
|||||||
private _createButton = document.getElementById("create-submit") as HTMLSpanElement;
|
private _createButton = document.getElementById("create-submit") as HTMLSpanElement;
|
||||||
private _profile = document.getElementById("create-profile") as HTMLSelectElement;
|
private _profile = document.getElementById("create-profile") as HTMLSelectElement;
|
||||||
private _label = document.getElementById("create-label") as HTMLInputElement;
|
private _label = document.getElementById("create-label") as HTMLInputElement;
|
||||||
|
private _userLabel = document.getElementById("create-user-label") as HTMLInputElement;
|
||||||
|
|
||||||
private _months = document.getElementById("create-months") as HTMLSelectElement;
|
private _months = document.getElementById("create-months") as HTMLSelectElement;
|
||||||
private _days = document.getElementById("create-days") as HTMLSelectElement;
|
private _days = document.getElementById("create-days") as HTMLSelectElement;
|
||||||
@ -572,6 +595,9 @@ export class createInvite {
|
|||||||
get label(): string { return this._label.value; }
|
get label(): string { return this._label.value; }
|
||||||
set label(label: string) { this._label.value = label; }
|
set label(label: string) { this._label.value = label; }
|
||||||
|
|
||||||
|
get user_label(): string { return this._userLabel.value; }
|
||||||
|
set user_label(label: string) { this._userLabel.value = label; }
|
||||||
|
|
||||||
get sendToEnabled(): boolean {
|
get sendToEnabled(): boolean {
|
||||||
return this._sendToEnabled.checked;
|
return this._sendToEnabled.checked;
|
||||||
}
|
}
|
||||||
@ -749,7 +775,8 @@ export class createInvite {
|
|||||||
"remaining-uses": this.uses,
|
"remaining-uses": this.uses,
|
||||||
"send-to": this.sendToEnabled ? this.sendTo : "",
|
"send-to": this.sendToEnabled ? this.sendTo : "",
|
||||||
"profile": this.profile,
|
"profile": this.profile,
|
||||||
"label": this.label
|
"label": this.label,
|
||||||
|
"user_label": this.user_label
|
||||||
};
|
};
|
||||||
_post("/invites", send, (req: XMLHttpRequest) => {
|
_post("/invites", send, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState == 4) {
|
||||||
|
@ -5,6 +5,7 @@ interface Profile {
|
|||||||
libraries: string;
|
libraries: string;
|
||||||
fromUser: string;
|
fromUser: string;
|
||||||
ombi: boolean;
|
ombi: boolean;
|
||||||
|
referrals_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class profile implements Profile {
|
class profile implements Profile {
|
||||||
@ -16,6 +17,8 @@ class profile implements Profile {
|
|||||||
private _fromUser: HTMLTableDataCellElement;
|
private _fromUser: HTMLTableDataCellElement;
|
||||||
private _defaultRadio: HTMLInputElement;
|
private _defaultRadio: HTMLInputElement;
|
||||||
private _ombi: boolean;
|
private _ombi: boolean;
|
||||||
|
private _referralsButton: HTMLSpanElement;
|
||||||
|
private _referralsEnabled: boolean;
|
||||||
|
|
||||||
get name(): string { return this._name.textContent; }
|
get name(): string { return this._name.textContent; }
|
||||||
set name(v: string) { this._name.textContent = v; }
|
set name(v: string) { this._name.textContent = v; }
|
||||||
@ -51,7 +54,22 @@ class profile implements Profile {
|
|||||||
|
|
||||||
get fromUser(): string { return this._fromUser.textContent; }
|
get fromUser(): string { return this._fromUser.textContent; }
|
||||||
set fromUser(v: string) { this._fromUser.textContent = v; }
|
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; }
|
get default(): boolean { return this._defaultRadio.checked; }
|
||||||
set default(v: boolean) { this._defaultRadio.checked = v; }
|
set default(v: boolean) { this._defaultRadio.checked = v; }
|
||||||
|
|
||||||
@ -64,6 +82,9 @@ class profile implements Profile {
|
|||||||
if (window.ombiEnabled) innerHTML += `
|
if (window.ombiEnabled) innerHTML += `
|
||||||
<td><span class="button @low profile-ombi"></span></td>
|
<td><span class="button @low profile-ombi"></span></td>
|
||||||
`;
|
`;
|
||||||
|
if (window.referralsEnabled) innerHTML += `
|
||||||
|
<td><span class="button @low profile-referrals"></span></td>
|
||||||
|
`;
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
<td class="profile-from ellipsis"></td>
|
<td class="profile-from ellipsis"></td>
|
||||||
<td class="profile-libraries"></td>
|
<td class="profile-libraries"></td>
|
||||||
@ -75,6 +96,8 @@ class profile implements Profile {
|
|||||||
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
|
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
|
||||||
if (window.ombiEnabled)
|
if (window.ombiEnabled)
|
||||||
this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement;
|
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._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
|
||||||
this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement;
|
this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement;
|
||||||
this._defaultRadio.onclick = () => document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name }));
|
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.fromUser = p.fromUser;
|
||||||
this.libraries = p.libraries;
|
this.libraries = p.libraries;
|
||||||
this.ombi = p.ombi;
|
this.ombi = p.ombi;
|
||||||
|
this.referrals_enabled = p.referrals_enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { this._ombiButton.onclick = () => ombiFunc(this._ombi); }
|
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(); }
|
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); }
|
||||||
|
|
||||||
@ -173,6 +198,14 @@ export class ProfileEditor {
|
|||||||
this._ombiProfiles.load(name);
|
this._ombiProfiles.load(name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (window.referralsEnabled)
|
||||||
|
this._profiles[name].setReferralFunc((enabled: boolean) => {
|
||||||
|
if (enabled) {
|
||||||
|
this.disableReferrals(name);
|
||||||
|
} else {
|
||||||
|
this.enableReferrals(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
this._table.appendChild(this._profiles[name].asElement());
|
this._table.appendChild(this._profiles[name].asElement());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,6 +218,62 @@ export class ProfileEditor {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
disableReferrals = (name: string) => _delete("/profiles/referral/" + name, null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState != 4) return;
|
||||||
|
this.load();
|
||||||
|
});
|
||||||
|
|
||||||
|
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<Invite>;
|
||||||
|
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 += `<option value="${inv.code}">${name}</option>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
innerHTML += `<option>${window.lang.strings("inviteNoInvites")}</option>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
(document.getElementById('setting-profiles') as HTMLSpanElement).onclick = this.load;
|
(document.getElementById('setting-profiles') as HTMLSpanElement).onclick = this.load;
|
||||||
document.addEventListener("profiles-default", (event: CustomEvent) => {
|
document.addEventListener("profiles-default", (event: CustomEvent) => {
|
||||||
|
@ -469,7 +469,9 @@ class DOMNote implements SNote {
|
|||||||
set name(n: string) { this._name.textContent = n; }
|
set name(n: string) { this._name.textContent = n; }
|
||||||
|
|
||||||
get description(): string { return this._description.textContent; }
|
get description(): string { return this._description.textContent; }
|
||||||
set description(d: string) { this._description.textContent = d; }
|
set description(d: string) {
|
||||||
|
this._description.innerHTML = d;
|
||||||
|
}
|
||||||
|
|
||||||
get value(): string { return ""; }
|
get value(): string { return ""; }
|
||||||
set value(v: string) { return; }
|
set value(v: string) { return; }
|
||||||
|
@ -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,8 @@ declare interface Modals {
|
|||||||
pwr?: Modal;
|
pwr?: Modal;
|
||||||
logs: Modal;
|
logs: Modal;
|
||||||
email?: Modal;
|
email?: Modal;
|
||||||
|
enableReferralsUser?: Modal;
|
||||||
|
enableReferralsProfile?: Modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Invite {
|
interface Invite {
|
||||||
@ -126,6 +129,7 @@ interface Invite {
|
|||||||
notifyCreation?: boolean;
|
notifyCreation?: boolean;
|
||||||
profile?: string;
|
profile?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
user_label?: string;
|
||||||
userExpiry?: boolean;
|
userExpiry?: boolean;
|
||||||
userExpiryTime?: string;
|
userExpiryTime?: string;
|
||||||
}
|
}
|
||||||
|
150
ts/user.ts
@ -1,7 +1,7 @@
|
|||||||
import { ThemeManager } from "./modules/theme.js";
|
import { ThemeManager } from "./modules/theme.js";
|
||||||
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
|
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
|
||||||
import { Modal } from "./modules/modal.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 { Login } from "./modules/login.js";
|
||||||
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
|
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
|
||||||
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
|
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
|
||||||
@ -18,6 +18,7 @@ interface userWindow extends Window {
|
|||||||
matrixUserID: string;
|
matrixUserID: string;
|
||||||
discordSendPINMessage: string;
|
discordSendPINMessage: string;
|
||||||
pwrEnabled: string;
|
pwrEnabled: string;
|
||||||
|
referralsEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare var window: userWindow;
|
declare var window: userWindow;
|
||||||
@ -107,6 +108,14 @@ interface MyDetails {
|
|||||||
discord?: MyDetailsContactMethod;
|
discord?: MyDetailsContactMethod;
|
||||||
telegram?: MyDetailsContactMethod;
|
telegram?: MyDetailsContactMethod;
|
||||||
matrix?: MyDetailsContactMethod;
|
matrix?: MyDetailsContactMethod;
|
||||||
|
has_referrals: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MyReferral {
|
||||||
|
code: string;
|
||||||
|
remaining_uses: number;
|
||||||
|
no_limit: boolean;
|
||||||
|
expiry: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContactDTO {
|
interface ContactDTO {
|
||||||
@ -237,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 = `
|
||||||
|
<div class="row my-3">
|
||||||
|
<div class="inline baseline">
|
||||||
|
<span class="text-2xl referral-remaining-uses"></span> <span class="text-gray-400 text-lg">${window.lang.strings("inviteRemainingUses")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row my-3">
|
||||||
|
<div class="inline baseline">
|
||||||
|
<span class="text-gray-400 text-lg">${window.lang.strings("expiry")}</span> <span class="text-2xl referral-expiry"></span>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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")} <i class="ri-check-line ml-2"></i>
|
||||||
|
`;
|
||||||
|
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 {
|
class ExpiryCard {
|
||||||
private _card: HTMLElement;
|
private _card: HTMLElement;
|
||||||
private _expiry: Date;
|
private _expiry: Date;
|
||||||
@ -318,6 +428,9 @@ class ExpiryCard {
|
|||||||
|
|
||||||
var expiryCard = new ExpiryCard(statusCard);
|
var expiryCard = new ExpiryCard(statusCard);
|
||||||
|
|
||||||
|
var referralCard: ReferralCard;
|
||||||
|
if (window.referralsEnabled) referralCard = new ReferralCard(document.getElementById("card-referrals"));
|
||||||
|
|
||||||
var contactMethodList = new ContactMethods(contactCard);
|
var contactMethodList = new ContactMethods(contactCard);
|
||||||
|
|
||||||
const addEditEmail = (add: boolean): void => {
|
const addEditEmail = (add: boolean): void => {
|
||||||
@ -337,6 +450,7 @@ const addEditEmail = (add: boolean): void => {
|
|||||||
_post("/my/email", {"email": input.value}, (req: XMLHttpRequest) => {
|
_post("/my/email", {"email": input.value}, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState == 4 && (req.status == 303 || req.status == 200)) {
|
if (req.readyState == 4 && (req.status == 303 || req.status == 200)) {
|
||||||
document.dispatchEvent(new CustomEvent("details-reload"));
|
document.dispatchEvent(new CustomEvent("details-reload"));
|
||||||
|
window.modals.email.close();
|
||||||
}
|
}
|
||||||
}, true, (req: XMLHttpRequest) => {
|
}, true, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState == 4 && req.status == 401) {
|
if (req.readyState == 4 && req.status == 401) {
|
||||||
@ -363,7 +477,8 @@ const discordConf: ServiceConfiguration = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let discord = new Discord(discordConf);
|
let discord: Discord;
|
||||||
|
if (window.discordEnabled) discord = new Discord(discordConf);
|
||||||
|
|
||||||
const telegramConf: ServiceConfiguration = {
|
const telegramConf: ServiceConfiguration = {
|
||||||
modal: window.modals.telegram as Modal,
|
modal: window.modals.telegram as Modal,
|
||||||
@ -378,7 +493,8 @@ const telegramConf: ServiceConfiguration = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let telegram = new Telegram(telegramConf);
|
let telegram: Telegram;
|
||||||
|
if (window.telegramEnabled) telegram = new Telegram(telegramConf);
|
||||||
|
|
||||||
const matrixConf: MatrixConfiguration = {
|
const matrixConf: MatrixConfiguration = {
|
||||||
modal: window.modals.matrix as Modal,
|
modal: window.modals.matrix as Modal,
|
||||||
@ -393,7 +509,8 @@ const matrixConf: MatrixConfiguration = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let matrix = new Matrix(matrixConf);
|
let matrix: Matrix;
|
||||||
|
if (window.matrixEnabled) matrix = new Matrix(matrixConf);
|
||||||
|
|
||||||
|
|
||||||
const oldPasswordField = document.getElementById("user-old-password") as HTMLInputElement;
|
const oldPasswordField = document.getElementById("user-old-password") as HTMLInputElement;
|
||||||
@ -468,14 +585,15 @@ document.addEventListener("details-reload", () => {
|
|||||||
// Note the weird format of the functions for discord/telegram:
|
// Note the weird format of the functions for discord/telegram:
|
||||||
// "this" was being redefined within the onclick() method, so
|
// "this" was being redefined within the onclick() method, so
|
||||||
// they had to be wrapped in an anonymous function.
|
// they had to be wrapped in an anonymous function.
|
||||||
const contactMethods: { name: string, icon: string, f: (add: boolean) => void, required: boolean }[] = [
|
const contactMethods: { name: string, icon: string, f: (add: boolean) => void, required: boolean, enabled: boolean }[] = [
|
||||||
{name: "email", icon: `<i class="ri-mail-fill ri-lg"></i>`, f: addEditEmail, required: true},
|
{name: "email", icon: `<i class="ri-mail-fill ri-lg"></i>`, f: addEditEmail, required: true, enabled: true},
|
||||||
{name: "discord", icon: `<i class="ri-discord-fill ri-lg"></i>`, f: (add: boolean) => { discord.onclick(); }, required: window.discordRequired},
|
{name: "discord", icon: `<i class="ri-discord-fill ri-lg"></i>`, f: (add: boolean) => { discord.onclick(); }, required: window.discordRequired, enabled: window.discordEnabled},
|
||||||
{name: "telegram", icon: `<i class="ri-telegram-fill ri-lg"></i>`, f: (add: boolean) => { telegram.onclick() }, required: window.telegramRequired},
|
{name: "telegram", icon: `<i class="ri-telegram-fill ri-lg"></i>`, f: (add: boolean) => { telegram.onclick() }, required: window.telegramRequired, enabled: window.telegramEnabled},
|
||||||
{name: "matrix", icon: `<span class="font-bold">[m]</span>`, f: (add: boolean) => { matrix.show(); }, required: window.matrixRequired}
|
{name: "matrix", icon: `<span class="font-bold">[m]</span>`, f: (add: boolean) => { matrix.show(); }, required: window.matrixRequired, enabled: window.matrixEnabled}
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let method of contactMethods) {
|
for (let method of contactMethods) {
|
||||||
|
if (!(method.enabled)) continue;
|
||||||
if (method.name in details) {
|
if (method.name in details) {
|
||||||
contactMethodList.append(method.name, details[method.name], method.icon, method.f, method.required);
|
contactMethodList.append(method.name, details[method.name], method.icon, method.f, method.required);
|
||||||
}
|
}
|
||||||
@ -503,12 +621,26 @@ document.addEventListener("details-reload", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messageCard.innerHTML = messageCard.innerHTML.replace(new RegExp("{username}", "g"), details.username);
|
||||||
|
|
||||||
if (typeof(messageCard) != "undefined" && messageCard != null) {
|
if (typeof(messageCard) != "undefined" && messageCard != null) {
|
||||||
setBestRowSpan(messageCard, false);
|
setBestRowSpan(messageCard, false);
|
||||||
// contactCard.querySelector(".content").classList.add("h-100");
|
// contactCard.querySelector(".content").classList.add("h-100");
|
||||||
} else if (!statusCard.classList.contains("unfocused")) {
|
} else if (!statusCard.classList.contains("unfocused")) {
|
||||||
setBestRowSpan(passwordCard, true);
|
setBestRowSpan(passwordCard, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
referralCard.update(referral);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
referralCard.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -213,7 +213,8 @@ func (ud *Updater) GetTag() (Tag, int, error) {
|
|||||||
|
|
||||||
func (t *Tag) IsNew() bool {
|
func (t *Tag) IsNew() bool {
|
||||||
// fmt.Printf("Build Time: %+v, Release Date: %+v", buildTime, t.ReleaseDate)
|
// fmt.Printf("Build Time: %+v, Release Date: %+v", buildTime, t.ReleaseDate)
|
||||||
return t.Version[:7] != commit && t.Ready && t.ReleaseDate.After(buildTime)
|
// Add 20 minutes to account for build time
|
||||||
|
return t.Version[:7] != commit && t.Ready && t.ReleaseDate.After(buildTime.Add(time.Duration(20)*time.Minute))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ud *Updater) getRelease() (release GHRelease, status int, err error) {
|
func (ud *Updater) getRelease() (release GHRelease, status int, err error) {
|
||||||
|
51
views.go
@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
@ -173,6 +175,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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,6 +206,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
|
|||||||
"langName": lang,
|
"langName": lang,
|
||||||
"jfLink": app.config.Section("ui").Key("redirect_url").String(),
|
"jfLink": app.config.Section("ui").Key("redirect_url").String(),
|
||||||
"requirements": app.validator.getCriteria(),
|
"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 {
|
if telegramEnabled {
|
||||||
data["telegramUsername"] = app.telegram.username
|
data["telegramUsername"] = app.telegram.username
|
||||||
@ -370,20 +374,16 @@ func (app *appContext) GetCaptcha(gc *gin.Context) {
|
|||||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
var capt *captcha.Data
|
var capt Captcha
|
||||||
|
ok = true
|
||||||
if inv.Captchas != nil {
|
if inv.Captchas != nil {
|
||||||
capt = inv.Captchas[captchaID]
|
capt, ok = inv.Captchas[captchaID]
|
||||||
}
|
}
|
||||||
if capt == nil {
|
if !ok {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := capt.WriteImage(gc.Writer); err != nil {
|
gc.Data(200, "image/png", capt.Image)
|
||||||
app.err.Printf("Failed to write CAPTCHA image: %v", err)
|
|
||||||
respondBool(500, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
gc.Status(200)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -412,10 +412,20 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if inv.Captchas == nil {
|
if inv.Captchas == nil {
|
||||||
inv.Captchas = map[string]*captcha.Data{}
|
inv.Captchas = map[string]Captcha{}
|
||||||
}
|
}
|
||||||
captchaID := genAuthToken()
|
captchaID := genAuthToken()
|
||||||
inv.Captchas[captchaID] = capt
|
var buf bytes.Buffer
|
||||||
|
if err := capt.WriteImage(bufio.NewWriter(&buf)); err != nil {
|
||||||
|
app.err.Printf("Failed to render captcha: %v", err)
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inv.Captchas[captchaID] = Captcha{
|
||||||
|
Answer: capt.Text,
|
||||||
|
Image: buf.Bytes(),
|
||||||
|
Generated: time.Now(),
|
||||||
|
}
|
||||||
app.storage.SetInvitesKey(code, inv)
|
app.storage.SetInvitesKey(code, inv)
|
||||||
gc.JSON(200, genCaptchaDTO{captchaID})
|
gc.JSON(200, genCaptchaDTO{captchaID})
|
||||||
return
|
return
|
||||||
@ -435,7 +445,7 @@ func (app *appContext) verifyCaptcha(code, id, text string) bool {
|
|||||||
app.debug.Printf("Couldn't find Captcha \"%s\"", id)
|
app.debug.Printf("Couldn't find Captcha \"%s\"", id)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return strings.ToLower(c.Text) == strings.ToLower(text)
|
return strings.ToLower(c.Answer) == strings.ToLower(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reCAPTCHA
|
// reCAPTCHA
|
||||||
@ -502,15 +512,15 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var capt *captcha.Data
|
var capt Captcha
|
||||||
if inv.Captchas != nil {
|
if inv.Captchas != nil {
|
||||||
capt = inv.Captchas[captchaID]
|
capt, ok = inv.Captchas[captchaID]
|
||||||
}
|
}
|
||||||
if capt == nil {
|
if !ok {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.ToLower(capt.Text) != strings.ToLower(text) {
|
if strings.ToLower(capt.Answer) != strings.ToLower(text) {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -617,6 +627,14 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
userPageAddress += "/my/account"
|
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{
|
data := gin.H{
|
||||||
"urlBase": app.getURLBase(gc),
|
"urlBase": app.getURLBase(gc),
|
||||||
"cssClass": app.cssClass,
|
"cssClass": app.cssClass,
|
||||||
@ -652,6 +670,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
"reCAPTCHASiteKey": app.config.Section("captcha").Key("recaptcha_site_key").MustString(""),
|
"reCAPTCHASiteKey": app.config.Section("captcha").Key("recaptcha_site_key").MustString(""),
|
||||||
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
|
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
|
||||||
"userPageAddress": userPageAddress,
|
"userPageAddress": userPageAddress,
|
||||||
|
"fromUser": fromUser,
|
||||||
}
|
}
|
||||||
if telegram {
|
if telegram {
|
||||||
data["telegramPIN"] = app.telegram.NewAuthToken()
|
data["telegramPIN"] = app.telegram.NewAuthToken()
|
||||||
|