Compare commits

...

52 Commits
db ... v0.5.0

Author SHA1 Message Date
Harvey Tindall
084a62e60f updater: account for build/publish time diff 2023-09-08 19:00:19 +01:00
Harvey Tindall
655dc88c62 node: update deps 2023-09-08 18:21:09 +01:00
Harvey Tindall
46109d1ea3 images: one more try 2023-09-08 17:46:58 +01:00
Harvey Tindall
f7d931be0c images: use non-svgo-ed banner 2023-09-08 17:45:00 +01:00
Harvey Tindall
8d6af53e54 images: fix main banner 2023-09-08 17:43:36 +01:00
Harvey Tindall
8d3bd52fc5 images: use hanken grotesk in banners 2023-09-08 17:41:27 +01:00
Harvey Tindall
8da95ed824 accounts: make sort-by card height = filter card height 2023-09-08 17:00:37 +01:00
Harvey Tindall
8207a75820 accounts: sort by referrals 2023-09-08 16:54:07 +01:00
Harvey Tindall
2c48ce0152 settings: rename "User Page" 2023-09-08 16:50:33 +01:00
Harvey Tindall
dae0ad1de5 invites: "User Label" 2/2
applies label to users. Also hide the user label element on the invite
dropdown when not set.
2023-09-08 14:37:07 +01:00
Harvey Tindall
7c76b58ab8 invites: add "User Label" 1/2
Adds a "User Label" invite field, which is a label applied to users
created with it. This commit contains everything apart from the code to
apply it on account creation.
2023-09-08 14:29:25 +01:00
Harvey Tindall
4ea2dfdfb7 accounts: fix announcements preview window
since the "Announcement" template doesn't actually exist, finding it in
the DB would fail, which is now ignored.
2023-09-08 13:54:01 +01:00
Harvey Tindall
d8d478a95e form: move referral info message to aside, change wording 2023-09-08 13:31:05 +01:00
Harvey Tindall
4c20250888 userpage: actually sub {username} 2023-09-07 23:40:15 +01:00
Harvey Tindall
f5a15905e4 userpage: fix email change modal show/hide 2023-09-07 23:35:41 +01:00
Harvey Tindall
53742e5ec2 setup: encourage user to browse settings for new features
Lots of things aren't present in setup, including the my account page
and referrals, so a nudge in the right direction is warranted.
2023-09-07 23:11:49 +01:00
Harvey Tindall
504c75566a README: Mention referrals 2023-09-07 23:07:52 +01:00
Harvey Tindall
ed4dcbac3b README: new images, remove GIF
don't really think it's necessary. also, they're annoying to make.
2023-09-07 23:01:12 +01:00
Harvey Tindall
a0f1cd5814 captcha: fix missing images
The captcha library's data struct wasn't being serialized/deserialized
fully, meaning the image was never stored. I never really wanted it to
be stored anyway, but as a compromise, the invite daemon now deletes
captcha images from the DB 20 minutes after generation.
2023-09-07 22:38:23 +01:00
Harvey Tindall
4607a30e6a accounts: fix filter card overlap 2023-09-07 21:59:35 +01:00
Harvey Tindall
fca370b9d9 discord: hide "Join Server" text when invite not provided 2023-09-07 21:40:44 +01:00
Harvey Tindall
dc3f1661e8 accounts: fix filter button appearing over lang dropdown 2023-09-07 21:37:25 +01:00
Harvey Tindall
463fe97b29 Merge Referrals
Add Referrals
2023-09-07 21:31:32 +01:00
Harvey Tindall
b08527bce2 userpage: cleanup referral code
moved to its own class, like the expiry card.
2023-09-07 20:42:40 +01:00
Harvey Tindall
41c092f578 referrals: show referrer username on form 2023-09-07 20:19:25 +01:00
Harvey Tindall
311ecb7030 userpage: generate & display referral links
shown on a new card, with an explanation, the number of remaining uses,
and expiry of the current referral.
2023-09-07 16:25:47 +01:00
Harvey Tindall
4a28ea7003 daemon: fix bug wiping out contact details
records were being left alonge if "... err != nil", instead of "... err
== nil". Sorry to anyone affected.
2023-09-07 14:50:26 +01:00
Harvey Tindall
0a82f889f3 daemon: fix bug wiping out contact details
records were being left alone if "status == 200 && err != nil", instead
of "... && err == nil". Sorry.
2023-09-07 14:48:12 +01:00
Harvey Tindall
00e6da520d userpage: cope with disabled contact methods 2023-09-07 14:40:24 +01:00
Harvey Tindall
0b830e9b5e referrals: enable for new users from profile 2023-09-07 14:31:42 +01:00
Harvey Tindall
468b2f3284 accounts: descriptive error when no template found 2023-09-07 14:04:32 +01:00
Harvey Tindall
db21131185 accounts: allow disabling of referrals for users 2023-09-07 14:00:30 +01:00
Harvey Tindall
7d9555fdf7 accounts: add referrals to search queries 2023-09-07 13:30:21 +01:00
Harvey Tindall
729552a827 referrals: Show enabled status on account list 2023-09-06 22:46:16 +01:00
Harvey Tindall
cdc8f9af4b referrals: unlink/disable referrals for profile 2023-09-06 22:12:36 +01:00
Harvey Tindall
9e5034ebab referrals: enable referral for users & profiles
Enabling for individual users works, as does adding a template to a
profile. Removing/Disabling for both needs to be completed.
2023-09-06 22:00:44 +01:00
Harvey Tindall
c2f835c897 referrals: show data on enable referral for user modal
profiles and invites are properly shown.
2023-06-30 16:47:35 +01:00
Harvey Tindall
9c2f27bcdb referrals: 1/2 generation routes, display route, partial frontend
route for generation/enabling of referral for user(s) done? the frontend
is mostly done, but functionality is not there yet. Route for finding
and displaying referral to user is done. Also the config option for
referral is there, in user page settings.
2023-06-28 16:05:24 +01:00
Harvey Tindall
423fc4ac80 accounts: non-case sensitive search 2023-06-27 07:35:28 +01:00
Harvey Tindall
e1292a0780 build: dont install swag if already present 2023-06-27 07:23:25 +01:00
Harvey Tindall
f72960635d build: include debug symbols & sourcemaps in unstable builds
should help with debugging.
2023-06-26 23:48:16 +01:00
Harvey Tindall
b5c80e9d27 config: make sure recaptcha is hidden when disabled 2023-06-26 23:24:58 +01:00
Harvey Tindall
3fa4b01115 setup: add user page
also sprinkled mentions of it throughout other relevant pages.
2023-06-26 21:29:49 +01:00
Harvey Tindall
65f402fd35 admin: hide my account button when disabled 2023-06-26 20:48:57 +01:00
Harvey Tindall
46f1bc20c8 css: fix font error
comment wasn't ended, so some font weights/styles weren't loading and
esbuild was complaining.
2023-06-26 20:29:59 +01:00
Harvey Tindall
a13a72c626 admin: fix logout when url base is used
two tries are made, with and without the url base.
2023-06-26 20:28:20 +01:00
Harvey Tindall
5a80145607 css: add notification animation
simple slide animation, plus a little scale effect when a duplicate
notification gets sent to make the notification more obvious.
2023-06-26 20:13:02 +01:00
Harvey Tindall
baf5e6a593 accounts: add dropdown arrow on "Announce" button 2023-06-26 13:08:34 +01:00
Harvey Tindall
850bb8f44e accounts: fix modify user card layout 2023-06-26 13:04:01 +01:00
Harvey Tindall
b17d8424e9 profiles: fix application
moving to a DB meant empty slices in the Configuration & Policy structs
were being stored as null, and striking a nerve with Jellyfin.
Mediabrowser library change fixed that by de-nulling them itself, and a
new bool field called "Homescreen" is now used to decide if a profile
has a homescreen layout stored or not. This field is hopefully correctly
filled in during migration.
2023-06-26 13:01:17 +01:00
Harvey Tindall
d2253ff069 accounts: fix filter card height
string filter cards were too tall, so bool cards now expand to the same
height. y-margins also removed it made the bottom get covered.
2023-06-25 21:28:38 +01:00
Harvey Tindall
0946b3a1da Merge Database Migration
Database Migration
2023-06-25 20:32:33 +01:00
100 changed files with 3339 additions and 1062 deletions

View File

@@ -103,6 +103,7 @@ steps:
from_secret: BUILDRONE_KEY
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
JFA_GO_SNAPSHOT: y
volumes:
- name: ssh_key

View File

@@ -26,12 +26,13 @@ before:
- scripts/dark-variant.sh tempts
- scripts/dark-variant.sh tempts/modules
- mkdir -p data/web/js
- npx esbuild --target=es6 --bundle tempts/admin.ts --outfile=./data/web/js/admin.js --minify
- npx esbuild --target=es6 --bundle tempts/user.ts --outfile=./data/web/js/user.js --minify
- npx esbuild --target=es6 --bundle tempts/pwr.ts --outfile=./data/web/js/pwr.js --minify
- npx esbuild --target=es6 --bundle tempts/form.ts --outfile=./data/web/js/form.js --minify
- npx esbuild --target=es6 --bundle tempts/setup.ts --outfile=./data/web/js/setup.js --minify
- npx esbuild --target=es6 --bundle tempts/crash.ts --outfile=./data/crash.js --minify
- npx esbuild --target=es6 --bundle tempts/admin.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/admin.js {{.Env.JFA_GO_MINIFY}}
- npx esbuild --target=es6 --bundle tempts/user.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/user.js {{.Env.JFA_GO_MINIFY}}
- npx esbuild --target=es6 --bundle tempts/pwr.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/pwr.js {{.Env.JFA_GO_MINIFY}}
- npx esbuild --target=es6 --bundle tempts/form.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/form.js {{.Env.JFA_GO_MINIFY}}
- npx esbuild --target=es6 --bundle tempts/setup.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/setup.js {{.Env.JFA_GO_MINIFY}}
- npx esbuild --target=es6 --bundle tempts/crash.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/crash.js {{.Env.JFA_GO_MINIFY}}
- bash -c "{{.Env.JFA_GO_COPYTS}}"
- rm -r tempts
- npx esbuild --bundle css/base.css --outfile=./data/web/css/bundle.css --external:remixicon.css --external:../fonts/hanken* --minify
- cp html/crash.html data/
@@ -49,7 +50,7 @@ builds:
env:
- CGO_ENABLED=0
ldflags:
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
goos:
- linux
- darwin
@@ -66,7 +67,7 @@ builds:
flags:
- -tags=tray
ldflags:
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}" -H=windowsgui
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}" -H=windowsgui
goos:
- windows
goarch:
@@ -78,7 +79,7 @@ builds:
flags:
- -tags=tray
ldflags:
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
goos:
- linux
goarch:

View File

@@ -88,6 +88,11 @@ else
NPMOPTS :=
endif
ifeq (, $(shell which swag))
SWAGINSTALL := $(GOBINARY) install github.com/swaggo/swag/cmd/swag@latest
else
SWAGINSTALL :=
endif
npm:
$(info installing npm dependencies)
@@ -122,7 +127,7 @@ typescript:
$(COPYTS)
swagger:
$(GOBINARY) install github.com/swaggo/swag/cmd/swag@latest
$(SWAGINSTALL)
swag init -g main.go
compile:

View File

