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

Compare commits

...

3 Commits

Author SHA1 Message Date
2d6b1717db
move all shared typescript to common.ts 2020-10-20 23:33:32 +01:00
9abb177427
use typescript for form.html in separate file, allow customization of
requirement strings

Password requirement text is now loaded by the typescript, and can be
customized by changing the validationStrings variable. See wiki for more
info.
2020-10-20 23:00:30 +01:00
2f9965bcda
Print full error if writing config fails 2020-10-20 21:16:46 +01:00
10 changed files with 272 additions and 213 deletions

View File

@ -465,7 +465,7 @@
<p>{{ .contactMessage }}</p>
</div>
</div>
<script src="serialize.js"></script>
<script src="common.js"></script>
<script>
var availableProfiles = [];
{{ if .notifications }}

View File

@ -102,8 +102,8 @@
<div class="card-body">
<ul class="list-group">
{{ range $key, $value := .requirements }}
<li id="{{ $key }}" class="list-group-item list-group-item-danger">
<div> {{ $value }}</div>
<li id="{{ $key }}" min="{{ $value }}" class="list-group-item list-group-item-danger">
<div></div>
</li>
{{ end }}
</ul>
@ -114,109 +114,32 @@
</div>
</div>
</div>
<script src="serialize.js"></script>
<script src="common.js"></script>
<script>
{{ if .bs5 }}
var bsVersion = 5;
{{ else }}
var bsVersion = 4;
{{ end }}
if (bsVersion == 5) {
var successBox = new bootstrap.Modal(document.getElementById('successBox'));
} else if (bsVersion == 4) {
var successBox = {
show : function() {
return $('#successBox').modal('show');
},
hide : function() {
return $('#successBox').modal('hide');
}
};
};
var code = window.location.href.split('/').pop();
function toggleSpinner () {
var submitButton = document.getElementById('submitButton');
var oldSpan = document.getElementById('createAccount');
var newSpan = document.createElement('span');
newSpan.id = 'createAccount';
if (document.getElementById('createAccountSpinner')) {
newSpan.appendChild(document.createTextNode('Create Account'));
submitButton.disabled = false;
} else {
var spinner = document.createElement('span');
spinner.id = 'createAccountSpinner';
spinner.classList.add('spinner-border', 'spinner-border-sm');
spinner.setAttribute('role', 'status');
spinner.setAttribute('aria-hidden', 'true');
var text = document.createTextNode(' Creating...');
newSpan.appendChild(spinner);
newSpan.appendChild(text);
submitButton.disabled = true;
var usernameEnabled = {{ .username }}
var 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"
}
submitButton.replaceChild(newSpan, oldSpan);
};
document.getElementById('accountForm').onsubmit = function() {
if (document.getElementById('errorMessage')) {
document.getElementById('errorMessage').remove();
}
toggleSpinner();
var send = serializeForm('accountForm');
send['code'] = code;
{{ if not .username }}
send['email'] = send['username'];
{{ end }}
send = JSON.stringify(send);
var req = new XMLHttpRequest();
req.open("POST", "/newUser", true);
req.responseType = 'json';
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = function() {
if (this.readyState == 4) {
toggleSpinner();
var data = this.response;
if ('error' in data || data['success'] == false) {
if (typeof(data['error']) != 'undefined') {
var errorMessage = data['error'];
} else {
var errorMessage = 'Unknown Error';
}
var text = document.createTextNode(errorMessage);
var error = document.createElement('button');
error.classList.add('btn', 'btn-outline-danger');
error.setAttribute('disabled', '');
error.appendChild(text);
error.id = 'errorMessage';
document.getElementById('errorBox').appendChild(error);
} else {
var valid = true
for (var key in data) {
if (data.hasOwnProperty(key)) {
var criterion = document.getElementById(key);
if (criterion) {
if (data[key] == false) {
valid = false;
if (criterion.classList.contains('list-group-item-success')) {
criterion.classList.remove('list-group-item-success');
criterion.classList.add('list-group-item-danger');
};
} else {
if (criterion.classList.contains('list-group-item-danger')) {
criterion.classList.remove('list-group-item-danger');
criterion.classList.add('list-group-item-success');
};
};
};
};
};
if (valid == true) {
successBox.show();
};
};
};
};
req.send(send);
return false;
};
}
</script>
<script src="form.js"></script>
</body>
</html>

