mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 03:50:10 +00:00
Compare commits
5 Commits
3dd2dbff15
...
67c36dd301
Author | SHA1 | Date | |
---|---|---|---|
67c36dd301 | |||
18458c2b0d | |||
862e85669e | |||
ba67fa7536 | |||
9850545f1b |
@ -11,7 +11,7 @@ before:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod download
|
||||
- python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json
|
||||
- python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||
- python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini --version {{.Version}}
|
||||
- python3 -m pip install libsass
|
||||
- python3 scss/get_node_deps.py
|
||||
- python3 scss/compile.py -y
|
||||
|
2
Makefile
2
Makefile
@ -2,7 +2,7 @@ configuration:
|
||||
echo "Fixing config-base"
|
||||
python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json
|
||||
echo "Generating config-default.ini"
|
||||
python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||
python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini --version git
|
||||
|
||||
sass:
|
||||
echo "Getting libsass"
|
||||
|
@ -10,6 +10,7 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
|
||||
* Granular control over invites: Validity period as well as number of uses can be specified.
|
||||
* 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.
|
||||
* 📨 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.
|
||||
|
52
api.go
52
api.go
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@ -253,6 +254,18 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
||||
app.storage.emails[id] = req.Email
|
||||
app.storage.storeEmails()
|
||||
}
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
app.storage.loadOmbiTemplate()
|
||||
if len(app.storage.ombi_template) != 0 {
|
||||
errors, code, err := app.ombi.newUser(req.Username, req.Password, req.Email, app.storage.ombi_template)
|
||||
if err != nil || code != 200 {
|
||||
app.info.Printf("Failed to create Ombi user (%d): %s", code, err)
|
||||
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
|
||||
} else {
|
||||
app.info.Println("Created Ombi user")
|
||||
}
|
||||
}
|
||||
}
|
||||
gc.JSON(200, validation)
|
||||
}
|
||||
|
||||
@ -471,6 +484,30 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
type ombiUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (app *appContext) OmbiUsers(gc *gin.Context) {
|
||||
app.debug.Println("Ombi users requested")
|
||||
users, status, err := app.ombi.getUsers()
|
||||
if err != nil || status != 200 {
|
||||
app.err.Printf("Failed to get users from Ombi: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
userlist := make([]ombiUser, len(users))
|
||||
for i, data := range users {
|
||||
userlist[i] = ombiUser{
|
||||
Name: data["userName"].(string),
|
||||
ID: data["id"].(string),
|
||||
}
|
||||
}
|
||||
gc.JSON(200, map[string][]ombiUser{"users": userlist})
|
||||
}
|
||||
|
||||
func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
var req map[string]string
|
||||
gc.BindJSON(&req)
|
||||
@ -492,6 +529,21 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
|
||||
var req ombiUser
|
||||
gc.BindJSON(&req)
|
||||
template, code, err := app.ombi.templateByID(req.ID)
|
||||
if err != nil || code != 200 || len(template) == 0 {
|
||||
app.err.Printf("Couldn't get user from Ombi: %d %s", code, err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
app.storage.ombi_template = template
|
||||
fmt.Println(app.storage.ombi_path)
|
||||
app.storage.storeOmbiTemplate()
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
type defaultsReq struct {
|
||||
Username string `json:"username"`
|
||||
Homescreen bool `json:"homescreen"`
|
||||
|
@ -48,7 +48,7 @@ func (app *appContext) loadConfig() error {
|
||||
// }
|
||||
key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json"))))
|
||||
}
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs"} {
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "ombi_template"} {
|
||||
// if app.config.Section("files").Key(key).MustString("") == "" {
|
||||
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
||||
// }
|
||||
|
@ -40,7 +40,7 @@
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jf-accounts",
|
||||
"value": "jfa-go",
|
||||
"description": "This and below settings will show on the Jellyfin dashboard when the program connects. You may as well leave them alone."
|
||||
},
|
||||
"version": {
|
||||
@ -55,14 +55,14 @@
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jf-accounts"
|
||||
"value": "jfa-go"
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jf-accounts-{version}"
|
||||
"value": "jfa-go-{version}"
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
@ -398,7 +398,7 @@
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "http://accounts.jellyf.in:8056/invite",
|
||||
"description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
|
||||
"description": "Base URL for jfa-go. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@ -511,6 +511,38 @@
|
||||
"value": "smtp password"
|
||||
}
|
||||
},
|
||||
"ombi": {
|
||||
"meta": {
|
||||
"name": "Ombi Integration",
|
||||
"description": "Connect to Ombi to automatically create a new user's account. You'll need to create an Ombi user template."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable to create an Ombi account for new Jellyfin users"
|
||||
},
|
||||
"server": {
|
||||
"name": "URL",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "localhost:5000",
|
||||
"depends_true": "enabled",
|
||||
"description": "Ombi server URL, including http(s)://."
|
||||
},
|
||||
"api_key": {
|
||||
"name": "API Key",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"depends_true": "enabled",
|
||||
"description": "API Key. Get this from the first tab in Ombi settings."
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"meta": {
|
||||
"name": "File Storage",
|
||||
@ -532,6 +564,14 @@
|
||||
"value": "",
|
||||
"description": "Location of stored email addresses (json)."
|
||||
},
|
||||
"ombi_template": {
|
||||
"name": "Ombi user template",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored Ombi user template."
|
||||
},
|
||||
"user_template": {
|
||||
"name": "User Template",
|
||||
"required": false,
|
||||
|
@ -7,6 +7,7 @@ from pathlib import Path
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-i", "--input", help="input config base from jf-accounts")
|
||||
parser.add_argument("-o", "--output", help="output ini")
|
||||
parser.add_argument("--version", help="version to include in file")
|
||||
|
||||
|
||||
def generate_ini(base_file, ini_file, version):
|
||||
@ -43,6 +44,8 @@ def generate_ini(base_file, ini_file, version):
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(generate_ini(base_file=args.input,
|
||||
ini_file=args.output,
|
||||
version="0.1.0"))
|
||||
version = "git"
|
||||
if args.version is not None:
|
||||
version = args.version
|
||||
|
||||
print(generate_ini(base_file=args.input, ini_file=args.output, version=version))
|
||||
|
@ -9,6 +9,7 @@
|
||||
"notifications",
|
||||
"mailgun",
|
||||
"smtp",
|
||||
"ombi",
|
||||
"files"
|
||||
],
|
||||
"jellyfin": {
|
||||
@ -62,7 +63,7 @@
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jf-accounts",
|
||||
"value": "jfa-go",
|
||||
"description": "This and below settings will show on the Jellyfin dashboard when the program connects. You may as well leave them alone."
|
||||
},
|
||||
"version": {
|
||||
@ -77,14 +78,14 @@
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jf-accounts"
|
||||
"value": "jfa-go"
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "jf-accounts-{version}"
|
||||
"value": "jfa-go-{version}"
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
@ -466,7 +467,7 @@
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "http://accounts.jellyf.in:8056/invite",
|
||||
"description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
|
||||
"description": "Base URL for jfa-go. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@ -596,10 +597,48 @@
|
||||
"value": "smtp password"
|
||||
}
|
||||
},
|
||||
"ombi": {
|
||||
"order": [
|
||||
"enabled",
|
||||
"server",
|
||||
"api_key"
|
||||
],
|
||||
"meta": {
|
||||
"name": "Ombi Integration",
|
||||
"description": "Connect to Ombi to automatically create a new user's account. You'll need to create an Ombi user template."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable to create an Ombi account for new Jellyfin users"
|
||||
},
|
||||
"server": {
|
||||
"name": "URL",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "localhost:5000",
|
||||
"depends_true": "enabled",
|
||||
"description": "Ombi server URL, including http(s)://."
|
||||
},
|
||||
"api_key": {
|
||||
"name": "API Key",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"depends_true": "enabled",
|
||||
"description": "API Key. Get this from the first tab in Ombi settings."
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"order": [
|
||||
"invites",
|
||||
"emails",
|
||||
"ombi_template",
|
||||
"user_template",
|
||||
"user_configuration",
|
||||
"user_displayprefs",
|
||||
@ -625,6 +664,14 @@
|
||||
"value": "",
|
||||
"description": "Location of stored email addresses (json)."
|
||||
},
|
||||
"ombi_template": {
|
||||
"name": "Ombi user template",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored Ombi user template."
|
||||
},
|
||||
"user_template": {
|
||||
"name": "User Template",
|
||||
"required": false,
|
||||
|
@ -627,9 +627,6 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
let users = req.response['users'];
|
||||
let radioList = document.getElementById('defaultUserRadios');
|
||||
radioList.textContent = '';
|
||||
if (document.getElementById('setDefaultUser')) {
|
||||
document.getElementById('setDefaultUser').remove();
|
||||
}
|
||||
let first = true;
|
||||
for (user of users) {
|
||||
let radio = document.createElement('div');
|
||||
@ -639,14 +636,14 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
first = false;
|
||||
} else {
|
||||
checked = '';
|
||||
};
|
||||
}
|
||||
radio.innerHTML =
|
||||
`<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
let button = document.getElementById('openDefaultsWizard');
|
||||
button.disabled = false;
|
||||
button.innerHTML = 'Set new account defaults';
|
||||
button.innerHTML = 'New User Defaults <i class="fa fa-user settingIcon"></i>';
|
||||
let submitButton = document.getElementById('storeDefaults');
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Submit';
|
||||
@ -715,6 +712,107 @@ document.getElementById('storeDefaults').onclick = function () {
|
||||
}
|
||||
};
|
||||
|
||||
var ombiDefaultsModal = '';
|
||||
if (ombiEnabled) {
|
||||
ombiDefaultsModal = createModal('ombiDefaults');
|
||||
document.getElementById('openOmbiDefaults').onclick = function() {
|
||||
this.disabled = true;
|
||||
this.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
let req = new XMLHttpRequest();
|
||||
req.responseType = 'json';
|
||||
req.open("GET", "/getOmbiUsers", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200) {
|
||||
let users = req.response['users'];
|
||||
let radioList = document.getElementById('ombiUserRadios');
|
||||
radioList.textContent = '';
|
||||
let first = true;
|
||||
// name and id
|
||||
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="ombiRadios" id="default_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
let button = document.getElementById('openOmbiDefaults');
|
||||
button.disabled = false;
|
||||
button.innerHTML = 'Ombi User Defaults <i class="fa fa-chain-broken settingIcon"></i>';
|
||||
let submitButton = document.getElementById('storeOmbiDefaults');
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Submit';
|
||||
if (submitButton.classList.contains('btn-success')) {
|
||||
submitButton.classList.remove('btn-success');
|
||||
submitButton.classList.add('btn-primary');
|
||||
} else if (submitButton.classList.contains('btn-danger')) {
|
||||
submitButton.classList.remove('btn-danger');
|
||||
submitButton.classList.add('btn-primary');
|
||||
}
|
||||
settingsModal.hide();
|
||||
ombiDefaultsModal.show();
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
};
|
||||
document.getElementById('storeOmbiDefaults').onclick = function() {
|
||||
this.disabled = true;
|
||||
this.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
let button = document.getElementById('storeOmbiDefaults');
|
||||
let radios = document.getElementsByName('ombiRadios');
|
||||
for (let radio of radios) {
|
||||
if (radio.checked) {
|
||||
let data = {
|
||||
'id': radio.id.slice(8),
|
||||
};
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/setOmbiDefaults", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
button.textContent = "Success";
|
||||
if (button.classList.contains('btn-danger')) {
|
||||
button.classList.remove('btn-danger');
|
||||
} else if (button.classList.contains('btn-primary')) {
|
||||
button.classList.remove('btn-primary');
|
||||
}
|
||||
button.classList.add('btn-success');
|
||||
button.disabled = false;
|
||||
setTimeout(function() { ombiDefaultsModal.hide(); }, 1000);
|
||||
} else {
|
||||
button.textContent = "Failed";
|
||||
button.classList.remove('btn-primary');
|
||||
button.classList.add('btn-danger');
|
||||
setTimeout(function() {
|
||||
let button = document.getElementById('storeOmbiDefaults');
|
||||
button.textContent = "Submit";
|
||||
button.classList.remove('btn-danger');
|
||||
button.classList.add('btn-primary');
|
||||
button.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('openUsers').onclick = function () {
|
||||
this.disabled = true;
|
||||
this.innerHTML =
|
||||
|
@ -52,6 +52,8 @@
|
||||
}
|
||||
css.setAttribute('href', cssFile);
|
||||
document.head.appendChild(css);
|
||||
// store whether ombi is enabled, 1 or 0.
|
||||
var ombiEnabled = {{ .ombiEnabled }}
|
||||
</script>
|
||||
{{ if not .bs5 }}
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||
@ -130,6 +132,10 @@
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.settingIcon {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
</style>
|
||||
<title>Admin</title>
|
||||
</head>
|
||||
@ -166,17 +172,22 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
<ul class="list-group list-group-flush" style="margin-bottom: 1rem;">
|
||||
<p>Note: <sup class="text-danger">*</sup> Indicates required field, <sup class="text-danger">R</sup> Indicates changes require a restart.</p>
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openAbout">
|
||||
About <i class="fa fa-info-circle"></i>
|
||||
About <i class="fa fa-info-circle settingIcon"></i>
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openUsers">
|
||||
Users <i class="fa fa-user"></i>
|
||||
Email Addresses <i class="fa fa-envelope settingIcon"></i>
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openDefaultsWizard">
|
||||
New account defaults
|
||||
New User Defaults <i class="fa fa-user settingIcon"></i>
|
||||
</button>
|
||||
{{ if .ombiEnabled }}
|
||||
<button type="button" class="list-group-item list-group-item-action" id="openOmbiDefaults">
|
||||
Ombi User Defaults <i class="fa fa-chain-broken settingIcon"></i>
|
||||
</button>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<div class="list-group list-group-flush" id="settingsList">
|
||||
</div>
|
||||
@ -230,6 +241,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .ombiEnabled }}
|
||||
<div class="modal fade" id="ombiDefaults" role="dialog" aria-labelledby="Ombi Users" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="ombiTitle">Ombi user defaults</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Create an Ombi user and configure it to your liking, then choose it from below to store the settings and permissions as a template for all new users.</p>
|
||||
<div id="ombiUserRadios"></div>
|
||||
</div>
|
||||
<div class="modal-footer" id="ombiFooter">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="storeOmbiDefaults">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="modal fade" id="restartModal" role="dialog" aria-labelledby="Restart Warning" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
|
14
jfapi.go
14
jfapi.go
@ -43,10 +43,10 @@ type Jellyfin struct {
|
||||
noFail bool
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) timeoutHandler() {
|
||||
func timeoutHandler(name, addr string, noFail bool) {
|
||||
if r := recover(); r != nil {
|
||||
out := fmt.Sprintf("Failed to authenticate with Jellyfin @ %s: Timed out", jf.server)
|
||||
if jf.noFail {
|
||||
out := fmt.Sprintf("Failed to authenticate with %s @ %s: Timed out", name, addr)
|
||||
if noFail {
|
||||
log.Printf(out)
|
||||
} else {
|
||||
log.Fatalf(out)
|
||||
@ -78,7 +78,7 @@ func newJellyfin(server, client, version, device, deviceId string) (*Jellyfin, e
|
||||
infoUrl := fmt.Sprintf("%s/System/Info/Public", server)
|
||||
req, _ := http.NewRequest("GET", infoUrl, nil)
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer jf.timeoutHandler()
|
||||
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||
if err == nil {
|
||||
data, _ := ioutil.ReadAll(resp.Body)
|
||||
json.Unmarshal(data, &jf.serverInfo)
|
||||
@ -106,7 +106,7 @@ func (jf *Jellyfin) authenticate(username, password string) (map[string]interfac
|
||||
// loginParams, _ := json.Marshal(jf.loginParams)
|
||||
url := fmt.Sprintf("%s/Users/authenticatebyname", jf.server)
|
||||
req, err := http.NewRequest("POST", url, buffer)
|
||||
defer jf.timeoutHandler()
|
||||
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@ -148,7 +148,7 @@ func (jf *Jellyfin) _getReader(url string, params map[string]string) (io.Reader,
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer jf.timeoutHandler()
|
||||
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 && jf.authenticated {
|
||||
jf.authenticated = false
|
||||
@ -180,7 +180,7 @@ func (jf *Jellyfin) _post(url string, data map[string]interface{}, response bool
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer jf.timeoutHandler()
|
||||
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 && jf.authenticated {
|
||||
jf.authenticated = false
|
||||
|
30
main.go
30
main.go
@ -45,6 +45,7 @@ type appContext struct {
|
||||
invalidTokens []string
|
||||
jf *Jellyfin
|
||||
authJf *Jellyfin
|
||||
ombi *Ombi
|
||||
datePattern string
|
||||
timePattern string
|
||||
storage Storage
|
||||
@ -225,17 +226,32 @@ func main() {
|
||||
|
||||
app.debug.Println("Loading storage")
|
||||
|
||||
app.storage.invite_path = filepath.Join(app.data_path, "invites.json")
|
||||
// app.storage.invite_path = filepath.Join(app.data_path, "invites.json")
|
||||
app.storage.invite_path = app.config.Section("files").Key("invites").String()
|
||||
app.storage.loadInvites()
|
||||
app.storage.emails_path = filepath.Join(app.data_path, "emails.json")
|
||||
// app.storage.emails_path = filepath.Join(app.data_path, "emails.json")
|
||||
app.storage.emails_path = app.config.Section("files").Key("emails").String()
|
||||
app.storage.loadEmails()
|
||||
app.storage.policy_path = filepath.Join(app.data_path, "user_template.json")
|
||||
// app.storage.policy_path = filepath.Join(app.data_path, "user_template.json")
|
||||
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
|
||||
app.storage.loadPolicy()
|
||||
app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
|
||||
// app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
|
||||
app.storage.policy_path = app.config.Section("files").Key("user_configuration").String()
|
||||
app.storage.loadConfiguration()
|
||||
app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
|
||||
// app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
|
||||
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
|
||||
app.storage.loadDisplayprefs()
|
||||
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String()
|
||||
app.storage.loadOmbiTemplate()
|
||||
app.ombi = newOmbi(
|
||||
app.config.Section("ombi").Key("server").String(),
|
||||
app.config.Section("ombi").Key("api_key").String(),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
app.configBase_path = filepath.Join(app.local_path, "config-base.json")
|
||||
config_base, _ := ioutil.ReadFile(app.configBase_path)
|
||||
json.Unmarshal(config_base, &app.configBase)
|
||||
@ -340,6 +356,10 @@ func main() {
|
||||
api.POST("/setDefaults", app.SetDefaults)
|
||||
api.GET("/getConfig", app.GetConfig)
|
||||
api.POST("/modifyConfig", app.ModifyConfig)
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
api.GET("/getOmbiUsers", app.OmbiUsers)
|
||||
api.POST("/setOmbiDefaults", app.SetOmbiDefaults)
|
||||
}
|
||||
app.info.Printf("Starting router @ %s", address)
|
||||
} else {
|
||||
router.GET("/", func(gc *gin.Context) {
|
||||
|
167
ombi.go
Normal file
167
ombi.go
Normal file
@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Ombi struct {
|
||||
server, key string
|
||||
header map[string]string
|
||||
httpClient *http.Client
|
||||
noFail bool
|
||||
}
|
||||
|
||||
func newOmbi(server, key string, noFail bool) *Ombi {
|
||||
return &Ombi{
|
||||
server: server,
|
||||
key: key,
|
||||
noFail: noFail,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
header: map[string]string{
|
||||
"ApiKey": key,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (ombi *Ombi) _getReader(url string, params map[string]string) (string, int, error) {
|
||||
if ombi.key == "" {
|
||||
return "", 401, fmt.Errorf("No API key provided")
|
||||
}
|
||||
var req *http.Request
|
||||
if params != nil {
|
||||
jsonParams, _ := json.Marshal(params)
|
||||
req, _ = http.NewRequest("GET", url, bytes.NewBuffer(jsonParams))
|
||||
} else {
|
||||
req, _ = http.NewRequest("GET", url, nil)
|
||||
}
|
||||
for name, value := range ombi.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := ombi.httpClient.Do(req)
|
||||
defer timeoutHandler("Ombi", ombi.server, ombi.noFail)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 {
|
||||
return "", 401, fmt.Errorf("Invalid API Key")
|
||||
}
|
||||
return "", resp.StatusCode, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var data io.Reader
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
data, _ = gzip.NewReader(resp.Body)
|
||||
default:
|
||||
data = resp.Body
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
_, err = io.Copy(buf, data)
|
||||
if err != nil {
|
||||
return "", 500, err
|
||||
}
|
||||
return buf.String(), resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (ombi *Ombi) _post(url string, data map[string]interface{}, response bool) (string, int, error) {
|
||||
responseText := ""
|
||||
params, _ := json.Marshal(data)
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
for name, value := range ombi.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := ombi.httpClient.Do(req)
|
||||
defer timeoutHandler("Ombi", ombi.server, ombi.noFail)
|
||||
if err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201) {
|
||||
if resp.StatusCode == 401 {
|
||||
return "", 401, fmt.Errorf("Invalid API Key")
|
||||
}
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
if response {
|
||||
defer resp.Body.Close()
|
||||
var out io.Reader
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
out, _ = gzip.NewReader(resp.Body)
|
||||
default:
|
||||
out = resp.Body
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
_, err = io.Copy(buf, out)
|
||||
if err != nil {
|
||||
return "", 500, err
|
||||
}
|
||||
responseText = buf.String()
|
||||
}
|
||||
return responseText, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (ombi *Ombi) userByID(id string) (result map[string]interface{}, code int, err error) {
|
||||
resp, code, err := ombi._getReader(fmt.Sprintf("%s/api/v1/Identity/User/%s", ombi.server, id), nil)
|
||||
json.Unmarshal([]byte(resp), &result)
|
||||
return
|
||||
}
|
||||
|
||||
func (ombi *Ombi) getUsers() (result []map[string]interface{}, code int, err error) {
|
||||
resp, code, err := ombi._getReader(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil)
|
||||
json.Unmarshal([]byte(resp), &result)
|
||||
return
|
||||
}
|
||||
|
||||
// Strip these from a user when saving as a template.
|
||||
// We also need to strip userQualityProfiles{"id", "userId"}
|
||||
var stripFromOmbi = []string{
|
||||
"alias",
|
||||
"emailAddress",
|
||||
"hasLoggedIn",
|
||||
"id",
|
||||
"lastLoggedIn",
|
||||
"password",
|
||||
"userName",
|
||||
}
|
||||
|
||||
func (ombi *Ombi) templateByID(id string) (result map[string]interface{}, code int, err error) {
|
||||
result, code, err = ombi.userByID(id)
|
||||
if err != nil || code != 200 {
|
||||
return
|
||||
}
|
||||
for _, key := range stripFromOmbi {
|
||||
if _, ok := result[key]; ok {
|
||||
delete(result, key)
|
||||
}
|
||||
}
|
||||
if qp, ok := result["userQualityProfiles"].(map[string]interface{}); ok {
|
||||
delete(qp, "id")
|
||||
delete(qp, "userId")
|
||||
result["userQualityProfiles"] = qp
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ombi *Ombi) newUser(username, password, email string, template map[string]interface{}) ([]string, int, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/Identity", ombi.server)
|
||||
user := template
|
||||
user["userName"] = username
|
||||
user["password"] = password
|
||||
user["emailAddress"] = email
|
||||
resp, code, err := ombi._post(url, user, true)
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal([]byte(resp), &data)
|
||||
if err != nil || code != 200 {
|
||||
var lst []string
|
||||
if data["errors"] != nil {
|
||||
lst = data["errors"].([]string)
|
||||
}
|
||||
return lst, code, err
|
||||
}
|
||||
return nil, code, err
|
||||
}
|
12
storage.go
12
storage.go
@ -8,9 +8,9 @@ import (
|
||||
|
||||
type Storage struct {
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path string
|
||||
invites Invites
|
||||
emails, policy, configuration, displayprefs map[string]interface{}
|
||||
emails, policy, configuration, displayprefs, ombi_template map[string]interface{}
|
||||
}
|
||||
|
||||
// timePattern: %Y-%m-%dT%H:%M:%S.%f
|
||||
@ -67,6 +67,14 @@ func (st *Storage) storeDisplayprefs() error {
|
||||
return storeJSON(st.displayprefs_path, st.displayprefs)
|
||||
}
|
||||
|
||||
func (st *Storage) loadOmbiTemplate() error {
|
||||
return loadJSON(st.ombi_path, &st.ombi_template)
|
||||
}
|
||||
|
||||
func (st *Storage) storeOmbiTemplate() error {
|
||||
return storeJSON(st.ombi_path, st.ombi_template)
|
||||
}
|
||||
|
||||
func loadJSON(path string, obj interface{}) error {
|
||||
var file []byte
|
||||
var err error
|
||||
|
2
views.go
2
views.go
@ -10,6 +10,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
bs5 := app.config.Section("ui").Key("bs5").MustBool(false)
|
||||
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)
|
||||
gc.HTML(http.StatusOK, "admin.html", gin.H{
|
||||
"bs5": bs5,
|
||||
"cssFile": app.cssFile,
|
||||
@ -18,6 +19,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
"notifications": notificationsEnabled,
|
||||
"version": VERSION,
|
||||
"commit": COMMIT,
|
||||
"ombiEnabled": ombiEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user