1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-28 03:50:10 +00:00

Compare commits

...

6 Commits

Author SHA1 Message Date
c0c91b4aad
drone: source buildrone key from drone in docker build 2023-12-20 20:06:44 +00:00
83712a6937
pwr: fix set password for jellyfin PWRs 2023-12-20 19:04:40 +00:00
290d02d248
pwr: include pwr-pin in build process, whoops
copying the PIN on the external PWR link page wasn't working since the
code's typescript wasn't being compiled.
2023-12-20 18:40:18 +00:00
9cd402a15d
logs: fix file identifier 2023-12-20 18:28:42 +00:00
1a6897637f
userpage: allow manual disable of pwr through username/email/contact
Checkboxes added to userpage settings allowing enabling/disabling of
specific ways of starting a PWR. For #312.
2023-12-20 18:18:39 +00:00
213b1e7f9e
accounts: allow setting exact expiry date
set with a text input field which uses the same date parsing library as
the search function. Parsed expiry date will appear once you've typed
something in, so you can make sure it's right.
2023-12-20 17:20:59 +00:00
17 changed files with 281 additions and 87 deletions

View File

@ -131,6 +131,9 @@ steps:
volumes: volumes:
- name: ssh_key - name: ssh_key
path: /root/drone_rsa path: /root/drone_rsa
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
settings: settings:
host: host:
from_secret: ssh2_host from_secret: ssh2_host
@ -140,13 +143,15 @@ steps:
from_secret: ssh2_port from_secret: ssh2_port
volumes: volumes:
- /root/.ssh/docker-build:/root/drone_rsa - /root/.ssh/docker-build:/root/drone_rsa
envs:
- buildrone_key
key_path: /root/drone_rsa key_path: /root/drone_rsa
command_timeout: 50m command_timeout: 50m
script: script:
- /mnt/buildx/jfa-go/build.sh - /mnt/buildx/jfa-go/build.sh
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py - wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
- pip3 install requests - pip3 install requests
- bash -c 'cd /mnt/buildx/jfa-go/jfa-go && BUILDRONE_KEY=$(cat /mnt/buildx/jfa-go/key) python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-unstable=true' - bash -c 'cd /mnt/buildx/jfa-go/jfa-go && python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-unstable=true'
- rm -f /mnt/buildx/jfa-go/jfa-go/upload.py - rm -f /mnt/buildx/jfa-go/jfa-go/upload.py
trigger: trigger:
branch: branch:

View File

@ -29,6 +29,7 @@ before:
- 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/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/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/pwr.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/pwr.js {{.Env.JFA_GO_MINIFY}}
- npx esbuild --target=es6 --bundle tempts/pwr-pin.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/pwr-pin.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/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/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}} - npx esbuild --target=es6 --bundle tempts/crash.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/crash.js {{.Env.JFA_GO_MINIFY}}

View File

@ -121,6 +121,7 @@ typescript:
$(ESBUILD) --target=es6 --bundle tempts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js --minify $(ESBUILD) --target=es6 --bundle tempts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js --minify
$(ESBUILD) --target=es6 --bundle tempts/user.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/user.js --minify $(ESBUILD) --target=es6 --bundle tempts/user.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/user.js --minify
$(ESBUILD) --target=es6 --bundle tempts/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.js --minify $(ESBUILD) --target=es6 --bundle tempts/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.js --minify
$(ESBUILD) --target=es6 --bundle tempts/pwr-pin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr-pin.js --minify
$(ESBUILD) --target=es6 --bundle tempts/form.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/form.js --minify $(ESBUILD) --target=es6 --bundle tempts/form.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/form.js --minify
$(ESBUILD) --target=es6 --bundle tempts/setup.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/setup.js --minify $(ESBUILD) --target=es6 --bundle tempts/setup.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/setup.js --minify
$(ESBUILD) --target=es6 --bundle tempts/crash.ts --outfile=./$(DATA)/crash.js --minify $(ESBUILD) --target=es6 --bundle tempts/crash.ts --outfile=./$(DATA)/crash.js --minify

View File

