1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-29 12:30:11 +00:00

Compare commits

..

No commits in common. "0f4e77364bd6d9604b20a580ea7ae85d040c6907" and "a89dc40ff2e769359e05206a99ba25a359141be2" have entirely different histories.

10 changed files with 77 additions and 188 deletions

68
api.go
View File

@ -194,29 +194,6 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
return false return false
} }
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
ombiUsers, code, err := app.ombi.getUsers()
if err != nil || code != 200 {
return nil, code, err
}
jfUser, code, err := app.jf.userById(jfID, false)
if err != nil || code != 200 {
return nil, code, err
}
username := jfUser["Name"].(string)
email := app.storage.emails[jfID].(string)
for _, ombiUser := range ombiUsers {
ombiAddr := ""
if a, ok := ombiUser["emailAddress"]; ok && a != nil {
ombiAddr = a.(string)
}
if ombiUser["userName"].(string) == username || (ombiAddr == email && email != "") {
return ombiUser, code, err
}
}
return nil, 400, fmt.Errorf("Couldn't find user")
}
// Routes from now on! // Routes from now on!
// @Summary Creates a new Jellyfin user without an invite. // @Summary Creates a new Jellyfin user without an invite.
@ -409,12 +386,31 @@ func (app *appContext) DeleteUser(gc *gin.Context) {
gc.BindJSON(&req) gc.BindJSON(&req)
errors := map[string]string{} errors := map[string]string{}
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
ombiUsers := []map[string]interface{}{}
var code int
var err error
if ombiEnabled {
ombiUsers, code, err = app.ombi.getUsers()
if code != 200 || err != nil {
respond(500, fmt.Sprintf("Couldn't get users: %s (%s)", code, err), gc)
return
}
}
for _, userID := range req.Users { for _, userID := range req.Users {
if ombiEnabled { if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(userID) ombiID := ""
if code == 200 && err == nil { user, status, err := app.jf.userById(userID, false)
if id, ok := ombiUser["id"]; ok { if err == nil && status == 200 {
status, err := app.ombi.deleteUser(id.(string)) username := user["Name"].(string)
email := app.storage.emails[userID].(string)
for _, ombiUser := range ombiUsers {
if ombiUser["userName"].(string) == username || (ombiUser["emailAddress"].(string) == email && email != "") {
ombiID = ombiUser["id"].(string)
break
}
}
if ombiID != "" {
status, err := app.ombi.deleteUser(ombiID)
if err != nil || status != 200 { if err != nil || status != 200 {
app.err.Printf("Failed to delete ombi user: %d %s", status, err) app.err.Printf("Failed to delete ombi user: %d %s", status, err)
errors[userID] = fmt.Sprintf("Ombi: %d %s, ", status, err) errors[userID] = fmt.Sprintf("Ombi: %d %s, ", status, err)
@ -609,7 +605,7 @@ func (app *appContext) SetDefaultProfile(gc *gin.Context) {
// @Security ApiKeyBlankPassword // @Security ApiKeyBlankPassword
// @tags Profiles & Settings // @tags Profiles & Settings
func (app *appContext) CreateProfile(gc *gin.Context) { func (app *appContext) CreateProfile(gc *gin.Context) {
app.info.Println("Profile creation requested") fmt.Println("Profile creation requested")
var req newProfileDTO var req newProfileDTO
gc.BindJSON(&req) gc.BindJSON(&req)
user, status, err := app.jf.userById(req.ID, false) user, status, err := app.jf.userById(req.ID, false)
@ -914,6 +910,7 @@ func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
return return
} }
app.storage.ombi_template = template app.storage.ombi_template = template
fmt.Println(app.storage.ombi_path)
app.storage.storeOmbiTemplate() app.storage.storeOmbiTemplate()
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -929,6 +926,7 @@ func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
func (app *appContext) ModifyEmails(gc *gin.Context) { func (app *appContext) ModifyEmails(gc *gin.Context) {
var req modifyEmailsDTO var req modifyEmailsDTO
gc.BindJSON(&req) gc.BindJSON(&req)
fmt.Println(req)
app.debug.Println("Email modification requested") app.debug.Println("Email modification requested")
users, status, err := app.jf.getUsers(false) users, status, err := app.jf.getUsers(false)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
@ -937,21 +935,9 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
respond(500, "Couldn't get users", gc) respond(500, "Couldn't get users", gc)
return return
} }
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
for _, jfUser := range users { for _, jfUser := range users {
id := jfUser["Id"].(string) if address, ok := req[jfUser["Id"].(string)]; ok {
if address, ok := req[id]; ok {
app.storage.emails[jfUser["Id"].(string)] = address app.storage.emails[jfUser["Id"].(string)] = address
if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(id)
if code == 200 && err == nil {
ombiUser["emailAddress"] = address
code, err = app.ombi.modifyUser(ombiUser)
if code != 200 || err != nil {
app.err.Printf("%s: Failed to change ombi email address: %d %s", ombiUser["userName"].(string), code, err)
}
}
}
} }
} }
app.storage.storeEmails() app.storage.storeEmails()

View File

@ -49,17 +49,6 @@
"name": "General", "name": "General",
"description": "Settings related to the UI and program functionality." "description": "Settings related to the UI and program functionality."
}, },
"language": {
"name": "Language",
"required": false,
"requires_restart": true,
"type": "select",
"options": [
"en-us"
],
"value": "en-US",
"description": "UI Language. Currently only implemented for account creation form. Submit a PR on github if you'd like to translate."
},
"theme": { "theme": {
"name": "Default Look", "name": "Default Look",
"required": false, "required": false,

View File

@ -1,39 +0,0 @@
{
"meta": {
"name": "English (US)"
},
"strings": {
"pageTitle": "Create Jellyfin Account",
"createAccountHeader": "Create Account",
"accountDetails": "Details",
"emailAddress": "Email",
"username": "Username",
"password": "Password",
"createAccountButton": "Create Account",
"passwordRequirementsHeader": "Password Requirements",
"successHeader": "Success!",
"successContinueButton": "Continue",
"validationStrings": {
"length": {
"singular": "Must have at least {n} character",
"plural": "Must have a least {n} characters"
},
"uppercase": {
"singular": "Must have at least {n} uppercase character",
"plural": "Must have at least {n} uppercase characters"
},
"lowercase": {
"singular": "Must have at least {n} lowercase character",
"plural": "Must have at least {n} lowercase characters"
},
"number": {
"singular": "Must have at least {n} number",
"plural": "Must have at least {n} numbers"
},
"special": {
"singular": "Must have at least {n} special character",
"plural": "Must have at least {n} special characters"
}
}
}
}

View File

@ -1,8 +1,7 @@
{{ define "form-base" }} {{ define "form-base" }}
<script> <script>
window.bs5 = {{ .settings.bs5 }}; window.bs5 = {{ .bs5 }};
window.usernameEnabled = {{ .settings.username }}; window.usernameEnabled = {{ .username }};
window.validationStrings = JSON.parse({{ .lang.validationStrings }});
</script> </script>
<script src="form.js" type="module"></script> <script src="form.js" type="module"></script>
{{ end }} {{ end }}

View File

@ -41,27 +41,27 @@
margin-bottom: 5%; margin-bottom: 5%;
} }
</style> </style>
<title>{{ .lang.pageTitle }}</title> <title>Create Jellyfin Account</title>
</head> </head>
<body> <body>
<div class="modal fade" id="successBox" tabindex="-1" role="dialog" aria-labelledby="successBox" aria-hidden="true" data-backdrop="static"> <div class="modal fade" id="successBox" tabindex="-1" role="dialog" aria-labelledby="successBox" aria-hidden="true" data-backdrop="static">
<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">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="successHeader">{{ .lang.successHeader }}</h5> <h5 class="modal-title" id="successTitle">Success!</h5>
</div> </div>
<div class="modal-body" id="successBody"> <div class="modal-body" id="successBody">
<p>{{ .successMessage }}</p> <p>{{ .successMessage }}</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="{{ .jfLink }}" class="btn btn-primary">{{ .lang.successContinueButton }}</a> <a href="{{ .jfLink }}" class="btn btn-primary">Continue</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="pageContainer"> <div class="pageContainer">
<h1> <h1>
{{ .lang.createAccountHeader }} Create Account
</h1> </h1>
<p>{{ .helpMessage }}</p> <p>{{ .helpMessage }}</p>
<p class="contactBox">{{ .contactMessage }}</p> <p class="contactBox">{{ .contactMessage }}</p>
@ -69,26 +69,26 @@
<div class="row" id="cardContainer"> <div class="row" id="cardContainer">
<div class="col-sm"> <div class="col-sm">
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header">{{ .lang.accountDetails }}</div> <div class="card-header">Details</div>
<div class="card-body"> <div class="card-body">
<form action="#" method="POST" id="accountForm"> <form action="#" method="POST" id="accountForm">
<div class="form-group"> <div class="form-group">
<label for="inputEmail">{{ .lang.emailAddress }}</label> <label for="inputEmail">Email</label>
<input type="email" class="form-control" id="{{ if .settings.username }}inputEmail{{ else }}inputUsername{{ end }}" name="{{ if .settings.username }}email{{ else }}username{{ end }}" placeholder="{{ .lang.emailAddress }}" value="{{ .email }}" required> <input type="email" class="form-control" id="{{ if .settings.username }}inputEmail{{ else }}inputUsername{{ end }}" name="{{ if .settings.username }}email{{ else }}username{{ end }}" placeholder="Email" value="{{ .email }}" required>
</div> </div>
{{ if .settings.username }} {{ if .settings.username }}
<div class="form-group"> <div class="form-group">
<label for="inputUsername">{{ .lang.username }}</label> <label for="inputUsername">Username</label>
<input type="username" class="form-control" id="inputUsername" name="username" placeholder="{{ .lang.username }}" required> <input type="username" class="form-control" id="inputUsername" name="username" placeholder="Username" required>
</div> </div>
{{ end }} {{ end }}
<div class="form-group"> <div class="form-group">
<label for="inputPassword">{{ .lang.password }}</label> <label for="inputPassword">Password</label>
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="{{ .lang.password }}" required> <input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
</div> </div>
<div class="btn-group" role="group" aria-label="Button & Error" id="errorBox" style="margin-top: 1rem;"> <div class="btn-group" role="group" aria-label="Button & Error" id="errorBox" style="margin-top: 1rem;">
<button type="submit" class="btn btn-outline-primary" id="submitButton"> <button type="submit" class="btn btn-outline-primary" id="submitButton">
<span id="createAccount">{{ .lang.createAccountButton }}</span> <span id="createAccount">Create Account</span>
</button> </button>
</div> </div>
</form> </form>
@ -98,7 +98,7 @@
{{ if .validate }} {{ if .validate }}
<div class="col-sm" id="requirementBox"> <div class="col-sm" id="requirementBox">
<div class="card mb-3 requirementBox"> <div class="card mb-3 requirementBox">
<div class="card-header">{{ .lang.passwordRequirementsHeader }}</div> <div class="card-header">Password Requirements</div>
<div class="card-body"> <div class="card-body">
<ul class="list-group"> <ul class="list-group">
{{ range $key, $value := .requirements }} {{ range $key, $value := .requirements }}
@ -114,10 +114,31 @@
</div> </div>
</div> </div>
</div> </div>
<script> <script>
window.validationStrings = {{ .lang.validationStrings }}; window.validationStrings = {
"length": {
"singular": "Must have at least {n} character",
"plural": "Must have a least {n} characters"
},
"uppercase": {
"singular": "Must have at least {n} uppercase character",
"plural": "Must have at least {n} uppercase characters"
},
"lowercase": {
"singular": "Must have at least {n} lowercase character",
"plural": "Must have at least {n} lowercase characters"
},
"number": {
"singular": "Must have at least {n} number",
"plural": "Must have at least {n} numbers"
},
"special": {
"singular": "Must have at least {n} special character",
"plural": "Must have at least {n} special characters"
}
};
</script> </script>
{{ template "form-base" . }} {{ template "form-base" .settings }}
</body> </body>
</html> </html>

View File

@ -263,12 +263,6 @@ func start(asDaemon, firstCall bool) {
if app.loadConfig() != nil { if app.loadConfig() != nil {
app.err.Fatalf("Failed to load config file \"%s\"", app.config_path) app.err.Fatalf("Failed to load config file \"%s\"", app.config_path)
} }
lang := app.config.Section("ui").Key("language").MustString("en-us")
app.storage.lang.FormPath = filepath.Join(app.local_path, "lang", "form", lang+".json")
if _, err := os.Stat(app.storage.lang.FormPath); os.IsNotExist(err) {
app.storage.lang.FormPath = filepath.Join(app.local_path, "lang", "form", "en-us.json")
}
app.storage.loadLang()
app.version = app.config.Section("jellyfin").Key("version").String() app.version = app.config.Section("jellyfin").Key("version").String()
// read from config... // read from config...
debugMode = app.config.Section("ui").Key("debug").MustBool(false) debugMode = app.config.Section("ui").Key("debug").MustBool(false)

44
ombi.go
View File

@ -16,9 +16,6 @@ type Ombi struct {
header map[string]string header map[string]string
httpClient *http.Client httpClient *http.Client
noFail bool noFail bool
userCache []map[string]interface{}
cacheExpiry time.Time
cacheLength int
} }
func newOmbi(server, key string, noFail bool) *Ombi { func newOmbi(server, key string, noFail bool) *Ombi {
@ -32,12 +29,10 @@ func newOmbi(server, key string, noFail bool) *Ombi {
header: map[string]string{ header: map[string]string{
"ApiKey": key, "ApiKey": key,
}, },
cacheLength: 30,
cacheExpiry: time.Now(),
} }
} }
// does a GET and returns the response as a string. // does a GET and returns the response as an io.reader.
func (ombi *Ombi) _getJSON(url string, params map[string]string) (string, int, error) { func (ombi *Ombi) _getJSON(url string, params map[string]string) (string, int, error) {
if ombi.key == "" { if ombi.key == "" {
return "", 401, fmt.Errorf("No API key provided") return "", 401, fmt.Errorf("No API key provided")
@ -77,10 +72,10 @@ func (ombi *Ombi) _getJSON(url string, params map[string]string) (string, int, e
} }
// does a POST and optionally returns response as string. Returns a string instead of an io.reader bcs i couldn't get it working otherwise. // does a POST and optionally returns response as string. Returns a string instead of an io.reader bcs i couldn't get it working otherwise.
func (ombi *Ombi) _send(mode string, url string, data map[string]interface{}, response bool) (string, int, error) { func (ombi *Ombi) _post(url string, data map[string]interface{}, response bool) (string, int, error) {
responseText := "" responseText := ""
params, _ := json.Marshal(data) params, _ := json.Marshal(data)
req, _ := http.NewRequest(mode, url, bytes.NewBuffer(params)) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params))
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
for name, value := range ombi.header { for name, value := range ombi.header {
req.Header.Add(name, value) req.Header.Add(name, value)
@ -112,23 +107,6 @@ func (ombi *Ombi) _send(mode string, url string, data map[string]interface{}, re
return responseText, resp.StatusCode, nil return responseText, resp.StatusCode, nil
} }
func (ombi *Ombi) _post(url string, data map[string]interface{}, response bool) (string, int, error) {
return ombi._send("POST", url, data, response)
}
func (ombi *Ombi) _put(url string, data map[string]interface{}, response bool) (string, int, error) {
return ombi._send("PUT", url, data, response)
}
func (ombi *Ombi) modifyUser(user map[string]interface{}) (status int, err error) {
if _, ok := user["id"]; !ok {
err = fmt.Errorf("No ID provided")
return
}
_, status, err = ombi._put(ombi.server+"/api/v1/Identity", user, false)
return
}
func (ombi *Ombi) deleteUser(id string) (code int, err error) { func (ombi *Ombi) deleteUser(id string) (code int, err error) {
url := fmt.Sprintf("%s/api/v1/Identity/%s", ombi.server, id) url := fmt.Sprintf("%s/api/v1/Identity/%s", ombi.server, id)
req, _ := http.NewRequest("DELETE", url, nil) req, _ := http.NewRequest("DELETE", url, nil)
@ -149,18 +127,10 @@ func (ombi *Ombi) userByID(id string) (result map[string]interface{}, code int,
} }
// gets a list of all users. // gets a list of all users.
func (ombi *Ombi) getUsers() ([]map[string]interface{}, int, error) { func (ombi *Ombi) getUsers() (result []map[string]interface{}, code int, err error) {
if time.Now().After(ombi.cacheExpiry) { resp, code, err := ombi._getJSON(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil)
resp, code, err := ombi._getJSON(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil) json.Unmarshal([]byte(resp), &result)
var result []map[string]interface{} return
json.Unmarshal([]byte(resp), &result)
ombi.userCache = result
if (code == 200 || code == 204) && err == nil {
ombi.cacheExpiry = time.Now().Add(time.Minute * time.Duration(ombi.cacheLength))
}
return result, code, err
}
return ombi.userCache, 200, nil
} }
// Strip these from a user when saving as a template. // Strip these from a user when saving as a template.

View File

@ -14,12 +14,6 @@ type Storage struct {
profiles map[string]Profile profiles map[string]Profile
defaultProfile string defaultProfile string
emails, policy, configuration, displayprefs, ombi_template map[string]interface{} emails, policy, configuration, displayprefs, ombi_template map[string]interface{}
lang Lang
}
type Lang struct {
FormPath string
Form map[string]interface{}
} }
// timePattern: %Y-%m-%dT%H:%M:%S.%f // timePattern: %Y-%m-%dT%H:%M:%S.%f
@ -55,22 +49,6 @@ func (st *Storage) storeInvites() error {
return storeJSON(st.invite_path, st.invites) return storeJSON(st.invite_path, st.invites)
} }
func (st *Storage) loadLang() error {
err := loadJSON(st.lang.FormPath, &st.lang.Form)
if err != nil {
return err
}
strings := st.lang.Form["strings"].(map[string]interface{})
validationStrings := strings["validationStrings"].(map[string]interface{})
vS, err := json.Marshal(validationStrings)
if err != nil {
return err
}
strings["validationStrings"] = string(vS)
st.lang.Form["strings"] = strings
return nil
}
func (st *Storage) loadEmails() error { func (st *Storage) loadEmails() error {
return loadJSON(st.emails_path, &st.emails) return loadJSON(st.emails_path, &st.emails)
} }

View File

@ -41,18 +41,15 @@ var defaultPwValStrings: pwValStrings = {
} }
} }
const toggleSpinner = (ogText?: string): string => { const toggleSpinner = (): void => {
const submitButton = document.getElementById('submitButton') as HTMLButtonElement; const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
if (document.getElementById('createAccountSpinner')) { if (document.getElementById('createAccountSpinner')) {
submitButton.innerHTML = ogText ? ogText : `<span>Create Account</span>`; submitButton.innerHTML = `<span>Create Account</span>`;
submitButton.disabled = false; submitButton.disabled = false;
return "";
} else { } else {
let ogText = submitButton.innerHTML;
submitButton.innerHTML = ` submitButton.innerHTML = `
<span id="createAccountSpinner" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>Creating... <span id="createAccountSpinner" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>Creating...
`; `;
return ogText;
} }
}; };
@ -87,7 +84,7 @@ var code = window.location.href.split('/').pop();
if (el) { if (el) {
el.remove(); el.remove();
} }
const ogText = toggleSpinner(); toggleSpinner();
let send: Object = serializeForm('accountForm'); let send: Object = serializeForm('accountForm');
send["code"] = code; send["code"] = code;
if (!window.usernameEnabled) { if (!window.usernameEnabled) {
@ -95,7 +92,7 @@ var code = window.location.href.split('/').pop();
} }
_post("/newUser", send, function (): void { _post("/newUser", send, function (): void {
if (this.readyState == 4) { if (this.readyState == 4) {
toggleSpinner(ogText); toggleSpinner();
let data: Object = this.response; let data: Object = this.response;
const errorGiven = ("error" in data) const errorGiven = ("error" in data)
if (errorGiven || data["success"] === false) { if (errorGiven || data["success"] === false) {

View File

@ -1,8 +1,6 @@
package main package main
import ( import (
"encoding/json"
"fmt"
"net/http" "net/http"
"strings" "strings"
@ -49,11 +47,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"bs5": app.config.Section("ui").Key("bs5").MustBool(false), "bs5": app.config.Section("ui").Key("bs5").MustBool(false),
"username": !app.config.Section("email").Key("no_username").MustBool(false), "username": !app.config.Section("email").Key("no_username").MustBool(false),
}, },
"lang": app.storage.lang.Form["strings"],
}) })
l, _ := json.MarshalIndent(app.storage.lang.Form, "", " ")
fmt.Println(app.storage.lang.Form["strings"].(map[string]interface{})["validationStrings"].(string))
fmt.Printf("%s\n", l)
} else { } else {
gc.HTML(404, "invalidCode.html", gin.H{ gc.HTML(404, "invalidCode.html", gin.H{
"bs5": app.config.Section("ui").Key("bs5").MustBool(false), "bs5": app.config.Section("ui").Key("bs5").MustBool(false),