From 6448a7db9e4e75954cee042d540d5b26ffa08cc3 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 9 Jan 2022 19:29:17 +0000 Subject: [PATCH] accounts: allow giving individual users jfa-go access New "Access jfa-go" column allows you to select users for jfa-go access. New "Allow All" setting allows all Jellyfin users access, as disabling "Admin Only" no longer does this. --- api.go | 58 ++++++++++++++++++++++++++++++++++------- auth.go | 10 +++++-- config/config-base.json | 9 +++++++ html/admin.html | 6 +++++ html/setup.html | 4 +++ lang/admin/en-us.json | 4 ++- lang/setup/en-us.json | 2 ++ models.go | 5 +++- router.go | 1 + setup.go | 9 ++++--- storage.go | 1 + ts/modules/accounts.ts | 45 +++++++++++++++++++++++++++++--- ts/setup.ts | 26 ++++++++++++++++++ ts/typings/d.ts | 3 +++ views.go | 5 ++++ 15 files changed, 167 insertions(+), 21 deletions(-) diff --git a/api.go b/api.go index 9292b48..a4a6821 100644 --- a/api.go +++ b/api.go @@ -1453,6 +1453,8 @@ func (app *appContext) GetUsers(gc *gin.Context) { respond(500, "Couldn't get users", gc) return } + adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true) + allowAll := app.config.Section("ui").Key("allow_all").MustBool(false) i := 0 app.storage.usersLock.Lock() defer app.storage.usersLock.Unlock() @@ -1470,6 +1472,7 @@ func (app *appContext) GetUsers(gc *gin.Context) { user.Email = email.Addr user.NotifyThroughEmail = email.Contact user.Label = email.Label + user.AccountsAdmin = (app.jellyfinLogin) && (email.Admin || (adminOnly && jfUser.Policy.IsAdministrator) || allowAll) } expiry, ok := app.storage.users[jfUser.ID] if ok { @@ -1580,6 +1583,43 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) { respondBool(204, true, gc) } +// @Summary Set whether or not a user can access jfa-go. Redundant if the user is a Jellyfin admin. +// @Produce json +// @Param setAccountsAdminDTO body setAccountsAdminDTO true "Map of userIDs whether or not they have access." +// @Success 204 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /users/accounts-admin [post] +// @Security Bearer +// @tags Users +func (app *appContext) SetAccountsAdmin(gc *gin.Context) { + var req setAccountsAdminDTO + gc.BindJSON(&req) + app.debug.Println("Admin modification requested") + users, status, err := app.jf.GetUsers(false) + if !(status == 200 || status == 204) || err != nil { + app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) + respond(500, "Couldn't get users", gc) + return + } + for _, jfUser := range users { + id := jfUser.ID + if admin, ok := req[id]; ok { + var emailStore = EmailAddress{} + if oldEmail, ok := app.storage.emails[id]; ok { + emailStore = oldEmail + } + emailStore.Admin = admin + app.storage.emails[id] = emailStore + } + } + if err := app.storage.storeEmails(); err != nil { + app.err.Printf("Failed to store email list: %v", err) + respondBool(500, false, gc) + } + app.info.Println("Email list modified") + respondBool(204, true, gc) +} + // @Summary Modify user's labels, which show next to their name in the accounts tab. // @Produce json // @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to labels" @@ -1601,13 +1641,12 @@ func (app *appContext) ModifyLabels(gc *gin.Context) { for _, jfUser := range users { id := jfUser.ID if label, ok := req[id]; ok { - addr := "" - contact := true + var emailStore = EmailAddress{} if oldEmail, ok := app.storage.emails[id]; ok { - addr = oldEmail.Addr - contact = oldEmail.Contact + emailStore = oldEmail } - app.storage.emails[id] = EmailAddress{Addr: addr, Contact: contact, Label: label} + emailStore.Label = label + app.storage.emails[id] = emailStore } } if err := app.storage.storeEmails(); err != nil { @@ -1640,11 +1679,12 @@ func (app *appContext) ModifyEmails(gc *gin.Context) { for _, jfUser := range users { id := jfUser.ID if address, ok := req[id]; ok { - contact := true - if oldAddr, ok := app.storage.emails[id]; ok { - contact = oldAddr.Contact + var emailStore = EmailAddress{} + if oldEmail, ok := app.storage.emails[id]; ok { + emailStore = oldEmail } - app.storage.emails[id] = EmailAddress{Addr: address, Contact: contact} + emailStore.Addr = address + app.storage.emails[id] = emailStore if ombiEnabled { ombiUser, code, err := app.getOmbiUser(id) if code == 200 && err == nil { diff --git a/auth.go b/auth.go index a8c5fc3..94344f6 100644 --- a/auth.go +++ b/auth.go @@ -145,8 +145,14 @@ func (app *appContext) getTokenLogin(gc *gin.Context) { return } jfID = user.ID - if app.config.Section("ui").Key("admin_only").MustBool(true) { - if !user.Policy.IsAdministrator { + if !app.config.Section("ui").Key("allow_all").MustBool(false) { + accountsAdmin := false + adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true) + if emailStore, ok := app.storage.emails[jfID]; ok { + accountsAdmin = emailStore.Admin + } + accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator) + if !accountsAdmin { app.debug.Printf("Auth denied: Users \"%s\" isn't admin", creds[0]) respond(401, "Unauthorized", gc) return diff --git a/config/config-base.json b/config/config-base.json index 5546d7d..d59f431 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -181,6 +181,15 @@ "value": true, "description": "Allows only admin users on Jellyfin to access the admin page." }, + "allow_all": { + "name": "Allow all users to login", + "required": false, + "requires_restart": true, + "depends_true": "jellyfin_login", + "type": "bool", + "value": false, + "description": "Allow all Jellyfin users to access jfa-go. Not recommended, add individual users in the Accounts tab instead." + }, "username": { "name": "Web Username", "required": true, diff --git a/html/admin.html b/html/admin.html index 12541c1..97567dd 100644 --- a/html/admin.html +++ b/html/admin.html @@ -14,6 +14,9 @@ window.langFile = JSON.parse({{ .language }}); window.linkResetEnabled = {{ .linkResetEnabled }}; window.language = "{{ .langName }}"; + window.jellyfinLogin = {{ .jellyfinLogin }}; + window.jfAdminOnly = {{ .jfAdminOnly }}; + window.jfAllowAll = {{ .jfAllowAll }}; Admin - jfa-go {{ template "header.html" . }} @@ -613,6 +616,9 @@ {{ .strings.username }} + {{ if .jellyfinLogin }} + {{ .strings.accessJFA }} + {{ end }} {{ .strings.emailAddress }} {{ if .telegramEnabled }} Telegram diff --git a/html/setup.html b/html/setup.html index 949a90c..e228ab7 100644 --- a/html/setup.html +++ b/html/setup.html @@ -139,6 +139,10 @@ + +