@ -590,6 +590,9 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
cancel := time.AfterFunc(1*time.Second, func() { cancel := time.AfterFunc(1*time.Second, func() {
timerWait <- true timerWait <- true
}) })
usernameAllowed := app.config.Section("user_page").Key("allow_pwr_username").MustBool(true)
emailAllowed := app.config.Section("user_page").Key("allow_pwr_email").MustBool(true)
contactMethodAllowed := app.config.Section("user_page").Key("allow_pwr_contact_method").MustBool(true)
address := gc.Param("address") address := gc.Param("address")
if address == "" { if address == "" {
app.debug.Println("Ignoring empty request for PWR") app.debug.Println("Ignoring empty request for PWR")
@ -600,7 +603,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
var pwr InternalPWR var pwr InternalPWR
var err error var err error
jfUser, ok := app.ReverseUserSearch(address) jfUser, ok := app.ReverseUserSearch(address, usernameAllowed, emailAllowed, contactMethodAllowed)
if !ok { if !ok {
app.debug.Printf("Ignoring PWR request: User not found") app.debug.Printf("Ignoring PWR request: User not found")

View File

@ -719,7 +719,7 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
var req extendExpiryDTO var req extendExpiryDTO
gc.BindJSON(&req) gc.BindJSON(&req)
app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users)) app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users))
if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 { if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 && req.Timestamp <= 0 {
respondBool(400, false, gc) respondBool(400, false, gc)
return return
} }
@ -731,7 +731,12 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
} else { } else {
app.debug.Printf("Created expiry for \"%s\"", id) app.debug.Printf("Created expiry for \"%s\"", id)
} }
expiry := UserExpiry{Expiry: base.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)} expiry := UserExpiry{}
if req.Timestamp != 0 {
expiry.Expiry = time.Unix(req.Timestamp, 0)
} else {
expiry.Expiry = base.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)
}
app.storage.SetUserExpiryKey(id, expiry) app.storage.SetUserExpiryKey(id, expiry)
} }
respondBool(204, true, gc) respondBool(204, true, gc)

View File

@ -122,6 +122,20 @@ func (app *appContext) loadConfig() error {
app.MustSetValue("password_resets", "url_base", strings.TrimSuffix(url1, "/invite")) app.MustSetValue("password_resets", "url_base", strings.TrimSuffix(url1, "/invite"))
app.MustSetValue("invite_emails", "url_base", url2) app.MustSetValue("invite_emails", "url_base", url2)
pwrMethods := []string{"allow_pwr_username", "allow_pwr_email", "allow_pwr_contact_method"}
allDisabled := true
for _, v := range pwrMethods {
if app.config.Section("user_page").Key(v).MustBool(true) {
allDisabled = false
}
}
if allDisabled {
fmt.Println("SETALLTRUE")
for _, v := range pwrMethods {
app.config.Section("user_page").Key(v).SetValue("true")
}
}
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false) messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false) telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false) discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)

View File

@ -629,6 +629,7 @@
"name": "Show Link on Admin Login page", "name": "Show Link on Admin Login page",
"required": false, "required": false,
"requires_restart": false, "requires_restart": false,
"depends_true": "enabled",
"type": "bool", "type": "bool",
"value": true, "value": true,
"description": "Whether or not to show a link to the \"My Account\" page on the admin login screen, to direct lost users." "description": "Whether or not to show a link to the \"My Account\" page on the admin login screen, to direct lost users."
@ -637,6 +638,7 @@
"name": "User Referrals", "name": "User Referrals",
"required": false, "required": false,
"requires_restart": true, "requires_restart": true,
"depends_true": "enabled",
"type": "bool", "type": "bool",
"value": true, "value": true,
"description": "Users are given their own \"invite\" to send to others." "description": "Users are given their own \"invite\" to send to others."
@ -648,6 +650,41 @@
"depends_true": "referrals", "depends_true": "referrals",
"required": "false", "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." "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."
},
"allow_pwr_username": {
"name": "Allow PWR with username",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "bool",
"value": true,
"description": "Allow users to start a Password Reset by inputting their username."
},
"allow_pwr_email": {
"name": "Allow PWR with email address",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "bool",
"value": true,
"description": "Allow users to start a Password Reset by inputting their email address."
},
"allow_pwr_contact_method": {
"name": "Allow PWR with Discord/Telegram/Matrix",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "bool",
"value": true,
"description": "Allow users to start a Password Reset by inputting their Discord/Telegram/Matrix username/id."
},
"pwr_note": {
"name": "PWR Methods",
"type": "note",
"depends_true": "enabled",
"value": "",
"required": "false",
"description": "Select at least one PWR initiation method. If none are selected, all will be enabled."
} }
} }
}, },

