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.
This commit is contained in:
Harvey Tindall 2023-06-16 14:43:37 +01:00
parent 54fde33a20
commit 726acb9c29
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
13 changed files with 225 additions and 55 deletions

View File

@ -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

7
api-userpage.go Normal file
View File

@ -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"})
}

10
api.go
View File

@ -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)

View File

@ -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")

View File

@ -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": {

53
html/user.html Normal file
View File

@ -0,0 +1,53 @@
<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.emailEnabled = {{ .emailEnabled }};
window.telegramEnabled = {{ .telegramEnabled }};
window.discordEnabled = {{ .discordEnabled }};
window.matrixEnabled = {{ .matrixEnabled }};
window.ombiEnabled = {{ .ombiEnabled }};
window.langFile = JSON.parse({{ .language }});
window.linkResetEnabled = {{ .linkResetEnabled }};
window.language = "{{ .langName }}";
</script>
{{ template "header.html" . }}
<title>{{ .lang.Strings.pageTitle }}</title>
</head>
<body class="max-w-full overflow-x-hidden section">
<div id="notification-box"></div>
<div class="top-4 left-4 absolute">
<span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<i class="ri-global-line"></i>
<span class="ml-2 chev"></span>
</span>
<div class="dropdown-display">
<div class="card ~neutral @low">
<label class="switch pb-4">
<input type="radio" name="lang-time" id="lang-12h">
<span>{{ .strings.time12h }}</span>
</label>
<label class="switch pb-4">
<input type="radio" name="lang-time" id="lang-24h">
<span>{{ .strings.time24h }}</span>
</label>
<div id="lang-list"></div>
</div>
</div>
</span>
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
</div>
{{ template "login-modal.html" . }}
<div class="page-container">
<div class="card @low dark:~d_neutral mb-4" id="card-user">
Not logged in.
</div>
</div>
<script src="{{ .urlBase }}/js/user.js" type="module"></script>
</body>
</html>

View File

@ -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"`

View File

@ -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"

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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;

37
ts/user.ts Normal file
View File

@ -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("", "");

View File

@ -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": `<span class="text-black dark:text-white font-mono">/` + app.config.Section("discord").Key("start_command").MustString("start") + `</span>`,
"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,