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

Compare commits

...

5 Commits

Author SHA1 Message Date
67c36dd301
mention ombi in readme 2020-09-05 17:59:20 +01:00
18458c2b0d
fix versioning mistake 2020-09-05 17:52:28 +01:00
862e85669e
polish settings menu 2020-09-05 17:49:23 +01:00
ba67fa7536
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.
2020-09-05 17:32:49 +01:00
9850545f1b
add version to default config file 2020-09-05 17:32:13 +01:00
15 changed files with 510 additions and 39 deletions

View File

@ -11,7 +11,7 @@ before:
# You may remove this if you don't use go modules. # You may remove this if you don't use go modules.
- go mod download - go mod download
- python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json - 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 -m pip install libsass
- python3 scss/get_node_deps.py - python3 scss/get_node_deps.py
- python3 scss/compile.py -y - python3 scss/compile.py -y

View File

@ -2,7 +2,7 @@ configuration:
echo "Fixing config-base" echo "Fixing config-base"
python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json
echo "Generating config-default.ini" 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: sass:
echo "Getting libsass" echo "Getting libsass"

View File

@ -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. * 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. * 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.
* 📨 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.

52
api.go
View File

@ -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"`

View File

@ -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")))
// } // }

View File

@ -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,

View File

@ -7,6 +7,7 @@ from pathlib import Path
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input", help="input config base from jf-accounts") parser.add_argument("-i", "--input", help="input config base from jf-accounts")
parser.add_argument("-o", "--output", help="output ini") 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): def generate_ini(base_file, ini_file, version):
@ -43,6 +44,8 @@ def generate_ini(base_file, ini_file, version):
args = parser.parse_args() args = parser.parse_args()
print(generate_ini(base_file=args.input, version = "git"
ini_file=args.output, if args.version is not None:
version="0.1.0")) version = args.version
print(generate_ini(base_file=args.input, ini_file=args.output, version=version))

View File

@ -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, 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": {
"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,

View File

@ -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 User Defaults <i class="fa fa-user settingIcon"></i>';
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 <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 () { document.getElementById('openUsers').onclick = function () {
this.disabled = true; this.disabled = true;
this.innerHTML = this.innerHTML =

View File

@ -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>
@ -130,6 +132,10 @@
white-space: nowrap; white-space: nowrap;
width: auto; width: auto;
} }
.settingIcon {
margin-left: 0.2rem;
}
</style> </style>
<title>Admin</title> <title>Admin</title>
</head> </head>
@ -166,17 +172,22 @@
</button> </button>
</div> </div>
<div class="modal-body"> <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> <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"> <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>
<button type="button" class="list-group-item list-group-item-action" id="openUsers"> <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>
<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 User Defaults <i class="fa fa-user settingIcon"></i>
</button> </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> </ul>
<div class="list-group list-group-flush" id="settingsList"> <div class="list-group list-group-flush" id="settingsList">
</div> </div>
@ -230,6 +241,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">&times;</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">

View File

@ -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
View File

@ -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
View 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
}

View File

@ -7,10 +7,10 @@ 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

View File

@ -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,
}) })
} }