View File

@ -899,55 +899,65 @@ func (app *appContext) getAddressOrName(jfID string) string {
// ReverseUserSearch returns the jellyfin ID of the user with the given username, email, or contact method username. // ReverseUserSearch returns the jellyfin ID of the user with the given username, email, or contact method username.
// returns "" if none found. returns only the first match, might be an issue if there are users with the same contact method usernames. // returns "" if none found. returns only the first match, might be an issue if there are users with the same contact method usernames.
func (app *appContext) ReverseUserSearch(address string) (user mediabrowser.User, ok bool) { func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEmail, matchContactMethod bool) (user mediabrowser.User, ok bool) {
ok = false ok = false
user, status, err := app.jf.UserByName(address, false) var status int
if status == 200 && err == nil { var err error = nil
ok = true if matchUsername {
return user, status, err = app.jf.UserByName(address, false)
if status == 200 && err == nil {
ok = true
return
}
} }
emailAddresses := []EmailAddress{}
err = app.storage.db.Find(&emailAddresses, badgerhold.Where("Addr").Eq(address)) if matchEmail {
if err == nil && len(emailAddresses) > 0 { emailAddresses := []EmailAddress{}
for _, emailUser := range emailAddresses { err = app.storage.db.Find(&emailAddresses, badgerhold.Where("Addr").Eq(address))
user, status, err = app.jf.UserByID(emailUser.JellyfinID, false) if err == nil && len(emailAddresses) > 0 {
if status == 200 && err == nil { for _, emailUser := range emailAddresses {
ok = true user, status, err = app.jf.UserByID(emailUser.JellyfinID, false)
return if status == 200 && err == nil {
ok = true
return
}
} }
} }
} }
// Dont know how we'd use badgerhold when we need to render each username, // Dont know how we'd use badgerhold when we need to render each username,
// Apart from storing the rendered name in the db. // Apart from storing the rendered name in the db.
for _, dcUser := range app.storage.GetDiscord() { if matchContactMethod {
if RenderDiscordUsername(dcUser) == strings.ToLower(address) { for _, dcUser := range app.storage.GetDiscord() {
user, status, err = app.jf.UserByID(dcUser.JellyfinID, false) if RenderDiscordUsername(dcUser) == strings.ToLower(address) {
if status == 200 && err == nil { user, status, err = app.jf.UserByID(dcUser.JellyfinID, false)
ok = true if status == 200 && err == nil {
return ok = true
return
}
} }
} }
} tgUsername := strings.TrimPrefix(address, "@")
tgUsername := strings.TrimPrefix(address, "@") telegramUsers := []TelegramUser{}
telegramUsers := []TelegramUser{} err = app.storage.db.Find(&telegramUsers, badgerhold.Where("Username").Eq(tgUsername))
err = app.storage.db.Find(&telegramUsers, badgerhold.Where("Username").Eq(tgUsername)) if err == nil && len(telegramUsers) > 0 {
if err == nil && len(telegramUsers) > 0 { for _, telegramUser := range telegramUsers {
for _, telegramUser := range telegramUsers { user, status, err = app.jf.UserByID(telegramUser.JellyfinID, false)
user, status, err = app.jf.UserByID(telegramUser.JellyfinID, false) if status == 200 && err == nil {
if status == 200 && err == nil { ok = true
ok = true return
return }
} }
} }
} matrixUsers := []MatrixUser{}
matrixUsers := []MatrixUser{} err = app.storage.db.Find(&matrixUsers, badgerhold.Where("UserID").Eq(address))
err = app.storage.db.Find(&matrixUsers, badgerhold.Where("UserID").Eq(address)) if err == nil && len(matrixUsers) > 0 {
if err == nil && len(matrixUsers) > 0 { for _, matrixUser := range matrixUsers {
for _, matrixUser := range matrixUsers { user, status, err = app.jf.UserByID(matrixUser.JellyfinID, false)
user, status, err = app.jf.UserByID(matrixUser.JellyfinID, false) if status == 200 && err == nil {
if status == 200 && err == nil { ok = true
ok = true return
return }
} }
} }
} }

View File

@ -181,39 +181,49 @@
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-extend-expiry" href=""> <form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-extend-expiry" href="">
<span class="heading"><span id="header-extend-expiry"></span> <span class="modal-close">&times;</span></span> <span class="heading"><span id="header-extend-expiry"></span> <span class="modal-close">&times;</span></span>
<div class="content mt-8"> <div class="content mt-8">
<div class="row"> <aside class="aside sm ~urge dark:~d_info mb-2 @low row unfocused" id="extend-expiry-date"></aside>
<div class="col"> <div>
<label class="label supra" for="extend-expiry-months">{{ .strings.inviteMonths }}</label> <span class="text-xl supra row py-1">{{ .strings.setExpiry }}</span>
<div class="select ~neutral @low mb-2 mt-4"> <div class="row">
<select id="extend-expiry-months"> <input type="text" id="extend-expiry-text" class="input ~neutral @low mb-2 mt-4" placeholder="{{ .strings.enterExpiry }}">
<option>0</option>
</select>
</div>
</div>
<div class="col">
<label class="label supra" for="extend-expiry-days">{{ .strings.inviteDays }}</label>
<div class="select ~neutral @low mb-2 mt-4">
<select id="extend-expiry-days">
<option>0</option>
</select>
</div>
</div> </div>
</div> </div>
<div class="row"> <div id="extend-expiry-field-inputs">
<div class="col"> <span class="text-xl supra row py-1">{{ .strings.extendExpiry }}</span>
<label class="label supra" for="extend-expiry-hours">{{ .strings.inviteHours }}</label> <div class="row">
<div class="select ~neutral @low mb-2 mt-4"> <div class="col">
<select id="extend-expiry-hours"> <label class="label supra" for="extend-expiry-months">{{ .strings.inviteMonths }}</label>
<option>0</option> <div class="select ~neutral @low mb-2 mt-4">
</select> <select id="extend-expiry-months">
<option>0</option>
</select>
</div>
</div>
<div class="col">
<label class="label supra" for="extend-expiry-days">{{ .strings.inviteDays }}</label>
<div class="select ~neutral @low mb-2 mt-4">
<select id="extend-expiry-days">
<option>0</option>
</select>
</div>
</div> </div>
</div> </div>
<div class="col"> <div class="row">
<label class="label supra" for="extend-expiry-minutes">{{ .strings.inviteMinutes }}</label> <div class="col">
<div class="select ~neutral @low mb-2 mt-4"> <label class="label supra" for="extend-expiry-hours">{{ .strings.inviteHours }}</label>
<select id="extend-expiry-minutes"> <div class="select ~neutral @low mb-2 mt-4">
<option>0</option> <select id="extend-expiry-hours">
</select> <option>0</option>
</select>
</div>
</div>
<div class="col">
<label class="label supra" for="extend-expiry-minutes">{{ .strings.inviteMinutes }}</label>
<div class="select ~neutral @low mb-2 mt-4">
<select id="extend-expiry-minutes">
<option>0</option>
</select>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -48,11 +48,17 @@
</div> </div>
{{ if .pwrEnabled }} {{ if .pwrEnabled }}
<div id="modal-pwr" class="modal"> <div id="modal-pwr" class="modal">
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low"> <div class="card content relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
<span class="heading">{{ .strings.resetPassword }}</span> <span class="heading">{{ .strings.resetPassword }}</span>
<p class="content my-2"> <p class="content my-2">
{{ if .linkResetEnabled }} {{ if .linkResetEnabled }}
{{ .strings.resetPasswordThroughLink }} {{ .strings.resetPasswordThroughLinkStart }}
<ul class="content">
{{ if .resetPasswordUsername }}<li>{{ .strings.resetPasswordUsername }}</li>{{ end }}
{{ if .resetPasswordEmail }}<li>{{ .strings.resetPasswordEmail }}</li>{{ end }}
{{ if .resetPasswordContactMethod }}<li>{{ .strings.resetPasswordContactMethod }}</li>{{ end }}
</ul>
{{ .strings.resetPasswordThroughLinkEnd }}
{{ else }} {{ else }}
{{ .strings.resetPasswordThroughJellyfin }} {{ .strings.resetPasswordThroughJellyfin }}
{{ end }} {{ end }}

View File

@ -64,6 +64,7 @@
"extendExpiry": "Extend expiry", "extendExpiry": "Extend expiry",
"setExpiry": "Set expiry", "setExpiry": "Set expiry",
"removeExpiry": "Remove expiry", "removeExpiry": "Remove expiry",
"enterExpiry": "Enter an expiry",
"sendPWRManual": "User {n} has no method of contact, press copy to get a link to send to them.", "sendPWRManual": "User {n} has no method of contact, press copy to get a link to send to them.",
"sendPWRSuccess": "Password reset link sent.", "sendPWRSuccess": "Password reset link sent.",
"sendPWRSuccessManual": "If the user hasn't received it, press copy to get a link to manually send to them.", "sendPWRSuccessManual": "If the user hasn't received it, press copy to get a link to manually send to them.",
@ -149,6 +150,7 @@
"accountDisabled": "Account disabled: {user}", "accountDisabled": "Account disabled: {user}",
"accountReEnabled": "Account re-enabled: {user}", "accountReEnabled": "Account re-enabled: {user}",
"accountExpired": "Account expired: {user}", "accountExpired": "Account expired: {user}",
"accountWillExpire": "Account will expire on {date}",
"userDeleted": "User was deleted.", "userDeleted": "User was deleted.",
"userDisabled": "User was disabled", "userDisabled": "User was disabled",
"inviteCreated": "Invite created: {invite}", "inviteCreated": "Invite created: {invite}",
@ -219,6 +221,7 @@
"errorCheckUpdate": "Failed to check for update.", "errorCheckUpdate": "Failed to check for update.",
"errorNoReferralTemplate": "Profile doesn't contain referral template, add one in settings.", "errorNoReferralTemplate": "Profile doesn't contain referral template, add one in settings.",
"errorLoadActivities": "Failed to load activities.", "errorLoadActivities": "Failed to load activities.",
"errorInvalidDate": "Date is invalid.",
"updateAvailable": "A new update is available, check settings.", "updateAvailable": "A new update is available, check settings.",
"noUpdatesAvailable": "No new updates available." "noUpdatesAvailable": "No new updates available."
}, },