@@ -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.
* 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.
* "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 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.
* Can also be done through the "My Account" page if enabled.
* Admin Notifications: Get notified when someone creates an account, or an invite expires.
* 📣 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.
@@ -52,13 +55,10 @@ a rewrite of [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) (or
#### Interface
<p align="center">
<img src="images/demo.gif" width="100%"></img>
</p>
<p align="center">
<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>
<img src="images/invites.png" width="47%" style="margin-left: 1.5%;" align="top" alt="Invites tab"></img>
<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>
<img src="images/accounts.png" width="47%" style="margin-right: 1.5%; margin-top: 1rem;" align="top" alt="Accounts tab"></img>
</p>
#### 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:
[<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)

View File

@@ -13,9 +13,29 @@ import (
"github.com/timshannon/badgerhold/v4"
)
const (
CAPTCHA_VALIDITY = 20 * 60 // Seconds
)
func (app *appContext) checkInvites() {
currentTime := time.Now()
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
if !currentTime.After(expiry) {
continue
@@ -141,6 +161,9 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
if req.Label != "" {
invite.Label = req.Label
}
if req.UserLabel != "" {
invite.UserLabel = req.UserLabel
}
invite.Created = currentTime
if req.MultipleUses {
if req.NoLimit {
@@ -222,6 +245,9 @@ func (app *appContext) GetInvites(gc *gin.Context) {
app.checkInvites()
var invites []inviteDTO
for _, inv := range app.storage.GetInvites() {
if inv.IsReferral {
continue
}
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
invite := inviteDTO{
Code: inv.Code,
@@ -238,6 +264,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
Profile: inv.Profile,
NoLimit: inv.NoLimit,
Label: inv.Label,
UserLabel: inv.UserLabel,
}
if len(inv.UsedBy) != 0 {
invite.UsedBy = map[string]int64{}

View File

@@ -162,7 +162,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
username := app.storage.lang.Email[lang].Strings.get("username")
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
customMessage, ok := app.storage.GetCustomContentKey(id)
if !ok {
if !ok && id != "Announcement" {
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
respondBool(400, false, gc)
return

View File

@@ -1,9 +1,11 @@
package main
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
)
@@ -19,13 +21,23 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
DefaultProfile: app.storage.GetDefaultProfile().Name,
Profiles: map[string]profileDTO{},
}
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
baseInv := Invite{}
for _, p := range app.storage.GetProfiles() {
out.Profiles[p.Name] = profileDTO{
Admin: p.Admin,
LibraryAccess: p.LibraryAccess,
FromUser: p.FromUser,
Ombi: p.Ombi != nil,
pdto := profileDTO{
Admin: p.Admin,
LibraryAccess: p.LibraryAccess,
FromUser: p.FromUser,
Ombi: p.Ombi != nil,
ReferralsEnabled: false,
}
if referralsEnabled {
err := app.storage.db.Get(p.ReferralTemplateKey, &baseInv)
if p.ReferralTemplateKey != "" && err == nil {
pdto.ReferralsEnabled = true
}
}
out.Profiles[p.Name] = pdto
}
gc.JSON(200, out)
}
@@ -79,8 +91,9 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
return
}
profile := Profile{
FromUser: user.Name,
Policy: user.Policy,
FromUser: user.Name,
Policy: user.Policy,
Homescreen: req.Homescreen,
}
app.debug.Printf("Creating profile from user \"%s\"", user.Name)
if req.Homescreen {
@@ -110,3 +123,76 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
app.storage.DeleteProfileKey(name)
respondBool(200, true, gc)
}
// @Summary Enable referrals for a profile, sourced from the given invite by its code.
// @Produce json
// @Param profile path string true "name of profile to enable referrals for."
// @Param invite path string true "invite code to create referral template from."
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 500 {object} stringResponse
// @Router /profiles/referral/{profile}/{invite} [post]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) EnableReferralForProfile(gc *gin.Context) {
profileName := gc.Param("profile")
invCode := gc.Param("invite")
inv, ok := app.storage.GetInvitesKey(invCode)
if !ok {
respond(400, "Invalid invite code", gc)
app.err.Printf("\"%s\": Failed to enable referrals: invite not found", profileName)
return
}
profile, ok := app.storage.GetProfileKey(profileName)
if !ok {
respond(400, "Invalid profile", gc)
app.err.Printf("\"%s\": Failed to enable referrals: profile not found", profileName)
return
}
// Generate new code for referral template
inv.Code = shortuuid.New()
// make sure code doesn't begin with number
_, err := strconv.Atoi(string(inv.Code[0]))
for err == nil {
inv.Code = shortuuid.New()
_, err = strconv.Atoi(string(inv.Code[0]))
}
inv.Created = time.Now()
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
inv.IsReferral = true
// Since this is a template for multiple users, ReferrerJellyfinID is not set.
// inv.ReferrerJellyfinID = ...
app.storage.SetInvitesKey(inv.Code, inv)
profile.ReferralTemplateKey = inv.Code
app.storage.SetProfileKey(profile.Name, profile)
respondBool(200, true, gc)
}
// @Summary Disable referrals for a profile, and removes the referral template. no-op if not enabled.
// @Produce json
// @Param profile path string true "name of profile to enable referrals for."
// @Success 200 {object} boolResponse
// @Router /profiles/referral/{profile} [delete]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) DisableReferralForProfile(gc *gin.Context) {
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)
}

View File

@@ -3,11 +3,18 @@ package main
import (
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
)
const (
REFERRAL_EXPIRY_DAYS = 90
)
// @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)
}
@@ -621,3 +647,63 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
}
respondBool(204, true, gc)
}
// @Summary Get or generate a new referral code.
// @Produce json
// @Success 200 {object} GetMyReferralRespDTO
// @Failure 400 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /my/referral [get]
// @Security Bearer
// @Tags User Page
func (app *appContext) GetMyReferral(gc *gin.Context) {
// 1. Look for existing template bound to this Jellyfin ID
// If one exists, that means its just for us and so we
// can use it directly.
inv := Invite{}
err := app.storage.db.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(),
})
}

View File

