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.
This commit is contained in:
Harvey Tindall 2022-01-09 19:29:17 +00:00
parent 46d1da7cd3
commit 6448a7db9e
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
15 changed files with 167 additions and 21 deletions

58
api.go
View File

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

10
auth.go
View File

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

View File

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

View File

@ -14,6 +14,9 @@
window.langFile = JSON.parse({{ .language }});
window.linkResetEnabled = {{ .linkResetEnabled }};
window.language = "{{ .langName }}";
window.jellyfinLogin = {{ .jellyfinLogin }};
window.jfAdminOnly = {{ .jfAdminOnly }};
window.jfAllowAll = {{ .jfAllowAll }};
</script>
<title>Admin - jfa-go</title>
{{ template "header.html" . }}
@ -613,6 +616,9 @@
<tr>
<th><input type="checkbox" value="" id="accounts-select-all"></th>
<th class="table-inline my-2">{{ .strings.username }}</th>
{{ if .jellyfinLogin }}
<th class="text-center-i">{{ .strings.accessJFA }}</th>
{{ end }}
<th>{{ .strings.emailAddress }}</th>
{{ if .telegramEnabled }}
<th class="text-center-i">Telegram</th>

View File

@ -139,6 +139,10 @@
<label class="row switch pl-4 pb-4">
<input type="checkbox" class="mr-2" id="ui-admin_only"><span>{{ .lang.Login.adminOnly }}</span>
</label>
<label class="row switch pl-4 pb-2">
<input type="checkbox" class="mr-2" id="ui-allow_all"><span>{{ .lang.Login.allowAll }}</span>
</label>
<p class="support pb-4 pl-4 mt-1" id="description-ui-allow_all">{{ .lang.Login.allowAllDescription }}</p>
<label class="row switch pb-4">
<input type="radio" class="mr-2" name="ui-jellyfin_login" value="false"><span>{{ .lang.Login.authorizeManual }}</span>
</label>

View File

@ -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}.",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = `
<td><input type="checkbox" value=""></td>
<td><input type="checkbox" class="accounts-select-user" value=""></td>
<td><div class="table-inline"><span class="accounts-username py-2 mr-2"></span><span class="accounts-label-container ml-2"></span> <i class="icon ri-edit-line accounts-label-edit"></i> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></span></div></td>
`;
if (window.jellyfinLogin) {
innerHTML += `
<td><div class="table-inline justify-center"><input type="checkbox" class="accounts-access-jfa" value=""></div></td>
`;
}
innerHTML += `
<td><div class="table-inline"><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-2"></span></div></td>
`;
if (window.telegramEnabled) {
@ -429,7 +449,8 @@ class user implements User {
this._row.innerHTML = innerHTML;
const emailEditor = `<input type="email" class="input ~neutral @low stealth-input">`;
const labelEditor = `<input type="text" class="field ~neutral @low stealth-input">`;
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;

View File

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

View File

@ -37,6 +37,9 @@ declare interface Window {
lang: Lang;
langFile: {};
updater: updater;
jellyfinLogin: boolean;
jfAdminOnly: boolean;
jfAllowAll: boolean;
}
declare interface Update {

View File

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