View File

@ -32,6 +32,11 @@
"resetPassword": "Reset Password", "resetPassword": "Reset Password",
"resetPasswordThroughJellyfin": "To reset your password, visit {jfLink} and press the \"Forgot Password\" button.", "resetPasswordThroughJellyfin": "To reset your password, visit {jfLink} and press the \"Forgot Password\" button.",
"resetPasswordThroughLink": "To reset your password, enter your username, email address or a linked contact method username, and submit. A link will be sent to reset your password.", "resetPasswordThroughLink": "To reset your password, enter your username, email address or a linked contact method username, and submit. A link will be sent to reset your password.",
"resetPasswordThroughLinkStart": "To reset your password, enter one of the following below:",
"resetPasswordThroughLinkEnd": "Then press submit. A link will be sent to reset your password.",
"resetPasswordUsername": "Your Jellyfin username",
"resetPasswordEmail": "Your email address",
"resetPasswordContactMethod": "The username of any contact method linked to your account",
"resetSent": "Reset Sent.", "resetSent": "Reset Sent.",
"resetSentDescription": "If an account with the given username/contact method exists, a password reset link has been sent via all contact methods available. The code will expire in 30 minutes.", "resetSentDescription": "If an account with the given username/contact method exists, a password reset link has been sent via all contact methods available. The code will expire in 30 minutes.",
"changePassword": "Change Password", "changePassword": "Change Password",

