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.
|
||||
* 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.
|
||||
* 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 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.
|
||||
@ -24,12 +25,12 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
|
||||
|
||||
## Interface
|
||||
<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 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://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/invites.png" width="48%" style="margin-left: 1.5%;" alt="Invites tab"></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>
|
||||
|
||||
#### Install
|
||||
@ -69,6 +70,8 @@ Usage of ./jfa-go:
|
||||
alternate path to config file. (default "~/.config/jfa-go/config.ini")
|
||||
-data string
|
||||
alternate path to data directory. (default "~/.config/jfa-go")
|
||||
-debug
|
||||
Enables debug logging and exposes pprof.
|
||||
-host string
|
||||
alternate address to host web ui on.
|
||||
-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 {
|
||||
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]
|
||||
}
|
||||
}
|
||||
@ -476,13 +476,8 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
type notifySetting struct {
|
||||
NotifyExpiry bool `json:"notify-expiry"`
|
||||
NotifyCreation bool `json:"notify-creation"`
|
||||
}
|
||||
|
||||
func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
var req map[string]notifySetting
|
||||
var req map[string]map[string]bool
|
||||
gc.BindJSON(&req)
|
||||
changed := false
|
||||
for code, settings := range req {
|
||||
@ -518,14 +513,14 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
} /*else {
|
||||
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
|
||||
*/
|
||||
if invite.Notify[address]["notify-expiry"] != settings.NotifyExpiry {
|
||||
invite.Notify[address]["notify-expiry"] = settings.NotifyExpiry
|
||||
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings.NotifyExpiry, address)
|
||||
if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] {
|
||||
invite.Notify[address]["notify-expiry"] = settings["notify-expiry"]
|
||||
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address)
|
||||
changed = true
|
||||
}
|
||||
if invite.Notify[address]["notify-creation"] != settings.NotifyCreation {
|
||||
invite.Notify[address]["notify-creation"] = settings.NotifyCreation
|
||||
app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings.NotifyExpiry, address)
|
||||
if _, ok := settings["notify-creation"]; ok && invite.Notify[address]["notify-creation"] != settings["notify-creation"] {
|
||||
invite.Notify[address]["notify-creation"] = settings["notify-creation"]
|
||||
app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings["notify-creation"], address)
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
|
@ -53,12 +53,16 @@ document.getElementById('accountsTabDelete').onclick = function() {
|
||||
}
|
||||
}
|
||||
let title = " user";
|
||||
let msg = "Notify user";
|
||||
if (selected.length > 1) {
|
||||
title += "s";
|
||||
msg += "s";
|
||||
}
|
||||
title = "Delete " + selected.length + title;
|
||||
msg += " of account deletion";
|
||||
document.getElementById('deleteModalTitle').textContent = title;
|
||||
document.getElementById('deleteModalNotify').checked = false;
|
||||
document.getElementById('deleteModalNotifyLabel').textContent = msg;
|
||||
document.getElementById('deleteModalReason').value = '';
|
||||
document.getElementById('deleteModalReasonBox').classList.add('unfocused');
|
||||
document.getElementById('deleteModalSend').textContent = 'Delete';
|
||||
@ -200,8 +204,12 @@ function populateUsers() {
|
||||
if (admin) {
|
||||
isAdmin = "Yes";
|
||||
}
|
||||
let fci = "form-check-input";
|
||||
if (bsVersion != 5) {
|
||||
fci = "";
|
||||
}
|
||||
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">${generateEmail(id, name, email)}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
|
||||
@ -246,22 +254,7 @@ document.getElementById('accountsTabSetDefaults').onclick = function() {
|
||||
if (userIDs.length == 0) {
|
||||
return;
|
||||
}
|
||||
let radioList = document.getElementById('defaultUserRadios');
|
||||
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);
|
||||
}
|
||||
populateRadios();
|
||||
let userstring = 'user';
|
||||
if (userIDs.length > 1) {
|
||||
userstring += 's';
|
||||
|
@ -647,6 +647,28 @@ document.getElementById('openAbout').onclick = function() {
|
||||
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() {
|
||||
this.disabled = true
|
||||
this.innerHTML =
|
||||
@ -659,23 +681,8 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200) {
|
||||
let users = req.response['users'];
|
||||
let radioList = document.getElementById('defaultUserRadios');
|
||||
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);
|
||||
}
|
||||
jfUsers = req.response['users'];
|
||||
populateRadios();
|
||||
let button = document.getElementById('openDefaultsWizard');
|
||||
button.disabled = false;
|
||||
button.innerHTML = 'New User Defaults <i class="fa fa-user settingIcon"></i>';
|
||||
|
@ -162,9 +162,9 @@
|
||||
</select>
|
||||
</div>
|
||||
<div id="defaultUserRadios"></div>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" value="" style="margin-right: 1rem;" id="storeDefaultHomescreen" checked>
|
||||
<label for="storeDefaultHomescreen" id="storeHomescreenLabel"></label>
|
||||
<div class="form-check" style="margin-top: 1rem;">
|
||||
<input class="form-check-input" type="checkbox" value="" id="storeDefaultHomescreen" checked>
|
||||
<label class="form-check-label" for="storeDefaultHomescreen" id="storeHomescreenLabel"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" id="defaultsFooter">
|
||||
@ -207,8 +207,8 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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-primary" id="applyandrestart" data-dismiss="modal">apply & restart</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -256,7 +256,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="form-check">
|
||||
<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 class="mb-3 unfocused" id="deleteModalReasonBox">
|
||||
<label for="deleteModalReason" class="form-label">Reason for deletion</label>
|
||||
@ -408,7 +408,11 @@
|
||||
<table class="table table-hover table-striped table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
{{ if .bs5 }}
|
||||
<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">Email Address</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