@@ -3,12 +3,15 @@ package main
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
)
// @Summary Creates a new Jellyfin user without an invite.
@@ -45,8 +48,13 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
}
id := user.ID
profile := app.storage.GetDefaultProfile()
// Check profile isn't empty
if profile.Policy.BlockedTags != nil {
if req.Profile != "" && req.Profile != "none" {
if p, ok := app.storage.GetProfileKey(req.Profile); ok {
profile = p
} else {
app.debug.Printf("Couldn't find profile \"%s\", using default", req.Profile)
}
status, err = app.jf.SetPolicy(id, profile.Policy)
if !(status == 200 || status == 204 || err == nil) {
app.err.Printf("%s: Failed to set user policy (%d): %v", req.Username, status, err)
@@ -64,7 +72,6 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
profile := app.storage.GetDefaultProfile()
if profile.Ombi == nil {
profile.Ombi = map[string]interface{}{}
}
@@ -297,6 +304,16 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
}
}
id := user.ID
emailStore := EmailAddress{
Addr: req.Email,
Contact: (req.Email != ""),
}
if invite.UserLabel != "" {
emailStore.Label = invite.UserLabel
}
var profile Profile
if invite.Profile != "" {
app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile)
@@ -305,27 +322,28 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
if !ok {
profile = app.storage.GetDefaultProfile()
}
if profile.Policy.BlockedTags != nil {
app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile)
status, err = app.jf.SetPolicy(id, profile.Policy)
if !((status == 200 || status == 204) && err == nil) {
app.err.Printf("%s: Failed to set user policy (%d): %v", req.Code, status, err)
}
app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile)
status, err = app.jf.SetPolicy(id, profile.Policy)
if !((status == 200 || status == 204) && err == nil) {
app.err.Printf("%s: Failed to set user policy (%d): %v", req.Code, status, err)
}
if profile.Configuration.GroupedFolders != nil && len(profile.Displayprefs) != 0 {
app.debug.Printf("Applying homescreen from profile \"%s\"", invite.Profile)
status, err = app.jf.SetConfiguration(id, profile.Configuration)
if (status == 200 || status == 204) && err == nil {
status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs)
}
if !((status == 200 || status == 204) && err == nil) {
app.err.Printf("%s: Failed to set configuration template (%d): %v", req.Code, status, err)
}
app.debug.Printf("Applying homescreen from profile \"%s\"", invite.Profile)
status, err = app.jf.SetConfiguration(id, profile.Configuration)
if (status == 200 || status == 204) && err == nil {
status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs)
}
if !((status == 200 || status == 204) && err == nil) {
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 req.Email != "" {
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
if req.Email != "" || invite.UserLabel != "" {
app.storage.SetEmailsKey(id, emailStore)
}
expiry := time.Time{}
if invite.UserExpiry {
@@ -629,6 +647,88 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
respondBool(204, true, gc)
}
// @Summary Enable referrals for the given user(s) based on the rules set in the given invite code, or profile.
// @Produce json
// @Param 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.
// @Produce json
// @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)
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
i := 0
for _, jfUser := range users {
user := respUser{
ID: jfUser.ID,
Name: jfUser.Name,
Admin: jfUser.Policy.IsAdministrator,
Disabled: jfUser.Policy.IsDisabled,
ID: jfUser.ID,
Name: jfUser.Name,
Admin: jfUser.Policy.IsAdministrator,
Disabled: jfUser.Policy.IsDisabled,
ReferralsEnabled: false,
}
if !jfUser.LastActivityDate.IsZero() {
user.LastActive = jfUser.LastActivityDate.Unix()
@@ -868,6 +970,18 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.DiscordID = dcUser.ID
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
i++
}
@@ -1010,13 +1124,13 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
if req.From == "profile" {
// Check profile exists & isn't empty
profile, ok := app.storage.GetProfileKey(req.Profile)
if !ok || profile.Policy.BlockedTags == nil {
if !ok {
app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile)
respond(500, "Couldn't find profile", gc)
return
}
if req.Homescreen {
if profile.Configuration.GroupedFolders == nil || len(profile.Displayprefs) == 0 {
if !profile.Homescreen {
app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile)
respond(500, "No homescreen template available", gc)
return

View File

@@ -341,6 +341,7 @@
"required": false,
"requires_restart": true,
"type": "bool",
"depends_true": "enabled",
"value": false,
"description": "More reliable, but requires some setup. See jfa-go wiki for more info."
},
@@ -376,15 +377,15 @@
"user_page": {
"order": [],
"meta": {
"name": "User Page",
"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. ",
"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, 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"
},
"settings": {
"enabled": {
"name": "Enabled",
"required": false,
"requires_restart": false,
"requires_restart": true,
"type": "bool",
"value": true
},
@@ -404,6 +405,22 @@
"depends_true": "enabled",
"required": "false",
"description": "Click the edit icon next to the \"User Page\" Setting to add custom Markdown messages that will be shown to the user. Note message cards are not private, little effort is required for anyone to view them."
},
"referrals": {
"name": "User Referrals",
"required": false,
"requires_restart": 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."
}
}
},

View File

@@ -22,7 +22,7 @@
font-family: 'Hanken Grotesk';
font-style: italic;
font-weight: 500;
src: url('../fonts/hanken-grotesk-v8-cyrillic-ext_latin_vietnamese-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ *
src: url('../fonts/hanken-grotesk-v8-cyrillic-ext_latin_vietnamese-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* hanken-grotesk-700 - cyrillic-ext_latin_vietnamese */

View File

@@ -10,7 +10,7 @@ func (app *appContext) clearEmails() {
emails := app.storage.GetEmails()
for _, email := range emails {
_, status, err := app.jf.UserByID(email.JellyfinID, false)
if status == 200 && err != nil {
if status == 200 && err == nil {
continue
}
app.storage.DeleteEmailsKey(email.JellyfinID)
@@ -23,7 +23,7 @@ func (app *appContext) clearDiscord() {
discordUsers := app.storage.GetDiscord()
for _, discordUser := range discordUsers {
_, status, err := app.jf.UserByID(discordUser.JellyfinID, false)
if status == 200 && err != nil {
if status == 200 && err == nil {
continue
}
app.storage.DeleteDiscordKey(discordUser.JellyfinID)
@@ -36,7 +36,7 @@ func (app *appContext) clearMatrix() {
matrixUsers := app.storage.GetMatrix()
for _, matrixUser := range matrixUsers {
_, status, err := app.jf.UserByID(matrixUser.JellyfinID, false)
if status == 200 && err != nil {
if status == 200 && err == nil {
continue
}
app.storage.DeleteMatrixKey(matrixUser.JellyfinID)
@@ -49,7 +49,7 @@ func (app *appContext) clearTelegram() {
telegramUsers := app.storage.GetTelegram()
for _, telegramUser := range telegramUsers {
_, status, err := app.jf.UserByID(telegramUser.JellyfinID, false)
if status == 200 && err != nil {
if status == 200 && err == nil {
continue
}
app.storage.DeleteTelegramKey(telegramUser.JellyfinID)

View File

@@ -222,7 +222,6 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
}
// FIXME: Fix CSS, and handle no icon
iconURL = guild.IconURL("256")
fmt.Println("GOT ICON", iconURL)
return
}

54
go.mod
View File

@@ -26,34 +26,35 @@ require (
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
github.com/hrfee/jfa-go/common v0.0.0-20230421170108-d800b97f69b6
github.com/hrfee/jfa-go/docs v0.0.0-20230421170108-d800b97f69b6
github.com/hrfee/jfa-go/linecache v0.0.0-20230421170108-d800b97f69b6
github.com/hrfee/jfa-go/logger v0.0.0-20230421170108-d800b97f69b6
github.com/hrfee/jfa-go/ombi v0.0.0-20230421170108-d800b97f69b6
github.com/hrfee/mediabrowser v0.3.8
github.com/hrfee/jfa-go/common v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/jfa-go/logger v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/jfa-go/ombi v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/mediabrowser v0.3.10
github.com/itchyny/timefmt-go v0.1.5
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mailgun/mailgun-go/v4 v4.9.0
github.com/mailgun/mailgun-go/v4 v4.9.1
github.com/robert-nix/ansihtml v1.0.1
github.com/steambap/captcha v1.4.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/timshannon/badgerhold/v4 v4.0.2
github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/xhit/go-simple-mail/v2 v2.13.0
gopkg.in/ini.v1 v1.67.0
maunium.net/go/mautrix v0.15.2
maunium.net/go/mautrix v0.15.3
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/bytedance/sonic v1.9.2 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgraph-io/badger/v3 v3.2103.1 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dgraph-io/badger/v3 v3.2103.5 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
github.com/getlantern/errors v1.0.3 // indirect
@@ -76,17 +77,17 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect
github.com/golang/glog v1.1.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v2.0.0+incompatible // indirect
github.com/google/flatbuffers v23.5.26+incompatible // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.1 // indirect
github.com/klauspost/compress v1.16.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@@ -104,11 +105,10 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/timshannon/badgerhold/v4 v4.0.2 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.opencensus.io v0.23.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
@@ -116,14 +116,14 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/image v0.7.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.9.3 // indirect
google.golang.org/protobuf v1.30.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/image v0.8.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/tools v0.10.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect
)

49
go.sum
View File

@@ -16,11 +16,15 @@ github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM=
github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
@@ -38,11 +42,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v3 v3.2103.1 h1:zaX53IRg7ycxVlkd5pYdCeFp1FynD6qBGQoQql3R3Hk=
github.com/dgraph-io/badger/v3 v3.2103.1/go.mod h1:dULbq6ehJ5K0cGW/1TQ9iSfUk0gbSiToDWmWmTsJ53E=
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:M88ob4TyDnEqNuL3PgsE/p3bDujfspnulR+0dQWNYZs=
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:buzQsO8HHkZX2Q45fdfGH1xejPjuDQaXH8btcYMFzPM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -166,6 +176,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU=
github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw=
github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@@ -185,14 +197,19 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a h1:AWZzzFrqyjYlRloN6edwTLTUbKxf5flLXNuTBDm3Ews=
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI=
github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -216,6 +233,10 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hrfee/mediabrowser v0.3.8 h1:y0iBCb6jE3QKcsiCJSYva2fFPHRn4UA+sGRzoPuJ/Dk=
github.com/hrfee/mediabrowser v0.3.8/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.9 h1:ecBUd7LMjQrh+9SFRen2T2DzQqI7W8J7vV2lGExD0YU=
github.com/hrfee/mediabrowser v0.3.9/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.10 h1:MUrgZQVY3mk76Bhn7PsZ4LFRhtGitkZA4FP+1qg1HFo=
github.com/hrfee/mediabrowser v0.3.10/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
@@ -232,6 +253,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@@ -255,6 +278,8 @@ github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailgun/mailgun-go/v4 v4.9.0 h1:wRbxvVQ5QObFewLxc1uVvipA16D8gxeiO+cBOca51Iw=
github.com/mailgun/mailgun-go/v4 v4.9.0/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40=
github.com/mailgun/mailgun-go/v4 v4.9.1 h1:D/jhJXYod4RqRsNOOSrjrtAcMEnz8mPYJmeA5cueHKY=
github.com/mailgun/mailgun-go/v4 v4.9.1/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -274,6 +299,7 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -335,6 +361,7 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
@@ -389,6 +416,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
@@ -421,12 +450,18 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg=
golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -437,6 +472,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -460,6 +496,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -497,11 +535,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -513,6 +554,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -529,6 +572,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -560,6 +605,8 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -588,4 +635,6 @@ maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
maunium.net/go/mautrix v0.15.2 h1:fUiVajeoOR92uJoSShHbCvh7uG6lDY4ZO4Mvt90LbjU=
maunium.net/go/mautrix v0.15.2/go.mod h1:h4NwfKqE4YxGTLSgn/gawKzXAb2sF4qx8agL6QEFtGg=
maunium.net/go/mautrix v0.15.3 h1:C9BHSUM0gYbuZmAtopuLjIcH5XHLb/ZjTEz7nN+0jN0=
maunium.net/go/mautrix v0.15.3/go.mod h1:zLrQqdxJlLkurRCozTc9CL6FySkgZlO/kpCYxBILSLE=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -17,6 +17,7 @@
window.jellyfinLogin = {{ .jellyfinLogin }};
window.jfAdminOnly = {{ .jfAdminOnly }};
window.jfAllowAll = {{ .jfAllowAll }};
window.referralsEnabled = {{ .referralsEnabled }};
</script>
<title>Admin - jfa-go</title>
{{ template "header.html" . }}
@@ -29,6 +30,11 @@
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="add-user-user">
<input type="email" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.emailAddress }}">
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="add-user-password">
<label class="label supra">{{ .strings.profile }}</label>
<div class="select ~neutral @low mb-2 mt-4">
<select id="add-user-profile">
</select>
</div>
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.create }}</span>
@@ -76,7 +82,7 @@
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-modify-user" href="">
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
<div class="flex-row mb-4">
<div class="flex flex-row mb-4">
<label class="flex-row-group mr-2">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-profile" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
@@ -102,6 +108,48 @@
</label>
</form>
</div>
{{ if .referralsEnabled }}
<div id="modal-enable-referrals-user" class="modal">
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-enable-referrals-user" href="">
<span class="heading"><span id="header-enable-referrals-user"></span> <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.enableReferralsDescription }}</p>
<div class="flex flex-row mb-4">
<label class="flex-row-group mr-2">
<input type="radio" name="enable-referrals-user-source" class="unfocused" id="radio-referrals-use-profile" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
</label>
<label class="flex-row-group ml-2">
<input type="radio" name="enable-referrals-user-source" class="unfocused" id="radio-referrals-use-invite">
<span class="button ~neutral @low supra full-width center">{{ .strings.invite }}</span>
</label>
</div>
<div class="select ~neutral @low mb-4">
<select id="enable-referrals-user-profiles"></select>
</div>
<div class="select ~neutral @low mb-4 unfocused">
<select id="enable-referrals-user-invites"></select>
</div>
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
</label>
</form>
</div>
<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">&times;</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">
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-delete-user" href="">
<span class="heading"><span id="header-delete-user"></span> <span class="modal-close">&times;</span></span>
@@ -298,6 +346,9 @@
{{ if .ombiEnabled }}
<th>Ombi</th>
{{ end }}
{{ if .referralsEnabled }}
<th>{{ .strings.referrals }}</th>
{{ end }}
<th>{{ .strings.from }}</th>
<th>{{ .strings.userProfilesLibraries }}</th>
<th><span class="button ~neutral @high" id="button-profile-create">{{ .strings.create }}</span></th>
@@ -391,7 +442,7 @@
</div>
<div id="notification-box"></div>
<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">
<i class="ri-global-line"></i>
<span class="ml-2 chev"></span>
@@ -412,9 +463,11 @@
</span>
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
</div>
<div class="top-4 right-4 absolute">
<a class="button ~info" href="/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
</div>
{{ if .userPageEnabled }}
<div class="top-4 right-4 absolute">
<a class="button ~info" href="/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
</div>
{{ end }}
<div class="page-container">
<div class="mb-4">
<header class="flex flex-wrap items-center justify-between">
@@ -536,6 +589,11 @@
<label class="label supra" for="create-label"> {{ .strings.label }}</label>
<input type="text" id="create-label" class="input ~neutral @low mb-2 mt-4">
</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 class="card ~neutral @low col">
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
@@ -589,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>
</div>
<div class="supra py-1 sm hidden" id="accounts-search-options-header">{{ .strings.searchOptions }}</div>
<div class="row -mx-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>
<div class="row -mx-2 mb-2">
<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>
</div>
<div class="supra py-1 sm">{{ .strings.actions }}</div>
@@ -606,6 +664,9 @@
</div>
</div>
<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>
<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>
@@ -637,6 +698,9 @@
{{ if .discordEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-discord">Discord</th>
{{ 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-last-active">{{ .strings.lastActiveTime }}</th>
</tr>

View File

@@ -43,7 +43,7 @@
<div id="notification-box"></div>
<div class="page-container">
<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">
{{ if .passwordReset }}
{{ .strings.passwordReset }}
@@ -53,9 +53,9 @@
</span>
<span class="subheading">
{{ if .passwordReset }}
{{ .strings.enterYourPassword }}
{{ .strings.enterYourPassword }}
{{ else }}
{{ .helpMessage }}
{{ .helpMessage }}
{{ end }}
</span>
</div>
@@ -123,6 +123,9 @@
</form>
</div>
<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">
<span class="label supra">{{ .strings.passwordRequirementsHeader }}</span>
<ul>
@@ -143,7 +146,7 @@
</div>
{{ end }}
{{ if .contactMessage }}
<aside class="col aside sm ~info mt-4">{{ .contactMessage }}</aside>
<aside class="col aside sm ~info mt-4">{{ .contactMessage }}</aside>
{{ end }}
</div>
</div>

View File

@@ -146,6 +146,7 @@
<label class="row switch pb-4">
<input type="radio" class="mr-2" name="ui-jellyfin_login" value="false"><span>{{ .lang.Login.authorizeManual }}</span>
</label>
<p class="support pb-4 pl-4 mt-1">{{ .lang.Login.authorizeManualUserPageNotice }}</p>
</div>
<div id="login-manual">
<label class="label">
@@ -238,6 +239,21 @@
</div>
</section>
</div>
<div class="card ~neutral @low mb-2 unfocused">
<span class="heading">{{ .lang.UserPage.title }}</span>
<p class="content my-2">{{ .lang.UserPage.description }}</p>
<p class="content my-2">{{ .lang.UserPage.customizeMessages }}</p>
<label class="row switch pb-4">
<input type="checkbox" class="mr-2" id="userpage-enabled"><span>{{ .lang.Strings.enabled }}</span>
</label>
<p class="support mb-1 mt-1">{{ .lang.UserPage.requiredSettings }}</p>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</div>
</section>
</div>
<div class="card ~neutral @low mb-2 unfocused">
<span class="heading">{{ .lang.Messages.title }}</span>
<p class="content my-2" id="messages-description"></p>
@@ -391,7 +407,7 @@
</label>
<label class="switch">
<input type="checkbox" class="mr-2" id="password_resets-link_reset"><span>{{ .lang.PasswordResets.resetLinks }}</span>
<p class="support mb-2 mt-1">{{ .lang.PasswordResets.resetLinksNotice }}</p>
<p class="support mb-2 mt-1">{{ .lang.PasswordResets.resetLinksNotice }} {{ .lang.PasswordResets.resetLinksRequiredForUserPage }}</p>
</label>
<label class="switch">
<input type="checkbox" class="mr-2" id="password_resets-set_password"><span>{{ .lang.PasswordResets.setPassword }}</span>

View File

@@ -24,6 +24,7 @@
window.matrixRequired = {{ .matrixRequired }};
window.matrixUserID = "{{ .matrixUser }}";
window.validationStrings = JSON.parse({{ .validationStrings }});
window.referralsEnabled = {{ .referralsEnabled }};
</script>
{{ template "header.html" . }}
<title>{{ .strings.myAccount }}</title>
@@ -150,6 +151,20 @@
<div class="user-expiry-countdown"></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>
<script src="{{ .urlBase }}/js/user.js" type="module"></script>

View File

@@ -1,6 +1,9 @@
# 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.
https://creativecommons.org/licenses/by/3.0/legalcode

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 523 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 38 KiB

BIN
images/myaccount.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 91 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -123,6 +123,7 @@ type setupLang struct {
Email langSection `json:"email"`
Messages langSection `json:"messages"`
Notifications langSection `json:"notifications"`
UserPage langSection `json:"userPage"`
WelcomeEmails langSection `json:"welcomeEmails"`
PasswordResets langSection `json:"passwordResets"`
InviteEmails langSection `json:"inviteEmails"`

View File

@@ -79,7 +79,6 @@
"inviteUsersCreated": "Oprettet brugere",
"inviteNoProfile": "Ingen Profil",
"inviteDateCreated": "Oprettet",
"inviteRemainingUses": "Resterende anvendelser",
"inviteNoInvites": "Ingen",
"inviteExpiresInTime": "Udløber om {n}",
"notifyEvent": "Meddel den:",

View File

@@ -53,7 +53,6 @@
"inviteUsersCreated": "Erstellte Benutzer",
"inviteNoProfile": "Kein Profil",
"inviteDateCreated": "Erstellt",
"inviteRemainingUses": "Verbleibende Verwendungen",
"inviteNoInvites": "Keine",
"inviteExpiresInTime": "Läuft in {n} ab",
"notifyEvent": "Benachrichtigen bei:",

View File

@@ -56,7 +56,6 @@
"inviteUsersCreated": "Δημιουργηθέντες χρήστες",
"inviteNoProfile": "Κανένα Προφίλ",
"inviteDateCreated": "Δημιουργηθέντα",
"inviteRemainingUses": "Εναπομείναντες χρήσεις",
"inviteNoInvites": "Καμία",
"inviteExpiresInTime": "Λήγει σε {n}",
"notifyEvent": "Ενημέρωση όταν:",

View File

@@ -124,7 +124,6 @@
"addProfileStoreHomescreenLayout": "Store homescreen layout",
"inviteNoUsersCreated": "None yet!",
"inviteUsersCreated": "Created users",
"inviteRemainingUses": "Remaining uses",
"inviteNoInvites": "None",
"inviteExpiresInTime": "Expires in {n}",
"notifyEvent": "Notify on:",

View File

@@ -4,6 +4,7 @@
},
"strings": {
"invites": "Invites",
"invite": "Invite",
"accounts": "Accounts",
"settings": "Settings",
"inviteMonths": "Months",
@@ -40,6 +41,8 @@
"profile": "Profile",
"unknown": "Unknown",
"label": "Label",
"userLabel": "User Label",
"userLabelDescription": "Label to apply to users created with this invite.",
"logs": "Logs",
"announce": "Announce",
"templates": "Templates",
@@ -63,6 +66,10 @@
"markdownSupported": "Markdown is supported.",
"modifySettings": "Modify Settings",
"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",
"sendDeleteNotificationEmail": "Send notification message",
"sendDeleteNotifiationExample": "Your account has been deleted.",
@@ -90,7 +97,6 @@
"inviteUsersCreated": "Created users",
"inviteNoProfile": "No Profile",
"inviteDateCreated": "Created",
"inviteRemainingUses": "Remaining uses",
"inviteNoInvites": "None",
"inviteExpiresInTime": "Expires in {n}",
"notifyEvent": "Notify on:",
@@ -132,6 +138,7 @@
"updateAppliedRefresh": "Update applied, please refresh.",
"telegramVerified": "Telegram account verified.",
"accountConnected": "Account connected.",
"referralsEnabled": "Referrals enabled.",
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
"errorSettingsFailed": "Application failed.",
@@ -152,6 +159,7 @@
"errorSendWelcomeEmail": "Failed to send welcome message (check console/logs)",
"errorApplyUpdate": "Failed to apply update, try manually.",
"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.",
"noUpdatesAvailable": "No new updates available."
},
@@ -160,6 +168,10 @@
"singular": "Modify Settings for {n} user",
"plural": "Modify Settings for {n} users"
},
"enableReferralsFor": {
"singular": "Enable Referrals for {n} user",
"plural": "Enable Referrals for {n} users"
},
"deleteNUsers": {
"singular": "Delete {n} user",
"plural": "Delete {n} users"

View File

@@ -75,7 +75,6 @@
"inviteUsersCreated": "Usuarios creados",
"inviteNoProfile": "Sin perfil",
"inviteDateCreated": "Creado",
"inviteRemainingUses": "Usos restantes",
"inviteNoInvites": "Ninguno",
"inviteExpiresInTime": "Caduca en {n}",
"notifyEvent": "Notificar en:",

View File

@@ -55,7 +55,6 @@
"inviteUsersCreated": "Utilisateurs créés",
"inviteNoProfile": "Aucun profil",
"inviteDateCreated": "Créer",
"inviteRemainingUses": "Utilisations restantes",
"inviteNoInvites": "Aucune",
"inviteExpiresInTime": "Expires dans {n}",
"notifyEvent": "Notifier sur :",

View File

@@ -87,7 +87,6 @@
"inviteUsersCreated": "",
"inviteNoProfile": "",
"inviteDateCreated": "",
"inviteRemainingUses": "",
"inviteNoInvites": "",
"inviteExpiresInTime": "",
"notifyEvent": "",

View File

@@ -56,7 +56,6 @@
"inviteUsersCreated": "Pengguna yang telah dibuat",
"inviteNoProfile": "Tidak ada profil",
"inviteDateCreated": "Dibuat",
"inviteRemainingUses": "Penggunaan yang tersisa",
"inviteNoInvites": "Tidak ada",
"inviteExpiresInTime": "Kadaluarsa dalam {n}",
"notifyEvent": "Beritahu pada:",

View File

@@ -53,7 +53,6 @@
"inviteUsersCreated": "Aangemaakte gebruikers",
"inviteNoProfile": "Geen profiel",
"inviteDateCreated": "Aangemaakt",
"inviteRemainingUses": "Resterend aantal keer te gebruiken",
"inviteNoInvites": "Geen",
"inviteExpiresInTime": "Verloopt over {n}",
"notifyEvent": "Meldingen:",

View File

@@ -87,7 +87,6 @@
"inviteUsersCreated": "",
"inviteNoProfile": "",
"inviteDateCreated": "Utworzone",
"inviteRemainingUses": "",
"inviteNoInvites": "",
"inviteExpiresInTime": "",
"notifyEvent": "",

View File

@@ -54,7 +54,6 @@
"inviteUsersCreated": "Usuários criado",
"inviteNoProfile": "Sem Perfil",
"inviteDateCreated": "Criado",
"inviteRemainingUses": "Uso restantes",
"inviteNoInvites": "Nenhum",
"inviteExpiresInTime": "Expira em {n}",
"notifyEvent": "Notificar em:",

View File

@@ -65,7 +65,6 @@
"inviteUsersCreated": "Skapade användare",
"inviteNoProfile": "Ingen profil",
"inviteDateCreated": "Skapad",
"inviteRemainingUses": "Återstående användningar",
"inviteNoInvites": "Ingen",
"inviteExpiresInTime": "Går ut om {n}",
"notifyEvent": "Meddela den:",

View File

@@ -86,7 +86,6 @@
"inviteUsersCreated": "Người dùng đã tạo",
"inviteNoProfile": "Không có Tài khoản mẫu",
"inviteDateCreated": "Tạo",
"inviteRemainingUses": "Số lần sử dụng còn lại",
"inviteNoInvites": "Không có",
"inviteExpiresInTime": "Hết hạn trong {n}",
"notifyEvent": "Thông báo khi:",

View File

@@ -80,7 +80,6 @@
"inviteUsersCreated": "已创建的用户",
"inviteNoProfile": "没有个人资料",
"inviteDateCreated": "已创建",
"inviteRemainingUses": "剩余使用次数",
"inviteNoInvites": "无",
"inviteExpiresInTime": "在 {n} 到期",
"notifyEvent": "通知:",

View File

@@ -87,7 +87,6 @@
"inviteUsersCreated": "創建的帳戶",
"inviteNoProfile": "無資料",
"inviteDateCreated": "已創建",
"inviteRemainingUses": "剩餘使用次數",
"inviteNoInvites": "無",
"inviteExpiresInTime": "在 {n} 到期",
"notifyEvent": "通知:",

View File

@@ -35,7 +35,8 @@
"expiry": "Udløb",
"add": "Tilføj",
"edit": "Rediger",
"delete": "Slet"
"delete": "Slet",
"inviteRemainingUses": "Resterende anvendelser"
},
"notifications": {
"errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.",

View File

@@ -35,7 +35,8 @@
"expiry": "Ablaufdatum",
"add": "Hinzufügen",
"edit": "Bearbeiten",
"delete": "Löschen"
"delete": "Löschen",
"inviteRemainingUses": "Verbleibende Verwendungen"
},
"notifications": {
"errorLoginBlank": "Der Benutzername und/oder das Passwort wurden nicht ausgefüllt.",

View File

@@ -25,7 +25,8 @@
"disable": "Απενεργοποίηση",
"expiry": "Λήξη",
"edit": "Επεξεργασία",
"delete": "Διαγραφή"
"delete": "Διαγραφή",
"inviteRemainingUses": "Εναπομείναντες χρήσεις"
},
"notifications": {
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",

View File

@@ -35,7 +35,8 @@
"expiry": "Expiry",
"add": "Add",
"edit": "Edit",
"delete": "Delete"
"delete": "Delete",
"inviteRemainingUses": "Remaining uses"
},
"notifications": {
"errorLoginBlank": "The username and/or password was left blank.",

View File

@@ -39,7 +39,9 @@
"add": "Add",
"edit": "Edit",
"delete": "Delete",
"myAccount": "My Account"
"myAccount": "My Account",
"referrals": "Referrals",
"inviteRemainingUses": "Remaining uses"
},
"notifications": {
"errorLoginBlank": "The username and/or password were left blank.",
@@ -62,4 +64,4 @@
"plural": "{n} Days"
}
}
}
}

View File

@@ -35,7 +35,8 @@
"expiry": "Expiración",
"add": "Agregar",
"edit": "Editar",
"delete": "Eliminar"
"delete": "Eliminar",
"inviteRemainingUses": "Usos restantes"
},
"notifications": {
"errorLoginBlank": "El nombre de usuario y/o la contraseña se dejaron en blanco.",

View File

@@ -35,7 +35,8 @@
"expiry": "Expiration",
"add": "Ajouter",
"edit": "Éditer",
"delete": "Effacer"
"delete": "Effacer",
"inviteRemainingUses": "Utilisations restantes"
},
"notifications": {
"errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.",

View File

@@ -19,7 +19,8 @@
"login": "Masuk",
"logout": "Keluar",
"edit": "Edit",
"delete": "Hapus"
"delete": "Hapus",
"inviteRemainingUses": "Penggunaan yang tersisa"
},
"notifications": {
"errorLoginBlank": "Nama pengguna dan / atau sandi kosong.",

View File

@@ -28,4 +28,4 @@
},
"notifications": {},
"quantityStrings": {}
}
}

8
lang/common/nds.json Normal file
View File

@@ -0,0 +1,8 @@
{
"meta": {
"name": "Nedderdütsch (NDS)"
},
"strings": {},
"notifications": {},
"quantityStrings": {}
}

View File

@@ -35,7 +35,8 @@
"expiry": "Verloop",
"add": "Voeg toe",
"edit": "Bewerken",
"delete": "Verwijderen"
"delete": "Verwijderen",
"inviteRemainingUses": "Resterend aantal keer te gebruiken"
},
"notifications": {
"errorLoginBlank": "De gebruikersnaam en/of wachtwoord is leeg.",

View File

@@ -35,7 +35,8 @@
"expiry": "Expira",
"add": "Adicionar",
"edit": "Editar",
"delete": "Deletar"
"delete": "Deletar",
"inviteRemainingUses": "Uso restantes"
},
"notifications": {
"errorLoginBlank": "O nome de usuário e/ou senha foram deixados em branco.",

View File

@@ -28,4 +28,4 @@
},
"notifications": {},
"quantityStrings": {}
}
}

View File

@@ -22,7 +22,8 @@
"disabled": "Inaktiverad",
"expiry": "Löper ut",
"edit": "Redigera",
"delete": "Radera"
"delete": "Radera",
"inviteRemainingUses": "Återstående användningar"
},
"notifications": {
"errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.",

View File

@@ -13,7 +13,8 @@
"expiry": "Hết hạn",
"add": "Thêm",
"edit": "Chỉnh sửa",
"delete": "Xóa"
"delete": "Xóa",
"inviteRemainingUses": "Số lần sử dụng còn lại"
},
"notifications": {
"errorConnection": "Không thể kết nối với jfa-go.",

View File

@@ -35,7 +35,8 @@
"expiry": "到期",
"add": "添加",
"edit": "编辑",
"delete": "删除"
"delete": "删除",
"inviteRemainingUses": "剩余使用次数"
},
"notifications": {
"errorLoginBlank": "用户名/密码留空。",

View File

@@ -35,7 +35,8 @@
"expiry": "到期",
"add": "添加",
"edit": "編輯",
"delete": "刪除"
"delete": "刪除",
"inviteRemainingUses": "剩餘使用次數"
},
"notifications": {
"errorLoginBlank": "帳戶名稱和/或密碼留空。",

View File

@@ -49,4 +49,4 @@
"clickBelow": "",
"confirmEmail": ""
}
}
}

View File

@@ -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.",
"resetSent": "Reset Sent.",
"resetSentDescription": "If an account with the given username/contact method exists, a password reset link has been sent via all contact methods available. The code will expire in 30 minutes.",
"changePassword": "Change Password"
"changePassword": "Change Password",
"referralsDescription": "Invite friends & family to Jellyfin with this link. Come back here for a new one if it expires.",
"copyReferral": "Copy Link",
"invitedBy": "You were invited by user {user}."
},
"notifications": {
"errorUserExists": "User already exists.",

View File

@@ -48,4 +48,4 @@
"plural": "Deve avere almeno {n} caratteri speciali"
}
}
}
}

