config: start adding path parameters

to change the urls of the admin page, the my account page and of
invites. Seems to work, but need to check all the code over and test.
This commit is contained in:
Harvey Tindall
2025-05-13 21:10:40 +01:00
parent 2d98c6cff4
commit c52ba2162e
43 changed files with 336 additions and 266 deletions

View File

@@ -414,7 +414,7 @@ func (app *appContext) TelegramVerified(gc *gin.Context) {
respondBool(200, ok, gc)
}
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code. NOTE: "/invite" might have been changed in Settings > URL Paths.
// @Produce json
// @Success 200 {object} boolResponse
// @Success 401 {object} boolResponse
@@ -438,7 +438,7 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
respondBool(200, ok, gc)
}
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code. NOTE: "/invite" might have been changed in Settings > URL Paths.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse
@@ -462,7 +462,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
respondBool(200, ok, gc)
}
// @Summary Returns a 10-minute, one-use Discord server invite
// @Summary Returns a 10-minute, one-use Discord server invite. NOTE: "/invite" might have been changed in Settings > URL Paths.
// @Produce json
// @Success 200 {object} DiscordInviteDTO
// @Failure 400 {object} boolResponse
@@ -489,7 +489,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) {
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
}
// @Summary Generate and send a new PIN to a specified Matrix user.
// @Summary Generate and send a new PIN to a specified Matrix user. NOTE: "/invite" might have been changed in Settings > URL Paths.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
@@ -528,7 +528,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
respondBool(200, true, gc)
}
// @Summary Check whether a matrix PIN is valid, and mark the token as verified if so. Requires invite code.
// @Summary Check whether a matrix PIN is valid, and mark the token as verified if so. Requires invite code. NOTE: "/invite" might have been changed in Settings > URL Paths.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse

View File

@@ -164,9 +164,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
var target ConfirmationTarget
var id string
fail := func() {
gcHTML(gc, 404, "404.html", gin.H{
"cssClass": app.cssClass,
"cssVersion": cssVersion,
app.gcHTML(gc, 404, "404.html", OtherPage, gin.H{
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
@@ -201,7 +199,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
// Perform an Action
if target == NoOp {
gc.Redirect(http.StatusSeeOther, "/my/account")
gc.Redirect(http.StatusSeeOther, PAGES.MyAccount)
return
} else if target == UserEmailChange {
app.modifyEmail(id, claims["email"].(string))
@@ -216,7 +214,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
}, gc, true)
app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId"))
gc.Redirect(http.StatusSeeOther, "/my/account")
gc.Redirect(http.StatusSeeOther, PAGES.MyAccount)
return
}
}

View File

@@ -22,6 +22,9 @@ var telegramEnabled = false
var discordEnabled = false
var matrixEnabled = false
// URL subpaths. Ignore the "Current" field.
var PAGES = PagePaths{}
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
val := app.config.Section(sect).Key(key).MustString("")
if strings.HasPrefix(val, "jfa-go:") {
@@ -35,6 +38,13 @@ func (app *appContext) MustSetValue(section, key, val string) {
app.config.Section(section).Key(key).SetValue(app.config.Section(section).Key(key).MustString(val))
}
func (app *appContext) MustSetURLPath(section, key, val string) {
if !strings.HasPrefix(val, "/") {
val = "/" + val
}
app.MustSetValue(section, key, val)
}
func (app *appContext) loadConfig() error {
var err error
app.config, err = ini.ShadowLoad(app.configPath)
@@ -42,6 +52,13 @@ func (app *appContext) loadConfig() error {
return err
}
app.MustSetURLPath("url_paths", "admin", "/")
app.MustSetURLPath("url_paths", "user_page", "/my/account")
app.MustSetURLPath("url_paths", "form", "/invite")
PAGES.Admin = app.config.Section("url_paths").Key("admin").MustString("/")
PAGES.MyAccount = app.config.Section("url_paths").Key("user_page").MustString("/my/account")
PAGES.Form = app.config.Section("url_paths").Key("form").MustString("/invite")
app.MustSetValue("jellyfin", "public_server", app.config.Section("jellyfin").Key("server").String())
app.MustSetValue("ui", "redirect_url", app.config.Section("jellyfin").Key("public_server").String())
@@ -58,12 +75,12 @@ func (app *appContext) loadConfig() error {
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".db"))))
}
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
if app.URLBase == "/invite" || app.URLBase == "/accounts" || app.URLBase == "/settings" || app.URLBase == "/activity" {
app.err.Printf(lm.BadURLBase, app.URLBase)
PAGES.Base = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
if PAGES.Base == "/invite" || PAGES.Base == "/accounts" || PAGES.Base == "/settings" || PAGES.Base == "/activity" {
app.err.Printf(lm.BadURLBase, PAGES.Base)
}
app.ExternalURI = strings.TrimSuffix(strings.TrimSuffix(app.config.Section("ui").Key("jfa_url").MustString(""), "/invite"), "/")
if !strings.HasSuffix(app.ExternalURI, app.URLBase) {
if !strings.HasSuffix(app.ExternalURI, PAGES.Base) {
app.err.Println(lm.NoURLSuffix)
}
if app.ExternalURI == "" {

View File

@@ -232,6 +232,33 @@ sections:
- ["opaque", "Opaque"]
value: clear
description: Appearance of the Admin login screen.
- section: url_paths
meta:
name: URL Paths
description: Settings for changing where different pages are accessed.
advanced: true
settings:
- setting: admin
name: Admin page subpath
type: text
required: true
requires_restart: true
value: "/"
description: URL subpath the admin page should be at.
- setting: user_page
name: "\"My Account\" subpath"
type: text
required: true
requires_restart: true
value: "/my/account"
description: URL subpath the "My Account" page should be at.
- setting: form
name: Invite subpath
type: text
required: true
requires_restart: true
value: "/invite"
description: URL subpath invites should be on.
- section: advanced
meta:
name: Advanced

View File

@@ -329,7 +329,7 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
if code == "" { // Personal email change
inviteLink = fmt.Sprintf("%s/my/confirm/%s", inviteLink, url.PathEscape(key))
} else { // Invite email confirmation
inviteLink = fmt.Sprintf("%s/invite/%s?key=%s", inviteLink, code, url.PathEscape(key))
inviteLink = fmt.Sprintf("%s%s/%s?key=%s", inviteLink, PAGES.Form, code, url.PathEscape(key))
}
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
template["confirmationURL"] = inviteLink
@@ -393,7 +393,7 @@ func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext
expiry := invite.ValidTill
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
message := app.config.Section("messages").Key("message").String()
inviteLink := fmt.Sprintf("%s/invite/%s", app.ExternalURI, code)
inviteLink := fmt.Sprintf("%s%s/%s", app.ExternalURI, PAGES.Form, code)
template := map[string]interface{}{
"hello": emailer.lang.InviteEmail.get("hello"),
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),

View File

@@ -1,9 +1,8 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>404 - jfa-go</title>
{{ template "header.html" . }}
</head>
<body class="section">
<div class="page-container m-2 lg:my-20 lg:mx-64">

View File

@@ -1,16 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
<script>
window.URLBase = "{{ .urlBase }}";
window.notificationsEnabled = {{ .notifications }};
window.emailEnabled = {{ .emailEnabled }};
window.telegramEnabled = {{ .telegramEnabled }};
window.discordEnabled = {{ .discordEnabled }};
window.matrixEnabled = {{ .matrixEnabled }};
window.ombiEnabled = {{ .ombiEnabled }};
window.jellyseerrEnabled = {{ .jellyseerrEnabled }};
window.usernameEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }});
window.linkResetEnabled = {{ .linkResetEnabled }};
@@ -18,7 +9,6 @@
window.jellyfinLogin = {{ .jellyfinLogin }};
window.jfAdminOnly = {{ .jfAdminOnly }};
window.jfAllowAll = {{ .jfAllowAll }};
window.referralsEnabled = {{ .referralsEnabled }};
window.loginAppearance = "{{ .loginAppearance }}";
</script>
<title>Admin - jfa-go</title>
@@ -47,7 +37,7 @@
</div>
<div id="modal-about" class="modal">
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/2 content card">
<img src="{{ .urlBase }}/banner.svg" class="banner header" alt="jfa-go banner">
<img src="{{ .pages.Base }}/banner.svg" class="banner header" alt="jfa-go banner">
<span class="heading"><span class="modal-close">&times;</span></span>
<p>{{ .strings.version }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .version }}</span></p>
<p>{{ .strings.commitNoun }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .commit }}</span></p>
@@ -551,7 +541,7 @@
<span class="button ~critical @low unfocused" id="logout-button">{{ .strings.logout }}</span>
{{ if .userPageEnabled }}
<div class="">
<a class="button ~info" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
<a class="button ~info" href="{{ .pages.Base }}{{ .pages.MyAccount }}"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
</div>
{{ end }}
</div>
@@ -925,6 +915,6 @@
</div>
</div>
</div>
<script src="{{ .urlBase }}/js/admin.js" type="module"></script>
<script src="{{ .pages.Base }}/js/admin.js" type="module"></script>
</body>
</html>

