mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 03:50:10 +00:00
Compare commits
5 Commits
5ba40cd6f8
...
49b056f1d6
Author | SHA1 | Date | |
---|---|---|---|
49b056f1d6 | |||
70cf706a82 | |||
7c247b0aae | |||
4e8628844e | |||
31aece5026 |
@ -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
21
api.go
@ -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 {
|
||||||
|
@ -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';
|
||||||
|
@ -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>';
|
||||||
|
@ -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 & restart</button>
|
<button type="button" class="btn btn-primary" id="applyAndRestart" data-dismiss="modal">Apply & 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
BIN
images/accounts.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
BIN
images/admin.png
BIN
images/admin.png
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
BIN
images/demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
BIN
images/invites.png
Normal file
BIN
images/invites.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
images/jfa.gif
BIN
images/jfa.gif
Binary file not shown.
Before Width: | Height: | Size: 3.2 MiB |
Loading…
Reference in New Issue
Block a user