View File

@@ -57,4 +57,4 @@
"plural": "Potrebnih je vsaj {n} posebnih znakov"
}
}
}
}

View File

@@ -13,4 +13,4 @@
"changeYourPassword": "Spremenite svoje geslo po prijavi.",
"enterYourPassword": "Vnesite svoje novo geslo spodaj."
}
}
}

View File

@@ -32,7 +32,7 @@
},
"endPage": {
"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"
},
"language": {
@@ -70,6 +70,7 @@
"adminOnly": "Admin users only (recommended)",
"allowAll": "Allow all Jellyfin users to login",
"allowAllDescription": "Not recommended, you should allow individual users to login once setup.",
"authorizeManualUserPageNotice": "Using this will disable the \"User Page\" feature.",
"emailNotice": "Your email address can be used to receive notifications."
},
"jellyfinEmby": {
@@ -109,6 +110,12 @@
"title": "Admin Notifications",
"description": "If enabled, you can choose (per invite) to receive an message when an invite expires, or a user is created. If you didn't choose the Jellyfin login method, make sure you provided your email address, or add another contact method later."
},
"userPage": {
"title": "User Page",
"description": "The user page (shown as \"My Account\") allows users to access information about their account, such as their contact methods and account expiry. They can also change their password, start a password reset, and link/change contact methods, without having to ask you. Additionally, customized Markdown messages can be shown to the users before and after logging in.",
"customizeMessages": "Click the edit button next to \"User Page\" in settings to set them later.",
"requiredSettings": "Log-in to jfa-go via Jellyfin must be set. Ensure \"reset password via link\" is selected later for self-service password resets."
},
"welcomeEmails": {
"title": "Welcome messages",
"description": "If enabled, an message will be sent to new users with the Jellyfin/Emby URL and their username."
@@ -119,10 +126,11 @@
},
"passwordResets": {
"title": "Password Resets",
"description": "When a user tries to reset their password, Jellyfin creates a file named 'passwordreset-*.json' which contains a PIN. jfa-go reads the file and sends the PIN to the user.",
"description": "When a user tries to reset their password, Jellyfin creates a file named 'passwordreset-*.json' which contains a PIN. jfa-go reads the file and sends the PIN to the user. If you enabled the \"User Page\" feature, a reset can also be performed there, given a username, email, or contact method.",
"pathToJellyfin": "Path to Jellyfin configuration directory",
"pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '<path to jellyfin>/passwordreset-*.json' will appear.",
"pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '<path to jellyfin>/passwordreset-*.json' will appear. This is not necessary if you only want to use self-service password resets through the \"User Page\".",
"resetLinks": "Send a link instead of a PIN",
"resetLinksRequiredForUserPage": "Required for self-service password reset on the User Page.",
"resetLinksNotice": "If Ombi integration is enabled, use this to sync Jellyfin password resets with Ombi.",
"resetLinksLanguage": "Default reset link language",
"setPassword": "Set password through link",
@@ -149,4 +157,4 @@
"emailMessage": "Email Message",
"emailMessageNotice": "Displays at the bottom of emails."
}
}
}