View File

@@ -1,6 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<!--- This CSS is inlined so we should keep this here! -->
<link inline rel="stylesheet" type="text/css" href="web/css/v3bundle.css">
{{ template "header.html" . }}
<title>Crash report</title>

View File

@@ -1,7 +1,6 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>{{ .strings.successHeader }} - jfa-go</title>
</head>

View File

@@ -3,7 +3,6 @@
window.usernameEnabled = {{ .username }};
window.validationStrings = JSON.parse({{ .validationStrings }});
window.invalidPassword = "{{ .strings.reEnterPasswordInvalid }}";
window.URLBase = "{{ .urlBase }}";
window.code = "{{ .code }}";
window.language = "{{ .langName }}";
window.messages = JSON.parse({{ .notifications }});
@@ -14,16 +13,13 @@
window.userExpiryHours = {{ .userExpiryHours }};
window.userExpiryMinutes = {{ .userExpiryMinutes }};
window.userExpiryMessage = {{ .userExpiryMessage }};
window.telegramEnabled = {{ .telegramEnabled }};
window.telegramRequired = {{ .telegramRequired }};
window.telegramPIN = "{{ .telegramPIN }}";
window.emailRequired = {{ .emailRequired }};
window.discordEnabled = {{ .discordEnabled }};
window.discordRequired = {{ .discordRequired }};
window.discordPIN = "{{ .discordPIN }}";
window.discordInviteLink = {{ .discordInviteLink }};
window.discordServerName = "{{ .discordServerName }}";
window.matrixEnabled = {{ .matrixEnabled }};
window.matrixRequired = {{ .matrixRequired }};
window.matrixUserID = "{{ .matrixUser }}";
window.captcha = {{ .captcha }};

View File

@@ -1,7 +1,6 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
{{ if .passwordReset }}
<title>{{ .strings.passwordReset }}</title>

View File

@@ -1,13 +1,32 @@
<link rel="stylesheet" type="text/css" href="{{ .pages.Base }}/css/{{ .cssVersion }}bundle.css">
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="Description" content="jfa-go, a better way to manage Jellyfin users.">
<meta name="color-scheme" content="dark light">
<meta name="robots" content="noindex">
<link rel="apple-touch-icon" sizes="180x180" href="{{ .urlBase }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ .urlBase }}/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .urlBase }}/favicon-16x16.png">
<link rel="manifest" href="{{ .urlBase }}/site.webmanifest">
<link rel="mask-icon" href="{{ .urlBase }}/safari-pinned-tab.svg" color="#5bbad5">
<link rel="apple-touch-icon" sizes="180x180" href="{{ .pages.Base }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ .pages.Base }}/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .pages.Base }}/favicon-16x16.png">
<link rel="manifest" href="{{ .pages.Base }}/site.webmanifest">
<link rel="mask-icon" href="{{ .pages.Base }}/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#603cba">
<meta name="theme-color" content="#ffffff">
<script>
window.pages = {
"Base": "{{ .pages.Base }}",
"Current": "{{ .pages.Current }}",
"Admin": "{{ .pages.Admin }}",
"MyAccount": "{{ .pages.MyAccount }}",
"Form": "{{ .pages.Form }}"
};
window.emailEnabled = {{ .emailEnabled }};
window.discordEnabled = {{ .discordEnabled }};
window.telegramEnabled = {{ .telegramEnabled }};
window.matrixEnabled = {{ .matrixEnabled }};
window.notificationsEnabled = {{ .notifications }};
window.ombiEnabled = {{ .ombiEnabled }};
window.jellyseerrEnabled = {{ .jellyseerrEnabled }};
window.referralsEnabled = {{ .referralsEnabled }};
window.pwrEnabled = {{ .pwrEnabled }};
</script>

View File

@@ -1,7 +1,6 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>Invalid Code - jfa-go</title>
</head>

View File