2
go.mod
View File

@ -31,7 +31,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
github.com/swaggo/gin-swagger v1.2.0
github.com/swaggo/swag v1.6.8 // indirect
github.com/swaggo/swag v1.6.9 // indirect
github.com/ugorji/go v1.1.9 // indirect
github.com/urfave/cli/v2 v2.2.0 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect

2
go.sum
View File

@ -210,6 +210,8 @@ github.com/swaggo/swag v1.6.7 h1:e8GC2xDllJZr3omJkm9YfmK0Y56+rMO3cg0JBKNz09s=
github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
github.com/swaggo/swag v1.6.8 h1:z3ZNcpJs/NLMpZcKqXUsBELmmY2Ocy09JXKx5gu3L4M=
github.com/swaggo/swag v1.6.8/go.mod h1:a0IpNeMfGidNOcm2TsqODUh9JHdHu3kxDA0UlGbBKjI=
github.com/swaggo/swag v1.6.9 h1:BukKRwZjnEcUxQt7Xgfrt9fpav0hiWw9YimdNO9wssw=
github.com/swaggo/swag v1.6.9/go.mod h1:a0IpNeMfGidNOcm2TsqODUh9JHdHu3kxDA0UlGbBKjI=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=

18
main.go
View File

@ -67,11 +67,6 @@ type appContext struct {
}
func (app *appContext) loadHTML(router *gin.Engine) {
// router.LoadHTMLGlob(filepath.Join(app.local_path, "templates", "*"))
// if customHTML := app.config.Section("files").Key("html_templates").MustString(""); customHTML != "" {
// app.info.Printf("Loading custom templates from \"%s\"", customHTML)
// router.LoadHTMLGlob(filepath.Join(customHTML, "*"))
// }
customPath := app.config.Section("files").Key("html_templates").MustString("")
templatePath := filepath.Join(app.local_path, "templates")
htmlFiles, err := ioutil.ReadDir(templatePath)
@ -252,7 +247,8 @@ func start(asDaemon, firstCall bool) {
var nConfig *os.File
nConfig, err := os.Create(app.config_path)
if err != nil {
app.err.Fatalf("Couldn't open config file for writing: \"%s\"", app.config_path)
app.err.Printf("Couldn't open config file for writing: \"%s\"", app.config_path)
app.err.Fatalf("Error: %s", err)
}
defer nConfig.Close()
_, err = io.Copy(nConfig, dConfig)
@ -451,11 +447,11 @@ func start(asDaemon, firstCall bool) {
app.loadStrftime()
validatorConf := ValidatorConf{
"characters": app.config.Section("password_validation").Key("min_length").MustInt(0),
"uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0),
"lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0),
"numbers": app.config.Section("password_validation").Key("number").MustInt(0),
"special characters": app.config.Section("password_validation").Key("special").MustInt(0),
"length": app.config.Section("password_validation").Key("min_length").MustInt(0),
"uppercase": app.config.Section("password_validation").Key("upper").MustInt(0),
"lowercase": app.config.Section("password_validation").Key("lower").MustInt(0),
"number": app.config.Section("password_validation").Key("number").MustInt(0),
"special": app.config.Section("password_validation").Key("special").MustInt(0),
}
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
for key := range validatorConf {

View File

@ -1,8 +1,6 @@
package main
import (
"fmt"
"strings"
"unicode"
)
@ -21,11 +19,11 @@ func (vd *Validator) init(criteria ValidatorConf) {
// This isn't used, its for swagger
type PasswordValidation struct {
Characters bool `json:"characters,omitempty"` // Number of characters
Lowercase bool `json:"lowercase characters,omitempty"` // Number of lowercase characters
Uppercase bool `json:"uppercase characters,omitempty"` // Number of uppercase characters
Numbers bool `json:"numbers,omitempty"` // Number of numbers
Specials bool `json:"special characters,omitempty"` // Number of special characters
Characters bool `json:"length,omitempty"` // Number of characters
Lowercase bool `json:"lowercase,omitempty"` // Number of lowercase characters
Uppercase bool `json:"uppercase,omitempty"` // Number of uppercase characters
Numbers bool `json:"number,omitempty"` // Number of numbers
Specials bool `json:"special,omitempty"` // Number of special characters
}
func (vd *Validator) validate(password string) map[string]bool {
@ -34,17 +32,17 @@ func (vd *Validator) validate(password string) map[string]bool {
count[key] = 0
}
for _, c := range password {
count["characters"] += 1
count["length"] += 1
if unicode.IsUpper(c) {
count["uppercase characters"] += 1
count["uppercase"] += 1
} else if unicode.IsLower(c) {
count["lowercase characters"] += 1
count["lowercase"] += 1
} else if unicode.IsNumber(c) {
count["numbers"] += 1
} else {
for _, s := range vd.specialChars {
if c == s {
count["special characters"] += 1
count["special"] += 1
}
}
}
@ -60,18 +58,12 @@ func (vd *Validator) validate(password string) map[string]bool {
return results
}
func (vd *Validator) getCriteria() map[string]string {
lines := map[string]string{}
for criterion, min := range vd.criteria {
if min > 0 {
text := fmt.Sprintf("Must have at least %d ", min)
if min == 1 {
text += strings.TrimSuffix(criterion, "s")
} else {
text += criterion
}
lines[criterion] = text
func (vd *Validator) getCriteria() ValidatorConf {
criteria := ValidatorConf{}
for key, num := range vd.criteria {
if num != 0 {
criteria[key] = num
}
}
return lines
return criteria
}

View File

@ -1,45 +1,6 @@
interface Window {
token: string;
}
// Set in admin.html
var cssFile: string;
const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
let req = new XMLHttpRequest();
req.open("GET", url, true);
req.responseType = 'json';
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = onreadystatechange;
req.send(JSON.stringify(data));
};
const _post = (url: string, data: Object, onreadystatechange: () => void): void => {
let req = new XMLHttpRequest();
req.open("POST", url, true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = onreadystatechange;
req.send(JSON.stringify(data));
};
function _delete(url: string, data: Object, onreadystatechange: () => void): void {
let req = new XMLHttpRequest();
req.open("DELETE", url, true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = onreadystatechange;
req.send(JSON.stringify(data));
}
const rmAttr = (el: HTMLElement, attr: string): void => {
if (el.classList.contains(attr)) {
el.classList.remove(attr);
}
};
const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused');
const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused');

79
ts/common.ts Normal file
View File

@ -0,0 +1,79 @@
interface Window {
token: string;
}
function serializeForm(id: string): Object {
const form = document.getElementById(id) as HTMLFormElement;
let formData = {};
for (let i = 0; i < form.elements.length; i++) {
const el = form.elements[i];
if ((el as HTMLInputElement).type == "submit") {
continue;
}
let name = (el as HTMLInputElement).name;
if (!name) {
name = el.id;
}
switch ((el as HTMLInputElement).type) {
case "checkbox":
formData[name] = (el as HTMLInputElement).checked;
break;
case "text":
case "password":
case "email":
case "number":
formData[name] = (el as HTMLInputElement).value;
break;
case "select-one":
case "select":
let val: string = (el as HTMLSelectElement).value.toString();
if (!isNaN(val as any)) {
formData[name] = +val;
} else {
formData[name] = val;
}
break;
}
}
return formData;
}
const rmAttr = (el: HTMLElement, attr: string): void => {
if (el.classList.contains(attr)) {
el.classList.remove(attr);
}
};
const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
let req = new XMLHttpRequest();
req.open("GET", url, true);
req.responseType = 'json';
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = onreadystatechange;
req.send(JSON.stringify(data));
};
const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => {
let req = new XMLHttpRequest();
req.open("POST", url, true);
if (response) {
req.responseType = 'json';
}
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = onreadystatechange;
req.send(JSON.stringify(data));
};
function _delete(url: string, data: Object, onreadystatechange: () => void): void {
let req = new XMLHttpRequest();
req.open("DELETE", url, true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = onreadystatechange;
req.send(JSON.stringify(data));
}

141
ts/form.ts Normal file
View File

@ -0,0 +1,141 @@
interface pwValString {
singular: string;
plural: string;
}
interface pwValStrings {
length, uppercase, lowercase, number, special: pwValString;
}
var validationStrings: pwValStrings;
var bsVersion: number;
var defaultPwValStrings: pwValStrings = {
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"
}
}
const toggleSpinner = (): void => {
const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
if (document.getElementById('createAccountSpinner')) {
submitButton.innerHTML = `<span>Create Account</span>`;
submitButton.disabled = false;
} else {
submitButton.innerHTML = `
<span id="createAccountSpinner" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>Creating...
`;
}
};
for (let key in validationStrings) {
if (validationStrings[key].singular == "" || !(validationStrings[key].plural.includes("{n}"))) {
validationStrings[key].singular = defaultPwValStrings[key].singular;
}
if (validationStrings[key].plural == "" || !(validationStrings[key].plural.includes("{n}"))) {
validationStrings[key].plural = defaultPwValStrings[key].plural;
}
let el = document.getElementById(key) as HTMLUListElement;
if (el) {
const min: number = +el.getAttribute("min");
let text = "";
if (min == 1) {
text = validationStrings[key].singular.replace("{n}", "1");
} else {
text = validationStrings[key].plural.replace("{n}", min.toString());
}
(document.getElementById(key).children[0] as HTMLDivElement).textContent = text;
}
}
interface Modal {
show: () => void;
hide: () => void;
}
var successBox: Modal;
if (bsVersion == 5) {
var bootstrap: any;
successBox = new bootstrap.Modal(document.getElementById('successBox'));
} else if (bsVersion == 4) {
successBox = {
show: (): void => {
($('#successBox') as any).modal('show');
},
hide: (): void => {
($('#successBox') as any).modal('hide');
}
};
}
var code = window.location.href.split('/').pop();
var usernameEnabled: boolean;
(document.getElementById('accountForm') as HTMLFormElement).addEventListener('submit', (event: any): boolean => {
event.preventDefault();
const el = document.getElementById('errorMessage');
if (el) {
el.remove();
}
toggleSpinner();
let send: Object = serializeForm('accountForm');
send["code"] = code;
if (!usernameEnabled) {
send["email"] = send["username"];
}
_post("/newUser", send, function (): void {
if (this.readyState == 4) {
toggleSpinner();
let data: Object = this.response;
const errorGiven = ("error" in data)
if (errorGiven || data["success"] === false) {
let errorMessage = "Unknown Error";
if (errorGiven && errorGiven != true) {
errorMessage = data["error"];
}
document.getElementById('errorBox').innerHTML += `
<button id="errorMessage" class="btn btn-outline-danger" disabled>${errorMessage}</button>
`;
} else {
let valid = true;
for (let key in data) {
if (data.hasOwnProperty(key)) {
const criterion = document.getElementById(key);
if (criterion) {
if (data[key] === false) {
valid = false;
addAttr(criterion, "list-group-item-danger");
rmAttr(criterion, "list-group-item-success");
} else {
addAttr(criterion, "list-group-item-success");
rmAttr(criterion, "list-group-item-danger");
}
}
}
}
if (valid) {
successBox.show();
}
}
}
}, true);
return false;
});

View File

@ -1,35 +0,0 @@
function serializeForm(id: string): Object {
const form = document.getElementById(id) as HTMLFormElement;
let formData = {};
for (let i = 0; i < form.elements.length; i++) {
const el = form.elements[i];
if ((el as HTMLInputElement).type == "submit") {
continue;
}
let name = (el as HTMLInputElement).name;
if (!name) {
name = el.id;
}
switch ((el as HTMLInputElement).type) {
case "checkbox":
formData[name] = (el as HTMLInputElement).checked;
break;
case "text":
case "password":
case "email":
case "number":
formData[name] = (el as HTMLInputElement).value;
break;
case "select-one":
case "select":
let val: string = (el as HTMLSelectElement).value.toString();
if (!isNaN(val as any)) {
formData[name] = +val;
} else {
formData[name] = val;
}
break;
}
}
return formData;
}