View File

@@ -149,4 +149,4 @@
"emailMessage": "",
"emailMessageNotice": ""
}
}
}

View File

@@ -146,4 +146,4 @@
"emailMessage": "",
"emailMessageNotice": ""
}
}
}

View File

@@ -13,4 +13,4 @@
"languageSet": "",
"discordDMs": ""
}
}
}

View File

@@ -13,4 +13,4 @@
"languageSet": "",
"discordDMs": ""
}
}
}

View File

@@ -13,4 +13,4 @@
"languageSet": "Jezik nastavljen na {language}.",
"discordDMs": "Prosimo preverite svoja zasebna sporočila za odziv."
}
}
}

View File

@@ -286,6 +286,9 @@ func migrateToBadger(app *appContext) {
}
for k, v := range app.storage.deprecatedProfiles {
if v.Configuration.GroupedFolders != nil || len(v.Displayprefs) != 0 {
v.Homescreen = true
}
app.storage.SetProfileKey(k, v)
}

View File

@@ -25,6 +25,7 @@ type newUserDTO struct {
MatrixContact bool `json:"matrix_contact"` // Whether or not to use matrix for notifications/pwrs
CaptchaID string `json:"captcha_id"` // Captcha ID (if enabled)
CaptchaText string `json:"captcha_text"` // Captcha text (if enabled)
Profile string `json:"profile"` // Profile (for admins only)
}
type newUserResponse struct {
@@ -47,21 +48,22 @@ type enableDisableUserDTO struct {
}
type generateInviteDTO struct {
Months int `json:"months" example:"0"` // Number of months
Days int `json:"days" example:"1"` // Number of days
Hours int `json:"hours" example:"2"` // Number of hours
Minutes int `json:"minutes" example:"3"` // Number of minutes
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
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
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
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
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
Months int `json:"months" example:"0"` // Number of months
Days int `json:"days" example:"1"` // Number of days
Hours int `json:"hours" example:"2"` // Number of hours
Minutes int `json:"minutes" example:"3"` // Number of minutes
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
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
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
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
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
UserLabel string `json:"user_label,omitempty" example:"Friend"` // Label to apply to users created w/ this invite.
}
type inviteProfileDTO struct {
@@ -70,10 +72,11 @@ type inviteProfileDTO struct {
}
type profileDTO struct {
Admin bool `json:"admin" example:"false"` // Whether profile has admin rights or not
LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
Ombi bool `json:"ombi"` // Whether or not Ombi settings are stored in this profile.
Admin bool `json:"admin" example:"false"` // Whether profile has admin rights or not
LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
Ombi bool `json:"ombi"` // Whether or not Ombi settings are stored in this profile.
ReferralsEnabled bool `json:"referrals_enabled" example:"true"` // Whether or not the profile has referrals enabled, and has a template invite stored.
}
type getProfilesDTO struct {
@@ -112,6 +115,7 @@ type inviteDTO struct {
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
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 {
@@ -149,6 +153,7 @@ type respUser struct {
NotifyThroughMatrix bool `json:"notify_matrix"`
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.
ReferralsEnabled bool `json:"referrals_enabled"`
}
type getUsersDTO struct {
@@ -387,6 +392,7 @@ type MyDetailsDTO struct {
Discord *MyDetailsContactMethodsDTO `json:"discord,omitempty"`
Telegram *MyDetailsContactMethodsDTO `json:"telegram,omitempty"`
Matrix *MyDetailsContactMethodsDTO `json:"matrix,omitempty"`
HasReferrals bool `json:"has_referrals,omitempty"`
}
type MyDetailsContactMethodsDTO struct {
@@ -413,3 +419,14 @@ type ChangeMyPasswordDTO struct {
Old string `json:"old"`
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"`
}

367
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"any-date-parser": "^1.5.4",
"browserslist": "^4.21.7",
"cheerio": "^1.0.0-rc.12",
"esbuild": "^0.18.20",
"fs-cheerio": "^3.0.0",
"inline-source": "^8.0.2",
"jsdom": "^22.1.0",
@@ -33,7 +34,7 @@
"live-server": "^1.2.2"
},
"optionalDependencies": {
"esbuild": "^0.18.6"
"esbuild": "^0.18.20"
}
},
"node_modules/@alloc/quick-lru": {
@@ -59,9 +60,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.6.tgz",
"integrity": "sha512-J3lwhDSXBBppSzm/LC1uZ8yKSIpExc+5T8MxrYD9KNVZG81FOAu2VF2gXi/6A/LwDDQQ+b6DpQbYlo3VwxFepQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
@@ -74,9 +75,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.6.tgz",
"integrity": "sha512-pL0Ci8P9q1sWbtPx8CXbc8JvPvvYdJJQ+LO09PLFsbz3aYNdFBGWJjiHU+CaObO4Ames+GOFpXRAJZS2L3ZK/A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
@@ -89,9 +90,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.6.tgz",
"integrity": "sha512-hE2vZxOlJ05aY28lUpB0y0RokngtZtcUB+TVl9vnLEnY0z/8BicSvrkThg5/iI1rbf8TwXrbr2heEjl9fLf+EA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
@@ -104,9 +105,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.6.tgz",
"integrity": "sha512-/tuyl4R+QhhoROQtuQj9E/yfJtZNdv2HKaHwYhhHGQDN1Teziem2Kh7BWQMumfiY7Lu9g5rO7scWdGE4OsQ6MQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
@@ -119,9 +120,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.6.tgz",
"integrity": "sha512-L7IQga2pDT+14Ti8HZwsVfbCjuKP4U213T3tuPggOzyK/p4KaUJxQFXJgfUFHKzU0zOXx8QcYRYZf0hSQtppkw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
@@ -134,9 +135,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.6.tgz",
"integrity": "sha512-bq10jFv42V20Kk77NvmO+WEZaLHBKuXcvEowixnBOMkaBgS7kQaqTc77ZJDbsUpXU3KKNLQFZctfaeINmeTsZA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
@@ -149,9 +150,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.6.tgz",
"integrity": "sha512-HbDLlkDZqUMBQaiday0pJzB6/8Xx/10dI3xRebJBReOEeDSeS+7GzTtW9h8ZnfB7/wBCqvtAjGtWQLTNPbR2+g==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
@@ -164,9 +165,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.6.tgz",
"integrity": "sha512-C+5kb6rgsGMmvIdUI7v1PPgC98A6BMv233e97aXZ5AE03iMdlILFD/20HlHrOi0x2CzbspXn9HOnlE4/Ijn5Kw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
@@ -179,9 +180,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.6.tgz",
"integrity": "sha512-NMY9yg/88MskEZH2s4i6biz/3av+M8xY5ua4HE7CCz5DBz542cr7REe317+v7oKjnYBCijHpkzo5vU85bkXQmQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
@@ -194,9 +195,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.6.tgz",
"integrity": "sha512-AXazA0ljvQEp7cA9jscABNXsjodKbEcqPcAE3rDzKN82Vb3lYOq6INd+HOCA7hk8IegEyHW4T72Z7QGIhyCQEA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
@@ -209,9 +210,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.6.tgz",
"integrity": "sha512-JjBf7TwY7ldcPgHYt9UcrjZB03+WZqg/jSwMAfzOzM5ZG+tu5umUqzy5ugH/crGI4eoDIhSOTDp1NL3Uo/05Fw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
@@ -224,9 +225,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.6.tgz",
"integrity": "sha512-kATNsslryVxcH1sO3KP2nnyUWtZZVkgyhAUnyTVVa0OQQ9pmDRjTpHaE+2EQHoCM5wt/uav2edrAUqbwn3tkKQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
@@ -239,9 +240,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.6.tgz",
"integrity": "sha512-B+wTKz+8pi7mcWXFQV0LA79dJ+qhiut5uK9q0omoKnq8yRIwQJwfg3/vclXoqqcX89Ri5Y5538V0Se2v5qlcLA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
@@ -254,9 +255,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.6.tgz",
"integrity": "sha512-h44RBLVXFUSjvhOfseE+5UxQ/r9LVeqK2S8JziJKOm9W7SePYRPDyn7MhzhNCCFPkcjIy+soCxfhlJXHXXCR0A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
@@ -269,9 +270,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.6.tgz",
"integrity": "sha512-FlYpyr2Xc2AUePoAbc84NRV+mj7xpsISeQ36HGf9etrY5rTBEA+IU9HzWVmw5mDFtC62EQxzkLRj8h5Hq85yOQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
@@ -284,9 +285,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.6.tgz",
"integrity": "sha512-Mc4EUSYwzLci77u0Kao6ajB2WbTe5fNc7+lHwS3a+vJISC/oprwURezUYu1SdWAYoczbsyOvKAJwuNftoAdjjg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
@@ -299,9 +300,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.6.tgz",
"integrity": "sha512-3hgZlp7NqIM5lNG3fpdhBI5rUnPmdahraSmwAi+YX/bp7iZ7mpTv2NkypGs/XngdMtpzljICxnUG3uPfqLFd3w==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
@@ -314,9 +315,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.6.tgz",
"integrity": "sha512-aEWTdZQHtSRROlDYn7ygB8yAqtnall/UnmoVIJVqccKitkAWVVSYocQUWrBOxLEFk8XdlRouVrLZe6WXszyviA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
@@ -329,9 +330,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.6.tgz",
"integrity": "sha512-uxk/5yAGpjKZUHOECtI9W+9IcLjKj+2m0qf+RG7f7eRBHr8wP6wsr3XbNbgtOD1qSpPapd6R2ZfSeXTkCcAo5g==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
@@ -344,9 +345,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.6.tgz",
"integrity": "sha512-oXlXGS9zvNCGoAT/tLHAsFKrIKye1JaIIP0anCdpaI+Dc10ftaNZcqfLzEwyhdzFAYInXYH4V7kEdH4hPyo9GA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
@@ -359,9 +360,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.6.tgz",
"integrity": "sha512-qh7IcAHUvvmMBmoIG+V+BbE9ZWSR0ohF51e5g8JZvU08kZF58uDFL5tHs0eoYz31H6Finv17te3W3QB042GqVA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
@@ -374,9 +375,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.6.tgz",
"integrity": "sha512-9UDwkz7Wlm4N9jnv+4NL7F8vxLhSZfEkRArz2gD33HesAFfMLGIGNVXRoIHtWNw8feKsnGly9Hq1EUuRkWl0zA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
@@ -1649,9 +1650,9 @@
}
},
"node_modules/esbuild": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.6.tgz",
"integrity": "sha512-5QgxWaAhU/tPBpvkxUmnFv2YINHuZzjbk0LeUUnC2i3aJHjfi5yR49lgKgF7cb98bclOp/kans8M5TGbGFfJlQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"hasInstallScript": true,
"optional": true,
"bin": {
@@ -1661,28 +1662,28 @@
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.6",
"@esbuild/android-arm64": "0.18.6",
"@esbuild/android-x64": "0.18.6",
"@esbuild/darwin-arm64": "0.18.6",
"@esbuild/darwin-x64": "0.18.6",
"@esbuild/freebsd-arm64": "0.18.6",
"@esbuild/freebsd-x64": "0.18.6",
"@esbuild/linux-arm": "0.18.6",
"@esbuild/linux-arm64": "0.18.6",
"@esbuild/linux-ia32": "0.18.6",
"@esbuild/linux-loong64": "0.18.6",
"@esbuild/linux-mips64el": "0.18.6",
"@esbuild/linux-ppc64": "0.18.6",
"@esbuild/linux-riscv64": "0.18.6",
"@esbuild/linux-s390x": "0.18.6",
"@esbuild/linux-x64": "0.18.6",
"@esbuild/netbsd-x64": "0.18.6",
"@esbuild/openbsd-x64": "0.18.6",
"@esbuild/sunos-x64": "0.18.6",
"@esbuild/win32-arm64": "0.18.6",
"@esbuild/win32-ia32": "0.18.6",
"@esbuild/win32-x64": "0.18.6"
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/escalade": {
@@ -6800,135 +6801,135 @@
}
},
"@esbuild/android-arm": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.6.tgz",
"integrity": "sha512-J3lwhDSXBBppSzm/LC1uZ8yKSIpExc+5T8MxrYD9KNVZG81FOAu2VF2gXi/6A/LwDDQQ+b6DpQbYlo3VwxFepQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"optional": true
},
"@esbuild/android-arm64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.6.tgz",
"integrity": "sha512-pL0Ci8P9q1sWbtPx8CXbc8JvPvvYdJJQ+LO09PLFsbz3aYNdFBGWJjiHU+CaObO4Ames+GOFpXRAJZS2L3ZK/A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"optional": true
},
"@esbuild/android-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.6.tgz",
"integrity": "sha512-hE2vZxOlJ05aY28lUpB0y0RokngtZtcUB+TVl9vnLEnY0z/8BicSvrkThg5/iI1rbf8TwXrbr2heEjl9fLf+EA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"optional": true
},
"@esbuild/darwin-arm64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.6.tgz",
"integrity": "sha512-/tuyl4R+QhhoROQtuQj9E/yfJtZNdv2HKaHwYhhHGQDN1Teziem2Kh7BWQMumfiY7Lu9g5rO7scWdGE4OsQ6MQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"optional": true
},
"@esbuild/darwin-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.6.tgz",
"integrity": "sha512-L7IQga2pDT+14Ti8HZwsVfbCjuKP4U213T3tuPggOzyK/p4KaUJxQFXJgfUFHKzU0zOXx8QcYRYZf0hSQtppkw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"optional": true
},
"@esbuild/freebsd-arm64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.6.tgz",
"integrity": "sha512-bq10jFv42V20Kk77NvmO+WEZaLHBKuXcvEowixnBOMkaBgS7kQaqTc77ZJDbsUpXU3KKNLQFZctfaeINmeTsZA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"optional": true
},
"@esbuild/freebsd-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.6.tgz",
"integrity": "sha512-HbDLlkDZqUMBQaiday0pJzB6/8Xx/10dI3xRebJBReOEeDSeS+7GzTtW9h8ZnfB7/wBCqvtAjGtWQLTNPbR2+g==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"optional": true
},
"@esbuild/linux-arm": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.6.tgz",
"integrity": "sha512-C+5kb6rgsGMmvIdUI7v1PPgC98A6BMv233e97aXZ5AE03iMdlILFD/20HlHrOi0x2CzbspXn9HOnlE4/Ijn5Kw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"optional": true
},
"@esbuild/linux-arm64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.6.tgz",
"integrity": "sha512-NMY9yg/88MskEZH2s4i6biz/3av+M8xY5ua4HE7CCz5DBz542cr7REe317+v7oKjnYBCijHpkzo5vU85bkXQmQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"optional": true
},
"@esbuild/linux-ia32": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.6.tgz",
"integrity": "sha512-AXazA0ljvQEp7cA9jscABNXsjodKbEcqPcAE3rDzKN82Vb3lYOq6INd+HOCA7hk8IegEyHW4T72Z7QGIhyCQEA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"optional": true
},
"@esbuild/linux-loong64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.6.tgz",
"integrity": "sha512-JjBf7TwY7ldcPgHYt9UcrjZB03+WZqg/jSwMAfzOzM5ZG+tu5umUqzy5ugH/crGI4eoDIhSOTDp1NL3Uo/05Fw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"optional": true
},
"@esbuild/linux-mips64el": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.6.tgz",
"integrity": "sha512-kATNsslryVxcH1sO3KP2nnyUWtZZVkgyhAUnyTVVa0OQQ9pmDRjTpHaE+2EQHoCM5wt/uav2edrAUqbwn3tkKQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"optional": true
},
"@esbuild/linux-ppc64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.6.tgz",
"integrity": "sha512-B+wTKz+8pi7mcWXFQV0LA79dJ+qhiut5uK9q0omoKnq8yRIwQJwfg3/vclXoqqcX89Ri5Y5538V0Se2v5qlcLA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"optional": true
},
"@esbuild/linux-riscv64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.6.tgz",
"integrity": "sha512-h44RBLVXFUSjvhOfseE+5UxQ/r9LVeqK2S8JziJKOm9W7SePYRPDyn7MhzhNCCFPkcjIy+soCxfhlJXHXXCR0A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"optional": true
},
"@esbuild/linux-s390x": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.6.tgz",
"integrity": "sha512-FlYpyr2Xc2AUePoAbc84NRV+mj7xpsISeQ36HGf9etrY5rTBEA+IU9HzWVmw5mDFtC62EQxzkLRj8h5Hq85yOQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"optional": true
},
"@esbuild/linux-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.6.tgz",
"integrity": "sha512-Mc4EUSYwzLci77u0Kao6ajB2WbTe5fNc7+lHwS3a+vJISC/oprwURezUYu1SdWAYoczbsyOvKAJwuNftoAdjjg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"optional": true
},
"@esbuild/netbsd-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.6.tgz",
"integrity": "sha512-3hgZlp7NqIM5lNG3fpdhBI5rUnPmdahraSmwAi+YX/bp7iZ7mpTv2NkypGs/XngdMtpzljICxnUG3uPfqLFd3w==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"optional": true
},
"@esbuild/openbsd-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.6.tgz",
"integrity": "sha512-aEWTdZQHtSRROlDYn7ygB8yAqtnall/UnmoVIJVqccKitkAWVVSYocQUWrBOxLEFk8XdlRouVrLZe6WXszyviA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"optional": true
},
"@esbuild/sunos-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.6.tgz",
"integrity": "sha512-uxk/5yAGpjKZUHOECtI9W+9IcLjKj+2m0qf+RG7f7eRBHr8wP6wsr3XbNbgtOD1qSpPapd6R2ZfSeXTkCcAo5g==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"optional": true
},
"@esbuild/win32-arm64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.6.tgz",
"integrity": "sha512-oXlXGS9zvNCGoAT/tLHAsFKrIKye1JaIIP0anCdpaI+Dc10ftaNZcqfLzEwyhdzFAYInXYH4V7kEdH4hPyo9GA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"optional": true
},
"@esbuild/win32-ia32": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.6.tgz",
"integrity": "sha512-qh7IcAHUvvmMBmoIG+V+BbE9ZWSR0ohF51e5g8JZvU08kZF58uDFL5tHs0eoYz31H6Finv17te3W3QB042GqVA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"optional": true
},
"@esbuild/win32-x64": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.6.tgz",
"integrity": "sha512-9UDwkz7Wlm4N9jnv+4NL7F8vxLhSZfEkRArz2gD33HesAFfMLGIGNVXRoIHtWNw8feKsnGly9Hq1EUuRkWl0zA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"optional": true
},
"@jridgewell/gen-mapping": {
@@ -7899,33 +7900,33 @@
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
},
"esbuild": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.6.tgz",
"integrity": "sha512-5QgxWaAhU/tPBpvkxUmnFv2YINHuZzjbk0LeUUnC2i3aJHjfi5yR49lgKgF7cb98bclOp/kans8M5TGbGFfJlQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"optional": true,
"requires": {
"@esbuild/android-arm": "0.18.6",
"@esbuild/android-arm64": "0.18.6",
"@esbuild/android-x64": "0.18.6",
"@esbuild/darwin-arm64": "0.18.6",
"@esbuild/darwin-x64": "0.18.6",
"@esbuild/freebsd-arm64": "0.18.6",
"@esbuild/freebsd-x64": "0.18.6",
"@esbuild/linux-arm": "0.18.6",
"@esbuild/linux-arm64": "0.18.6",
"@esbuild/linux-ia32": "0.18.6",
"@esbuild/linux-loong64": "0.18.6",
"@esbuild/linux-mips64el": "0.18.6",
"@esbuild/linux-ppc64": "0.18.6",
"@esbuild/linux-riscv64": "0.18.6",
"@esbuild/linux-s390x": "0.18.6",
"@esbuild/linux-x64": "0.18.6",
"@esbuild/netbsd-x64": "0.18.6",
"@esbuild/openbsd-x64": "0.18.6",
"@esbuild/sunos-x64": "0.18.6",
"@esbuild/win32-arm64": "0.18.6",
"@esbuild/win32-ia32": "0.18.6",
"@esbuild/win32-x64": "0.18.6"
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"escalade": {

View File

@@ -41,6 +41,6 @@
"live-server": "^1.2.2"
},
"optionalDependencies": {
"esbuild": "^0.18.6"
"esbuild": "^0.18.20"
}
}

