1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-28 03:50:10 +00:00

Compare commits

...

5 Commits

Author SHA1 Message Date
49b056f1d6
fix notification buttons
their current status wouldn't display because of a slight mistake, and
they did the wrong thing because i forgot there isn't a nil value for
bools.
2020-09-19 17:05:09 +01:00
70cf706a82
fix image links 2020-09-19 16:40:33 +01:00
7c247b0aae
update readme; new images 2020-09-19 16:38:53 +01:00
4e8628844e
fix decapitalized words
I have no idea how this happened.
2020-09-19 16:13:17 +01:00
31aece5026
fix bs4 compatibility, small ui tweaks 2020-09-19 15:32:01 +01:00
11 changed files with 58 additions and 56 deletions

View File

@ -11,6 +11,7 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
* Account defaults: Configure an example account to your liking, and its permissions, access rights and homescreen layout can be applied to all new users. * Account defaults: Configure an example account to your liking, and its permissions, access rights and homescreen layout can be applied to all new users.
* Password validation: Ensure users choose a strong password. * Password validation: Ensure users choose a strong password.
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions. * 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
* 📨 Email storage: Add your existing user's email addresses through the UI, and jfa-go will ask new users for them on account creation. * 📨 Email storage: Add your existing user's email addresses through the UI, and jfa-go will ask new users for them on account creation.
* Email addresses can optionally be used instead of usernames * Email addresses can optionally be used instead of usernames
* 🔑 Password resets: When user's forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email. * 🔑 Password resets: When user's forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email.
@ -24,12 +25,12 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
## Interface ## Interface
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/jfa.gif" width="100%"></img> <img src="https://github.com/hrfee/jfa-go/blob/main/images/demo.gif" width="100%"></img>
</p> </p>
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/admin.png" width="48%" style="margin-right: 1.5%;" alt="Admin page"></img> <img src="https://github.com/hrfee/jfa-go/blob/main/images/invites.png" width="48%" style="margin-left: 1.5%;" alt="Invites tab"></img>
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/create.png" width="48%" style="margin-left: 1.5%;" alt="Account creation page"></img> <img src="https://github.com/hrfee/jfa-go/blob/main/images/accounts.png" width="48%" style="margin-right: 1.5%;" alt="Accounts tab"></img>
</p> </p>
#### Install #### Install
@ -69,6 +70,8 @@ Usage of ./jfa-go:
alternate path to config file. (default "~/.config/jfa-go/config.ini") alternate path to config file. (default "~/.config/jfa-go/config.ini")
-data string -data string
alternate path to data directory. (default "~/.config/jfa-go") alternate path to data directory. (default "~/.config/jfa-go")
-debug
Enables debug logging and exposes pprof.
-host string -host string
alternate address to host web ui on. alternate address to host web ui on.
-port int -port int

21
api.go
View File