{{ .lang.Login.allowAllDescription }}

diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index cddefc8..824431e 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -111,7 +111,9 @@ "matrixHomeServer": "Home server address", "saveAsTemplate": "Save as template", "deleteTemplate": "Delete template", - "templateEnterName": "Enter a name to save this template." + "templateEnterName": "Enter a name to save this template.", + "accessJFA": "Access jfa-go", + "accessJFASettings": "Cannot be changed as this either \"Admin Only\" or \"Allow All\" has been set in Settings > General." }, "notifications": { "changedEmailAddress": "Changed email address of {n}.", diff --git a/lang/setup/en-us.json b/lang/setup/en-us.json index 52f89e9..3a12342 100644 --- a/lang/setup/en-us.json +++ b/lang/setup/en-us.json @@ -65,6 +65,8 @@ "authorizeWithJellyfin": "Authorize with Jellyfin/Emby: Login details are shared with Jellyfin, which allows for multiple users.", "authorizeManual": "Username and Password: Manually set the username and password.", "adminOnly": "Admin users only (recommended)", + "allowAll": "Allow all Jellyfin users to login", + "allowAllDescription": "Not recommended, you should allow individual users to login once setup.", "emailNotice": "Your email address can be used to receive notifications." }, "jellyfinEmby": { diff --git a/models.go b/models.go index e220da5..ba74494 100644 --- a/models.go +++ b/models.go @@ -145,7 +145,8 @@ type respUser struct { NotifyThroughDiscord bool `json:"notify_discord"` Matrix string `json:"matrix"` // Matrix ID (if known) NotifyThroughMatrix bool `json:"notify_matrix"` - Label string `json:"label"` // Label of user, shown next to their name. + Label string `json:"label"` // Label of user, shown next to their name. + AccountsAdmin bool `json:"accounts_admin"` // Whether or not the user is a jfa-go admin. } type getUsersDTO struct { @@ -346,3 +347,5 @@ type InternalPWR struct { type LogDTO struct { Log string `json:"log"` } + +type setAccountsAdminDTO map[string]bool diff --git a/router.go b/router.go index bd66b48..65186f5 100644 --- a/router.go +++ b/router.go @@ -161,6 +161,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) { api.POST(p+"/invites/notify", app.SetNotify) api.POST(p+"/users/emails", app.ModifyEmails) api.POST(p+"/users/labels", app.ModifyLabels) + api.POST(p+"/users/accounts-admin", app.SetAccountsAdmin) // api.POST(p + "/setDefaults", app.SetDefaults) api.POST(p+"/users/settings", app.ApplySettings) api.POST(p+"/users/announce", app.Announce) diff --git a/setup.go b/setup.go index 10f7c14..361f928 100644 --- a/setup.go +++ b/setup.go @@ -38,10 +38,11 @@ func (app *appContext) ServeSetup(gc *gin.Context) { return } gc.HTML(200, "setup.html", gin.H{ - "lang": app.storage.lang.Setup[lang], - "emailLang": app.storage.lang.Email[emailLang], - "language": app.storage.lang.Setup[lang].JSON, - "messages": string(msg), + "cssVersion": cssVersion, + "lang": app.storage.lang.Setup[lang], + "emailLang": app.storage.lang.Email[emailLang], + "language": app.storage.lang.Setup[lang].JSON, + "messages": string(msg), }) } diff --git a/storage.go b/storage.go index f7f133a..56f646a 100644 --- a/storage.go +++ b/storage.go @@ -54,6 +54,7 @@ type EmailAddress struct { Addr string Label string // User Label. Contact bool + Admin bool // Whether or not user is jfa-go admin. } type customEmails struct { diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 2ffade6..e9ee48c 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -21,6 +21,7 @@ interface User { matrix: string; notify_matrix: boolean; label: string; + accounts_admin: boolean; } interface getPinResponse { @@ -64,6 +65,7 @@ class user implements User { private _label: HTMLInputElement; private _userLabel: string; private _labelEditButton: HTMLElement; + private _accounts_admin: HTMLInputElement id = ""; private _selected: boolean; @@ -98,6 +100,18 @@ class user implements User { } } + get accounts_admin(): boolean { return this._accounts_admin.checked; } + set accounts_admin(a: boolean) { + if (!window.jellyfinLogin) return; + this._accounts_admin.checked = a; + this._accounts_admin.disabled = (window.jfAllowAll || (a && this.admin && window.jfAdminOnly)); + if (this._accounts_admin.disabled) { + this._accounts_admin.title = window.lang.strings("accessJFASettings"); + } else { + this._accounts_admin.title = ""; + } + } + get disabled(): boolean { return this._disabled.classList.contains("chip"); } set disabled(state: boolean) { if (state) { @@ -386,7 +400,6 @@ class user implements User { get label(): string { return this._userLabel; } set label(l: string) { - console.log(l); this._userLabel = l ? l : ""; this._label.innerHTML = l ? l : ""; this._labelEditButton.classList.add("ri-edit-line"); @@ -403,8 +416,15 @@ class user implements User { constructor(user: User) { this._row = document.createElement("tr") as HTMLTableRowElement; let innerHTML = ` - +
+ `; + if (window.jellyfinLogin) { + innerHTML += ` +
+ `; + } + innerHTML += `
`; if (window.telegramEnabled) { @@ -429,7 +449,8 @@ class user implements User { this._row.innerHTML = innerHTML; const emailEditor = ``; const labelEditor = ``; - this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement; + this._check = this._row.querySelector("input[type=checkbox].accounts-select-user") as HTMLInputElement; + this._accounts_admin = this._row.querySelector("input[type=checkbox].accounts-access-jfa") as HTMLInputElement; this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement; this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement; this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement; @@ -443,6 +464,22 @@ class user implements User { this._label = this._row.querySelector(".accounts-label-container") as HTMLInputElement; this._labelEditButton = this._row.querySelector(".accounts-label-edit") as HTMLElement; this._check.onchange = () => { this.selected = this._check.checked; } + + if (window.jellyfinLogin) { + this._accounts_admin.onchange = () => { + this.accounts_admin = this._accounts_admin.checked; + let send = {}; + send[this.id] = this.accounts_admin; + _post("/users/accounts-admin", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 204) { + this.accounts_admin = !this.accounts_admin; + window.notifications.customError("accountsAdminChanged", window.lang.notif("errorUnknown")); + } + } + }); + }; + } this._notifyDropdown = this._constructDropdown(); @@ -611,6 +648,7 @@ class user implements User { this.notify_email = user.notify_email; this.discord_id = user.discord_id; this.label = user.label; + this.accounts_admin = user.accounts_admin; } asElement = (): HTMLTableRowElement => { return this._row; } @@ -1145,7 +1183,6 @@ export class accountsList { let manualUser: user; for (let id of list) { let user = this._users[id]; - console.log(user, user.notify_email, user.notify_matrix, user.notify_discord, user.notify_telegram); if (!user.lastNotifyMethod() && !user.email) { manualUser = user; break; diff --git a/ts/setup.ts b/ts/setup.ts index 02fb37d..5ecf84c 100644 --- a/ts/setup.ts +++ b/ts/setup.ts @@ -239,6 +239,7 @@ const settings = { "language-admin": new LangSelect("admin", get("ui-language-admin")), "jellyfin_login": new BoolRadios("ui-jellyfin_login", "", false, "ui", "jellyfin_login"), "admin_only": new Checkbox(get("ui-admin_only"), "jellyfin_login", true, "ui"), + "allow_all": new Checkbox(get("ui-allow_all"), "jellyfin_login", true, "ui"), "username": new Input(get("ui-username"), "", "", "jellyfin_login", false, "ui"), "password": new Input(get("ui-password"), "", "", "jellyfin_login", false, "ui"), "email": new Input(get("ui-email"), "", "", "jellyfin_login", false, "ui"), @@ -389,6 +390,31 @@ settings["email"]["method"].onchange = emailMethodChange; settings["messages"]["enabled"].onchange = emailMethodChange; emailMethodChange(); +const jellyfinLoginAccessChange = () => { + const adminOnly = settings["ui"]["admin_only"].value == "true"; + const allowAll = settings["ui"]["allow_all"].value == "true"; + const adminOnlyEl = document.getElementById("ui-admin_only") as HTMLInputElement; + const allowAllEls = [document.getElementById("ui-allow_all"), document.getElementById("description-ui-allow_all")]; + const nextButton = adminOnlyEl.parentElement.parentElement.parentElement.querySelector("span.next") as HTMLSpanElement; + if (adminOnly && !allowAll) { + (allowAllEls[0] as HTMLInputElement).disabled = true; + adminOnlyEl.disabled = false; + nextButton.removeAttribute("disabled"); + } else if (!adminOnly && allowAll) { + adminOnlyEl.disabled = true; + (allowAllEls[0] as HTMLInputElement).disabled = false; + nextButton.removeAttribute("disabled"); + } else { + adminOnlyEl.disabled = false; + (allowAllEls[0] as HTMLInputElement).disabled = false; + nextButton.setAttribute("disabled", "true") + } +}; + +settings["ui"]["admin_only"].onchange = jellyfinLoginAccessChange; +settings["ui"]["allow_all"].onchange = jellyfinLoginAccessChange; +jellyfinLoginAccessChange(); + const embyHidePWR = () => { const pwr = document.getElementById("password-resets"); const val = settings["jellyfin"]["type"].value; diff --git a/ts/typings/d.ts b/ts/typings/d.ts index 379a98d..c24e17b 100644 --- a/ts/typings/d.ts +++ b/ts/typings/d.ts @@ -37,6 +37,9 @@ declare interface Window { lang: Lang; langFile: {}; updater: updater; + jellyfinLogin: boolean; + jfAdminOnly: boolean; + jfAllowAll: boolean; } declare interface Update { diff --git a/views.go b/views.go index 191cc73..dfbf751 100644 --- a/views.go +++ b/views.go @@ -110,6 +110,8 @@ func (app *appContext) AdminPage(gc *gin.Context) { 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) + jfAdminOnly := app.config.Section("ui").Key("admin_only").MustBool(true) + jfAllowAll := app.config.Section("ui").Key("allow_all").MustBool(false) var license string l, err := fs.ReadFile(localFS, "LICENSE") if err != nil { @@ -137,6 +139,9 @@ func (app *appContext) AdminPage(gc *gin.Context) { "language": app.storage.lang.Admin[lang].JSON, "langName": lang, "license": license, + "jellyfinLogin": app.jellyfinLogin, + "jfAdminOnly": jfAdminOnly, + "jfAllowAll": jfAllowAll, }) }