@@ -15,7 +15,7 @@
{{ $hasTwoCards = 1 }}
<div class="card mx-2 flex-initial w-full lg:w-[35%] mb-4 lg:mb-0 dark:~d_neutral @low content">
<span class="heading row">{{ .strings.loginNotAdmin }}</span>
<a class="button ~info h-12 w-full" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
<a class="button ~info h-12 w-full" href="{{ .pages.Base }}{{ .pages.MyAccount }}"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
</div>
{{ end }}
{{ end }}

View File

@@ -1,7 +1,6 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>{{ .strings.passwordReset }} - jfa-go</title>
</head>
@@ -40,6 +39,6 @@
</div>
<i class="content">{{ .contactMessage }}</i>
</div>
<script src="{{ .urlBase }}/js/pwr-pin.js" type="module"></script>
<script src="{{ .pages.Base }}/js/pwr-pin.js" type="module"></script>
</body>
</html>

View File

@@ -1,7 +1,6 @@
<!DOCTYPE html>
<html lang="en" class="light">
<head>
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>{{ .lang.Strings.pageTitle }}</title>
</head>

View File

@@ -1,31 +1,21 @@
<!DOCTYPE html>
<html lang="en" class="light">
<head>
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
<script>
window.URLBase = "{{ .urlBase }}";
window.notificationsEnabled = {{ .notifications }};
window.ombiEnabled = {{ .ombiEnabled }};
window.langFile = JSON.parse({{ .language }});
window.pwrEnabled = {{ .pwrEnabled }};
window.linkResetEnabled = {{ .linkResetEnabled }};
window.language = "{{ .langName }}";
window.telegramEnabled = {{ .telegramEnabled }};
window.telegramRequired = {{ .telegramRequired }};
window.telegramUsername = {{ .telegramUsername }};
window.telegramURL = {{ .telegramURL }};
window.emailEnabled = {{ .emailEnabled }};
window.emailRequired = {{ .emailRequired }};
window.discordEnabled = {{ .discordEnabled }};
window.discordRequired = {{ .discordRequired }};
window.discordServerName = "{{ .discordServerName }}";
window.discordInviteLink = {{ .discordInviteLink }};
window.discordSendPINMessage = "{{ .discordSendPINMessage }}";
window.matrixEnabled = {{ .matrixEnabled }};
window.matrixRequired = {{ .matrixRequired }};
window.matrixUserID = "{{ .matrixUser }}";
window.validationStrings = JSON.parse({{ .validationStrings }});
window.referralsEnabled = {{ .referralsEnabled }};
</script>
{{ template "header.html" . }}
<title>{{ .strings.myAccount }}</title>
@@ -156,7 +146,7 @@
{{ end }}
</div>
</div>
<script src="{{ .urlBase }}/js/user.js" type="module"></script>
<script src="{{ .pages.Base }}/js/user.js" type="module"></script>
</body>
</html>

64
main.go
View File

