mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-23 01:20:11 +00:00
Initial Ombi integration
When enabled, an account for the user is created on both Jellyfin and Ombi. Account defaults can be stored similarly to jf.
This commit is contained in:
parent
9850545f1b
commit
ba67fa7536
52
api.go
52
api.go
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -253,6 +254,18 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
|||||||
app.storage.emails[id] = req.Email
|
app.storage.emails[id] = req.Email
|
||||||
app.storage.storeEmails()
|
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)
|
gc.JSON(200, validation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,6 +484,30 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
|||||||
gc.JSON(200, resp)
|
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) {
|
func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||||
var req map[string]string
|
var req map[string]string
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
@ -492,6 +529,21 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
|||||||
gc.JSON(200, map[string]bool{"success": true})
|
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 {
|
type defaultsReq struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Homescreen bool `json:"homescreen"`
|
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"))))
|
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("") == "" {
|
// if app.config.Section("files").Key(key).MustString("") == "" {
|
||||||
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
||||||
// }
|
// }
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"requires_restart": true,
|
"requires_restart": true,
|
||||||
"type": "text",
|
"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."
|
"description": "This and below settings will show on the Jellyfin dashboard when the program connects. You may as well leave them alone."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
@ -55,14 +55,14 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"requires_restart": true,
|
"requires_restart": true,
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "jf-accounts"
|
"value": "jfa-go"
|
||||||
},
|
},
|
||||||
"device_id": {
|
"device_id": {
|
||||||
"name": "Device ID",
|
"name": "Device ID",
|
||||||
"required": true,
|
"required": true,
|
||||||
"requires_restart": true,
|
"requires_restart": true,
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "jf-accounts-{version}"
|
"value": "jfa-go-{version}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
@ -398,7 +398,7 @@
|
|||||||
"depends_true": "enabled",
|
"depends_true": "enabled",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "http://accounts.jellyf.in:8056/invite",
|
"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": {
|
"notifications": {
|
||||||
@ -511,6 +511,38 @@
|
|||||||
"value": "smtp password"
|
"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": {
|
"files": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "File Storage",
|
"name": "File Storage",
|
||||||
@ -532,6 +564,14 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"description": "Location of stored email addresses (json)."
|
"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": {
|
"user_template": {
|
||||||
"name": "User Template",
|
"name": "User Template",
|
||||||
"required": false,
|
"required": false,
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"notifications",
|
"notifications",
|
||||||
"mailgun",
|
"mailgun",
|
||||||
"smtp",
|
"smtp",
|
||||||
|
"ombi",
|
||||||
"files"
|
"files"
|
||||||
],
|
],
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
@ -62,7 +63,7 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"requires_restart": true,
|
"requires_restart": true,
|
||||||
"type": "text",
|
"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."
|
"description": "This and below settings will show on the Jellyfin dashboard when the program connects. You may as well leave them alone."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
@ -77,14 +78,14 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"requires_restart": true,
|
"requires_restart": true,
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "jf-accounts"
|
"value": "jfa-go"
|
||||||
},
|
},
|
||||||
"device_id": {
|
"device_id": {
|
||||||
"name": "Device ID",
|
"name": "Device ID",
|
||||||
"required": true,
|
"required": true,
|
||||||
"requires_restart": true,
|
"requires_restart": true,
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "jf-accounts-{version}"
|
"value": "jfa-go-{version}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
@ -466,7 +467,7 @@
|
|||||||
"depends_true": "enabled",
|
"depends_true": "enabled",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "http://accounts.jellyf.in:8056/invite",
|
"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": {
|
"notifications": {
|
||||||
@ -596,10 +597,48 @@
|
|||||||
"value": "smtp password"
|
"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."
|
||||||
|
},
|
||||||
|
"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": {
|
"files": {
|
||||||
"order": [
|
"order": [
|
||||||
"invites",
|
"invites",
|
||||||
"emails",
|
"emails",
|
||||||
|
"ombi_template",
|
||||||
"user_template",
|
"user_template",
|
||||||
"user_configuration",
|
"user_configuration",
|
||||||
"user_displayprefs",
|
"user_displayprefs",
|
||||||
@ -625,6 +664,14 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"description": "Location of stored email addresses (json)."
|
"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": {
|
"user_template": {
|
||||||
"name": "User Template",
|
"name": "User Template",
|
||||||
"required": false,
|
"required": false,
|
||||||
|
@ -627,9 +627,6 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
|||||||
let users = req.response['users'];
|
let users = req.response['users'];
|
||||||
let radioList = document.getElementById('defaultUserRadios');
|
let radioList = document.getElementById('defaultUserRadios');
|
||||||
radioList.textContent = '';
|
radioList.textContent = '';
|
||||||
if (document.getElementById('setDefaultUser')) {
|
|
||||||
document.getElementById('setDefaultUser').remove();
|
|
||||||
}
|
|
||||||
let first = true;
|
let first = true;
|
||||||
for (user of users) {
|
for (user of users) {
|
||||||
let radio = document.createElement('div');
|
let radio = document.createElement('div');
|
||||||
@ -639,14 +636,14 @@ document.getElementById('openDefaultsWizard').onclick = function() {
|
|||||||
first = false;
|
first = false;
|
||||||
} else {
|
} else {
|
||||||
checked = '';
|
checked = '';
|
||||||
};
|
}
|
||||||
radio.innerHTML =
|
radio.innerHTML =
|
||||||
`<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
`<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
|
||||||
radioList.appendChild(radio);
|
radioList.appendChild(radio);
|
||||||
}
|
}
|
||||||
let button = document.getElementById('openDefaultsWizard');
|
let button = document.getElementById('openDefaultsWizard');
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
button.innerHTML = 'Set new account defaults';
|
button.innerHTML = 'New account defaults';
|
||||||
let submitButton = document.getElementById('storeDefaults');
|
let submitButton = document.getElementById('storeDefaults');
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
submitButton.textContent = 'Submit';
|
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';
|
||||||
|
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 () {
|
document.getElementById('openUsers').onclick = function () {
|
||||||
this.disabled = true;
|
this.disabled = true;
|
||||||
this.innerHTML =
|
this.innerHTML =
|
||||||
|
@ -52,6 +52,8 @@
|
|||||||
}
|
}
|
||||||
css.setAttribute('href', cssFile);
|
css.setAttribute('href', cssFile);
|
||||||
document.head.appendChild(css);
|
document.head.appendChild(css);
|
||||||
|
// store whether ombi is enabled, 1 or 0.
|
||||||
|
var ombiEnabled = {{ .ombiEnabled }}
|
||||||
</script>
|
</script>
|
||||||
{{ if not .bs5 }}
|
{{ 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>
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||||
@ -177,6 +179,11 @@
|
|||||||
<button type="button" class="list-group-item list-group-item-action" id="openDefaultsWizard">
|
<button type="button" class="list-group-item list-group-item-action" id="openDefaultsWizard">
|
||||||
New account defaults
|
New account defaults
|
||||||
</button>
|
</button>
|
||||||
|
{{ if .ombiEnabled }}
|
||||||
|
<button type="button" class="list-group-item list-group-item-action" id="openOmbiDefaults">
|
||||||
|
Ombi user defaults
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="list-group list-group-flush" id="settingsList">
|
<div class="list-group list-group-flush" id="settingsList">
|
||||||
</div>
|
</div>
|
||||||
@ -230,6 +237,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 fade" id="restartModal" role="dialog" aria-labelledby="Restart Warning" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
14
jfapi.go
14
jfapi.go
@ -43,10 +43,10 @@ type Jellyfin struct {
|
|||||||
noFail bool
|
noFail bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (jf *Jellyfin) timeoutHandler() {
|
func timeoutHandler(name, addr string, noFail bool) {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
out := fmt.Sprintf("Failed to authenticate with Jellyfin @ %s: Timed out", jf.server)
|
out := fmt.Sprintf("Failed to authenticate with %s @ %s: Timed out", name, addr)
|
||||||
if jf.noFail {
|
if noFail {
|
||||||
log.Printf(out)
|
log.Printf(out)
|
||||||
} else {
|
} else {
|
||||||
log.Fatalf(out)
|
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)
|
infoUrl := fmt.Sprintf("%s/System/Info/Public", server)
|
||||||
req, _ := http.NewRequest("GET", infoUrl, nil)
|
req, _ := http.NewRequest("GET", infoUrl, nil)
|
||||||
resp, err := jf.httpClient.Do(req)
|
resp, err := jf.httpClient.Do(req)
|
||||||
defer jf.timeoutHandler()
|
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
data, _ := ioutil.ReadAll(resp.Body)
|
data, _ := ioutil.ReadAll(resp.Body)
|
||||||
json.Unmarshal(data, &jf.serverInfo)
|
json.Unmarshal(data, &jf.serverInfo)
|
||||||
@ -106,7 +106,7 @@ func (jf *Jellyfin) authenticate(username, password string) (map[string]interfac
|
|||||||
// loginParams, _ := json.Marshal(jf.loginParams)
|
// loginParams, _ := json.Marshal(jf.loginParams)
|
||||||
url := fmt.Sprintf("%s/Users/authenticatebyname", jf.server)
|
url := fmt.Sprintf("%s/Users/authenticatebyname", jf.server)
|
||||||
req, err := http.NewRequest("POST", url, buffer)
|
req, err := http.NewRequest("POST", url, buffer)
|
||||||
defer jf.timeoutHandler()
|
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
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)
|
req.Header.Add(name, value)
|
||||||
}
|
}
|
||||||
resp, err := jf.httpClient.Do(req)
|
resp, err := jf.httpClient.Do(req)
|
||||||
defer jf.timeoutHandler()
|
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||||
if err != nil || resp.StatusCode != 200 {
|
if err != nil || resp.StatusCode != 200 {
|
||||||
if resp.StatusCode == 401 && jf.authenticated {
|
if resp.StatusCode == 401 && jf.authenticated {
|
||||||
jf.authenticated = false
|
jf.authenticated = false
|
||||||
@ -180,7 +180,7 @@ func (jf *Jellyfin) _post(url string, data map[string]interface{}, response bool
|
|||||||
req.Header.Add(name, value)
|
req.Header.Add(name, value)
|
||||||
}
|
}
|
||||||
resp, err := jf.httpClient.Do(req)
|
resp, err := jf.httpClient.Do(req)
|
||||||
defer jf.timeoutHandler()
|
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
|
||||||
if err != nil || resp.StatusCode != 200 {
|
if err != nil || resp.StatusCode != 200 {
|
||||||
if resp.StatusCode == 401 && jf.authenticated {
|
if resp.StatusCode == 401 && jf.authenticated {
|
||||||
jf.authenticated = false
|
jf.authenticated = false
|
||||||
|
30
main.go
30
main.go
@ -45,6 +45,7 @@ type appContext struct {
|
|||||||
invalidTokens []string
|
invalidTokens []string
|
||||||
jf *Jellyfin
|
jf *Jellyfin
|
||||||
authJf *Jellyfin
|
authJf *Jellyfin
|
||||||
|
ombi *Ombi
|
||||||
datePattern string
|
datePattern string
|
||||||
timePattern string
|
timePattern string
|
||||||
storage Storage
|
storage Storage
|
||||||
@ -225,17 +226,32 @@ func main() {
|
|||||||
|
|
||||||
app.debug.Println("Loading storage")
|
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.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.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.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.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()
|
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")
|
app.configBase_path = filepath.Join(app.local_path, "config-base.json")
|
||||||
config_base, _ := ioutil.ReadFile(app.configBase_path)
|
config_base, _ := ioutil.ReadFile(app.configBase_path)
|
||||||
json.Unmarshal(config_base, &app.configBase)
|
json.Unmarshal(config_base, &app.configBase)
|
||||||
@ -340,6 +356,10 @@ func main() {
|
|||||||
api.POST("/setDefaults", app.SetDefaults)
|
api.POST("/setDefaults", app.SetDefaults)
|
||||||
api.GET("/getConfig", app.GetConfig)
|
api.GET("/getConfig", app.GetConfig)
|
||||||
api.POST("/modifyConfig", app.ModifyConfig)
|
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)
|
app.info.Printf("Starting router @ %s", address)
|
||||||
} else {
|
} else {
|
||||||
router.GET("/", func(gc *gin.Context) {
|
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 {
|
type Storage struct {
|
||||||
timePattern string
|
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
|
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
|
// timePattern: %Y-%m-%dT%H:%M:%S.%f
|
||||||
@ -67,6 +67,14 @@ func (st *Storage) storeDisplayprefs() error {
|
|||||||
return storeJSON(st.displayprefs_path, st.displayprefs)
|
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 {
|
func loadJSON(path string, obj interface{}) error {
|
||||||
var file []byte
|
var file []byte
|
||||||
var err error
|
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)
|
bs5 := app.config.Section("ui").Key("bs5").MustBool(false)
|
||||||
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
||||||
notificationsEnabled, _ := app.config.Section("notifications").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{
|
gc.HTML(http.StatusOK, "admin.html", gin.H{
|
||||||
"bs5": bs5,
|
"bs5": bs5,
|
||||||
"cssFile": app.cssFile,
|
"cssFile": app.cssFile,
|
||||||
@ -18,6 +19,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
|||||||
"notifications": notificationsEnabled,
|
"notifications": notificationsEnabled,
|
||||||
"version": VERSION,
|
"version": VERSION,
|
||||||
"commit": COMMIT,
|
"commit": COMMIT,
|
||||||
|
"ombiEnabled": ombiEnabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user