View File

@@ -226,6 +226,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.DELETE(p+"/profiles/ombi/:profile", app.DeleteOmbiProfile)
}
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 {
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+"/matrix", app.UnlinkMyMatrix)
user.POST(p+"/password", app.ChangeMyPassword)
if app.config.Section("user_page").Key("referrals").MustBool(false) {
user.GET(p+"/referral", app.GetMyReferral)
}
}
}
}

View File

@@ -38,7 +38,10 @@
"expiry": "common",
"add": "common",
"edit": "common",
"delete": "admin"
"delete": "common",
"myAccount": "common",
"referrals": "common",
"inviteRemainingUses": "admin"
},
"notifications": {
"errorLoginBlank": "common",

View File

@@ -2,6 +2,18 @@
# sets version environment variable for goreleaser to use
# scripts/version.sh goreleaser ...
if [[ -z "${JFA_GO_SNAPSHOT}" ]]; then
export JFA_GO_SOURCEMAP=""
export JFA_GO_COPYTS="echo skipping sourcemaps"
export JFA_GO_STRIP=""
export JFA_GO_MINIFY="--minify"
else
echo "SNAPSHOT"
export JFA_GO_SOURCEMAP="--sourcemap"
export JFA_GO_COPYTS="cp -r tempts data/web/js/ts"
export JFA_GO_STRIP="-s -w"
export JFA_GO_MINIFY=""
fi
JFA_GO_VERSION=$(git describe --exact-match HEAD 2> /dev/null || echo 'vgit')
JFA_GO_CSS_VERSION="v3" JFA_GO_NFPM_EPOCH=$(git rev-list --all --count) JFA_GO_BUILD_TIME=$(date +%s) JFA_GO_BUILT_BY=${JFA_GO_BUILT_BY:-"???"} JFA_GO_VERSION="$(echo $JFA_GO_VERSION | sed 's/v//g')" $@

View File

@@ -133,6 +133,7 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
patchLang(&lang.Email, &fallback.Email, &english.Email)
patchLang(&lang.Messages, &fallback.Messages, &english.Messages)
patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications)
patchLang(&lang.UserPage, &fallback.UserPage, &english.UserPage)
patchLang(&lang.PasswordResets, &fallback.PasswordResets, &english.PasswordResets)
patchLang(&lang.InviteEmails, &fallback.InviteEmails, &english.InviteEmails)
patchLang(&lang.PasswordValidation, &fallback.PasswordValidation, &english.PasswordValidation)
@@ -150,6 +151,7 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
patchLang(&lang.Email, &english.Email)
patchLang(&lang.Messages, &english.Messages)
patchLang(&lang.Notifications, &english.Notifications)
patchLang(&lang.UserPage, &english.UserPage)
patchLang(&lang.PasswordResets, &english.PasswordResets)
patchLang(&lang.InviteEmails, &english.InviteEmails)
patchLang(&lang.PasswordValidation, &english.PasswordValidation)

View File