@@ -103,37 +103,37 @@ type appContext struct {
adminUsers []User
invalidTokens []string
// Keeping jf name because I can't think of a better one
jf *mediabrowser.MediaBrowser
authJf *mediabrowser.MediaBrowser
ombi *OmbiWrapper
js *JellyseerrWrapper
thirdPartyServices []ThirdPartyService
datePattern string
timePattern string
storage Storage
validator Validator
email *Emailer
telegram *TelegramDaemon
discord *DiscordDaemon
matrix *MatrixDaemon
contactMethods []ContactMethodLinker
info, debug, err *logger.Logger
host string
port int
version string
URLBase, ExternalURI, ExternalDomain string
updater *Updater
webhooks *WebhookSender
newUpdate bool // Whether whatever's in update is new.
tag Tag
update Update
proxyEnabled bool
proxyTransport *http.Transport
proxyConfig easyproxy.ProxyConfig
internalPWRs map[string]InternalPWR
pwrCaptchas map[string]Captcha
ConfirmationKeys map[string]map[string]ConfirmationKey // Map of invite code to jwt to request
confirmationKeysLock sync.Mutex
jf *mediabrowser.MediaBrowser
authJf *mediabrowser.MediaBrowser
ombi *OmbiWrapper
js *JellyseerrWrapper
thirdPartyServices []ThirdPartyService
datePattern string
timePattern string
storage Storage
validator Validator
email *Emailer
telegram *TelegramDaemon
discord *DiscordDaemon
matrix *MatrixDaemon
contactMethods []ContactMethodLinker
info, debug, err *logger.Logger
host string
port int
version string
ExternalURI, ExternalDomain string
updater *Updater
webhooks *WebhookSender
newUpdate bool // Whether whatever's in update is new.
tag Tag
update Update
proxyEnabled bool
proxyTransport *http.Transport
proxyConfig easyproxy.ProxyConfig
internalPWRs map[string]InternalPWR
pwrCaptchas map[string]Captcha
ConfirmationKeys map[string]map[string]ConfirmationKey // Map of invite code to jwt to request
confirmationKeysLock sync.Mutex
}
func generateSecret(length int) (string, error) {
@@ -147,7 +147,7 @@ func generateSecret(length int) (string, error) {
func test(app *appContext) {
fmt.Printf("\n\n----\n\n")
settings := map[string]interface{}{
settings := map[string]any{
"server": app.jf.Server,
"server version": app.jf.ServerInfo.Version,
"server name": app.jf.ServerInfo.Name,

View File

@@ -467,3 +467,18 @@ type ContactMethodKey struct {
PIN string
User ContactMethodUser
}
type PagePaths struct {
// The base subfolder the app is hosted on.
Base string `json:"Base"`
// Those for other pages
Admin string `json:"Admin"`
MyAccount string `json:"MyAccount"`
Form string `json:"Form"`
}
type PagePathsDTO struct {
PagePaths
// The subdirectory this bit of the app is hosted on (e.g. admin is usually on "/", myacc is usually on "/my/account")
Current string `json:"Current"`
}

View File

@@ -108,8 +108,8 @@ func (app *appContext) loadRouter(address string, debug bool) *gin.Engine {
}
func (app *appContext) loadRoutes(router *gin.Engine) {
routePrefixes := []string{app.URLBase}
if app.URLBase != "" {
routePrefixes := []string{PAGES.Base}
if PAGES.Base != "" {
routePrefixes = append(routePrefixes, "")
}
@@ -118,7 +118,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
for _, p := range routePrefixes {
router.GET(p+"/lang/:page", app.GetLanguages)
router.Use(static.Serve(p+"/", app.webFS))
router.GET(p+"/", app.AdminPage)
router.GET(p+PAGES.Admin, app.AdminPage)
if app.config.Section("password_resets").Key("link_reset").MustBool(false) {
router.GET(p+"/reset", app.ResetPassword)
@@ -127,39 +127,39 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
}
}
router.GET(p+"/accounts", app.AdminPage)
router.GET(p+"/settings", app.AdminPage)
router.GET(p+"/activity", app.AdminPage)
router.GET(p+"/accounts/user/:userID", app.AdminPage)
router.GET(p+"/invites/:code", app.AdminPage)
router.GET(p+PAGES.Admin+"/accounts", app.AdminPage)
router.GET(p+PAGES.Admin+"/settings", app.AdminPage)
router.GET(p+PAGES.Admin+"/activity", app.AdminPage)
router.GET(p+PAGES.Admin+"/accounts/user/:userID", app.AdminPage)
router.GET(p+PAGES.Admin+"/invites/:code", app.AdminPage)
router.GET(p+"/lang/:page/:file", app.ServeLang)
router.GET(p+"/token/login", app.getTokenLogin)
router.GET(p+"/token/refresh", app.getTokenRefresh)
router.POST(p+"/user/invite", app.NewUserFromInvite)
router.Use(static.Serve(p+"/invite/", app.webFS))
router.GET(p+"/invite/:invCode", app.InviteProxy)
router.Use(static.Serve(p+PAGES.Form, app.webFS))
router.GET(p+PAGES.Form+"/:invCode", app.InviteProxy)
if app.config.Section("captcha").Key("enabled").MustBool(false) {
router.GET(p+"/captcha/gen/:invCode", app.GenCaptcha)
router.GET(p+"/captcha/img/:invCode/:captchaID", app.GetCaptcha)
router.POST(p+"/captcha/verify/:invCode/:captchaID/:text", app.VerifyCaptcha)
}
if telegramEnabled {
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
router.GET(p+PAGES.Form+"/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
}
if discordEnabled {
router.GET(p+"/invite/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite)
router.GET(p+PAGES.Form+"/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite)
if app.config.Section("discord").Key("provide_invite").MustBool(false) {
router.GET(p+"/invite/:invCode/discord/invite", app.DiscordServerInvite)
router.GET(p+PAGES.Form+"/:invCode/discord/invite", app.DiscordServerInvite)
}
}
if matrixEnabled {
router.GET(p+"/invite/:invCode/matrix/verified/:userID/:pin", app.MatrixCheckPIN)
router.POST(p+"/invite/:invCode/matrix/user", app.MatrixSendPIN)
router.GET(p+PAGES.Form+"/:invCode/matrix/verified/:userID/:pin", app.MatrixCheckPIN)
router.POST(p+PAGES.Form+"/:invCode/matrix/user", app.MatrixSendPIN)
router.POST(p+"/users/matrix", app.MatrixConnect)
}
if userPageEnabled {
router.GET(p+"/my/account", app.MyUserPage)
router.GET(p+"/my/account/password/reset", app.MyUserPage)
router.GET(p+PAGES.MyAccount, app.MyUserPage)
router.GET(p+PAGES.MyAccount+"/password/reset", app.MyUserPage)
router.GET(p+"/my/token/login", app.getUserTokenLogin)
router.GET(p+"/my/token/refresh", app.getUserTokenRefresh)
router.GET(p+"/my/confirm/:jwt", app.ConfirmMyAction)

View File

@@ -39,8 +39,10 @@ func (app *appContext) ServeSetup(gc *gin.Context) {
respond(500, "Failed to fetch default values", gc)
return
}
pages := PagePathsDTO{PagePaths: PAGES}
gc.HTML(200, "setup.html", gin.H{
"cssVersion": cssVersion,
"pages": pages,
"lang": app.storage.lang.Setup[lang],
"strings": app.storage.lang.Setup[lang].Strings,
"emailLang": app.storage.lang.Email[emailLang],

View File

@@ -11,6 +11,8 @@ import { _get, _post, notificationBox, whichAnimationEvent, bindManualDropdowns
import { Updater } from "./modules/update.js";
import { Login } from "./modules/login.js";
declare var window: GlobalWindow;
const theme = new ThemeManager(document.getElementById("button-theme"));
window.lang = new lang(window.langFile as LangFile);
@@ -165,12 +167,12 @@ const defaultTab = tabs[0];
window.tabs = new Tabs();
for (let tab of tabs) {
window.tabs.addTab(tab.id, tab.url, null, tab.reloader);
window.tabs.addTab(tab.id, window.pages.Admin + "/" + tab.url, null, tab.reloader);
}
let matchedTab = false
for (let tab of tabs) {
if (window.location.pathname.startsWith(window.URLBase + "/" + tab.url)) {
for (const tab of tabs) {
if (window.location.pathname.startsWith(window.pages.Base + window.pages.Current + "/" + tab.url)) {
window.tabs.switch(tab.url, true);
matchedTab = true;
}

View File

@@ -6,7 +6,7 @@ import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
import { Captcha, GreCAPTCHA } from "./modules/captcha.js";
interface formWindow extends Window {
interface formWindow extends GlobalWindow {
invalidPassword: string;
successModal: Modal;
telegramModal: Modal;
@@ -59,7 +59,7 @@ if (window.telegramEnabled) {
modal: window.telegramModal as Modal,
pin: window.telegramPIN,
pinURL: "",
verifiedURL: "/invite/" + window.code + "/telegram/verified/",
verifiedURL: window.pages.Form + "/" + window.code + "/telegram/verified/",
invalidCodeError: window.messages["errorInvalidPIN"],
accountLinkedError: window.messages["errorAccountLinked"],
successError: window.messages["verified"],
@@ -89,9 +89,9 @@ if (window.discordEnabled) {
const discordConf: ServiceConfiguration = {
modal: window.discordModal as Modal,
pin: window.discordPIN,
inviteURL: window.discordInviteLink ? ("/invite/" + window.code + "/discord/invite") : "",
inviteURL: window.discordInviteLink ? (window.pages.Form + "/" + window.code + "/discord/invite") : "",
pinURL: "",
verifiedURL: "/invite/" + window.code + "/discord/verified/",
verifiedURL: window.pages.Form + "/" + window.code + "/discord/verified/",
invalidCodeError: window.messages["errorInvalidPIN"],
accountLinkedError: window.messages["errorAccountLinked"],
successError: window.messages["verified"],
@@ -121,8 +121,8 @@ if (window.matrixEnabled) {
const matrixConf: MatrixConfiguration = {
modal: window.matrixModal as Modal,
sendMessageURL: "/invite/" + window.code + "/matrix/user",
verifiedURL: "/invite/" + window.code + "/matrix/verified/",
sendMessageURL: window.pages.Form + "/" + window.code + "/matrix/user",
verifiedURL: window.pages.Form + "/" + window.code + "/matrix/verified/",
invalidCodeError: window.messages["errorInvalidPIN"],
accountLinkedError: window.messages["errorAccountLinked"],
unknownError: window.messages["errorUnknown"],

View File

@@ -1,7 +1,7 @@
import { Modal } from "../modules/modal.js";
import { _get, _post, toggleLoader, addLoader, removeLoader } from "../modules/common.js";
interface formWindow extends Window {
interface formWindow extends GlobalWindow {
invalidPassword: string;
successModal: Modal;
telegramModal: Modal;

View File

@@ -6,6 +6,8 @@ import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modules/search.js";
import { HiddenInputField } from "./ui.js";
declare var window: GlobalWindow;
const dateParser = require("any-date-parser");
interface User {

View File

@@ -3,6 +3,8 @@ import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modul
import { accountURLEvent } from "../modules/accounts.js";
import { inviteURLEvent } from "../modules/invites.js";
declare var window: GlobalWindow;
export interface activity {
id: string;
type: string;
@@ -52,8 +54,8 @@ export class Activity implements activity, SearchableItem {
link = link.split(split)[0];
}
if (link.slice(-1) != "/") { link += "/"; }
// FIXME: I should probably just be using window.URLBase, but incase thats not right, i'll put this warning here
if (link != window.URLBase) console.error(`URL Bases don't match: "${link}" != "${window.URLBase}"`);
// FIXME: I should probably just be using window.pages.Base, but incase thats not right, i'll put this warning here
if (link != window.pages.Base) console.error(`URL Bases don't match: "${link}" != "${window.pages.Base}"`);
return link;
})(); */
@@ -66,17 +68,17 @@ export class Activity implements activity, SearchableItem {
}
_genUserLink = (): string => {
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" href="${window.URLBase}/accounts?user=${this._act.user_id}">${this._genUserText()}</a>`;
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" href="${window.pages.Base}${window.pages.Admin}/accounts?user=${this._act.user_id}">${this._genUserText()}</a>`;
}
_genSrcUserLink = (): string => {
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" href="${window.URLBase}/accounts?user=${this._act.source}">${this._genSrcUserText()}</a>`;
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" href="${window.pages.Base}${window.pages.Admin}/accounts?user=${this._act.source}">${this._genSrcUserText()}</a>`;
}
private _renderInvText = (): string => { return `<span class="font-medium font-mono">${this.value || this.invite_code || "???"}</span>`; }
private _genInvLink = (): string => {
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-invite" data-id="${this.invite_code}" href="${window.URLBase}/?invite=${this.invite_code}">${this._renderInvText()}</a>`;
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-invite" data-id="${this.invite_code}" href="${window.pages.Base}${window.pages.Admin}/?invite=${this.invite_code}">${this._renderInvText()}</a>`;
}

View File

@@ -1,4 +1,4 @@
declare var window: Window;
declare var window: GlobalWindow;
export function toDateString(date: Date): string {
const locale = window.language || (window as any).navigator.userLanguage || window.navigator.language;
@@ -23,7 +23,7 @@ export function toDateString(date: Date): string {
export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => {
let req = new XMLHttpRequest();
if (window.URLBase) { url = window.URLBase + url; }
if (window.pages) { url = window.pages.Base + url; }
req.open("GET", url, true);
req.responseType = 'json';
req.setRequestHeader("Authorization", "Bearer " + window.token);
@@ -42,7 +42,7 @@ export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHtt
export const _download = (url: string, fname: string): void => {
let req = new XMLHttpRequest();
if (window.URLBase) { url = window.URLBase + url; }
if (window.pages) { url = window.pages.Base + url; }
req.open("GET", url, true);
req.responseType = 'blob';
req.setRequestHeader("Authorization", "Bearer " + window.token);
@@ -58,7 +58,7 @@ export const _download = (url: string, fname: string): void => {
export const _upload = (url: string, formData: FormData): void => {
let req = new XMLHttpRequest();
if (window.URLBase) { url = window.URLBase + url; }
if (window.pages) { url = window.pages.Base + url; }
req.open("POST", url, true);
req.setRequestHeader("Authorization", "Bearer " + window.token);
// req.setRequestHeader('Content-Type', 'multipart/form-data');
@@ -67,7 +67,8 @@ export const _upload = (url: string, formData: FormData): void => {
export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => {
let req = new XMLHttpRequest();
req.open("POST", window.URLBase + url, true);
if (window.pages) { url = window.pages.Base + url; }
req.open("POST", url, true);
if (response) {
req.responseType = 'json';
}
@@ -88,7 +89,8 @@ export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHt
export function _delete(url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void {
let req = new XMLHttpRequest();
req.open("DELETE", window.URLBase + url, true);
if (window.pages) { url = window.pages.Base + url; }
req.open("DELETE", url, true);
req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = () => {

View File

@@ -1,5 +1,7 @@
import {addLoader, removeLoader, _get} from "../modules/common.js";
declare var window: GlobalWindow;
export interface DiscordUser {
name: string;
avatar_url: string;

View File

@@ -2,6 +2,8 @@ import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from ".
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
import { reloadProfileNames } from "../modules/profiles.js";
declare var window: GlobalWindow;
class DOMInvite implements Invite {
updateNotify = (checkbox: HTMLInputElement) => {
let state: { [code: string]: { [type: string]: boolean } } = {};
@@ -66,7 +68,7 @@ class DOMInvite implements Invite {
codeLink = codeLink.split(split)[0];
}
if (codeLink.slice(-1) != "/") { codeLink += "/"; }
this._codeLink = codeLink + "invite/" + code;
this._codeLink = codeLink + window.pages.Form + "/" + code;
const linkEl = this._codeArea.querySelector("a") as HTMLAnchorElement;
if (this.label == "") {
linkEl.textContent = code.replace(/-/g, '-');

View File

@@ -1,6 +1,8 @@
import { Modal } from "../modules/modal.js";
import { toggleLoader, _post, unicodeB64Encode } from "../modules/common.js";
declare var window: GlobalWindow;
export class Login {
loggedIn: boolean = false;
private _modal: Modal;
@@ -14,7 +16,7 @@ export class Login {
constructor(modal: Modal, endpoint: string, appearance: string) {
this._endpoint = endpoint;
this._url = window.URLBase + endpoint;
this._url = window.pages.Base + endpoint;
if (this._url[this._url.length-1] != '/') this._url += "/";
this._modal = modal;

View File

@@ -1,4 +1,4 @@
declare var window: Window;
declare var window: GlobalWindow;
export class Modal implements Modal {
modal: HTMLElement;

View File

@@ -73,6 +73,7 @@ export class PageManager {
}
loadPage (p: Page) {
console.log("loading page with", p.name || this.defaultName, p.title, p.url + window.location.search);
window.history.pushState(p.name || this.defaultName, p.title, p.url + window.location.search);
}

View File

@@ -1,5 +1,7 @@
import { _get, _post, _delete, toggleLoader } from "../modules/common.js";
declare var window: GlobalWindow;
export const profileLoadEvent = new CustomEvent("profileLoadEvent");
export const reloadProfileNames = (then?: () => void) => _get("/profiles/names", null, (req: XMLHttpRequest) => {
if (req.readyState != 4) return;

View File

@@ -1,5 +1,7 @@
const dateParser = require("any-date-parser");
declare var window: GlobalWindow;
export interface QueryType {
name: string;
description?: string;

View File

@@ -2,6 +2,8 @@ import { _get, _post, _delete, _download, _upload, toggleLoader, addLoader, remo
import { Marked } from "@ts-stack/markdown";
import { stripMarkdown } from "../modules/stripmd.js";
declare var window: GlobalWindow;
const toBool = (s: string): boolean => {
let b = Boolean(s);
if (s == "false") b = false;

View File

@@ -1,5 +1,7 @@
import { PageManager, Page } from "../modules/pages.js";
declare var window: GlobalWindow;
export interface Tab {
page: Page;
tabEl: HTMLDivElement;
@@ -38,7 +40,7 @@ export class Tabs implements Tabs {
tab.page = {
name: tabID,
title: document.title, /*FIXME: Get actual names from translations*/
url: window.URLBase + "/" + url,
url: url,
show: () => {
tab.buttonEl.classList.add("active", "~urge");
tab.tabEl.classList.remove("unfocused");

View File

@@ -1,6 +1,8 @@
import { _get, _post, toggleLoader, toDateString } from "../modules/common.js";
import { Marked, Renderer } from "@ts-stack/markdown";
declare var window: GlobalWindow;
interface updateDTO {
new: boolean;
update: Update;

View File

@@ -1,5 +1,7 @@
import { toClipboard, notificationBox } from "./modules/common.js";
declare var window: GlobalWindow;
const pin = document.getElementById("pin") as HTMLSpanElement;
if (pin) {

View File

@@ -3,12 +3,11 @@ import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { ThemeManager } from "./modules/theme.js";
import { PageManager } from "./modules/pages.js";
interface sWindow extends Window {
interface sWindow extends GlobalWindow {
messages: {};
}
declare var window: sWindow;
window.URLBase = "";
const theme = new ThemeManager(document.getElementById("button-theme"));

View File

@@ -12,8 +12,19 @@ interface ArrayConstructor {
from(arrayLike: any, mapFn?, thisArg?): Array<any>;
}
declare interface Window {
URLBase: string;
declare interface PagePaths {
// The base subfolder the app is hosted on.
Base: string;
// The subdirectory this bit of the app is hosted on (e.g. admin is usually on "/", myacc is usually on "/my/account")
Current: string;
// Those for other pages
Admin: string;
MyAccount: string;
Form: string;
}
declare interface GlobalWindow extends Window {
pages: PagePaths;
modals: Modals;
cssFile: string;
availableProfiles: string[];
@@ -25,6 +36,7 @@ declare interface Window {
matrixEnabled: boolean;
ombiEnabled: boolean;
jellyseerrEnabled: boolean;
pwrEnabled: boolean;
usernameEnabled: boolean;
linkResetEnabled: boolean;
token: string;

View File

@@ -7,7 +7,7 @@ import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration }
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
import { PageManager } from "./modules/pages.js";
interface userWindow extends Window {
interface userWindow extends GlobalWindow {
jellyfinID: string;
username: string;
emailRequired: boolean;
@@ -18,14 +18,13 @@ interface userWindow extends Window {
discordInviteLink: boolean;
matrixUserID: string;
discordSendPINMessage: string;
pwrEnabled: string;
referralsEnabled: boolean;
}
const basePath = window.location.pathname.replace("/password/reset", "");
declare var window: userWindow;
const basePath = window.location.pathname.replace("/password/reset", "");
const theme = new ThemeManager(document.getElementById("button-theme"));
window.lang = new lang(window.langFile as LangFile);
@@ -38,7 +37,7 @@ window.token = "";
window.modals = {} as Modals;
let pages = new PageManager({
const pages = new PageManager({
hideOthersOnPageShow: true,
defaultName: "",
defaultTitle: document.title,
@@ -311,7 +310,7 @@ class ReferralCard {
path = path.split(split)[0];
}
if (path.slice(-1) != "/") { path += "/"; }
path = path + "invite/" + this._code;
path = path + window.pages.Form + "/" + this._code;
u.pathname = path;
u.hash = "";
@@ -661,7 +660,7 @@ document.addEventListener("details-reload", () => {
expiryCard.expiry = details.expiry;
const adminBackButton = document.getElementById("admin-back-button") as HTMLAnchorElement;
adminBackButton.href = window.location.href.replace("my/account", "");
adminBackButton.href = window.location.href.replace(window.pages.MyAccount, window.pages.Admin);
let messageCard = document.getElementById("card-message");
if (details.accounts_admin) {

View File

@@ -67,7 +67,8 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
// host := gc.Request.URL.Hostname()
host := app.ExternalDomain
uri := "/my"
if strings.HasPrefix(gc.Request.RequestURI, app.URLBase) {
// FIXME: This seems like a bad idea? I think it's to deal with people having Reverse proxy subfolder/URL base set to /accounts.
if strings.HasPrefix(gc.Request.RequestURI, PAGES.Base) {
uri = "/accounts/my"
}
gc.SetCookie("user-refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, uri, host, true, true)

227
views.go
View File

@@ -32,7 +32,7 @@ func (app *appContext) loadCSSHeader() string {
l := len(css)
h := ""
for i, f := range css {
h += "<" + app.URLBase + "/css/" + f + ">; rel=preload; as=style"
h += "<" + PAGES.Base + "/css/" + f + ">; rel=preload; as=style"
if l > 1 && i != (l-1) {
h += ", "
}
@@ -41,18 +41,19 @@ func (app *appContext) loadCSSHeader() string {
}
func (app *appContext) getURLBase(gc *gin.Context) string {
if strings.HasPrefix(gc.Request.URL.String(), app.URLBase) {
if strings.HasPrefix(gc.Request.URL.String(), PAGES.Base) {
// Hack to fix the common URL base /accounts
if app.URLBase == "/accounts" && strings.HasPrefix(gc.Request.URL.String(), "/accounts/user/") {
if PAGES.Base == "/accounts" && strings.HasPrefix(gc.Request.URL.String(), "/accounts/user/") {
return ""
}
return app.URLBase
return PAGES.Base
}
return ""
}
func gcHTML(gc *gin.Context, code int, file string, templ gin.H) {
func (app *appContext) gcHTML(gc *gin.Context, code int, file string, page Page, templ gin.H) {
gc.Header("Cache-Control", "no-cache")
app.BasePageTemplateValues(gc, page, templ)
gc.HTML(code, file, templ)
}
@@ -61,16 +62,14 @@ func (app *appContext) pushResources(gc *gin.Context, page Page) {
switch page {
case AdminPage:
toPush = []string{"/js/admin.js", "/js/theme.js", "/js/lang.js", "/js/modal.js", "/js/tabs.js", "/js/invites.js", "/js/accounts.js", "/js/settings.js", "/js/profiles.js", "/js/common.js"}
break
case UserPage:
toPush = []string{"/js/user.js", "/js/theme.js", "/js/lang.js", "/js/modal.js", "/js/common.js"}
break
default:
toPush = []string{}
}
if pusher := gc.Writer.Pusher(); pusher != nil {
for _, f := range toPush {
if err := pusher.Push(app.URLBase+f, nil); err != nil {
if err := pusher.Push(PAGES.Base+f, nil); err != nil {
app.debug.Printf(lm.FailedServerPush, err)
}
}
@@ -78,6 +77,48 @@ func (app *appContext) pushResources(gc *gin.Context, page Page) {
gc.Header("Link", cssHeader)
}
// Returns a gin.H with general values (url base, css version, etc.)
func (app *appContext) BasePageTemplateValues(gc *gin.Context, page Page, base gin.H) {
set := func(k string, v any) {
if _, ok := base[k]; !ok {
base[k] = v
}
}
pages := PagePathsDTO{
PagePaths: PAGES,
}
pages.Base = app.getURLBase(gc)
switch page {
case AdminPage:
pages.Current = PAGES.Admin
case FormPage:
pages.Current = PAGES.Form
case UserPage:
pages.Current = PAGES.MyAccount
default:
pages.Current = "/"
}
set("pages", pages)
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
jellyseerrEnabled := app.config.Section("jellyseerr").Key("enabled").MustBool(false)
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
set("notifications", notificationsEnabled)
set("cssClass", app.cssClass)
set("cssVersion", cssVersion)
set("emailEnabled", emailEnabled)
set("telegramEnabled", telegramEnabled)
set("discordEnabled", discordEnabled)
set("matrixEnabled", matrixEnabled)
set("ombiEnabled", ombiEnabled)
set("jellyseerrEnabled", jellyseerrEnabled)
// QUIRK: The login modal html template uses this' existence to check if the modal is for the admin or user page.
if page != AdminPage {
set("pwrEnabled", app.config.Section("password_resets").Key("enabled").MustBool(false))
}
set("referralsEnabled", app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false))
}
type Page int
const (
@@ -132,10 +173,6 @@ func (app *appContext) getLang(gc *gin.Context, page Page, chosen string) string
func (app *appContext) AdminPage(gc *gin.Context) {
app.pushResources(gc, AdminPage)
lang := app.getLang(gc, AdminPage, app.storage.lang.chosenAdminLang)
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
jellyseerrEnabled := app.config.Section("jellyseerr").Key("enabled").MustBool(false)
jfAdminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
jfAllowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
var license string
@@ -157,62 +194,36 @@ func (app *appContext) AdminPage(gc *gin.Context) {
builtBy = "???"
}
gcHTML(gc, http.StatusOK, "admin.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": "",
"emailEnabled": emailEnabled,
"telegramEnabled": telegramEnabled,
"discordEnabled": discordEnabled,
"matrixEnabled": matrixEnabled,
"ombiEnabled": ombiEnabled,
"jellyseerrEnabled": jellyseerrEnabled,
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
"notifications": notificationsEnabled,
"version": version,
"commit": commit,
"buildTime": buildTime,
"builtBy": builtBy,
"buildTags": buildTags,
"username": !app.config.Section("email").Key("no_username").MustBool(false),
"strings": app.storage.lang.Admin[lang].Strings,
"quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
"language": app.storage.lang.Admin[lang].JSON,
"langName": lang,
"license": license,
"jellyfinLogin": app.jellyfinLogin,
"jfAdminOnly": jfAdminOnly,
"jfAllowAll": jfAllowAll,
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
"showUserPageLink": app.config.Section("user_page").Key("show_link").MustBool(true),
"referralsEnabled": app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false),
"loginAppearance": app.config.Section("ui").Key("login_appearance").MustString("clear"),
app.gcHTML(gc, http.StatusOK, "admin.html", AdminPage, gin.H{
"contactMessage": "",
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
"version": version,
"commit": commit,
"buildTime": buildTime,
"builtBy": builtBy,
"buildTags": buildTags,
"username": !app.config.Section("email").Key("no_username").MustBool(false),
"strings": app.storage.lang.Admin[lang].Strings,
"quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
"language": app.storage.lang.Admin[lang].JSON,
"langName": lang,
"license": license,
"jellyfinLogin": app.jellyfinLogin,
"jfAdminOnly": jfAdminOnly,
"jfAllowAll": jfAllowAll,
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
"showUserPageLink": app.config.Section("user_page").Key("show_link").MustBool(true),
"loginAppearance": app.config.Section("ui").Key("login_appearance").MustString("clear"),
})
}
func (app *appContext) MyUserPage(gc *gin.Context) {
app.pushResources(gc, UserPage)
lang := app.getLang(gc, UserPage, app.storage.lang.chosenUserLang)
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
jellyseerrEnabled := app.config.Section("jellyseerr").Key("enabled").MustBool(false)
data := gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"emailEnabled": emailEnabled,
"emailRequired": app.config.Section("email").Key("required").MustBool(false),
"telegramEnabled": telegramEnabled,
"discordEnabled": discordEnabled,
"matrixEnabled": matrixEnabled,
"ombiEnabled": ombiEnabled,
"jellyseerrEnabled": jellyseerrEnabled,
"pwrEnabled": app.config.Section("password_resets").Key("enabled").MustBool(false),
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
"notifications": notificationsEnabled,
"username": !app.config.Section("email").Key("no_username").MustBool(false),
"strings": app.storage.lang.User[lang].Strings,
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
@@ -220,7 +231,6 @@ 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
@@ -264,7 +274,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
data[name+"MessageContent"] = template.HTML(markdown.ToHTML([]byte(msg.Content), nil, markdownRenderer))
}
gcHTML(gc, http.StatusOK, "user.html", data)
app.gcHTML(gc, http.StatusOK, "user.html", UserPage, data)
}
func (app *appContext) ResetPassword(gc *gin.Context) {
@@ -278,14 +288,9 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
app.pushResources(gc, PWRPage)
lang := app.getLang(gc, PWRPage, app.storage.lang.chosenPWRLang)
data := gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"strings": app.storage.lang.PasswordReset[lang].Strings,
"success": false,
"ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false),
"jellyseerrEnabled": app.config.Section("jellyseerr").Key("enabled").MustBool(false),
"customSuccessCard": false,
}
pwr, isInternal := app.internalPWRs[pin]
@@ -299,6 +304,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
data["requirements"] = app.validator.getCriteria()
data["strings"] = app.storage.lang.PasswordReset[lang].Strings
data["validationStrings"] = app.storage.lang.User[lang].validationStringsJSON
// ewwwww, reusing an existing field, FIXME!
data["notifications"] = app.storage.lang.User[lang].notificationsJSON
data["langName"] = lang
data["passwordReset"] = true
@@ -309,10 +315,10 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
data["reCAPTCHA"] = app.config.Section("captcha").Key("recaptcha").MustBool(false)
data["reCAPTCHASiteKey"] = app.config.Section("captcha").Key("recaptcha_site_key").MustString("")
data["pwrPIN"] = pin
gcHTML(gc, http.StatusOK, "form-loader.html", data)
app.gcHTML(gc, http.StatusOK, "form-loader.html", PWRPage, data)
return
}
defer gcHTML(gc, http.StatusOK, "password-reset.html", data)
defer app.gcHTML(gc, http.StatusOK, "password-reset.html", PWRPage, data)
// If it's a bot, pretend to be a success so the preview is nice.
if isBot {
app.debug.Println(lm.IgnoreBotPWR)
@@ -413,10 +419,7 @@ func (app *appContext) GetCaptcha(gc *gin.Context) {
if !isPWR {
inv, ok = app.storage.GetInvitesKey(code)
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
app.gcHTML(gc, 404, "invalidCode.html", OtherPage, gin.H{
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
@@ -453,10 +456,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
}
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
app.gcHTML(gc, 404, "invalidCode.html", OtherPage, gin.H{
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
@@ -587,10 +587,7 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
if !isPWR {
inv, ok = app.storage.GetInvitesKey(code)
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
app.gcHTML(gc, 404, "invalidCode.html", OtherPage, gin.H{
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
return
@@ -617,10 +614,7 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
func (app *appContext) NewUserFromConfirmationKey(invite Invite, key string, lang string, gc *gin.Context) {
fail := func() {
gcHTML(gc, 404, "404.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
app.gcHTML(gc, 404, "404.html", OtherPage, gin.H{
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
@@ -691,10 +685,7 @@ func (app *appContext) NewUserFromConfirmationKey(invite Invite, key string, lan
if app.config.Section("ui").Key("auto_redirect").MustBool(false) {
gc.Redirect(301, jfLink)
} else {
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
app.gcHTML(gc, http.StatusOK, "create-success.html", OtherPage, gin.H{
"strings": app.storage.lang.User[lang].Strings,
"successMessage": app.config.Section("ui").Key("success_message").String(),
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
@@ -725,10 +716,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
// if app.checkInvite(code, false, "") {
invite, ok := app.storage.GetInvitesKey(gc.Param("invCode"))
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
app.gcHTML(gc, 404, "invalidCode.html", FormPage, gin.H{
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
return
@@ -747,7 +735,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
discord := discordEnabled && app.config.Section("discord").Key("show_on_reg").MustBool(true)
matrix := matrixEnabled && app.config.Section("matrix").Key("show_on_reg").MustBool(true)
userPageAddress := fmt.Sprintf("%s/my/account", app.ExternalURI)
userPageAddress := app.ExternalURI + PAGES.MyAccount
fromUser := ""
if invite.ReferrerJellyfinID != "" {
@@ -758,9 +746,6 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
}
data := gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"helpMessage": app.config.Section("ui").Key("help_message").String(),
"successMessage": app.config.Section("ui").Key("success_message").String(),
@@ -772,28 +757,29 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"username": !app.config.Section("email").Key("no_username").MustBool(false),
"strings": app.storage.lang.User[lang].Strings,
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
"notifications": app.storage.lang.User[lang].notificationsJSON,
"code": invite.Code,
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
"userExpiry": invite.UserExpiry,
"userExpiryMonths": invite.UserMonths,
"userExpiryDays": invite.UserDays,
"userExpiryHours": invite.UserHours,
"userExpiryMinutes": invite.UserMinutes,
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang,
"passwordReset": false,
"customSuccessCard": false,
"telegramEnabled": telegram,
"discordEnabled": discord,
"matrixEnabled": matrix,
"emailRequired": app.config.Section("email").Key("required").MustBool(false),
"captcha": app.config.Section("captcha").Key("enabled").MustBool(false),
"reCAPTCHA": app.config.Section("captcha").Key("recaptcha").MustBool(false),
"reCAPTCHASiteKey": app.config.Section("captcha").Key("recaptcha_site_key").MustString(""),
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
"userPageAddress": userPageAddress,
"fromUser": fromUser,
// ewwwww, reusing an existing field, FIXME!
"notifications": app.storage.lang.User[lang].notificationsJSON,
"code": invite.Code,
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
"userExpiry": invite.UserExpiry,
"userExpiryMonths": invite.UserMonths,
"userExpiryDays": invite.UserDays,
"userExpiryHours": invite.UserHours,
"userExpiryMinutes": invite.UserMinutes,
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang,
"passwordReset": false,
"customSuccessCard": false,
"telegramEnabled": telegram,
"discordEnabled": discord,
"matrixEnabled": matrix,
"emailRequired": app.config.Section("email").Key("required").MustBool(false),
"captcha": app.config.Section("captcha").Key("enabled").MustBool(false),
"reCAPTCHA": app.config.Section("captcha").Key("recaptcha").MustBool(false),
"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()
@@ -837,15 +823,12 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
// pin := ""
// for _, token := range app.discord.tokens {
// if
gcHTML(gc, http.StatusOK, "form-loader.html", data)
app.gcHTML(gc, http.StatusOK, "form-loader.html", OtherPage, data)
}
func (app *appContext) NoRouteHandler(gc *gin.Context) {
app.pushResources(gc, OtherPage)
gcHTML(gc, 404, "404.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
app.gcHTML(gc, 404, "404.html", OtherPage, gin.H{
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}