@ -462,7 +462,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
} }
if _, ok := inv.Notify[address]; ok { if _, ok := inv.Notify[address]; ok {
for _, notifyType := range []string{"notify-expiry", "notify-creation"} { for _, notifyType := range []string{"notify-expiry", "notify-creation"} {
if _, ok = inv.Notify[notifyType]; ok { if _, ok = inv.Notify[address][notifyType]; ok {
invite[notifyType] = inv.Notify[address][notifyType] invite[notifyType] = inv.Notify[address][notifyType]
} }
} }
@ -476,13 +476,8 @@ func (app *appContext) GetInvites(gc *gin.Context) {
gc.JSON(200, resp) gc.JSON(200, resp)
} }
type notifySetting struct {
NotifyExpiry bool `json:"notify-expiry"`
NotifyCreation bool `json:"notify-creation"`
}
func (app *appContext) SetNotify(gc *gin.Context) { func (app *appContext) SetNotify(gc *gin.Context) {
var req map[string]notifySetting var req map[string]map[string]bool
gc.BindJSON(&req) gc.BindJSON(&req)
changed := false changed := false
for code, settings := range req { for code, settings := range req {
@ -518,14 +513,14 @@ func (app *appContext) SetNotify(gc *gin.Context) {
} /*else { } /*else {
if _, ok := invite.Notify[address]["notify-expiry"]; !ok { if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
*/ */
if invite.Notify[address]["notify-expiry"] != settings.NotifyExpiry { if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] {
invite.Notify[address]["notify-expiry"] = settings.NotifyExpiry invite.Notify[address]["notify-expiry"] = settings["notify-expiry"]
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings.NotifyExpiry, address) app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address)
changed = true changed = true
} }
if invite.Notify[address]["notify-creation"] != settings.NotifyCreation { if _, ok := settings["notify-creation"]; ok && invite.Notify[address]["notify-creation"] != settings["notify-creation"] {
invite.Notify[address]["notify-creation"] = settings.NotifyCreation invite.Notify[address]["notify-creation"] = settings["notify-creation"]
app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings.NotifyExpiry, address) app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings["notify-creation"], address)
changed = true changed = true
} }
if changed { if changed {

View File

@ -53,12 +53,16 @@ document.getElementById('accountsTabDelete').onclick = function() {
} }
} }
let title = " user"; let title = " user";
let msg = "Notify user";
if (selected.length > 1) { if (selected.length > 1) {
title += "s"; title += "s";
msg += "s";
} }
title = "Delete " + selected.length + title; title = "Delete " + selected.length + title;
msg += " of account deletion";
document.getElementById('deleteModalTitle').textContent = title; document.getElementById('deleteModalTitle').textContent = title;
document.getElementById('deleteModalNotify').checked = false; document.getElementById('deleteModalNotify').checked = false;
document.getElementById('deleteModalNotifyLabel').textContent = msg;
document.getElementById('deleteModalReason').value = ''; document.getElementById('deleteModalReason').value = '';
document.getElementById('deleteModalReasonBox').classList.add('unfocused'); document.getElementById('deleteModalReasonBox').classList.add('unfocused');
document.getElementById('deleteModalSend').textContent = 'Delete'; document.getElementById('deleteModalSend').textContent = 'Delete';
@ -200,8 +204,12 @@ function populateUsers() {
if (admin) { if (admin) {
isAdmin = "Yes"; isAdmin = "Yes";
} }
let fci = "form-check-input";
if (bsVersion != 5) {
fci = "";
}
return ` return `
<td nowrap="nowrap" class="align-middle" scope="row"><input class="form-check-input" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td> <td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
<td nowrap="nowrap" class="align-middle">${username}</td> <td nowrap="nowrap" class="align-middle">${username}</td>
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td> <td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
<td nowrap="nowrap" class="align-middle">${lastActive}</td> <td nowrap="nowrap" class="align-middle">${lastActive}</td>
@ -246,22 +254,7 @@ document.getElementById('accountsTabSetDefaults').onclick = function() {
if (userIDs.length == 0) { if (userIDs.length == 0) {
return; return;
} }
let radioList = document.getElementById('defaultUserRadios'); populateRadios();
radioList.textContent = '';
let first = true;
for (user of jfUsers) {
let radio = document.createElement('div');
radio.classList.add('radio');
let checked = 'checked';
if (first) {
first = false;
} else {
checked = '';
}
radio.innerHTML = `
<label><input type="radio" name="defaultRadios" id="default_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
radioList.appendChild(radio);
}
let userstring = 'user'; let userstring = 'user';
if (userIDs.length > 1) { if (userIDs.length > 1) {
userstring += 's'; userstring += 's';

View File

@ -647,6 +647,28 @@ document.getElementById('openAbout').onclick = function() {
aboutModal.show(); aboutModal.show();
}; };
function populateRadios() {
let radioList = document.getElementById('defaultUserRadios');
radioList.textContent = '';
let first = true;
for (user of jfUsers) {
let radio = document.createElement('div');
radio.classList.add('form-check');
let checked = 'checked';
if (first) {
first = false;
} else {
checked = '';
}
// radio.innerHTML =
// `<label><input type="radio" name="defaultRadios" id="default_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
radio.innerHTML = `
<input class="form-check-input" type="radio" name="defaultRadios" id="default_${user['id']}" ${checked}>
<label class="form-check-label" for="default_${user['id']}">${user['name']}</label>`;
radioList.appendChild(radio);
}
}
document.getElementById('openDefaultsWizard').onclick = function() { document.getElementById('openDefaultsWizard').onclick = function() {
this.disabled = true this.disabled = true
this.innerHTML = this.innerHTML =
@ -659,23 +681,8 @@ document.getElementById('openDefaultsWizard').onclick = function() {
req.onreadystatechange = function() { req.onreadystatechange = function() {
if (this.readyState == 4) { if (this.readyState == 4) {
if (this.status == 200) { if (this.status == 200) {
let users = req.response['users']; jfUsers = req.response['users'];
let radioList = document.getElementById('defaultUserRadios'); populateRadios();
radioList.textContent = '';
let first = true;
for (user of users) {
let radio = document.createElement('div');
radio.classList.add('radio');
let checked = 'checked';
if (first) {
first = false;
} else {
checked = '';
}
radio.innerHTML =
`<label><input type="radio" name="defaultRadios" id="default_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
radioList.appendChild(radio);
}
let button = document.getElementById('openDefaultsWizard'); let button = document.getElementById('openDefaultsWizard');
button.disabled = false; button.disabled = false;
button.innerHTML = 'New User Defaults <i class="fa fa-user settingIcon"></i>'; button.innerHTML = 'New User Defaults <i class="fa fa-user settingIcon"></i>';

View File

@ -162,9 +162,9 @@
</select> </select>
</div> </div>
<div id="defaultUserRadios"></div> <div id="defaultUserRadios"></div>
<div class="checkbox"> <div class="form-check" style="margin-top: 1rem;">
<input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked> <input class="form-check-input" type="checkbox" value="" id="storeDefaultHomescreen" checked>
<label for="storeDefaultHomescreen" id="storeHomescreenLabel"></label> <label class="form-check-label" for="storeDefaultHomescreen" id="storeHomescreenLabel"></label>
</div> </div>
</div> </div>
<div class="modal-footer" id="defaultsFooter"> <div class="modal-footer" id="defaultsFooter">
@ -207,8 +207,8 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" id="applyrestarts" data-dismiss="modal">apply, restart later</button> <button type="button" class="btn btn-secondary" id="applyRestarts" data-dismiss="modal">Apply, Restart later</button>
<button type="button" class="btn btn-primary" id="applyandrestart" data-dismiss="modal">apply &amp; restart</button> <button type="button" class="btn btn-primary" id="applyAndRestart" data-dismiss="modal">Apply &amp; Restart</button>
</div> </div>
</div> </div>
</div> </div>
@ -256,7 +256,7 @@
<div class="modal-body"> <div class="modal-body">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="deleteModalNotify"> <input class="form-check-input" type="checkbox" value="" id="deleteModalNotify">
<label class="form-check-label" for="deleteModalNotify">Notify users of account deletion</label> <label class="form-check-label" for="deleteModalNotify" id="deleteModalNotifyLabel">Notify users of account deletion</label>
</div> </div>
<div class="mb-3 unfocused" id="deleteModalReasonBox"> <div class="mb-3 unfocused" id="deleteModalReasonBox">
<label for="deleteModalReason" class="form-label">Reason for deletion</label> <label for="deleteModalReason" class="form-label">Reason for deletion</label>
@ -408,7 +408,11 @@
<table class="table table-hover table-striped table-borderless"> <table class="table table-hover table-striped table-borderless">
<thead> <thead>
<tr> <tr>
{{ if .bs5 }}
<th scope="col"><input class="form-check-input" type="checkbox" value="" id="selectAll"></th> <th scope="col"><input class="form-check-input" type="checkbox" value="" id="selectAll"></th>
{{ else }}
<th scope="col"><input type="checkbox" value="" id="selectAll"></th>
{{ end }}
<th scope="col">Username</th> <th scope="col">Username</th>
<th scope="col">Email Address</th> <th scope="col">Email Address</th>
<th scope="col">Last Active</th> <th scope="col">Last Active</th>

BIN
images/accounts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

BIN
images/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
images/invites.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB