From 726acb9c295086526de87a4a00bd457456a00767 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 16 Jun 2023 14:43:37 +0100 Subject: [PATCH] userpage: initial page login, lang, and theme work. Currently only makes a request to a hello-world type endpoint to verify auth works. Accessible at /my/account. --- Makefile | 1 + api-userpage.go | 7 ++++ api.go | 10 ++--- config.go | 4 +- config/config-base.json | 17 +++++++++ html/user.html | 53 ++++++++++++++++++++++++++ lang.go | 6 +-- main.go | 2 +- router.go | 18 +++++++++ storage.go | 40 ++++++++++---------- ts/admin.ts | 1 + ts/user.ts | 37 ++++++++++++++++++ views.go | 84 +++++++++++++++++++++++++++++------------ 13 files changed, 225 insertions(+), 55 deletions(-) create mode 100644 api-userpage.go create mode 100644 html/user.html create mode 100644 ts/user.ts 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" . }} +
+
+ Not logged in. +
+
+ + + + 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,