@@ -11,7 +11,6 @@ import (
"time"
"github.com/hrfee/mediabrowser"
"github.com/steambap/captcha"
"github.com/timshannon/badgerhold/v4"
)
@@ -330,6 +329,9 @@ func (st *Storage) GetProfileKey(k string) (Profile, bool) {
// fmt.Printf("Failed to find profile: %v\n", err)
ok = false
}
if result.Policy.BlockedTags == nil {
result.Policy.BlockedTags = []interface{}{}
}
return result, ok
}
@@ -426,15 +428,16 @@ type DiscordUser struct {
Discriminator string
Lang string
Contact bool
JellyfinID string `json:"-" badgerhold:"key"` // Used internally in discord.go
JellyfinID string `json:"-" badgerhold:"key"`
}
type EmailAddress struct {
Addr string `badgerhold:"index"`
Label string // User Label.
Contact bool
Admin bool // Whether or not user is jfa-go admin.
JellyfinID string `badgerhold:"key"`
Addr string `badgerhold:"index"`
Label string // User Label.
Contact bool
Admin bool // Whether or not user is jfa-go admin.
JellyfinID string `badgerhold:"key"`
ReferralTemplateKey string
}
type customEmails struct {
@@ -467,15 +470,17 @@ type userPageContent struct {
// timePattern: %Y-%m-%dT%H:%M:%S.%f
type Profile struct {
Name string `badgerhold:"key"`
Admin bool `json:"admin,omitempty" badgerhold:"index"`
LibraryAccess string `json:"libraries,omitempty"`
FromUser string `json:"fromUser,omitempty"`
Policy mediabrowser.Policy `json:"policy,omitempty"`
Configuration mediabrowser.Configuration `json:"configuration,omitempty"`
Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
Default bool `json:"default,omitempty"`
Ombi map[string]interface{} `json:"ombi,omitempty"`
Name string `badgerhold:"key"`
Admin bool `json:"admin,omitempty" badgerhold:"index"`
LibraryAccess string `json:"libraries,omitempty"`
FromUser string `json:"fromUser,omitempty"`
Homescreen bool `json:"homescreen"`
Policy mediabrowser.Policy `json:"policy,omitempty"`
Configuration mediabrowser.Configuration `json:"configuration,omitempty"`
Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
Default bool `json:"default,omitempty"`
Ombi map[string]interface{} `json:"ombi,omitempty"`
ReferralTemplateKey string
}
type Invite struct {
@@ -491,11 +496,21 @@ type Invite struct {
UserMinutes int `json:"user-minutes,omitempty"`
SendTo string `json:"email"`
// Used to be stored as formatted time, now as Unix.
UsedBy [][]string `json:"used-by"`
Notify map[string]map[string]bool `json:"notify"`
Profile string `json:"profile"`
Label string `json:"label,omitempty"`
Captchas map[string]*captcha.Data // Map of Captcha IDs to answers
UsedBy [][]string `json:"used-by"`
Notify map[string]map[string]bool `json:"notify"`
Profile string `json:"profile"`
Label string `json:"label,omitempty"`
UserLabel string `json:"user_label,omitempty" example:"Friend"` // Label to apply to users created w/ this invite.
Captchas map[string]Captcha // Map of Captcha IDs to images & answers
IsReferral bool `json:"is_referral" badgerhold:"index"`
ReferrerJellyfinID string `json:"referrer_id"`
ReferrerTemplateForProfile string
}
type Captcha struct {
Answer string
Image []byte // image/png
Generated time.Time
}
type Lang struct {

View File

@@ -23,10 +23,44 @@ module.exports = {
opacity: '0'
}
},
'slide-in': {
'0%': {
opacity: '0',
transform: 'translateY(-100%)'
},
'100%': {
opacity: '1',
transform: 'translateY(0%)'
},
},
'slide-out': {
'0%': {
opacity: '1',
transform: 'translateY(0%)'
},
'100%': {
opacity: '0',
transform: 'translateY(-100%)'
},
},
'pulse': {
'0%': {
transform: 'scale(1)'
},
'50%': {
transform: 'scale(1.05)'
},
'100%': {
transform: 'scale(1)'
}
}
},
animation: {
'fade-in': 'fade-in 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
'fade-out': 'fade-out 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)'
'fade-out': 'fade-out 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
'slide-in': 'slide-in 0.2s cubic-bezier(.08,.52,.01,.98)',
'slide-out': 'slide-out 0.2s cubic-bezier(.08,.52,.01,.98)',
'pulse': 'pulse 0.2s cubic-bezier(0.25, 0.45, 0.45, 0.94)'
},
colors: {
neutral: colors.slate,

View File

@@ -78,6 +78,11 @@ window.availableProfiles = window.availableProfiles || [];
if (window.linkResetEnabled) {
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();

View File

@@ -392,3 +392,8 @@ const create = (event: SubmitEvent) => {
validator.validate();
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"));
}

View File

@@ -162,6 +162,8 @@ export class Discord extends ServiceLinker {
onclick() {
if (this._conf.inviteURL != "") {
this._getInviteURL();
} else {
(document.getElementById("discord-invite") as HTMLSpanElement).parentElement.remove();
}
super.onclick();

View File

@@ -23,6 +23,7 @@ interface User {
notify_matrix: boolean;
label: string;
accounts_admin: boolean;
referrals_enabled: boolean;
}
interface getPinResponse {
@@ -69,6 +70,8 @@ class user implements User {
private _labelEditButton: HTMLElement;
private _accounts_admin: HTMLInputElement
private _selected: boolean;
private _referralsEnabled: boolean;
private _referralsEnabledCheck: HTMLElement;
lastNotifyMethod = (): string => {
// 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 => {
const el = document.createElement("div") as HTMLDivElement;
const telegram = this._telegramUsername != "";
@@ -462,15 +476,15 @@ class user implements User {
}
matchesSearch = (query: string): boolean => {
console.log(this.name, "matches", query, ":", this.name.includes(query));
return (
this.name.includes(query) ||
this.label.includes(query) ||
this.discord.includes(query) ||
this.email.includes(query) ||
this.id.includes(query) ||
this.label.includes(query) ||
this.matrix.includes(query) ||
this.telegram.includes(query)
this.name.toLowerCase().includes(query) ||
this.label.toLowerCase().includes(query) ||
this.email.toLowerCase().includes(query) ||
this.discord.toLowerCase().includes(query) ||
this.matrix.toLowerCase().includes(query) ||
this.telegram.toLowerCase().includes(query)
);
}
@@ -506,6 +520,11 @@ class user implements User {
<td class="accounts-discord"></td>
`;
}
if (window.referralsEnabled) {
innerHTML += `
<td class="accounts-referrals text-center-i grid gap-4 place-items-stretch"></td>
`;
}
innerHTML += `
<td class="accounts-expiry"></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();
@@ -716,6 +739,7 @@ class user implements User {
this.discord_id = user.discord_id;
this.label = user.label;
this.accounts_admin = user.accounts_admin;
this.referrals_enabled = user.referrals_enabled;
}
asElement = (): HTMLTableRowElement => { return this._row; }
@@ -748,9 +772,14 @@ export class accountsList {
private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement;
private _modifySettingsProfile = document.getElementById("radio-use-profile") 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 _profileSelect = document.getElementById("modify-user-profiles") 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 _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
@@ -765,6 +794,7 @@ export class accountsList {
private _addUserName = this._addUserForm.querySelector("input[type=text]") as HTMLInputElement;
private _addUserEmail = this._addUserForm.querySelector("input[type=email]") as HTMLInputElement;
private _addUserPassword = this._addUserForm.querySelector("input[type=password]") as HTMLInputElement;
private _addUserProfile = this._addUserForm.querySelector("select") as HTMLSelectElement;
// Columns for sorting.
private _columns: { [className: string]: Column } = {};
@@ -905,6 +935,14 @@ export class accountsList {
bool: true,
string: false,
date: true
},
"referrals-enabled": {
name: window.lang.strings("referrals"),
getter: "referrals_enabled",
bool: true,
string: false,
date: false,
dependsOnTableHeader: "accounts-header-referrals"
}
}
@@ -918,7 +956,6 @@ export class accountsList {
// const words = query.split(" ");
let words: string[] = [];
// FIXME: SPLIT BY SPACE, UNLESS IN QUOTES
let quoteSymbol = ``;
let queryStart = -1;
@@ -984,10 +1021,9 @@ export class accountsList {
boolState = false;
}
if (isBool) {
// FIXME: Generate filter card for each filter class
const filterCard = document.createElement("span");
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "m-2");
filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "mx-2", "h-full");
filterCard.innerHTML = `
<span class="font-bold mr-2">${queryFormat.name}</span>
<i class="text-2xl ri-${boolState? "checkbox" : "close"}-circle-fill"></i>
@@ -1021,7 +1057,7 @@ export class accountsList {
if (queryFormat.string) {
const filterCard = document.createElement("span");
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
filterCard.classList.add("button", "~neutral", "@low", "center", "m-2", "h-full");
filterCard.classList.add("button", "~neutral", "@low", "center", "mx-2", "h-full");
filterCard.innerHTML = `
<span class="font-bold mr-2">${queryFormat.name}:</span> "${split[1]}"
`;
@@ -1057,7 +1093,7 @@ export class accountsList {
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
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;
console.log("Read", attempt, "and", date);
@@ -1123,7 +1159,7 @@ export class accountsList {
}
}
}
return result
return result;
};
@@ -1153,6 +1189,9 @@ export class accountsList {
this._selectAll.indeterminate = false;
this._selectAll.checked = false;
this._modifySettings.classList.add("unfocused");
if (window.referralsEnabled) {
this._enableReferrals.classList.add("unfocused");
}
this._deleteUser.classList.add("unfocused");
if (window.emailEnabled || window.telegramEnabled) {
this._announceButton.parentElement.classList.add("unfocused");
@@ -1175,6 +1214,9 @@ export class accountsList {
this._selectAll.indeterminate = true;
}
this._modifySettings.classList.remove("unfocused");
if (window.referralsEnabled) {
this._enableReferrals.classList.remove("unfocused");
}
this._deleteUser.classList.remove("unfocused");
this._deleteUser.textContent = window.lang.quantity("deleteUser", list.length);
if (window.emailEnabled || window.telegramEnabled) {
@@ -1183,6 +1225,7 @@ export class accountsList {
let anyNonExpiries = list.length == 0 ? true : false;
let allNonExpiries = true;
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.
this._shouldEnable = this._users[list[0]].disabled
let showDisableEnable = true;
@@ -1202,6 +1245,9 @@ export class accountsList {
if (!this._users[id].lastNotifyMethod()) {
noContactCount++;
}
if (window.referralsEnabled && referralState != -1 && Number(this._users[id].referrals_enabled) != referralState) {
referralState = -1;
}
}
this._settingExpiry = false;
if (!anyNonExpiries && !allNonExpiries) {
@@ -1235,6 +1281,22 @@ export class accountsList {
this._disableEnable.parentElement.classList.remove("unfocused");
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");
}
}
}
}
@@ -1252,7 +1314,8 @@ export class accountsList {
const send = {
"username": this._addUserName.value,
"email": this._addUserEmail.value,
"password": this._addUserPassword.value
"password": this._addUserPassword.value,
"profile": this._addUserProfile.value,
};
for (let field in send) {
if (!send[field]) {
@@ -1402,6 +1465,9 @@ export class accountsList {
this._announceButton.nextElementSibling.children[0].classList.add("unfocused");
return;
}
if (list.length > 0) {
this._announceButton.innerHTML = `${window.lang.strings("announce")} <i class="ml-2 ri-arrow-drop-down-line"></i>`;
}
const dList = document.getElementById("accounts-announce-templates") as HTMLDivElement;
dList.textContent = '';
for (let name of list) {
@@ -1657,6 +1723,90 @@ export class accountsList {
};
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) => {
const list = this._collectUsers();
@@ -1733,6 +1883,15 @@ export class accountsList {
}
}
private _populateAddUserProfiles = () => {
this._addUserProfile.textContent = "";
let innerHTML = `<option value="none">${window.lang.strings("inviteNoProfile")}</option>`;
for (let i = 0; i < window.availableProfiles.length; i++) {
innerHTML += `<option value="${window.availableProfiles[i]}" ${i == 0 ? "selected" : ""}>${window.availableProfiles[i]}</option>`;
}
this._addUserProfile.innerHTML = innerHTML;
}
constructor() {
this._populateNumbers();
this._users = {};
@@ -1743,7 +1902,10 @@ export class accountsList {
document.addEventListener("accounts-reload", this.reload);
document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); });
document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); });
this._addUserButton.onclick = window.modals.addUser.toggle;
this._addUserButton.onclick = () => {
this._populateAddUserProfiles();
window.modals.addUser.toggle();
};
this._addUserForm.addEventListener("submit", this._addUser);
this._deleteNotify.onchange = () => {
@@ -1777,6 +1939,43 @@ export class accountsList {
this._modifySettingsProfile.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.classList.add("unfocused");
@@ -1856,8 +2055,8 @@ export class accountsList {
this.loadPreview();
};
const headerNames: string[] = ["username", "access-jfa", "email", "telegram", "matrix", "discord", "expiry", "last-active"];
const headerGetters: string[] = ["name", "accounts_admin", "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", "referrals_enabled"];
for (let i = 0; i < headerNames.length; i++) {
const header: HTMLTableHeaderCellElement = document.querySelector(".accounts-header-" + headerNames[i]) as HTMLTableHeaderCellElement;
if (header !== null) {

View File

@@ -113,7 +113,8 @@ export class notificationBox implements NotificationBox {
const closeButton = document.createElement('span') as HTMLSpanElement;
closeButton.classList.add("button", "~critical", "@low", "ml-4");
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
closeButton.onclick = () => { this._box.removeChild(noti); };
closeButton.onclick = () => this._close(noti);
noti.classList.add("animate-slide-in");
noti.appendChild(closeButton);
return noti;
}
@@ -125,11 +126,21 @@ export class notificationBox implements NotificationBox {
const closeButton = document.createElement('span') as HTMLSpanElement;
closeButton.classList.add("button", "~positive", "@low", "ml-4");
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
closeButton.onclick = () => { this._box.removeChild(noti); };
closeButton.onclick = () => this._close(noti);
noti.classList.add("animate-slide-in");
noti.appendChild(closeButton);
return noti;
}
private _close = (noti: HTMLElement) => {
noti.classList.remove("animate-slide-in");
noti.classList.add("animate-slide-out");
noti.addEventListener(window.animationEvent, () => {
this._box.removeChild(noti);
}, false);
}
connectionError = () => { this.customError("connectionError", window.lang.notif("errorConnection")); }
customError = (type: string, message: string) => {
@@ -138,11 +149,13 @@ export class notificationBox implements NotificationBox {
noti.classList.add("error-" + type);
const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.error-" + type);
if (this._errorTypes[type] && previousNoti !== undefined && previousNoti != null) {
previousNoti.remove();
this._box.removeChild(previousNoti);
noti.classList.add("animate-pulse");
noti.classList.remove("animate-slide-in");
}
this._box.appendChild(noti);
this._errorTypes[type] = true;
setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._errorTypes[type] = false; } }, this.timeout*1000);
setTimeout(() => { if (this._box.contains(noti)) { this._close(noti); this._errorTypes[type] = false; } }, this.timeout*1000);
}
customPositive = (type: string, bold: string, message: string) => {
@@ -151,11 +164,13 @@ export class notificationBox implements NotificationBox {
noti.classList.add("positive-" + type);
const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.positive-" + type);
if (this._positiveTypes[type] && previousNoti !== undefined && previousNoti != null) {
previousNoti.remove();
this._box.removeChild(previousNoti);
noti.classList.add("animate-pulse");
noti.classList.remove("animate-slide-in");
}
this._box.appendChild(noti);
this._positiveTypes[type] = true;
setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._positiveTypes[type] = false; } }, this.timeout*1000);
setTimeout(() => { if (this._box.contains(noti)) { this._close(noti); this._positiveTypes[type] = false; } }, this.timeout*1000);
}
customSuccess = (type: string, message: string) => this.customPositive(type, window.lang.strings("success") + ":", message)

View File

@@ -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";
get code(): string { return this._code; }
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">${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="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;
@@ -386,6 +404,9 @@ class DOMInvite implements Invite {
if (invite.label) {
this.label = invite.label;
}
if (invite.user_label) {
this.user_label = invite.user_label;
}
this.userExpiryTime = invite.userExpiryTime || "";
}
@@ -486,6 +507,7 @@ function parseInvite(invite: { [f: string]: string | number | { [name: string]:
parsed.code = invite["code"] as string;
parsed.send_to = invite["send_to"] as string || "";
parsed.label = invite["label"] as string || "";
parsed.user_label = invite["user_label"] as string || "";
let time = "";
let userExpiryTime = "";
const fields = ["months", "days", "hours", "minutes"];
@@ -530,6 +552,7 @@ export class createInvite {
private _createButton = document.getElementById("create-submit") as HTMLSpanElement;
private _profile = document.getElementById("create-profile") as HTMLSelectElement;
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 _days = document.getElementById("create-days") as HTMLSelectElement;
@@ -572,6 +595,9 @@ export class createInvite {
get label(): string { return this._label.value; }
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 {
return this._sendToEnabled.checked;
}
@@ -749,7 +775,8 @@ export class createInvite {
"remaining-uses": this.uses,
"send-to": this.sendToEnabled ? this.sendTo : "",
"profile": this.profile,
"label": this.label
"label": this.label,
"user_label": this.user_label
};
_post("/invites", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {

View File

@@ -56,17 +56,19 @@ export const loadLangSelector = (page: string) => {
localStorage.setItem("timefmt", fmt);
};
const t12 = document.getElementById("lang-12h") as HTMLInputElement;
t12.onchange = () => setTimefmt("12h");
const t24 = document.getElementById("lang-24h") as HTMLInputElement;
t24.onchange = () => setTimefmt("24h");
if (typeof(t12) !== "undefined" && t12 != null) {
t12.onchange = () => setTimefmt("12h");
const t24 = document.getElementById("lang-24h") as HTMLInputElement;
t24.onchange = () => setTimefmt("24h");
const preference = localStorage.getItem("timefmt");
if (preference == "12h") {
t12.checked = true;
t24.checked = false;
} else if (preference == "24h") {
t24.checked = true;
t12.checked = false;
const preference = localStorage.getItem("timefmt");
if (preference == "12h") {
t12.checked = true;
t24.checked = false;
} else if (preference == "24h") {
t24.checked = true;
t12.checked = false;
}
}
}
let queryString = new URLSearchParams(window.location.search);

View File

@@ -5,11 +5,12 @@ export class Login {
private _modal: Modal;
private _form: HTMLFormElement;
private _url: string;
private _endpoint: string;
private _onLogin: (username: string, password: string) => void;
private _logoutButton: HTMLElement = null;
constructor(modal: Modal, endpoint: string) {
this._endpoint = endpoint;
this._url = window.URLBase + endpoint;
if (this._url[this._url.length-1] != '/') this._url += "/";
@@ -32,13 +33,21 @@ export class Login {
bindLogout = (button: HTMLElement) => {
this._logoutButton = button;
this._logoutButton.classList.add("unfocused");
this._logoutButton.onclick = () => _post(this._url + "logout", null, (req: XMLHttpRequest): boolean => {
if (req.readyState == 4 && req.status == 200) {
window.token = "";
location.reload();
return false;
}
});
const logoutFunc = (url: string, tryAgain: boolean) => {
_post(url + "logout", null, (req: XMLHttpRequest): boolean => {
if (req.readyState == 4 && req.status == 200) {
window.token = "";
location.reload();
return false;
}
}, false, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 404 && tryAgain) {
console.log("trying without URL Base...");
logoutFunc(this._endpoint, false);
}
});
};
this._logoutButton.onclick = () => logoutFunc(this._url, true);
};
get onLogin() { return this._onLogin; }

View File

@@ -5,6 +5,7 @@ interface Profile {
libraries: string;
fromUser: string;
ombi: boolean;
referrals_enabled: boolean;
}
class profile implements Profile {
@@ -16,6 +17,8 @@ class profile implements Profile {
private _fromUser: HTMLTableDataCellElement;
private _defaultRadio: HTMLInputElement;
private _ombi: boolean;
private _referralsButton: HTMLSpanElement;
private _referralsEnabled: boolean;
get name(): string { return this._name.textContent; }
set name(v: string) { this._name.textContent = v; }
@@ -51,7 +54,22 @@ class profile implements Profile {
get fromUser(): string { return this._fromUser.textContent; }
set fromUser(v: string) { this._fromUser.textContent = v; }
get referrals_enabled(): boolean { return this._referralsEnabled; }
set referrals_enabled(v: boolean) {
if (!window.referralsEnabled) return;
this._referralsEnabled = v;
if (v) {
this._referralsButton.textContent = window.lang.strings("delete");
this._referralsButton.classList.add("~critical");
this._referralsButton.classList.remove("~neutral");
} else {
this._referralsButton.textContent = window.lang.strings("add");
this._referralsButton.classList.add("~neutral");
this._referralsButton.classList.remove("~critical");
}
}
get default(): boolean { return this._defaultRadio.checked; }
set default(v: boolean) { this._defaultRadio.checked = v; }
@@ -64,6 +82,9 @@ class profile implements Profile {
if (window.ombiEnabled) innerHTML += `
<td><span class="button @low profile-ombi"></span></td>
`;
if (window.referralsEnabled) innerHTML += `
<td><span class="button @low profile-referrals"></span></td>
`;
innerHTML += `
<td class="profile-from ellipsis"></td>
<td class="profile-libraries"></td>
@@ -75,6 +96,8 @@ class profile implements Profile {
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
if (window.ombiEnabled)
this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement;
if (window.referralsEnabled)
this._referralsButton = this._row.querySelector("span.profile-referrals") as HTMLSpanElement;
this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement;
this._defaultRadio.onclick = () => document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name }));
@@ -89,9 +112,11 @@ class profile implements Profile {
this.fromUser = p.fromUser;
this.libraries = p.libraries;
this.ombi = p.ombi;
this.referrals_enabled = p.referrals_enabled;
}
setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { this._ombiButton.onclick = () => ombiFunc(this._ombi); }
setReferralFunc = (referralFunc: (enabled: boolean) => void) => { this._referralsButton.onclick = () => referralFunc(this._referralsEnabled); }
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); }
@@ -173,6 +198,14 @@ export class ProfileEditor {
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());
}
}
@@ -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() {
(document.getElementById('setting-profiles') as HTMLSpanElement).onclick = this.load;
document.addEventListener("profiles-default", (event: CustomEvent) => {

View File

@@ -283,6 +283,9 @@ const settings = {
"notifications": {
"enabled": new Checkbox(get("notifications-enabled"))
},
"user_page": {
"enabled": new Checkbox(get("userpage-enabled"))
},
"welcome_email": {
"enabled": new Checkbox(get("welcome_email-enabled"), "", false, "welcome_email", "enabled"),
"subject": new Input(get("welcome_email-subject"), "", "", "enabled", true, "welcome_email")

View File

@@ -40,6 +40,7 @@ declare interface Window {
jellyfinLogin: boolean;
jfAdminOnly: boolean;
jfAllowAll: boolean;
referralsEnabled: boolean;
}
declare interface Update {
@@ -113,6 +114,8 @@ declare interface Modals {
pwr?: Modal;
logs: Modal;
email?: Modal;
enableReferralsUser?: Modal;
enableReferralsProfile?: Modal;
}
interface Invite {
@@ -126,6 +129,7 @@ interface Invite {
notifyCreation?: boolean;
profile?: string;
label?: string;
user_label?: string;
userExpiry?: boolean;
userExpiryTime?: string;
}

View File

@@ -1,7 +1,7 @@
import { ThemeManager } from "./modules/theme.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { Modal } from "./modules/modal.js";
import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader, addLoader, removeLoader } from "./modules/common.js";
import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader, addLoader, removeLoader, toClipboard } from "./modules/common.js";
import { Login } from "./modules/login.js";
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
@@ -18,6 +18,7 @@ interface userWindow extends Window {
matrixUserID: string;
discordSendPINMessage: string;
pwrEnabled: string;
referralsEnabled: boolean;
}
declare var window: userWindow;
@@ -107,6 +108,14 @@ interface MyDetails {
discord?: MyDetailsContactMethod;
telegram?: MyDetailsContactMethod;
matrix?: MyDetailsContactMethod;
has_referrals: boolean;
}
interface MyReferral {
code: string;
remaining_uses: number;
no_limit: boolean;
expiry: number;
}
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 {
private _card: HTMLElement;
private _expiry: Date;
@@ -318,6 +428,9 @@ class ExpiryCard {
var expiryCard = new ExpiryCard(statusCard);
var referralCard: ReferralCard;
if (window.referralsEnabled) referralCard = new ReferralCard(document.getElementById("card-referrals"));
var contactMethodList = new ContactMethods(contactCard);
const addEditEmail = (add: boolean): void => {
@@ -337,6 +450,7 @@ const addEditEmail = (add: boolean): void => {
_post("/my/email", {"email": input.value}, (req: XMLHttpRequest) => {
if (req.readyState == 4 && (req.status == 303 || req.status == 200)) {
document.dispatchEvent(new CustomEvent("details-reload"));
window.modals.email.close();
}
}, true, (req: XMLHttpRequest) => {
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 = {
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 = {
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;
@@ -468,14 +585,15 @@ document.addEventListener("details-reload", () => {
// Note the weird format of the functions for discord/telegram:
// "this" was being redefined within the onclick() method, so
// they had to be wrapped in an anonymous function.
const contactMethods: { name: string, icon: string, f: (add: boolean) => void, required: boolean }[] = [
{name: "email", icon: `<i class="ri-mail-fill ri-lg"></i>`, f: addEditEmail, required: true},
{name: "discord", icon: `<i class="ri-discord-fill ri-lg"></i>`, f: (add: boolean) => { discord.onclick(); }, required: window.discordRequired},
{name: "telegram", icon: `<i class="ri-telegram-fill ri-lg"></i>`, f: (add: boolean) => { telegram.onclick() }, required: window.telegramRequired},
{name: "matrix", icon: `<span class="font-bold">[m]</span>`, f: (add: boolean) => { matrix.show(); }, required: window.matrixRequired}
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, enabled: true},
{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, enabled: window.telegramEnabled},
{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) {
if (!(method.enabled)) continue;
if (method.name in details) {
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) {
setBestRowSpan(messageCard, false);
// contactCard.querySelector(".content").classList.add("h-100");
} else if (!statusCard.classList.contains("unfocused")) {
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();
}
}
}
});
});

View File

@@ -213,7 +213,8 @@ func (ud *Updater) GetTag() (Tag, int, error) {
func (t *Tag) IsNew() bool {
// 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) {

View File

@@ -1,6 +1,8 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"html/template"
"io"
@@ -173,6 +175,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"jfAdminOnly": jfAdminOnly,
"jfAllowAll": jfAllowAll,
"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,
"jfLink": app.config.Section("ui").Key("redirect_url").String(),
"requirements": app.validator.getCriteria(),
"referralsEnabled": app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false),
}
if telegramEnabled {
data["telegramUsername"] = app.telegram.username
@@ -370,20 +374,16 @@ func (app *appContext) GetCaptcha(gc *gin.Context) {
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
var capt *captcha.Data
var capt Captcha
ok = true
if inv.Captchas != nil {
capt = inv.Captchas[captchaID]
capt, ok = inv.Captchas[captchaID]
}
if capt == nil {
if !ok {
respondBool(400, false, gc)
return
}
if err := capt.WriteImage(gc.Writer); err != nil {
app.err.Printf("Failed to write CAPTCHA image: %v", err)
respondBool(500, false, gc)
return
}
gc.Status(200)
gc.Data(200, "image/png", capt.Image)
return
}
@@ -412,10 +412,20 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
return
}
if inv.Captchas == nil {
inv.Captchas = map[string]*captcha.Data{}
inv.Captchas = map[string]Captcha{}
}
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)
gc.JSON(200, genCaptchaDTO{captchaID})
return
@@ -435,7 +445,7 @@ func (app *appContext) verifyCaptcha(code, id, text string) bool {
app.debug.Printf("Couldn't find Captcha \"%s\"", id)
return false
}
return strings.ToLower(c.Text) == strings.ToLower(text)
return strings.ToLower(c.Answer) == strings.ToLower(text)
}
// reCAPTCHA
@@ -502,15 +512,15 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
})
return
}
var capt *captcha.Data
var capt Captcha
if inv.Captchas != nil {
capt = inv.Captchas[captchaID]
capt, ok = inv.Captchas[captchaID]
}
if capt == nil {
if !ok {
respondBool(400, false, gc)
return
}
if strings.ToLower(capt.Text) != strings.ToLower(text) {
if strings.ToLower(capt.Answer) != strings.ToLower(text) {
respondBool(400, false, gc)
return
}
@@ -617,6 +627,14 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
}
userPageAddress += "/my/account"
fromUser := ""
if inv.ReferrerJellyfinID != "" {
sender, status, err := app.jf.UserByID(inv.ReferrerJellyfinID, false)
if status == 200 && err == nil {
fromUser = sender.Name
}
}
data := gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
@@ -652,6 +670,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"reCAPTCHASiteKey": app.config.Section("captcha").Key("recaptcha_site_key").MustString(""),
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
"userPageAddress": userPageAddress,
"fromUser": fromUser,
}
if telegram {
data["telegramPIN"] = app.telegram.NewAuthToken()