View File

@ -49,7 +49,7 @@ func Lshortfile(level int) string {
} }
func lshortfile() string { func lshortfile() string {
return Lshortfile(2) return Lshortfile(3)
} }
func NewLogger(out io.Writer, prefix string, flag int, color c.Attribute) (l *Logger) { func NewLogger(out io.Writer, prefix string, flag int, color c.Attribute) (l *Logger) {

View File

@ -261,11 +261,12 @@ type customEmailDTO struct {
} }
type extendExpiryDTO struct { type extendExpiryDTO struct {
Users []string `json:"users"` // List of user IDs to apply to. Users []string `json:"users"` // List of user IDs to apply to.
Months int `json:"months" example:"1"` // Number of months to add. Months int `json:"months" example:"1"` // Number of months to add.
Days int `json:"days" example:"1"` // Number of days to add. Days int `json:"days" example:"1"` // Number of days to add.
Hours int `json:"hours" example:"2"` // Number of hours to add. Hours int `json:"hours" example:"2"` // Number of hours to add.
Minutes int `json:"minutes" example:"3"` // Number of minutes to add. Minutes int `json:"minutes" example:"3"` // Number of minutes to add.
Timestamp int64 `json:"timestamp"` // Optional, exact time to expire at. Overrides other fields.
} }
type checkUpdateDTO struct { type checkUpdateDTO struct {

View File

@ -771,6 +771,12 @@ export class accountsList {
private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement; private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement;
private _expiryDropdown = document.getElementById("accounts-expiry-dropdown") as HTMLElement; private _expiryDropdown = document.getElementById("accounts-expiry-dropdown") as HTMLElement;
private _extendExpiry = document.getElementById("accounts-extend-expiry") as HTMLSpanElement; private _extendExpiry = document.getElementById("accounts-extend-expiry") as HTMLSpanElement;
private _extendExpiryForm = document.getElementById("form-extend-expiry") as HTMLFormElement;
private _extendExpiryTextInput = document.getElementById("extend-expiry-text") as HTMLInputElement;
private _extendExpiryFieldInputs = document.getElementById("extend-expiry-field-inputs") as HTMLElement;
private _usingExtendExpiryTextInput = true;
private _extendExpiryDate = document.getElementById("extend-expiry-date") as HTMLElement;
private _removeExpiry = document.getElementById("accounts-remove-expiry") as HTMLSpanElement; private _removeExpiry = document.getElementById("accounts-remove-expiry") as HTMLSpanElement;
private _enableExpiryNotify = document.getElementById("expiry-extend-enable") as HTMLInputElement; private _enableExpiryNotify = document.getElementById("expiry-extend-enable") as HTMLInputElement;
private _enableExpiryReason = document.getElementById("textarea-extend-enable") as HTMLTextAreaElement; private _enableExpiryReason = document.getElementById("textarea-extend-enable") as HTMLTextAreaElement;
@ -1625,6 +1631,45 @@ export class accountsList {
this.reload(); this.reload();
} }
_displayExpiryDate = () => {
let date: Date;
let invalid = false;
if (this._usingExtendExpiryTextInput) {
date = (Date as any).fromString(this._extendExpiryTextInput.value) as Date;
invalid = "invalid" in (date as any);
} else {
let fields: Array<HTMLSelectElement> = [
document.getElementById("extend-expiry-months") as HTMLSelectElement,
document.getElementById("extend-expiry-days") as HTMLSelectElement,
document.getElementById("extend-expiry-hours") as HTMLSelectElement,
document.getElementById("extend-expiry-minutes") as HTMLSelectElement
];
invalid = fields[0].value == "0" && fields[1].value == "0" && fields[2].value == "0" && fields[3].value == "0";
let id = this._collectUsers().length == 1 ? this._collectUsers()[0] : "";
if (!id) invalid = true;
else {
date = new Date(this._users[id].expiry*1000);
if (this._users[id].expiry == 0) date = new Date();
date.setMonth(date.getMonth() + (+fields[0].value))
date.setDate(date.getDate() + (+fields[1].value));
date.setHours(date.getHours() + (+fields[2].value));
date.setMinutes(date.getMinutes() + (+fields[3].value));
}
}
const submit = this._extendExpiryForm.querySelector(`input[type="submit"]`) as HTMLInputElement;
const submitSpan = submit.nextElementSibling;
if (invalid) {
submit.disabled = true;
submitSpan.classList.add("opacity-60");
this._extendExpiryDate.classList.add("unfocused");
} else {
submit.disabled = false;
submitSpan.classList.remove("opacity-60");
this._extendExpiryDate.textContent = window.lang.strings("accountWillExpire").replace("{date}", toDateString(date));
this._extendExpiryDate.classList.remove("unfocused");
}
}
extendExpiry = (enableUser?: boolean) => { extendExpiry = (enableUser?: boolean) => {
const list = this._collectUsers(); const list = this._collectUsers();
let applyList: string[] = []; let applyList: string[] = [];
@ -1647,10 +1692,20 @@ export class accountsList {
} }
document.getElementById("header-extend-expiry").textContent = header; document.getElementById("header-extend-expiry").textContent = header;
const extend = () => { const extend = () => {
let send = { "users": applyList } let send = { "users": applyList, "timestamp": 0 }
for (let field of ["months", "days", "hours", "minutes"]) { if (this._usingExtendExpiryTextInput) {
send[field] = +(document.getElementById("extend-expiry-"+field) as HTMLSelectElement).value; let date = (Date as any).fromString(this._extendExpiryTextInput.value) as Date;
send["timestamp"] = Math.floor(date.getTime() / 1000);
if ("invalid" in (date as any)) {
window.notifications.customError("extendExpiryError", window.lang.notif("errorInvalidDate"));
return;
}
} else {
for (let field of ["months", "days", "hours", "minutes"]) {
send[field] = +(document.getElementById("extend-expiry-"+field) as HTMLSelectElement).value;
}
} }
_post("/users/extend", send, (req: XMLHttpRequest) => { _post("/users/extend", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status != 200 && req.status != 204) { if (req.status != 200 && req.status != 204) {
@ -1663,8 +1718,7 @@ export class accountsList {
} }
}); });
}; };
const form = document.getElementById("form-extend-expiry") as HTMLFormElement; this._extendExpiryForm.onsubmit = (event: Event) => {
form.onsubmit = (event: Event) => {
event.preventDefault(); event.preventDefault();
if (enableUser) { if (enableUser) {
this._enableDisableUsers(applyList, true, this._enableExpiryNotify.checked, this._enableExpiryNotify ? this._enableExpiryReason.value : null, (req: XMLHttpRequest) => { this._enableDisableUsers(applyList, true, this._enableExpiryNotify.checked, this._enableExpiryNotify ? this._enableExpiryReason.value : null, (req: XMLHttpRequest) => {
@ -1685,6 +1739,7 @@ export class accountsList {
extend(); extend();
} }
} }
this._extendExpiryTextInput.value = "";
window.modals.extendExpiry.show(); window.modals.extendExpiry.show();
} }
@ -1821,6 +1876,37 @@ export class accountsList {
this._extendExpiry.onclick = () => { this.extendExpiry(); }; this._extendExpiry.onclick = () => { this.extendExpiry(); };
this._removeExpiry.onclick = () => { this.removeExpiry(); }; this._removeExpiry.onclick = () => { this.removeExpiry(); };
this._expiryDropdown.classList.add("unfocused"); this._expiryDropdown.classList.add("unfocused");
this._extendExpiryDate.classList.add("unfocused");
this._extendExpiryTextInput.onkeyup = () => {
this._extendExpiryTextInput.parentElement.parentElement.classList.remove("opacity-60");
this._extendExpiryFieldInputs.classList.add("opacity-60");
this._usingExtendExpiryTextInput = true;
this._displayExpiryDate();
}
this._extendExpiryTextInput.onclick = () => {
this._extendExpiryTextInput.parentElement.parentElement.classList.remove("opacity-60");
this._extendExpiryFieldInputs.classList.add("opacity-60");
this._usingExtendExpiryTextInput = true;
this._displayExpiryDate();
};
this._extendExpiryFieldInputs.onclick = () => {
this._extendExpiryFieldInputs.classList.remove("opacity-60");
this._extendExpiryTextInput.parentElement.parentElement.classList.add("opacity-60");
this._usingExtendExpiryTextInput = false;
this._displayExpiryDate();
};
for (let field of ["months", "days", "hours", "minutes"]) {
(document.getElementById("extend-expiry-"+field) as HTMLSelectElement).onchange = () => {
this._extendExpiryFieldInputs.classList.remove("opacity-60");
this._extendExpiryTextInput.parentElement.parentElement.classList.add("opacity-60");
this._usingExtendExpiryTextInput = false;
this._displayExpiryDate();
};
}
this._disableEnable.onclick = this.enableDisableUsers; this._disableEnable.onclick = this.enableDisableUsers;
this._disableEnable.parentElement.classList.add("unfocused"); this._disableEnable.parentElement.classList.add("unfocused");

View File

@ -161,6 +161,7 @@ func newUpdater(buildroneURL, namespace, repo, version, commit, buildType string
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
binary += ".exe" binary += ".exe"
} }
fmt.Println("monitoring", tag)
return &Updater{ return &Updater{
httpClient: &http.Client{Timeout: 10 * time.Second}, httpClient: &http.Client{Timeout: 10 * time.Second},
timeoutHandler: common.NewTimeoutHandler("updater", buildroneURL, true), timeoutHandler: common.NewTimeoutHandler("updater", buildroneURL, true),
@ -211,7 +212,7 @@ func (ud *Updater) GetTag() (Tag, int, error) {
var tag Tag var tag Tag
err = json.Unmarshal(body, &tag) err = json.Unmarshal(body, &tag)
if tag.Version == "" { if tag.Version == "" {
err = errors.New("Tag was empty") err = errors.New("Tag at \"" + url + "\" was empty")
} }
return tag, resp.StatusCode, err return tag, resp.StatusCode, err
} }

View File

@ -234,6 +234,11 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
data["discordServerName"] = app.discord.serverName data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.inviteChannelName != "" data["discordInviteLink"] = app.discord.inviteChannelName != ""
} }
if data["linkResetEnabled"].(bool) {
data["resetPasswordUsername"] = app.config.Section("user_page").Key("allow_pwr_username").MustBool(true)
data["resetPasswordEmail"] = app.config.Section("user_page").Key("allow_pwr_email").MustBool(true)
data["resetPasswordContactMethod"] = app.config.Section("user_page").Key("allow_pwr_contact_method").MustBool(true)
}
pageMessagesExist := map[string]bool{} pageMessagesExist := map[string]bool{}
pageMessages := map[string]CustomContent{} pageMessages := map[string]CustomContent{}
@ -275,7 +280,8 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
"ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false), "ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false),
} }
pwr, isInternal := app.internalPWRs[pin] pwr, isInternal := app.internalPWRs[pin]
if isInternal && setPassword { // if isInternal && setPassword {
if setPassword {
data["helpMessage"] = app.config.Section("ui").Key("help_message").String() data["helpMessage"] = app.config.Section("ui").Key("help_message").String()
data["successMessage"] = app.config.Section("ui").Key("success_message").String() data["successMessage"] = app.config.Section("ui").Key("success_message").String()
data["jfLink"] = app.config.Section("ui").Key("redirect_url").String() data["jfLink"] = app.config.Section("ui").Key("redirect_url").String()
@ -319,7 +325,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
var status int var status int
var err error var err error
var username string var username string
if !isInternal { if !isInternal && !setPassword {
resp, status, err = app.jf.ResetPassword(pin) resp, status, err = app.jf.ResetPassword(pin)
} else if time.Now().After(pwr.Expiry) { } else if time.Now().After(pwr.Expiry) {
app.debug.Printf("Ignoring PWR request due to expired internal PIN: %s", pin) app.debug.Printf("Ignoring PWR request due to expired internal PIN: %s", pin)