diff --git a/Makefile b/Makefile
index bf9fb77..4b54df2 100644
--- a/Makefile
+++ b/Makefile
@@ -104,6 +104,7 @@ typescript:
$(info compiling typescript)
mkdir -p $(DATA)/web/js
$(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/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.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
diff --git a/api-userpage.go b/api-userpage.go
new file mode 100644
index 0000000..37672f1
--- /dev/null
+++ b/api-userpage.go
@@ -0,0 +1,7 @@
+package main
+
+import "github.com/gin-gonic/gin"
+
+func (app *appContext) HelloWorld(gc *gin.Context) {
+ gc.JSON(200, stringResponse{"It worked!", "none"})
+}
diff --git a/api.go b/api.go
index 7fc4740..06e2ffd 100644
--- a/api.go
+++ b/api.go
@@ -214,7 +214,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
app.info.Println("Config requested")
resp := app.configBase
// Load language options
- formOptions := app.storage.lang.Form.getOptions()
+ formOptions := app.storage.lang.User.getOptions()
fl := resp.Sections["ui"].Settings["language-form"]
fl.Options = formOptions
fl.Value = app.config.Section("ui").Key("language-form").MustString("en-us")
@@ -452,8 +452,8 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
page := gc.Param("page")
resp := langDTO{}
switch page {
- case "form":
- for key, lang := range app.storage.lang.Form {
+ case "form", "user":
+ for key, lang := range app.storage.lang.User {
resp[key] = lang.Meta.Name
}
case "admin":
@@ -494,8 +494,8 @@ func (app *appContext) ServeLang(gc *gin.Context) {
if page == "admin" {
gc.JSON(200, app.storage.lang.Admin[lang])
return
- } else if page == "form" {
- gc.JSON(200, app.storage.lang.Form[lang])
+ } else if page == "form" || page == "user" {
+ gc.JSON(200, app.storage.lang.User[lang])
return
}
respondBool(400, false, gc)
diff --git a/config.go b/config.go
index 3a0748c..a4316ab 100644
--- a/config.go
+++ b/config.go
@@ -169,11 +169,11 @@ func (app *appContext) loadConfig() error {
oldFormLang := app.config.Section("ui").Key("language").MustString("")
if oldFormLang != "" {
- app.storage.lang.chosenFormLang = oldFormLang
+ app.storage.lang.chosenUserLang = oldFormLang
}
newFormLang := app.config.Section("ui").Key("language-form").MustString("")
if newFormLang != "" {
- app.storage.lang.chosenFormLang = newFormLang
+ app.storage.lang.chosenUserLang = newFormLang
}
app.storage.lang.chosenAdminLang = app.config.Section("ui").Key("language-admin").MustString("en-us")
app.storage.lang.chosenEmailLang = app.config.Section("email").Key("language").MustString("en-us")
diff --git a/config/config-base.json b/config/config-base.json
index f8ed5ee..20b6e7b 100644
--- a/config/config-base.json
+++ b/config/config-base.json
@@ -373,6 +373,23 @@
}
}
},
+ "user_page": {
+ "order": [],
+ "meta": {
+ "name": "User Page",
+ "description": "Settings for the user page, which provides useful info and tools to users directly. NOTE: Jellyfin Login must be enabled to use this feature.",
+ "depends_true": "ui|jellyfin_login"
+ },
+ "settings": {
+ "enabled": {
+ "name": "Enabled",
+ "required": false,
+ "requires_restart": false,
+ "type": "bool",
+ "value": true
+ }
+ }
+ },
"password_validation": {
"order": [],
"meta": {
diff --git a/html/user.html b/html/user.html
new file mode 100644
index 0000000..9bb58e4
--- /dev/null
+++ b/html/user.html
@@ -0,0 +1,53 @@
+
+
+
+
+ {{ template "header.html" . }}
+ {{ .lang.Strings.pageTitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ .strings.logout }}
+
+ {{ template "login-modal.html" . }}
+
+
+
+
+
diff --git a/lang.go b/lang.go
index e78a81d..0691417 100644
--- a/lang.go
+++ b/lang.go
@@ -38,9 +38,9 @@ type adminLang struct {
JSON string
}
-type formLangs map[string]formLang
+type userLangs map[string]userLang
-func (ls *formLangs) getOptions() [][2]string {
+func (ls *userLangs) getOptions() [][2]string {
opts := make([][2]string, len(*ls))
i := 0
for key, lang := range *ls {
@@ -50,7 +50,7 @@ func (ls *formLangs) getOptions() [][2]string {
return opts
}
-type formLang struct {
+type userLang struct {
Meta langMeta `json:"meta"`
Strings langSection `json:"strings"`
Notifications langSection `json:"notifications"`
diff --git a/main.go b/main.go
index 64ac367..777078c 100644
--- a/main.go
+++ b/main.go
@@ -284,7 +284,7 @@ func start(asDaemon, firstCall bool) {
}
app.storage.lang.CommonPath = "common"
- app.storage.lang.FormPath = "form"
+ app.storage.lang.UserPath = "form"
app.storage.lang.AdminPath = "admin"
app.storage.lang.EmailPath = "email"
app.storage.lang.TelegramPath = "telegram"
diff --git a/router.go b/router.go
index eae0637..43ee80e 100644
--- a/router.go
+++ b/router.go
@@ -101,6 +101,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
if app.URLBase != "" {
routePrefixes = append(routePrefixes, "")
}
+
+ userPageEnabled := app.config.Section("user_page").Key("enabled").MustBool(true) && app.config.Section("ui").Key("jellyfin_login").MustBool(true)
+
for _, p := range routePrefixes {
router.GET(p+"/lang/:page", app.GetLanguages)
router.Use(static.Serve(p+"/", app.webFS))
@@ -140,6 +143,11 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.POST(p+"/invite/: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/token/login", app.getUserTokenLogin)
+ router.GET(p+"/my/token/refresh", app.getUserTokenRefresh)
+ }
}
if *SWAGGER {
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
@@ -147,7 +155,14 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
}
+
api := router.Group("/", app.webAuth())
+
+ var user *gin.RouterGroup
+ if userPageEnabled {
+ user = router.Group("/my", app.userAuth())
+ }
+
for _, p := range routePrefixes {
router.POST(p+"/logout", app.Logout)
api.DELETE(p+"/users", app.DeleteUsers)
@@ -210,6 +225,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
}
api.POST(p+"/matrix/login", app.MatrixLogin)
+ if userPageEnabled {
+ user.GET(p+"/hello", app.HelloWorld)
+ }
}
}
diff --git a/storage.go b/storage.go
index 888b9d1..f487847 100644
--- a/storage.go
+++ b/storage.go
@@ -116,9 +116,9 @@ type Lang struct {
chosenAdminLang string
Admin adminLangs
AdminJSON map[string]string
- FormPath string
- chosenFormLang string
- Form formLangs
+ UserPath string
+ chosenUserLang string
+ User userLangs
PasswordResetPath string
chosenPWRLang string
PasswordReset pwrLangs
@@ -144,7 +144,7 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
if err != nil {
return
}
- err = st.loadLangForm(filesystems...)
+ err = st.loadLangUser(filesystems...)
if err != nil {
return
}
@@ -395,16 +395,16 @@ func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
return nil
}
-func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
- st.lang.Form = map[string]formLang{}
- var english formLang
+func (st *Storage) loadLangUser(filesystems ...fs.FS) error {
+ st.lang.User = map[string]userLang{}
+ var english userLang
loadedLangs := make([]map[string]bool, len(filesystems))
var load loadLangFunc
load = func(fsIndex int, fname string) error {
filesystem := filesystems[fsIndex]
index := strings.TrimSuffix(fname, filepath.Ext(fname))
- lang := formLang{}
- f, err := fs.ReadFile(filesystem, FSJoin(st.lang.FormPath, fname))
+ lang := userLang{}
+ f, err := fs.ReadFile(filesystem, FSJoin(st.lang.UserPath, fname))
if err != nil {
return err
}
@@ -418,11 +418,11 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
st.lang.Common.patchCommon(&lang.Strings, index)
if fname != "en-us.json" {
if lang.Meta.Fallback != "" {
- fallback, ok := st.lang.Form[lang.Meta.Fallback]
+ fallback, ok := st.lang.User[lang.Meta.Fallback]
err = nil
if !ok {
err = load(fsIndex, lang.Meta.Fallback+".json")
- fallback = st.lang.Form[lang.Meta.Fallback]
+ fallback = st.lang.User[lang.Meta.Fallback]
}
if err == nil {
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
@@ -447,7 +447,7 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
}
lang.notificationsJSON = string(notifications)
lang.validationStringsJSON = string(validationStrings)
- st.lang.Form[index] = lang
+ st.lang.User[index] = lang
return nil
}
engFound := false
@@ -463,10 +463,10 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
if !engFound {
return err
}
- english = st.lang.Form["en-us"]
- formLoaded := false
+ english = st.lang.User["en-us"]
+ userLoaded := false
for i := range filesystems {
- files, err := fs.ReadDir(filesystems[i], st.lang.FormPath)
+ files, err := fs.ReadDir(filesystems[i], st.lang.UserPath)
if err != nil {
continue
}
@@ -474,13 +474,13 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
if !loadedLangs[i][f.Name()] {
err = load(i, f.Name())
if err == nil {
- formLoaded = true
+ userLoaded = true
loadedLangs[i][f.Name()] = true
}
}
}
}
- if !formLoaded {
+ if !userLoaded {
return err
}
return nil
@@ -540,7 +540,7 @@ func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
return err
}
english = st.lang.PasswordReset["en-us"]
- formLoaded := false
+ userLoaded := false
for i := range filesystems {
files, err := fs.ReadDir(filesystems[i], st.lang.PasswordResetPath)
if err != nil {
@@ -550,13 +550,13 @@ func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
if !loadedLangs[i][f.Name()] {
err = load(i, f.Name())
if err == nil {
- formLoaded = true
+ userLoaded = true
loadedLangs[i][f.Name()] = true
}
}
}
}
- if !formLoaded {
+ if !userLoaded {
return err
}
return nil
diff --git a/ts/admin.ts b/ts/admin.ts
index 6eb50f8..a13bcb5 100644
--- a/ts/admin.ts
+++ b/ts/admin.ts
@@ -160,6 +160,7 @@ window.onpopstate = (event: PopStateEvent) => {
const login = new Login(window.modals.login as Modal, "/");
login.onLogin = () => {
+ console.log("Logged in.");
window.updater = new Updater();
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
const currentTab = window.tabs.current;
diff --git a/ts/user.ts b/ts/user.ts
new file mode 100644
index 0000000..c224bac
--- /dev/null
+++ b/ts/user.ts
@@ -0,0 +1,37 @@
+import { ThemeManager } from "./modules/theme.js";
+import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
+import { Modal } from "./modules/modal.js";
+import { _get, _post, notificationBox, whichAnimationEvent } from "./modules/common.js";
+import { Login } from "./modules/login.js";
+
+const theme = new ThemeManager(document.getElementById("button-theme"));
+
+window.lang = new lang(window.langFile as LangFile);
+
+loadLangSelector("user");
+
+window.animationEvent = whichAnimationEvent();
+
+window.token = "";
+
+window.modals = {} as Modals;
+
+(() => {
+ window.modals.login = new Modal(document.getElementById("modal-login"), true);
+})();
+
+window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
+
+const login = new Login(window.modals.login as Modal, "/my/");
+login.onLogin = () => {
+ console.log("Logged in.");
+ document.getElementById("card-user").textContent = "Logged In!";
+ _get("/my/hello", null, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ const card = document.getElementById("card-user");
+ card.textContent = card.textContent + " got response " + req.response["response"];
+ }
+ });
+};
+
+login.login("", "");
diff --git a/views.go b/views.go
index ac07c91..2194b96 100644
--- a/views.go
+++ b/views.go
@@ -44,15 +44,23 @@ func gcHTML(gc *gin.Context, code int, file string, templ gin.H) {
gc.HTML(code, file, templ)
}
-func (app *appContext) pushResources(gc *gin.Context, admin bool) {
+func (app *appContext) pushResources(gc *gin.Context, page Page) {
+ var toPush []string
+ 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 {
app.debug.Println("Using HTTP2 Server push")
- if admin {
- 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"}
- for _, f := range toPush {
- if err := pusher.Push(app.URLBase+f, nil); err != nil {
- app.debug.Printf("Failed HTTP2 ServerPush of \"%s\": %+v", f, err)
- }
+ for _, f := range toPush {
+ if err := pusher.Push(app.URLBase+f, nil); err != nil {
+ app.debug.Printf("Failed HTTP2 ServerPush of \"%s\": %+v", f, err)
}
}
}
@@ -65,6 +73,8 @@ const (
AdminPage Page = iota + 1
FormPage
PWRPage
+ UserPage
+ OtherPage
)
func (app *appContext) getLang(gc *gin.Context, page Page, chosen string) string {
@@ -77,8 +87,8 @@ func (app *appContext) getLang(gc *gin.Context, page Page, chosen string) string
gc.SetCookie("lang", lang, (365 * 3600), "/", gc.Request.URL.Hostname(), true, true)
return lang
}
- case FormPage:
- if _, ok := app.storage.lang.Form[lang]; ok {
+ case FormPage, UserPage:
+ if _, ok := app.storage.lang.User[lang]; ok {
gc.SetCookie("lang", lang, (365 * 3600), "/", gc.Request.URL.Hostname(), true, true)
return lang
}
@@ -95,8 +105,8 @@ func (app *appContext) getLang(gc *gin.Context, page Page, chosen string) string
if _, ok := app.storage.lang.Admin[cookie]; ok {
return cookie
}
- case FormPage:
- if _, ok := app.storage.lang.Form[cookie]; ok {
+ case FormPage, UserPage:
+ if _, ok := app.storage.lang.User[cookie]; ok {
return cookie
}
case PWRPage:
@@ -109,7 +119,7 @@ func (app *appContext) getLang(gc *gin.Context, page Page, chosen string) string
}
func (app *appContext) AdminPage(gc *gin.Context) {
- app.pushResources(gc, true)
+ 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()
@@ -149,6 +159,32 @@ func (app *appContext) AdminPage(gc *gin.Context) {
})
}
+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)
+ gcHTML(gc, http.StatusOK, "user.html", gin.H{
+ "urlBase": app.getURLBase(gc),
+ "cssClass": app.cssClass,
+ "cssVersion": cssVersion,
+ "contactMessage": app.config.Section("ui").Key("contact_message").String(),
+ "emailEnabled": emailEnabled,
+ "telegramEnabled": telegramEnabled,
+ "discordEnabled": discordEnabled,
+ "matrixEnabled": matrixEnabled,
+ "ombiEnabled": ombiEnabled,
+ "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.Admin[lang].Strings,
+ "quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
+ "language": app.storage.lang.Admin[lang].JSON,
+ "langName": lang,
+ })
+}
+
func (app *appContext) ResetPassword(gc *gin.Context) {
isBot := strings.Contains(gc.Request.Header.Get("User-Agent"), "Bot")
setPassword := app.config.Section("password_resets").Key("set_password").MustBool(false)
@@ -157,7 +193,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
app.NoRouteHandler(gc)
return
}
- app.pushResources(gc, false)
+ app.pushResources(gc, PWRPage)
lang := app.getLang(gc, PWRPage, app.storage.lang.chosenPWRLang)
data := gin.H{
"urlBase": app.getURLBase(gc),
@@ -177,8 +213,8 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
data["validate"] = app.config.Section("password_validation").Key("enabled").MustBool(false)
data["requirements"] = app.validator.getCriteria()
data["strings"] = app.storage.lang.PasswordReset[lang].Strings
- data["validationStrings"] = app.storage.lang.Form[lang].validationStringsJSON
- data["notifications"] = app.storage.lang.Form[lang].notificationsJSON
+ data["validationStrings"] = app.storage.lang.User[lang].validationStringsJSON
+ data["notifications"] = app.storage.lang.User[lang].notificationsJSON
data["langName"] = lang
data["passwordReset"] = true
data["telegramEnabled"] = false
@@ -422,9 +458,9 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
}
func (app *appContext) InviteProxy(gc *gin.Context) {
- app.pushResources(gc, false)
+ app.pushResources(gc, FormPage)
code := gc.Param("invCode")
- lang := app.getLang(gc, FormPage, app.storage.lang.chosenFormLang)
+ lang := app.getLang(gc, FormPage, app.storage.lang.chosenUserLang)
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
// if app.checkInvite(code, false, "") {
inv, ok := app.storage.invites[code]
@@ -493,7 +529,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
} else {
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
"cssClass": app.cssClass,
- "strings": app.storage.lang.Form[lang].Strings,
+ "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(),
"jfLink": jfLink,
@@ -528,9 +564,9 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"requirements": app.validator.getCriteria(),
"email": email,
"username": !app.config.Section("email").Key("no_username").MustBool(false),
- "strings": app.storage.lang.Form[lang].Strings,
- "validationStrings": app.storage.lang.Form[lang].validationStringsJSON,
- "notifications": app.storage.lang.Form[lang].notificationsJSON,
+ "strings": app.storage.lang.User[lang].Strings,
+ "validationStrings": app.storage.lang.User[lang].validationStringsJSON,
+ "notifications": app.storage.lang.User[lang].notificationsJSON,
"code": code,
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
"userExpiry": inv.UserExpiry,
@@ -538,7 +574,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"userExpiryDays": inv.UserDays,
"userExpiryHours": inv.UserHours,
"userExpiryMinutes": inv.UserMinutes,
- "userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"),
+ "userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang,
"passwordReset": false,
"telegramEnabled": telegram,
@@ -563,7 +599,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
data["discordPIN"] = app.discord.NewAuthToken()
data["discordUsername"] = app.discord.username
data["discordRequired"] = app.config.Section("discord").Key("required").MustBool(false)
- data["discordSendPINMessage"] = template.HTML(app.storage.lang.Form[lang].Strings.template("sendPINDiscord", tmpl{
+ data["discordSendPINMessage"] = template.HTML(app.storage.lang.User[lang].Strings.template("sendPINDiscord", tmpl{
"command": `/` + app.config.Section("discord").Key("start_command").MustString("start") + ``,
"server_channel": app.discord.serverChannelName,
}))
@@ -579,7 +615,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
}
func (app *appContext) NoRouteHandler(gc *gin.Context) {
- app.pushResources(gc, false)
+ app.pushResources(gc, OtherPage)
gcHTML(gc, 404, "404.html", gin.H{
"cssClass": app.cssClass,
"cssVersion": cssVersion,